Salesperson Extension Using Vue

In this help document, we’ll explain the steps to install and configure Vue and build the settings widget required for the Salesperson extension using Vue. You’ll have to add the code for the UI components required for the extension in the settings widget.

You can follow the steps in this help document or refer to our Git project on the Salesperson extension using Vue.

Prerequisite: You need to download and install Node.js and Node Package Manager (npm) in your device. You can install Node.js from the Node.js website.

Folders for Vue and Settings Widget

You have to create separate folders for Vue and the settings widget.

Vue

To create a folder for Vue:

1. Install Vue Command Line Interface (CLI) globally on your device. Enter the following command in your terminal or command prompt.

npm install -g @vue/cli

2. Enter the following command in your terminal or command prompt to create the project. Here, salesperson-vue-app is the folder name. You can enter a folder name of your choice.

vue create salesperson-vue-app

3. Pick a preset for the project. A preset is the pre-configured set of options, settings, and plugins that can be used to set up your Vue project quickly. You can choose either the Default preset or Manually select features. In this project, we’ve used Manually select features. The presets required for the project are explained in the subsequent steps.

4. Select all the project configurations such as Babel, Router, and Linter/Formatter. You can choose the options that best suit your project’s requirements.

5. Choose either the Vue 2 or Vue 3 framework. In this project, we’ve used the Vue 2 framework.

6. Select the required configurations.

Settings Widget

To create the settings widget:

1. Install the Zoho Extension Toolkit (ZET) globally by entering the following command in your terminal or command prompt.

npx install -g zoho-extension-toolkit

2. Enter the command:

zet init

3. Select Zoho Books from the options listed.

After creating the Vue project and the settings widget, your project’s structure should look similar to the project structure shown in the image below.

Vue project structure

Configure Vue

Now that you’ve installed Vue and created a Vue project, and the settings widget, you have to configure Vue so that the SDK methods for the widget can function properly. Here’s how:

1. Go to your Vue project folder > public > index.html.

2. Paste the following command in the body tag of the index.html file. You can find this code by navigating to your widget folder > app > widget’s html file.

<script src="https://js.zohostatic.com/zohofinance/v1/zf_sdk.js"></script>

Refer to the image below to know where to paste the code.

Configure Vue

3. Go to your Vue project folder > src > vue.config.js.

4.Add the line publicPath:"." to the file. The publicPath field is a configuration that helps Vue applications work correctly when deployed to sub-directories or specific hosting environments.

Refer to the image below to know where to paste the code.

Configure Vue

Build UI

To build the UI required for the Salesperson extension, you have to install Bootstrap on your device.

To do this, enter the following command in your terminal or command prompt.

 npm i bootstrap

Next, create the following folders and files in the src folder as shown in the image below.

src folder

In the subsequent sections, we’ll explain the need for each file or folder and what code you have to paste into each file.

main.js

The main.js serves as the entry point for your application. It’s the file where you create the root Vue instance, setup the required global configurations, and initialize ZFAPPS.

Insight: ZFAPPS is the name of the Zoho Extension Toolkit (ZET) framework used by the Zoho Finance apps.

Paste the following code in the main.js file:

import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import "bootstrap/dist/css/bootstrap.css";
Vue.config.productionTip = false;
async function initializeApp() {  
  if (window.ZFAPPS) {
    window.ZFAPPS.extension.init().then(async (zapp) => {
      window.ZFAPPS.invoke("RESIZE", { width: "550px", height: "600px" });
      const organizationMap = await window.ZFAPPS.get("organization");
      const { organization: { api_root_endpoint } } = organizationMap;
      window.zapp = zapp;
      // Store the data globally 
      Vue.prototype.$appMeta = {
        organizationMap:JSON.stringify(organizationMap),
        domainURL: api_root_endpoint,
        connection_link_name:"salesperson_commission_books",// created in the sigma developer portal
        orgVariablePlaceholder:"vl__u76nt_data_store", // paste the placeholder created in the sigma developer portal 
      };
      new Vue({
        zapp: zapp,
        router,
        ZFAPPS: window.ZFAPPS,
        render: h => h(App),
      
      }).$mount("#app");
    });
  }
}
initializeApp();

