From 6a07e6ae01b3fa1e30a9f06ed104dcbbb9b8cf49 Mon Sep 17 00:00:00 2001
From: "alonso.torres" <alonso.torres@kaleidos.net>
Date: Thu, 10 Oct 2024 17:12:39 +0200
Subject: [PATCH] :sparkles: Add update plugin permission dialog

---
 frontend/src/app/main/data/plugins.cljs       |  50 ++++-
 frontend/src/app/main/ui/dashboard.cljs       |  12 +-
 .../src/app/main/ui/workspace/plugins.cljs    | 178 ++++++++++++------
 .../src/app/main/ui/workspace/plugins.scss    |   5 +
 frontend/src/app/plugins/register.cljs        |   1 +
 frontend/translations/en.po                   |   6 +
 frontend/translations/es.po                   |   6 +
 7 files changed, 182 insertions(+), 76 deletions(-)

diff --git a/frontend/src/app/main/data/plugins.cljs b/frontend/src/app/main/data/plugins.cljs
index 97d286040..ba27e6a0a 100644
--- a/frontend/src/app/main/data/plugins.cljs
+++ b/frontend/src/app/main/data/plugins.cljs
@@ -6,14 +6,24 @@
 
 (ns app.main.data.plugins
   (:require
-   [app.common.data :as d]
    [app.common.data.macros :as dm]
+   [app.main.data.modal :as modal]
    [app.main.store :as st]
-   [app.plugins.register :as pr]
+   [app.plugins.register :as preg]
    [app.util.globals :as ug]
+   [app.util.http :as http]
    [beicon.v2.core :as rx]
    [potok.v2.core :as ptk]))
 
