diff --git a/frontend/src/app/main/data/comments.cljs b/frontend/src/app/main/data/comments.cljs index 0229db75a..61dc3dee7 100644 --- a/frontend/src/app/main/data/comments.cljs +++ b/frontend/src/app/main/data/comments.cljs @@ -56,27 +56,30 @@ (declare refresh-comment-thread) (defn created-thread-on-workspace - [{:keys [id comment page-id] :as thread}] - (ptk/reify ::created-thread-on-workspace - ptk/UpdateEvent - (update [_ state] - (let [position (select-keys thread [:position :frame-id])] - (-> state - (update :comment-threads assoc id (dissoc thread :comment)) - (update-in [:workspace-data :pages-index page-id :options :comment-threads-position] assoc id position) - (update :comments-local assoc :open id) - (update :comments-local assoc :options nil) - (update :comments-local dissoc :draft) - (update :workspace-drawing dissoc :comment) - (update-in [:comments id] assoc (:id comment) comment)))) + ([params] + (created-thread-on-workspace params true)) + ([{:keys [id comment page-id] :as thread} open?] + (ptk/reify ::created-thread-on-workspace + ptk/UpdateEvent + (update [_ state] + (let [position (select-keys thread [:position :frame-id])] + (-> state + (update :comment-threads assoc id (dissoc thread :comment)) + (update-in [:workspace-data :pages-index page-id :options :comment-threads-position] assoc id position) + (cond-> open? + (update :comments-local assoc :open id)) + (update :comments-local assoc :options nil) + (update :comments-local dissoc :draft) + (update :workspace-drawing dissoc :comment) + (update-in [:comments id] assoc (:id comment) comment)))) - ptk/WatchEvent - (watch [_ _ _] - (rx/of (ptk/data-event ::ev/event - {::ev/name "create-comment-thread" - ::ev/origin "workspace" - :id id - :content-size (count (:content comment))}))))) + ptk/WatchEvent + (watch [_ _ _] + (rx/of (ptk/data-event ::ev/event + {::ev/name "create-comment-thread" + ::ev/origin "workspace" + :id id + :content-size (count (:content comment))})))))) @@ -89,24 +92,27 @@ [:content :string]]) (defn create-thread-on-workspace - [params] - (dm/assert! (sm/check! schema:create-thread-on-workspace params)) + ([params] + (create-thread-on-workspace params identity true)) + ([params on-thread-created open?] + (dm/assert! (sm/check! schema:create-thread-on-workspace params)) - (ptk/reify ::create-thread-on-workspace - ptk/WatchEvent - (watch [_ state _] - (let [page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) - frame-id (ctst/get-frame-id-by-position objects (:position params)) - params (assoc params :frame-id frame-id)] - (->> (rp/cmd! :create-comment-thread params) - (rx/mapcat #(rp/cmd! :get-comment-thread {:file-id (:file-id %) :id (:id %)})) - (rx/map created-thread-on-workspace) - (rx/catch (fn [{:keys [type code] :as cause}] - (if (and (= type :restriction) - (= code :max-quote-reached)) - (rx/throw cause) - (rx/throw {:type :comment-error}))))))))) + (ptk/reify ::create-thread-on-workspace + ptk/WatchEvent + (watch [_ state _] + (let [page-id (:current-page-id state) + objects (wsh/lookup-page-objects state page-id) + frame-id (ctst/get-frame-id-by-position objects (:position params)) + params (assoc params :frame-id frame-id)] + (->> (rp/cmd! :create-comment-thread params) + (rx/mapcat #(rp/cmd! :get-comment-thread {:file-id (:file-id %) :id (:id %)})) + (rx/tap on-thread-created) + (rx/map #(created-thread-on-workspace % open?)) + (rx/catch (fn [{:keys [type code] :as cause}] + (if (and (= type :restriction) + (= code :max-quote-reached)) + (rx/throw cause) + (rx/throw {:type :comment-error})))))))))) (defn created-thread-on-viewer [{:keys [id comment page-id] :as thread}] @@ -257,29 +263,31 @@ (rx/map #(retrieve-comment-threads file-id))))))) (defn delete-comment-thread-on-workspace - [{:keys [id] :as thread}] - (dm/assert! - "expected valid comment thread" - (check-comment-thread! thread)) - (ptk/reify ::delete-comment-thread-on-workspace - ptk/UpdateEvent - (update [_ state] - (let [page-id (:current-page-id state)] - (-> state - (update-in [:workspace-data :pages-index page-id :options :comment-threads-position] dissoc id) - (update :comments dissoc id) - (update :comment-threads dissoc id)))) + ([params] + (delete-comment-thread-on-workspace params identity)) + ([{:keys [id] :as thread} on-delete] + (dm/assert! (uuid? id)) + + (ptk/reify ::delete-comment-thread-on-workspace + ptk/UpdateEvent + (update [_ state] + (let [page-id (:current-page-id state)] + (-> state + (update-in [:workspace-data :pages-index page-id :options :comment-threads-position] dissoc id) + (update :comments dissoc id) + (update :comment-threads dissoc id)))) - ptk/WatchEvent - (watch [_ _ _] - (rx/concat - (->> (rp/cmd! :delete-comment-thread {:id id}) - (rx/catch #(rx/throw {:type :comment-error})) - (rx/ignore)) - (rx/of (ptk/data-event ::ev/event - {::ev/name "delete-comment-thread" - ::ev/origin "workspace" - :id id})))))) + ptk/WatchEvent + (watch [_ _ _] + (rx/concat + (->> (rp/cmd! :delete-comment-thread {:id id}) + (rx/catch #(rx/throw {:type :comment-error})) + (rx/tap on-delete) + (rx/ignore)) + (rx/of (ptk/data-event ::ev/event + {::ev/name "delete-comment-thread" + ::ev/origin "workspace" + :id id}))))))) (defn delete-comment-thread-on-viewer [{:keys [id] :as thread}] diff --git a/frontend/src/app/plugins/api.cljs b/frontend/src/app/plugins/api.cljs index a7f52d164..d53a53d8e 100644 --- a/frontend/src/app/plugins/api.cljs +++ b/frontend/src/app/plugins/api.cljs @@ -368,7 +368,33 @@ (openPage [_ page] (let [id (obj/get page "$id")] - (st/emit! (dw/go-to-page id))))) + (st/emit! (dw/go-to-page id)))) + + (alignHorizontal + [_ _shapes _direction] + ;; TODO + ) + + (alignVertical + [_ _shapes _direction] + ;; TODO + ) + + (distributeHorizontal + [_ _shapes] + ;; TODO + ) + + (distributeVertical + [_ _shapes] + ;; TODO + ) + + (flatten + [_ _shapes] + ;; TODO + ) + ) (defn create-context [plugin-id] diff --git a/frontend/src/app/plugins/comments.cljs b/frontend/src/app/plugins/comments.cljs new file mode 100644 index 000000000..0e99af204 --- /dev/null +++ b/frontend/src/app/plugins/comments.cljs @@ -0,0 +1,164 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.plugins.comments + (:require + [app.common.geom.point :as gpt] + [app.common.record :as crc] + [app.common.spec :as us] + [app.main.data.comments :as dc] + [app.main.data.workspace.comments :as dwc] + [app.main.repo :as rp] + [app.main.store :as st] + [app.plugins.format :as format] + [app.plugins.parser :as parser] + [app.plugins.register :as r] + [app.plugins.shape :as shape] + [app.plugins.user :as user] + [app.plugins.utils :as u] + [beicon.v2.core :as rx] + [promesa.core :as p])) + +(deftype CommentProxy [$plugin $file $page $thread $id] + Object + (remove [_] + (p/create + (fn [resolve reject] + (->> (rp/cmd! :delete-comment {:id $id}) + (rx/tap #(st/emit! (dc/retrieve-comment-threads $file))) + (rx/subs! #(resolve) reject)))))) + +(defn comment-proxy? [p] + (instance? CommentProxy p)) + +(defn comment-proxy + [plugin-id file-id page-id thread-id users data] + (let [data* (atom data)] + (crc/add-properties! + (CommentProxy. plugin-id file-id page-id thread-id (:id data)) + {:name "$plugin" :enumerable false :get (constantly plugin-id)} + {:name "$file" :enumerable false :get (constantly file-id)} + {:name "$page" :enumerable false :get (constantly page-id)} + {:name "$thread" :enumerable false :get (constantly thread-id)} + {:name "$id" :enumerable false :get (constantly (:id data))} + + {:name "user" :get (fn [_] (user/user-proxy plugin-id (get users (:owner-id data))))} + {:name "date" :get (fn [_] (:created-at data))} + + {:name "content" + :get (fn [_] (:content @data*)) + :set + (fn [_ content] + (let [profile (:profile @st/state)] + (cond + (or (not (string? content)) (empty? content)) + (u/display-not-valid :content "Not valid") + + (not= (:id profile) (:owner-id data)) + (u/display-not-valid :content "Cannot change content from another user's comments") + + (not (r/check-permission plugin-id "content:write")) + (u/display-not-valid :content "Plugin doesn't have 'content:write' permission") + + :else + (->> (rp/cmd! :update-comment {:id (:id data) :content content}) + (rx/tap #(st/emit! (dc/retrieve-comment-threads file-id))) + (rx/subs! #(swap! data* assoc :content content))))))}))) + +(deftype CommentThreadProxy [$plugin $file $page $users $id owner] + Object + (findComments + [_] + (p/create + (fn [resolve reject] + (->> (rp/cmd! :get-comments {:thread-id $id}) + (rx/subs! + (fn [comments] + (resolve + (format/format-array + #(comment-proxy $plugin $file $page $id $users %) comments))) + reject))))) + + (reply + [_ content] + (cond + (not (r/check-permission $plugin "content:write")) + (u/display-not-valid :content "Plugin doesn't have 'content:write' permission") + + (or (not (string? content)) (empty? content)) + (u/display-not-valid :content "Not valid") + + :else + (p/create + (fn [resolve reject] + (->> (rp/cmd! :create-comment {:thread-id $id :content content}) + (rx/subs! #(resolve (comment-proxy $plugin $file $page $id $users %)) reject)))))) + + (remove [_] + (let [profile (:profile @st/state)] + (cond + (not (r/check-permission $plugin "content:write")) + (u/display-not-valid :removeCommentThread "Plugin doesn't have 'content:write' permission") + + (not= (:id profile) owner) + (u/display-not-valid :content "Cannot change content from another user's comments") + + :else + (p/create + (fn [resolve] + (p/create + (st/emit! (dc/delete-comment-thread-on-workspace {:id $id} #(resolve)))))))))) + +(defn comment-thread-proxy? [p] + (instance? CommentThreadProxy p)) + +(defn comment-thread-proxy + [plugin-id file-id page-id users data] + (let [data* (atom data)] + (crc/add-properties! + (CommentThreadProxy. plugin-id file-id page-id users (:id data) (:owner-id data)) + {:name "$plugin" :enumerable false :get (constantly plugin-id)} + {:name "$file" :enumerable false :get (constantly file-id)} + {:name "$page" :enumerable false :get (constantly page-id)} + {:name "$id" :enumerable false :get (constantly (:id data))} + {:name "$users" :enumerable false :get (constantly users)} + {:name "page" :enumerable false :get (fn [_] (u/locate-page file-id page-id))} + + {:name "seqNumber" :get (fn [_] (:seqn data))} + {:name "owner" :get (fn [_] (user/user-proxy plugin-id (get users (:owner-id data))))} + {:name "board" :get (fn [_] (shape/shape-proxy plugin-id file-id page-id (:frame-id data)))} + + {:name "position" + :get (fn [_] (format/format-point (:position @data*))) + :set + (fn [_ position] + (let [position (parser/parse-point position)] + (cond + (or (not (us/safe-number? (:x position))) (not (us/safe-number? (:y position)))) + (u/display-not-valid :position "Not valid point") + + (not (r/check-permission plugin-id "content:write")) + (u/display-not-valid :content "Plugin doesn't have 'content:write' permission") + + :else + (do (st/emit! (dwc/update-comment-thread-position @data* [(:x position) (:y position)])) + (swap! data* assoc :position (gpt/point position))))))} + + {:name "resolved" + :get (fn [_] (:is-resolved @data*)) + :set + (fn [_ is-resolved] + (cond + (not (boolean? is-resolved)) + (u/display-not-valid :resolved "Not a boolean type") + + (not (r/check-permission plugin-id "content:write")) + (u/display-not-valid :resolved "Plugin doesn't have 'content:write' permission") + + :else + (do (st/emit! (dc/update-comment-thread (assoc @data* :is-resolved is-resolved))) + (swap! data* assoc :is-resolved is-resolved))))}))) + diff --git a/frontend/src/app/plugins/page.cljs b/frontend/src/app/plugins/page.cljs index c62a2cece..defb243d4 100644 --- a/frontend/src/app/plugins/page.cljs +++ b/frontend/src/app/plugins/page.cljs @@ -10,13 +10,17 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.files.helpers :as cfh] + [app.common.geom.point :as gpt] [app.common.record :as crc] [app.common.spec :as us] [app.common.uuid :as uuid] + [app.main.data.comments :as dc] [app.main.data.workspace :as dw] [app.main.data.workspace.guides :as dwgu] [app.main.data.workspace.interactions :as dwi] + [app.main.repo :as rp] [app.main.store :as st] + [app.plugins.comments :as pc] [app.plugins.format :as format] [app.plugins.parser :as parser] [app.plugins.register :as r] @@ -24,7 +28,9 @@ [app.plugins.shape :as shape] [app.plugins.utils :as u] [app.util.object :as obj] - [cuerdas.core :as str])) + [beicon.v2.core :as rx] + [cuerdas.core :as str] + [promesa.core :as p])) (deftype FlowProxy [$plugin $file $page $id] Object @@ -78,8 +84,10 @@ (u/display-not-valid :getShapeById shape-id) :else - (let [shape-id (uuid/uuid shape-id)] - (shape/shape-proxy $plugin $file $id shape-id)))) + (let [shape-id (uuid/uuid shape-id) + shape (u/locate-shape $file $id shape-id)] + (when (some? shape) + (shape/shape-proxy $plugin $file $id shape-id))))) (getRoot [_] @@ -215,7 +223,7 @@ (st/emit! (dwi/remove-flow $id (obj/get flow "$id"))))) (addRulerGuide - [self orientation value board] + [_ orientation value board] (let [shape (u/proxy->shape board)] (cond (not (us/safe-number? value)) @@ -224,14 +232,14 @@ (not (contains? #{"vertical" "horizontal"} orientation)) (u/display-not-valid :addRulerGuide "Orientation should be either 'vertical' or 'horizontal'") - (or (not (shape/shape-proxy? shape)) + (or (not (shape/shape-proxy? board)) (not (cfh/frame-shape? shape))) (u/display-not-valid :addRulerGuide "The shape is not a board") (not (r/check-permission $plugin "content:write")) (u/display-not-valid :addRulerGuide "Plugin doesn't have 'content:write' permission") - :ellse + :else (let [id (uuid/next)] (st/emit! (dwgu/update-guides @@ -253,7 +261,82 @@ :else (let [guide (u/proxy->ruler-guide value)] - (st/emit! (dwgu/remove-guide guide)))))) + (st/emit! (dwgu/remove-guide guide))))) + + (addCommentThread + [_ content position board] + (let [shape (when board (u/proxy->shape board))] + (cond + (or (not (string? content)) (empty? content)) + (u/display-not-valid :addCommentThread "Content not valid") + + (or (not (us/safe-number? (:x position))) (not (us/safe-number? (:y position)))) + (u/display-not-valid :addCommentThread "Position not valid") + + (and (some? board) (or (not (shape/shape-proxy? board)) (not (cfh/frame-shape? shape)))) + (u/display-not-valid :addCommentThread "Board not valid") + + (not (r/check-permission $plugin "content:write")) + (u/display-not-valid :addCommentThread "Plugin doesn't have 'content:write' permission") + + :else + (p/create + (fn [resolve] + (st/emit! + (dc/create-thread-on-workspace + {:file-id $file + :page-id $id + :position (gpt/point (parser/parse-point position)) + :content content} + + (fn [data] + (->> (rp/cmd! :get-team-users {:file-id $file}) + (rx/subs! + (fn [users] + (let [users (d/index-by :id users)] + (resolve (pc/comment-thread-proxy $plugin $file $id users data))))))) + false))))))) + + (removeCommentThread + [_ thread] + (cond + (not (pc/comment-thread-proxy? thread)) + (u/display-not-valid :removeCommentThread "Comment thread not valid") + + (not (r/check-permission $plugin "content:write")) + (u/display-not-valid :removeCommentThread "Plugin doesn't have 'content:write' permission") + + :else + (p/create + (fn [resolve] + (let [thread-id (obj/get thread "$id")] + (p/create + (st/emit! (dc/delete-comment-thread-on-workspace {:id thread-id} #(resolve))))))))) + + (findCommentThreads + [_ criteria] + (let [only-yours (boolean (obj/get criteria "onlyYours" false)) + show-resolved (boolean (obj/get criteria "showResolved" true)) + user-id (-> @st/state :profile :id)] + (p/create + (fn [resolve reject] + (->> (rx/zip (rp/cmd! :get-team-users {:file-id $file}) + (rp/cmd! :get-comment-threads {:file-id $file})) + (rx/take 1) + (rx/subs! + (fn [[users comments]] + (let [users (d/index-by :id users)] + (let [comments + (cond->> comments + (not show-resolved) + (filter (comp not :is-resolved)) + + only-yours + (filter #(contains? (:participants %) user-id)))] + (resolve + (format/format-array + #(pc/comment-thread-proxy $plugin $file $id users %) comments))))) + reject))))))) (crc/define-properties! PageProxy diff --git a/frontend/src/app/plugins/ruler_guides.cljs b/frontend/src/app/plugins/ruler_guides.cljs index 59685374d..d0e8addbf 100644 --- a/frontend/src/app/plugins/ruler_guides.cljs +++ b/frontend/src/app/plugins/ruler_guides.cljs @@ -17,8 +17,8 @@ [app.plugins.utils :as u] [app.util.object :as obj])) -(def shape-proxy) -(def shape-proxy?) +(def shape-proxy identity) +(def shape-proxy? identity) (deftype RulerGuideProxy [$plugin $file $page $id] Object diff --git a/frontend/src/app/plugins/shape.cljs b/frontend/src/app/plugins/shape.cljs index 0616288b3..891888688 100644 --- a/frontend/src/app/plugins/shape.cljs +++ b/frontend/src/app/plugins/shape.cljs @@ -591,7 +591,7 @@ (not (r/check-permission $plugin "content:write")) (u/display-not-valid :addRulerGuide "Plugin doesn't have 'content:write' permission") - :ellse + :else (let [id (uuid/next) axis (parser/orientation->axis orientation) objects (u/locate-objects $file $page) diff --git a/frontend/src/app/plugins/user.cljs b/frontend/src/app/plugins/user.cljs index 220a5e08c..20af5481e 100644 --- a/frontend/src/app/plugins/user.cljs +++ b/frontend/src/app/plugins/user.cljs @@ -12,13 +12,13 @@ [app.plugins.utils :as u] [app.util.object :as obj])) -(deftype CurrentUserProxy [$plugin $session]) -(deftype ActiveUserProxy [$plugin $session]) +(deftype CurrentUserProxy [$plugin]) +(deftype ActiveUserProxy [$plugin]) +(deftype UserProxy [$plugin]) -(defn add-user-properties - [user-proxy] - (let [plugin-id (obj/get user-proxy "$plugin") - session-id (obj/get user-proxy "$session")] +(defn- add-session-properties + [user-proxy session-id] + (let [plugin-id (obj/get user-proxy "$plugin")] (crc/add-properties! user-proxy {:name "$plugin" :enumerable false :get (constantly plugin-id)} @@ -39,21 +39,43 @@ {:name "sessionId" :get (fn [_] (str session-id))}))) + (defn current-user-proxy? [p] (instance? CurrentUserProxy p)) (defn current-user-proxy [plugin-id session-id] - (-> (CurrentUserProxy. plugin-id session-id) - (add-user-properties))) + (-> (CurrentUserProxy. plugin-id) + (add-session-properties session-id))) (defn active-user-proxy? [p] (instance? ActiveUserProxy p)) (defn active-user-proxy [plugin-id session-id] - (-> (ActiveUserProxy. plugin-id session-id) - (add-user-properties) + (-> (ActiveUserProxy. plugin-id) + (add-session-properties session-id) (crc/add-properties! {:name "position" :get (fn [_] (-> (u/locate-presence session-id) :point format/format-point))} {:name "zoom" :get (fn [_] (-> (u/locate-presence session-id) :zoom))}))) + +(defn- add-user-properties + [user-proxy data] + (let [plugin-id (obj/get user-proxy "$plugin")] + (crc/add-properties! + user-proxy + {:name "$plugin" :enumerable false :get (constantly plugin-id)} + + {:name "id" + :get (fn [_] (-> data :id str))} + + {:name "name" + :get (fn [_] (-> data :fullname))} + + {:name "avatarUrl" + :get (fn [_] (cfg/resolve-profile-photo-url data))}))) + +(defn user-proxy + [plugin-id data] + (-> (UserProxy. plugin-id) + (add-user-properties data))) diff --git a/frontend/src/app/plugins/viewport.cljs b/frontend/src/app/plugins/viewport.cljs index 6973f33f7..8f2fb4c32 100644 --- a/frontend/src/app/plugins/viewport.cljs +++ b/frontend/src/app/plugins/viewport.cljs @@ -20,6 +20,14 @@ (deftype ViewportProxy [$plugin] Object + (zoomReset [_] + ;;TODO + ) + + (zoomToFitAll [_] + ;;TODO + ) + (zoomIntoView [_ shapes] (let [ids (->> shapes