App.vue

The App.vue file is used to route the view of the HomeView.vue file. The App.vue file:

  1. Handles the routing of the application.
  2. Specifies which content or component should be displayed based on the current route or user interaction.

When a certain route is matched, the App.vue file is responsible for rendering the content of the HomeView.vue file.

Paste the following code in the App.Vue file:

<template>
  <div id="app">
    <router-view/>
  </div>
</template>
<script>
export default {
  name: "App",
  created() {
    // Automatically navigate to the home route ("/") when the component is created
    this.$router.push("/");
  },
};
</script>
<style>
* {
  box-sizing: border-box;
}
#app {
  height: 100%;
}
</style>

index.js

The index.js will be present in the router folder. This file defines the routes for your application. It specifies the mapping between URLs and Vue components that should be displayed when you navigate to these URLs.

Paste the following code in the index.js file:

import Vue from "vue";
import VueRouter from "vue-router";
import HomeView from "../views/HomeView.vue";
Vue.use(VueRouter);
const routes = [
  {
    path: "/",
    name: "home",
    component: HomeView
  }, 
];
const router = new VueRouter({
  mode: "hash",
  base: process.env.BASE_URL,
  routes
});
console.log("base"+router);
export default router;

Components Folder

The components folder is the centralized location for all reusable and shareable UI elements.
For the Salesperson extension, the following components are required:

Paste the following code in the dropdown.vue file:

<template>
    <div class="box" @click="showUsers" >
      <!-- toggler -->
      <button class="form-control selected" :style="style" >{{ title }}</button>
  
      <!-- Content Container -->
  
      <slot />
  
    </div>
  </template>
<script>
import { provide, ref } from "vue";
export default {
    name: "DropDown",
    props:{
        title: String,
        width:
        {
            type:String,
            default:""
        }
    },
    computed: {
      style() 
      {
        return ("width:"+this.size);
      }
    },
    setup(props) {
        const usersToggle = ref(false);
        const showUsers = () => {
            usersToggle.value = !usersToggle.value;
        };
        provide("usersToggle", usersToggle);
        provide("dropDownWidth", props.width);
        return {
            usersToggle,
            showUsers,
        };
    },
    data(){
        return{
            size:this.width,
        };
    }, 
};
</script>
<style scoped>
.box{
    box-sizing: border-box;
}
.selected{
    cursor: pointer;
    text-align: left;
    height: 34px;
    overflow: hidden;
    align-items: center;
    white-space: nowrap;
    line-height: 28px;
    color: #21263c;
    text-decoration: none;
    border-radius: 6px;
    border: 1px solid #d7d5e2;
    background-clip: padding-box;
    user-select: none;
}
.form-control
{
    display: block;
    width: 100%;
    padding: 5px 8px;
    font-size: 13px;
    line-height: 1.5;
    color: #495057;
    background-color: #fff;
    background-clip: padding-box;
    border: 1px solid #ced4da;
    border-radius: .25rem;
    transition: border-color .15s ease-in-out,box-shadow .15s ease-in-out;
}
.form-control:focus {
    color: #495057;
    background-color: #fff;
    border-color: #408dfb;
    outline: 0;
    box-shadow: 0 0 0 3px rgba(64,141,251,0.16);
}
</style>

Paste the following code in the dropdownContainer.vue file:

<template>
    <div v-show="usersToggle" class="content-continer">
        <div class="select-result" :style="style" >
            <slot />
        </div>
    </div>