+(defn fetch-manifest
+  [plugin-url]
+  (->> (http/send! {:method :get
+                    :uri plugin-url
+                    :omit-default-headers true
+                    :response-type :json})
+       (rx/map :body)
+       (rx/map #(preg/parse-manifest plugin-url %))))
+
 (defn save-current-plugin
   [id]
   (ptk/reify ::save-current-plugin
@@ -28,7 +38,7 @@
     (update [_ state]
       (update-in state [:workspace-local :open-plugins] (fnil disj #{}) id))))
 
-(defn open-plugin!
+(defn- load-plugin!
   [{:keys [plugin-id name description host code icon permissions]}]
   (try
     (st/emit! (save-current-plugin plugin-id))
@@ -48,6 +58,36 @@
       (st/emit! (remove-current-plugin plugin-id))
       (.error js/console "Error" e))))
 
+(defn open-plugin!
+  [{:keys [url] :as manifest}]
+  (if url
+    ;; If the saved manifest has a URL we fetch the manifest to check
+    ;; for updates
+    (->> (fetch-manifest url)
+         (rx/subs!
+          (fn [new-manifest]
+            (let [new-manifest (merge new-manifest (select-keys manifest [:plugin-id]))]
+              (cond
+                (not= (:permissions new-manifest) (:permissions manifest))
+                (modal/show!
+                 :plugin-permissions-update
+                 {:plugin new-manifest
+                  :on-accept
+                  #(do
+                     (preg/install-plugin! new-manifest)
+                     (load-plugin! new-manifest))})
+
+                (not= new-manifest manifest)
+                (do (preg/install-plugin! new-manifest)
+                    (load-plugin! manifest))
+                :else
+                (load-plugin! manifest))))
+          (fn []
+            ;; Error fetching the manifest we'll load the plugin with the
+            ;; old manifest
+            (load-plugin! manifest))))
+    (load-plugin! manifest)))
+
 (defn close-plugin!
   [{:keys [plugin-id]}]
   (try
@@ -62,7 +102,7 @@
     (effect [_ state _]
       (let [ids (dm/get-in state [:workspace-local :open-plugins])]
         (doseq [id ids]
-          (close-plugin! (pr/get-plugin id)))))))
+          (close-plugin! (preg/get-plugin id)))))))
 
 (defn delay-open-plugin
   [plugin]
@@ -77,5 +117,5 @@
     ptk/WatchEvent
     (watch [_ state _]
       (when-let [pid (::open-plugin state)]
-        (open-plugin! (pr/get-plugin pid))
+        (open-plugin! (preg/get-plugin pid))
         (rx/of #(dissoc % ::open-plugin))))))
diff --git a/frontend/src/app/main/ui/dashboard.cljs b/frontend/src/app/main/ui/dashboard.cljs
index 277690ae9..a15822104 100644
--- a/frontend/src/app/main/ui/dashboard.cljs
+++ b/frontend/src/app/main/ui/dashboard.cljs
@@ -33,7 +33,6 @@
    [app.main.ui.workspace.plugins]
    [app.plugins.register :as preg]
    [app.util.dom :as dom]
-   [app.util.http :as http]
    [app.util.keyboard :as kbd]
    [app.util.object :as obj]
    [app.util.router :as rt]
@@ -202,19 +201,14 @@
     (mf/with-layout-effect
       [plugin-url team-id project-id]
       (when plugin-url
-        (->> (http/send! {:method :get
-                          :uri plugin-url
-                          :omit-default-headers true
-                          :response-type :json})
-             (rx/map :body)
+        (->> (dp/fetch-manifest plugin-url)
              (rx/subs!
-              (fn [body]
-                (if-let [plugin (preg/parse-manifest plugin-url body)]
+              (fn [plugin]
+                (if plugin
                   (do
                     (st/emit! (ptk/event ::ev/event {::ev/name "install-plugin" :name (:name plugin) :url plugin-url}))
                     (open-permissions-dialog plugin))
                   (st/emit! (notif/error "Cannot parser the plugin manifest"))))
-
               (fn [_]
                 (st/emit! (notif/error "The plugin URL is incorrect")))))))))
 
diff --git a/frontend/src/app/main/ui/workspace/plugins.cljs b/frontend/src/app/main/ui/workspace/plugins.cljs
index ff0a18e78..cecc22bf6 100644
--- a/frontend/src/app/main/ui/workspace/plugins.cljs
+++ b/frontend/src/app/main/ui/workspace/plugins.cljs
@@ -20,7 +20,6 @@
    [app.plugins.register :as preg]
    [app.util.avatars :as avatars]
    [app.util.dom :as dom]
-   [app.util.http :as http]
    [app.util.i18n :as i18n :refer [tr]]
    [beicon.v2.core :as rx]
    [cuerdas.core :as str]
@@ -96,15 +95,11 @@
          (mf/deps plugins-state plugin-url)
          (fn []
            (reset! fetching-manifest? true)
-           (->> (http/send! {:method :get
-                             :uri plugin-url
-                             :omit-default-headers true
-                             :response-type :json})
-                (rx/map :body)
+           (->> (dp/fetch-manifest plugin-url)
                 (rx/subs!
-                 (fn [body]
+                 (fn [plugin]
                    (reset! fetching-manifest? false)
-                   (if-let [plugin (preg/parse-manifest plugin-url body)]
+                   (if plugin
                      (do
                        (st/emit! (ptk/event ::ev/event {::ev/name "install-plugin" :name (:name plugin) :url plugin-url}))
                        (modal/show!
@@ -118,7 +113,8 @@
                        (reset! plugin-url* ""))
                      ;; Cannot get the manifest
                      (reset! input-status* :error-manifest)))
-                 (fn [_]
+                 (fn [err]
+                   (.error js/console err)
                    (reset! fetching-manifest? false)
                    (reset! input-status* :error-url))))))
 
@@ -199,6 +195,62 @@
                                :on-open-plugin handle-open-plugin
                                :on-remove-plugin handle-remove-plugin}])]])]]]))
 
