From cb1a4844a0cc1e0ad769aff20f5574f283b5bee8 Mon Sep 17 00:00:00 2001
From: pbalint <pbalint@gmail.com>
Date: Tue, 12 Dec 2023 17:36:04 +0100
Subject: [PATCH] Changes for renderer API integration into IDE (#1215)

-Implemented optional save button, enabled by a query parameter and controlled by the dirty state
-Added listeners for cross-iframe messages
-Added notification to parent app when ZAP has finished loading
---
 .../ide-integration/studio-rest-api.js        |  2 +-
 src/App.vue                                   | 44 +++++++++++++++++--
 src/components/ZCLToolbar.vue                 | 19 ++++++++
 src/store/zap/actions.js                      |  8 ++++
 src/store/zap/mutations.js                    | 11 +++++
 src/store/zap/state.js                        |  2 +
 6 files changed, 82 insertions(+), 4 deletions(-)

diff --git a/src-electron/ide-integration/studio-rest-api.js b/src-electron/ide-integration/studio-rest-api.js
index 008ae79e..89dc0752 100644
--- a/src-electron/ide-integration/studio-rest-api.js
+++ b/src-electron/ide-integration/studio-rest-api.js
@@ -103,7 +103,7 @@ async function isComponentTogglingDisabled(db, sessionId) {
  * @returns URL for rest api.
  */
 function restApiUrl(api, path, queryParams = {}) {
-  let base = localhost + studioHttpPort + api + path
+  let base = localhost + studioHttpPort + api + encodeURIComponent(path)
   let params = Object.entries(queryParams)
   if (params.length) {
     let queries = new URLSearchParams()
diff --git a/src/App.vue b/src/App.vue
index 784abdc4..75e84c7d 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -34,16 +34,30 @@ limitations under the License.
 import { defineComponent } from 'vue'
 import { QSpinnerGears } from 'quasar'
 import ZclTour from './tutorials/ZclTour.vue'
-
-// import VueTour from './tutorials/VueTour.vue'
-
 import CommonMixin from './util/common-mixin'
+
 const rendApi = require(`../src-shared/rend-api.js`)
 const restApi = require(`../src-shared/rest-api.js`)
 const observable = require('./util/observable.js')
 const dbEnum = require(`../src-shared/db-enum.js`)
 const storage = require('./util/storage.js')
 
+window.addEventListener('message', (event) => {
+    const eventData = event?.data?.eventData
+    switch (event?.data?.eventId) {
+      case 'theme':
+        window[rendApi.GLOBAL_SYMBOL_EXECUTE](rendApi.id.setDarkTheme, eventData.theme === 'dark')
+        break
+      case 'save':
+        if (eventData.shouldSave) {
+          window[rendApi.GLOBAL_SYMBOL_EXECUTE](rendApi.id.save)
+        }
+        break
+    }
+  }, 
+  false
+)
+
 async function initLoad(store) {
   let promises = []
   promises.push(store.dispatch('zap/loadInitialData'))
@@ -180,6 +194,16 @@ export default defineComponent({
         this.$store.dispatch('zap/setStandalone', query['standalone'])
       }
 
+      if (`setSaveButtonVisible` in query) {
+        this.$store.dispatch(
+          'zap/setSaveButtonVisible',
+          query[`setSaveButtonVisible`] === 'true'
+        )
+      } else {
+        // If we don't specify it, default is off.
+        this.$store.dispatch('zap/setSaveButtonVisible', false)
+      }
+
       this.zclDialogTitle = 'ZCL tab!'
       this.zclDialogText =
         'Welcome to ZCL tab. This is just a test of a dialog.'
@@ -206,6 +230,13 @@ export default defineComponent({
           this.$store.dispatch('zap/updateSelectedUcComponentState', resp)
         }
       )
+
+      this.$onWebSocket(
+        dbEnum.wsCategory.dirtyFlag,
+        (resp) => {
+          this.$store.dispatch('zap/setDirtyState', resp)
+        }
+      )
     },
     addClassToBody() {
       if (this.uiThemeCategory === 'zigbee') {
@@ -233,6 +264,13 @@ export default defineComponent({
   },
   mounted() {
     this.addClassToBody()
+    window?.parent?.postMessage({
+        eventId: 'mounted',
+        eventData: {
+          hasMounted: true
+        }
+      },
+      '*')
   },
   unmounted() {
     if (this.uiThemeCategory === 'zigbee') {
diff --git a/src/components/ZCLToolbar.vue b/src/components/ZCLToolbar.vue
index 61c309d3..a25a6122 100644
--- a/src/components/ZCLToolbar.vue
+++ b/src/components/ZCLToolbar.vue
@@ -38,6 +38,20 @@
         <div>Generate</div>
       </div>
     </q-btn>
+    <q-btn
+      id="save"
+      color="grey"
+      flat
+      no-caps
+      class="cursor-pointer"
+      @click="saveChanges"
+      v-if="this.$store.state.zap.saveButtonVisible && this.$store.state.zap.isDirty"
+    >
+      <div class="text-center">
+        <q-icon name="o_save" />
+        <div>Save</div>
+      </div>
+    </q-btn>
     <q-btn
       v-if="isCoreDocumentationAvailable"
       id="documentation"
@@ -250,6 +264,11 @@ export default {
         )
       }
     },
+    saveChanges() {
+      window[rendApi.GLOBAL_SYMBOL_EXECUTE] (
+        rendApi.id.save
+      )
+    },
     // This function will start vue tour steps
     startTour,
     togglePreviewTab() {
diff --git a/src/store/zap/actions.js b/src/store/zap/actions.js
index 03513c32..ff17e160 100644
--- a/src/store/zap/actions.js
+++ b/src/store/zap/actions.js
@@ -752,6 +752,10 @@ export function setDebugNavBar(context, debugNavBar) {
   context.commit('setDebugNavBar', debugNavBar)
 }
 
+export function setSaveButtonVisible(context, saveButtonVisible) {
+  context.commit('setSaveButtonVisible', saveButtonVisible)
+}
+
 export function setStandalone(context, standalone) {
   context.commit('setStandalone', standalone)
 }
@@ -818,6 +822,10 @@ export function updateSelectedUcComponentState(context, projectInfo) {
   })
 }
 
+export function setDirtyState(context, isDirty) {
+  context.commit('setDirtyState', isDirty)
+}
+
 export function loadZclClusterToUcComponentDependencyMap(context) {
   axiosRequests
     .$serverGet(`/zclExtension/cluster/component`)
diff --git a/src/store/zap/mutations.js b/src/store/zap/mutations.js
index ec8d4964..a9d4b8f4 100644
--- a/src/store/zap/mutations.js
+++ b/src/store/zap/mutations.js
@@ -452,6 +452,10 @@ export function setDebugNavBar(state, debugNavBar) {
   state.debugNavBar = debugNavBar
 }
 
+export function setSaveButtonVisible(state, saveButtonVisible) {
+  state.saveButtonVisible = saveButtonVisible
+}
+
 export function setStandalone(state, standalone) {
   state.standalone = standalone
 }
@@ -627,3 +631,10 @@ export function updateIsClusterOptionChanged(state, value) {
 export function updateNotificationCount(state, value) {
   state.notificationCount = value
 }
+
+export function setDirtyState(state, isDirty) {
+  if (state.isDirty != isDirty) {
+    state.isDirty = isDirty;
+    window.parent?.postMessage({eventId: 'dirty', eventData: { isDirty: isDirty }}, '*')
+  }
+} 
\ No newline at end of file
diff --git a/src/store/zap/state.js b/src/store/zap/state.js
index 27f508d2..2c933b26 100644
--- a/src/store/zap/state.js
+++ b/src/store/zap/state.js
@@ -45,6 +45,8 @@ export default function () {
     selectedGenericOptions: {},
     projectPackages: [],
     allPackages: [],
+    isDirty: false,
+    saveButtonVisible: false,
     clusterManager: {
       openDomains: {},
       lastSelectedDomain: null,
-- 
GitLab