</template>
<script>
import { inject } from "vue";
export default {
    name: "DropDownContent",
    inject:["dropDownWidth"],
    props:{    
        max_height:{
            type:String,
            default:""
        }
    },
    data(){
        return{
            size:this.dropDownWidth,
            maxHeight:this.max_height,
        };
    },
    setup() {
        const usersToggle = inject("usersToggle");
        return {
            usersToggle,
        };
    },
    computed: {
      style() 
      {
        return {"width":this.size, "max-height":this.maxHeight};
      }
    },   
};
</script>
<style scoped>
.content-continer{
    z-index: 9999;
    margin-top: 6px;    
    top: 100%;
    width: 100%;
}
.select-result{
    position: absolute;
    display: block;
    min-height: 0;
    max-height: 280px;
    min-width: 0;
    overflow-x: hidden;
    margin: 0;
    padding: 4px;
    border-radius: 0 0 6px 6px;
    border-top: none;
    border-bottom: none;
    background-color: #fff;
    border: 1px solid #ebeaf2;
    border-radius: 8px;
    box-shadow: 0 4px 12px 0 #d7d5e2;
    width: 100%;
 }
 ::-webkit-scrollbar {
    width: 8px;
    height : 8px;
}
::-webkit-scrollbar-track {
      background-color:#F3F3F3;
      border-radius: 50px;
}
::-webkit-scrollbar-thumb {
      background-color: #E0E0E0;
      border-radius: 50px;
}
</style>

Paste the following code in the dropdownItem.vue file:

<template>
    <div class="option-container">
       <div class="result" :class="isActive?'active':''" @click="emitCustomEvent"  :value="value" :label="label">{{ label }} </div> 
       <img  v-show="isActive" src="../../../public/tick.svg" class="tick"/>
       <slot />
    </div>
         
 
   </template>
  <script>
  export default {
      name: "DropDownItems",
      props:{
        label:{
          type:String
        },
        value:{
          type:String
        },
        updatedValue:{
          type:String
        }
      },
      data(){
       return{
          isActive:false,
       };
      },
      watch:{
       updatedValue:function(new_value){
          if(new_value === this.value)
          {
             this.isActive = true;   
          }
          else{
             this.isActive = false;
          }
        },
        immediate: true,
      },  
      methods:{
        emitCustomEvent(event) {
            this.$emit("custom-event", event);
      },
      changeClass(value){
        if(value === this.value)
        {
           this.isActive = true; 
        }
        else{
           this.isActive = false;
        }
      }
    },
    mounted:function(){
        this.changeClass(this.updatedValue);
      }, 
  };
  </script>
  <style scoped>
 .result{
     padding: 7px 0 7px 10px;
     color: #777;
     display: flex;
     align-items: center;
     width: 100%;
     clear: both;
     font-weight: 400;
     text-align: inherit;
     white-space: nowrap;
     background-color: transparent;
     border: 0;
     font-size: 1 rem !important;
     margin: 2px 0;
     cursor: pointer;
 }
  .result:hover{
     background-color: #408dfb;;
     color: #fff;
     border-radius: 6px;   
     color:#fff; 
  }
  .active{
    background-color: #e9ebf3;
     color: #21263c;
     border-radius: 6px;    
  }
  .tick{
    height: 10px;
    width: 10px;
    color:#408dfb;
    position: absolute;
    right: 20px;
    top:13px;
  }
  .option-container{
    position: relative;
  }
  
  </style>

input.vue

Paste the following code in the input.vue file:

<template>
    <div >
        <input 
        v-bind="$attrs" 
        class="mx-5 my-4 form-control" 
        :class="width" 
        :placeholder="label" 
        :value="value"
        @input="$emit('input', $event.target.value)"
        />
        
    </div>