+(mf/defc plugins-permission-list
+  [{:keys [permissions]}]
+  [:div {:class (stl/css :permissions-list)}
+   (cond
+     (contains? permissions "content:write")
+     [:div {:class (stl/css :permissions-list-entry)}
+      i/oauth-1
+      [:p {:class (stl/css :permissions-list-text)}
+       (tr "workspace.plugins.permissions.content-write")]]
+
+     (contains? permissions "content:read")
+     [:div {:class (stl/css :permissions-list-entry)}
+      i/oauth-1
+      [:p {:class (stl/css :permissions-list-text)}
+       (tr "workspace.plugins.permissions.content-read")]])
+
+   (cond
+     (contains? permissions "user:read")
+     [:div {:class (stl/css :permissions-list-entry)}
+      i/oauth-2
+      [:p {:class (stl/css :permissions-list-text)}
+       (tr "workspace.plugins.permissions.user-read")]])
+
+   (cond
+     (contains? permissions "library:write")
+     [:div {:class (stl/css :permissions-list-entry)}
+      i/oauth-3
+      [:p {:class (stl/css :permissions-list-text)}
+       (tr "workspace.plugins.permissions.library-write")]]
+
+     (contains? permissions "library:read")
+     [:div {:class (stl/css :permissions-list-entry)}
+      i/oauth-3
+      [:p {:class (stl/css :permissions-list-text)}
+       (tr "workspace.plugins.permissions.library-read")]])
+
+   (cond
+     (contains? permissions "comment:write")
+     [:div {:class (stl/css :permissions-list-entry)}
+      i/oauth-1
+      [:p {:class (stl/css :permissions-list-text)}
+       (tr "workspace.plugins.permissions.comment-write")]]
+
+     (contains? permissions "comment:read")
+     [:div {:class (stl/css :permissions-list-entry)}
+      i/oauth-1
+      [:p {:class (stl/css :permissions-list-text)}
+       (tr "workspace.plugins.permissions.comment-read")]])
+
+   (cond
+     (contains? permissions "allow:downloads")
+     [:div {:class (stl/css :permissions-list-entry)}
+      i/oauth-1
+      [:p {:class (stl/css :permissions-list-text)}
+       (tr "workspace.plugins.permissions.allow-download")]])])
+
 (mf/defc plugins-permissions-dialog
   {::mf/register modal/components
    ::mf/register-as :plugin-permissions}
@@ -233,59 +285,7 @@
       [:div {:class (stl/css :modal-title)} (tr "workspace.plugins.permissions.title" (str/upper (:name plugin)))]
 
       [:div {:class (stl/css :modal-content)}
-       [:div {:class (stl/css :permissions-list)}
-        (cond
-          (contains? permissions "content:write")
-          [:div {:class (stl/css :permissions-list-entry)}
-           i/oauth-1
-           [:p {:class (stl/css :permissions-list-text)}
-            (tr "workspace.plugins.permissions.content-write")]]
-
-          (contains? permissions "content:read")
-          [:div {:class (stl/css :permissions-list-entry)}
-           i/oauth-1
-           [:p {:class (stl/css :permissions-list-text)}
-            (tr "workspace.plugins.permissions.content-read")]])
-
-        (cond
-          (contains? permissions "user:read")
-          [:div {:class (stl/css :permissions-list-entry)}
-           i/oauth-2
-           [:p {:class (stl/css :permissions-list-text)}
-            (tr "workspace.plugins.permissions.user-read")]])
-
-        (cond
-          (contains? permissions "library:write")
-          [:div {:class (stl/css :permissions-list-entry)}
-           i/oauth-3
-           [:p {:class (stl/css :permissions-list-text)}
-            (tr "workspace.plugins.permissions.library-write")]]
-
-          (contains? permissions "library:read")
-          [:div {:class (stl/css :permissions-list-entry)}
-           i/oauth-3
-           [:p {:class (stl/css :permissions-list-text)}
-            (tr "workspace.plugins.permissions.library-read")]])
-
-        (cond
-          (contains? permissions "comment:write")
-          [:div {:class (stl/css :permissions-list-entry)}
-           i/oauth-1
-           [:p {:class (stl/css :permissions-list-text)}
-            (tr "workspace.plugins.permissions.comment-write")]]
-
-          (contains? permissions "comment:read")
-          [:div {:class (stl/css :permissions-list-entry)}
-           i/oauth-1
-           [:p {:class (stl/css :permissions-list-text)}
-            (tr "workspace.plugins.permissions.comment-read")]])
-
-        (cond
-          (contains? permissions "allow:downloads")
-          [:div {:class (stl/css :permissions-list-entry)}
-           i/oauth-1
-           [:p {:class (stl/css :permissions-list-text)}
-            (tr "workspace.plugins.permissions.allow-download")]])]
+       [:& plugins-permission-list {:permissions permissions}]
 
        [:div {:class (stl/css :permissions-disclaimer)}
         (tr "workspace.plugins.permissions.disclaimer")]]
@@ -305,6 +305,60 @@
           :on-click handle-accept-dialog}]]]]]))
 
 
+(mf/defc plugins-permissions-updated-dialog
+  {::mf/register modal/components
+   ::mf/register-as :plugin-permissions-update}
+  [{:keys [plugin on-accept on-close]}]
+
+  (let [{:keys [host permissions]} plugin
+        permissions (set permissions)
+
+        handle-accept-dialog
+        (mf/use-callback
+         (fn [event]
+           (dom/prevent-default event)
+           (st/emit! (ptk/event ::ev/event {::ev/name "allow-plugin-permissions"
+                                            :host host
+                                            :permissions (->> permissions (str/join ", "))})
+                     (modal/hide))
+           (when on-accept (on-accept))))
+
+        handle-close-dialog
+        (mf/use-callback
+         (fn [event]
+           (dom/prevent-default event)
+           (st/emit! (ptk/event ::ev/event {::ev/name "reject-plugin-permissions"
+                                            :host host
+                                            :permissions (->> permissions (str/join ", "))})
+                     (modal/hide))
+           (when on-close (on-close))))]
+
+    [:div {:class (stl/css :modal-overlay)}
+     [:div {:class (stl/css :modal-dialog :plugin-permissions)}
+      [:button {:class (stl/css :close-btn) :on-click handle-close-dialog} close-icon]
+      [:div {:class (stl/css :modal-title)}
+       (tr "workspace.plugins.permissions-update.title" (str/upper (:name plugin)))]
+
+      [:div {:class (stl/css :modal-content)}
+       [:div {:class (stl/css :modal-paragraph)}
+        (tr "workspace.plugins.permissions-update.warning")]
+       [:& plugins-permission-list {:permissions permissions}]]
+
+      [:div {:class (stl/css :modal-footer)}
+       [:div {:class (stl/css :action-buttons)}
+        [:input
+         {:class (stl/css :cancel-button :button-expand)
+          :type "button"
+          :value (tr "ds.confirm-cancel")
+          :on-click handle-close-dialog}]
+
+        [:input
+         {:class (stl/css :primary-button :button-expand)
+          :type "button"
+          :value (tr "ds.confirm-allow")
+          :on-click handle-accept-dialog}]]]]]))
+
+
 (mf/defc plugins-try-out-dialog
   {::mf/register modal/components
    ::mf/register-as :plugin-try-out}
diff --git a/frontend/src/app/main/ui/workspace/plugins.scss b/frontend/src/app/main/ui/workspace/plugins.scss
index bc63bbe1f..ba736acf8 100644
--- a/frontend/src/app/main/ui/workspace/plugins.scss
+++ b/frontend/src/app/main/ui/workspace/plugins.scss
@@ -76,6 +76,11 @@
   color: var(--color-foreground-secondary);
 }
 
+.modal-paragraph {
+  font-size: $fs-14;
+  color: var(--color-foreground-primary);
+}
+
 .primary-button {
   @extend .button-primary;
   @include headlineSmallTypography;
diff --git a/frontend/src/app/plugins/register.cljs b/frontend/src/app/plugins/register.cljs
index 19e98a805..f8e3e03be 100644
--- a/frontend/src/app/plugins/register.cljs
+++ b/frontend/src/app/plugins/register.cljs
@@ -64,6 +64,7 @@
         manifest
         (d/without-nils
          {:plugin-id plugin-id
+          :url plugin-url
           :name name
           :description desc
           :host origin
diff --git a/frontend/translations/en.po b/frontend/translations/en.po
index a87e94f7c..b0b931a05 100644
--- a/frontend/translations/en.po
+++ b/frontend/translations/en.po
@@ -5610,6 +5610,12 @@ msgstr "'%s' PLUGIN WANTS ACCESS TO:"
 msgid "workspace.plugins.permissions.user-read"
 msgstr "Read the profile information of the current user."
 
+msgid "workspace.plugins.permissions-update.title"
+msgstr "UPDATE THIS PLUGIN"
+
+msgid "workspace.plugins.permissions-update.warning"
+msgstr "The plugin has been modified since you last opened it. It now also wants to access:"
+
 msgid "workspace.plugins.try-out.title"
 msgstr "'%s' PLUGIN IS INSTALLED FOR YOUR USER!"
 
diff --git a/frontend/translations/es.po b/frontend/translations/es.po
index 54cbc1a11..7400fd0b9 100644
--- a/frontend/translations/es.po
+++ b/frontend/translations/es.po
@@ -5594,6 +5594,12 @@ msgstr "LA EXTENSIÓN '%s' SOLICITA PERMISO PARA ACCEDER:"
 msgid "workspace.plugins.permissions.user-read"
 msgstr "Leer la información del usuario actual."
 
+msgid "workspace.plugins.permissions-update.title"
+msgstr "EXTENSIÓN ACTUALIZADA"
+
+msgid "workspace.plugins.permissions-update.warning"
+msgstr "La extensión ha cambiado desde la última vez que la abriste. Ahora quiere acceder a:"
+
 msgid "workspace.plugins.try-out.title"
 msgstr "¡LA EXTENSIÓN '%s' HA SIDO INSTALADA PARA TU USUARIO!"