</template>
<script>
export default {
     name: "inputField",
     props: {
        width: {
            type:String,
            defalut:""
        },
        label:{
            type:String,
            default:""
        },
        value:{
            type:[String,Number],
            default:""
        }
    },  
};
</script>
<style scoped>
.form-control{
    display: block;
    width: 50% !important;
    padding: .375rem .75rem;
    font-size: 13px !important;
    line-height: 1.5;
    color: #495057;
    background-color: #fff;
    background-clip: padding-box;
    border: 1px solid #ced4da;
    border-radius: .25rem;
    transition: border-color .15s ease-in-out,box-shadow .15s ease-in-out;
    height: 34px !important;
    margin-left: 0px !important;
    margin-top: 0px !important;
}
.form-control:focus {
    color: #495057 !important;
    background-color: #fff !important;
    border-color: #408dfb !important;
    outline: 0 !important;
    box-shadow: 0 0 0 3px rgba(64,141,251,0.16) !important;
}
</style>

loaderComponent.vue

Paste the following code in the loaderComponent.vue file:

 <template>
    <div class="loading">
    <div class="load-circle1"></div>
    <div class="load-circle2"></div>
    <div class="load-circle3"></div>
    <div class="load-circle4"></div>
    <div class="load-circle5"></div>
  </div>
</template>
<style scoped>
.loading {
  text-align: center;
    position: relative;
    top: 190px;
}
.load-circle1,
.load-circle2,
.load-circle3,
.load-circle4,
.load-circle5 {
  width: 8px;
  height: 8px;
  background: grey;
  display: inline-block;
  border-radius: 20px;
  animation: loader 1.5s infinite;
  margin-right: 3px;
}
@keyframes loader {
  from {
    opacity: 1;
    scale: 1;
  }
  to {
    opacity: 0.25;
    scale: 0.3;
  }
}
.load-circle2 {
  animation-delay: 0.25s;
}
.load-circle3 {
  animation-delay: 0.5s;
}
.load-circle4 {
  animation-delay: 0.75s;
}
.load-circle5 {
  animation-delay: 1s;
}
</style>

radioButton.vue

Paste the following code in the radioButton.vue file:

<template>
    <div>
        <input class="form-check-input"
          :id="id"
          type="radio"
          :name="name"
          :value="value"
          :checked="value === checked"          
          @change="updateSelectedValue"
        />
        <label :for="id" class="form-check-label">{{label}}</label>
    </div>
</template>
<script>
export default {
  name: "radioButton",
  props: {
    name:{
        type:String,
        default:""
    },     
    id:{
        type:String,
        default:""
    },    
    label:{
        type:String,
        default:""
    }, 
    value: {
        type:String,
        default:""
    }, 
    checked:{
      type:String
    },
    selectedValue: {
        type:String,
        default:""
    },  
},
methods: {
    updateSelectedValue() {
      this.$emit("update:selectedValue", this.value); // Emit an event to update selectedValue in the parent component    
    },
  },
};
</script>

<style scoped>
input[type=radio]:checked {
  background-color: #408dfb !important;
  border-color: #408dfb !important;
}
input[type=radio] {
  cursor: pointer;
  width: 14px !important;
  height: 14px !important;
  vertical-align: top !important;
  background-color: #fff !important;
  background-repeat: no-repeat !important;
  background-position: center !important;
  background-size: contain !important;
  border: 1px solid #00000040 !important;
  -webkit-appearance: none !important;
  -moz-appearance: none !important;
  appearance: none !important;
 
}
input[type=radio]:hover:enabled {
  border-color: #408dfb !important;
  outline: 0 !important;
  box-shadow: 0 0 0 3px rgba(64,141,251,.16) !important;
}
input[type=radio]:focus {
  outline: 0 !important;
  box-shadow: 0 0 0 3px rgba(64,141,251,0.16) !important;
}
.form-check-label {
  margin-left: 4px !important;
  margin-bottom: 0 !important;
  cursor: pointer !important;
}
</style>

HomeView.vue

The HomeView.vue file contains the overall UI for the Salesperson extension. The code to store data in the global field will be available in this file.

Paste the following code in the HomeView.Vue file:

<template>
  <div class="home">
    <loader-vue v-if="isloading"></loader-vue>
    <div v-if="isVisible">
      <h4 class="heading">
        When do you wish to create the expenses Sales Person Commissions ?
      </h4>
      <div class="flex-row">
        <radiobutton-vue
          id="sent-radio"
          label="When an Invoice is Sent"
          :name="invoiceStatusRadioGroupName"
          :checked="status"
          value="sent"
          @update:selectedValue="status = $event"
        />
        <radiobutton-vue
          id="paid-radio"
          label="When an Invoice is Paid"
          :name="invoiceStatusRadioGroupName"
          :checked="status"
          value="paid"
          @update:selectedValue="status = $event"
        />
      </div>
      <div class="container">
        <div class="container">
          <h5 class="side-heading">Commission Type</h5>
          <div class="flex-row">
            <radiobutton-vue
              id="percentage-radio"
              label="Percentage"
              :name="comissionTypeRadioGroupName"
              :checked="type"
              value="Percentage"
              @update:selectedValue="type = $event"
            />
            <radiobutton-vue
              id="amount-radio"
              label="Amount"
              :name="comissionTypeRadioGroupName"
              :checked="type"
              value="Amount"
              @update:selectedValue="type = $event"
            />
          </div>
        </div>
        <div class="container">
          <h5 class="side-heading">Commission Rate</h5>
          <input-vue
            width="w-25"
            :label="type"
            v-model="commission"
            type="number"
          />
        </div>
        <div class="container" v-if="status === 'sent'">
          <h5 class="side-heading">
            Select the paid through account for expense created
          </h5>
          <dropdown-vue :title="paidAccountlabel" :width="dropdown_width">
            <dropdown-container-vue :max_height="dropdown_maxHeight">
              <dropdown-item-vue
                v-for="(option, index) in paidAccountArray"
                :key="index"
                :value="option.id"
                :label="option.text"
                @custom-event="handlePaidAccountCustomEvent"
                :updatedValue="paid_account_id"
              >
              </dropdown-item-vue>
            </dropdown-container-vue>
          </dropdown-vue>
        </div>
        <div class="container">
          <h5 class="side-heading">Select the expense account</h5>
          <dropdown-vue :title="expenseAccountlabel" :width="dropdown_width">
            <dropdown-container-vue :max_height="dropdown_maxHeight">
              <dropdown-item-vue
                v-for="(option, index) in expenseAccountArray"
                :key="index"
                :value="option.id"
                :label="option.text"
                @custom-event="handleExpenseAccountCustomEvent"
                :updatedValue="expense_account_id"
              >
              </dropdown-item-vue>
            </dropdown-container-vue>
          </dropdown-vue>
        </div>
        <div class="container">
          <h5 class="side-heading">Commission Specification</h5>
          <div class="flex-row">
            <radiobutton-vue
              id="subtotal-radio"
              label="Commission on SubTotal"
              :name="specificationRadioGroupName"
              :checked="specification_type"
              value="SubTotal"
              @update:selectedValue="specification_type = $event"
            />
            <radiobutton-vue
              id="total-radio"
              label="Commission on Total"
              :name="specificationRadioGroupName"
              :checked="specification_type"
              value="Total"
              @update:selectedValue="specification_type = $event"
            />
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
// @ is an alias to /src
import inputVue from "@/components/input.vue";
import radiobuttonVue from "@/components/radiobutton.vue";
import dropdownContainerVue from "@/components/Dropdown/dropdownContainer.vue";
import dropdownItemVue from "@/components/Dropdown/dropdownItem.vue";
import dropdownVue from "@/components/Dropdown/dropdown.vue";
import loaderVue from "@/components/loaderComponent.vue";
export default {
  name: "HomeView",
  components: {
    inputVue,
    radiobuttonVue,
    dropdownVue,
    dropdownContainerVue,
    dropdownItemVue,
    loaderVue,
  },
  data() {
    return {
      isloading: true,
      isVisible: false,
      status: "sent",
      invoiceStatusRadioGroupName: "invoiceStatusGroup",
      type: "Percentage",
      comissionTypeRadioGroupName: "comissionTypeGroup",
      commission: "",
      paidAccountArray: [],
      expenseAccountArray: [],
      dropdown_width: "250px",
      dropdown_maxHeight: "180px",
      paidAccountlabel: "Select",
      expenseAccountlabel: "Select",
      paid_account_id: "",
      expense_account_id: "",
      specification_type: "SubTotal",
      specificationRadioGroupName: "specificationGroup",
    };
  },
  methods: {
    handlePaidAccountCustomEvent(event) {
      this.paid_account_id = event.target.getAttribute("value");
      this.paidAccountlabel = event.target.getAttribute("label");
    },
    handleExpenseAccountCustomEvent(event) {
      this.expense_account_id = event.target.getAttribute("value");
      this.expenseAccountlabel = event.target.getAttribute("label");
    },
  },
  created: async function () {
    let { organization } = JSON.parse(this.$appMeta.organizationMap);
    let domainURL = this.$appMeta.domainURL;
    let orgVariablePlaceholder = this.$appMeta.orgVariablePlaceholder;
    // Methods 
    // Error Notification 
    let showErrorNotification = async (msg) => {
      await window.ZFAPPS.invoke("SHOW_NOTIFICATION", {
        type: "error",
        message: msg,
      });
    };
    // GET Paid Through Account 
    let getPaidThroughAccount = async () =>{
      try {
        // GET the Paid Through Account
        let getPaidThroughAccount = {
          url: `${domainURL}/autocomplete/paidthroughaccountslist`,
          method: "GET",
          url_query: [
            {
              key: "organization_id",
              value: organization.organization_id,
            },
          ],
          connection_link_name: this.$appMeta.connection_link_name,
        };
        let {
          data: { body },
        } = await window.ZFAPPS.request(getPaidThroughAccount);
        let { results } = JSON.parse(body);
        this.paidAccountArray = results;
      } catch (err) {
        await showErrorNotification(err);
      }
    };
    // GET Expense Account 
    let getExpenseAccount = async ()=>{
      try {
        //GET Expense Account
        let getExpenseAccount = {
          url: `${domainURL}/autocomplete/expenseaccountslist`,
          method: "GET",
          url_query: [
            {
              key: "organization_id",
              value: organization.organization_id,
            },
          ],
          connection_link_name: this.$appMeta.connection_link_name,
        };
        let {
          data: { body },
        } = await window.ZFAPPS.request(getExpenseAccount);
        let { results } = JSON.parse(body);
        this.expenseAccountArray = results;
      } catch (err) {
        await showErrorNotification(err);
      }
    };
    //  GET Global Fields 
    let getOrgVariable = async ()=>{
      try {
        //Get the GlobalField
        let getOrgVariable = {
          url: `${domainURL}/settings/orgvariables/${orgVariablePlaceholder}`,
          method: "GET",
          url_query: [
            {
              key: "organization_id",
              value: organization.organization_id,
            },
          ],
          connection_link_name: this.$appMeta.connection_link_name,
        };
        let {
          data: { body },
        } = await window.ZFAPPS.request(getOrgVariable);
        let {
          orgvariable: { value },
        } = JSON.parse(body);
        if(value !==""){
          value = JSON.parse(value);
          let {
            status,
            commission,
            type,
            specification_type,
            paid_through_account,
            expense_account,
          } = value;
          Object.assign(this, {
            status,
            commission,
            type,
            specification_type,
            paid_through_account,
            expense_account,
          });
          this.paid_account_id = this.paid_through_account.id;
          this.paidAccountlabel = this.paid_through_account.text;
          this.expense_account_id = this.expense_account.id;
          this.expenseAccountlabel = this.expense_account.text;
        }
      } catch (err) {
        await showErrorNotification(err);
      }
    };
    await getPaidThroughAccount();
    await getExpenseAccount();
    await getOrgVariable();
    this.isloading = false;
    this.isVisible = true;
    // ON PRE SAVE CHECK
    window.zapp.instance.on("ON_SETTINGS_WIDGET_PRE_SAVE", async () => {
      if (this.commission !== undefined) {
        if (this.status === "paid") {
          let isError = await checkAccount("paid");
          if (isError) {
            return {
              prevent_save: true,
            };
          } else {
            await updateOrgVariable();
          }
        } else {
          let isError = await checkAccount("sent");
          if (isError) {
            return {
              prevent_save: true,
            };
          } else {
            await updateOrgVariable();
          }
        }
      } else {
        await showErrorNotification("Commission Rate cannot be empty");
        return {
          prevent_save: true,
        };
      }
    });
    let checkAccount = async (status) => {
      if (this.expense_account_id ==="") {
        await showErrorNotification("Please select the Expense Account");
        return true;
      }
      if (status !== "paid" && this.paid_account_id ==="") {
        await showErrorNotification("Please select the Paid Through Account");
        return true;
      }
    };
    // UPDATE Global Fields
    let updateOrgVariable = async () => {
      let value = {};
      let {
        status,
        expense_account,
        paid_through_account,
        type,
        commission,
        specification_type,
      } = this;
      Object.assign(value, {
        status,
        expense_account,
        type,
        commission,
        specification_type,
        paid_through_account,
      });
      let data = { value: value };
      window.ZFAPPS.request({
        url: `${domainURL}/settings/orgvariables/${orgVariablePlaceholder}`,
        method: "PUT",
        url_query: [
          {
            key: "organization_id",
            value: organization.organization_id,
          },
        ],
        body: {
          mode: "formdata",
          formdata: [
            {
              key: "JSONString",
              value: JSON.stringify(data),
            },
          ],
        },
        connection_link_name: this.$appMeta.connection_link_name,
      });
    };
  },
};
</script>
<style scoped>
.side-heading {
  font-weight: 400;
  font-size: 13px;
}
.side-heading:after {
  content: "*";
  color: red;
}
.heading {
  font-size: 16px;
  font-weight: 600;
}
.flex-row {
  display: flex;
  flex-direction: row;
  gap: 30px !important;
}
.container {
  margin-bottom: 10px;
  margin-top: 10px;
  margin-left: 0px !important;
}
</style>

Build Project

Now that you’ve made the necessary configurations for the Vue project, you’ll have to build it. To build the project, run the npm run build command in your terminal or command prompt. This will create an optimized build for the project in the build folder.

Paste the Build Folder Into the Widget Folder

After building the Vue project, you have to copy the build folder and paste it into the widget folder. To automate this process, you can create a file (say updateWidget.js) in the Vue folder, where you can implement the logic for copying and pasting.

Update the plugin-manifest.json File

The widget creation process happens during the extension’s installation. By default, the widget’s location will be the widgets pane in the right sidebar of the invoice creation page. To change its location, set the location attribute to plugin.globalfield and change the url to /app/index.html.

Additionally, you have to include the widget’s scope in the usedConnection array. To find the widget’s scope, go to the connection created for the extension in the Zoho Books Developer Portal and copy the JSON code. You have to paste this code inside the usedConnection array.

Refer to the image below to know where to paste the code.

plugin manifest file

Validate Widget

The next step is to validate the widget to ensure if it adheres to the guidelines specified in the plugin-manifest.json file. To do this, enter the command zet validate in the terminal or command prompt.

Pack Widget

To upload the widget in Zoho Books Developer Portal, you’ll have to pack it. Not all the files of your project directory are required while packing. Enter the command zet pack in your terminal or command prompt to pack the essential files and folders. After the command is executed, a ZIP file will be created.

Upload Widget

To upload your widget into Zoho Books Developer Portal:

With this, you’ve built the settings widget for the extension using Vue.


Was this document helpful?
Yes
No
Thank you for your feedback!
Want a feature?
Suggest
Switch to smart accounting software. Switch to Zoho Books.   Start my free 14-day trial Explore Demo Account

Books

Online accounting software
for small businesses.