From c8102f4bffaf062b14ede85e02a61263fb8ca1e9 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 16 Aug 2021 15:46:02 +0200 Subject: [PATCH] :tada: Share link & pages on viewer. --- backend/src/app/db.clj | 32 +- backend/src/app/http/middleware.clj | 3 + backend/src/app/migrations.clj | 6 + .../sql/0063-add-share-link-table.sql | 12 + backend/src/app/rpc.clj | 1 + backend/src/app/rpc/mutations/share_link.clj | 67 +++ backend/src/app/rpc/permissions.clj | 35 ++ backend/src/app/rpc/queries/files.clj | 19 +- backend/src/app/rpc/queries/viewer.clj | 100 ++++- backend/src/app/tasks/delete_object.clj | 2 +- backend/test/app/services_viewer_test.clj | 83 ++-- .../resources/styles/common/framework.scss | 19 +- frontend/resources/styles/main-default.scss | 1 + .../styles/main/partials/dropdown.scss | 4 +- .../styles/main/partials/share-link.scss | 135 ++++++ .../styles/main/partials/viewer-header.scss | 264 ++++-------- .../main/partials/viewer-thumbnails.scss | 8 +- .../styles/main/partials/zoom-widget.scss | 4 +- frontend/src/app/main/data/common.cljs | 46 +++ frontend/src/app/main/data/viewer.cljs | 209 +++++----- frontend/src/app/main/data/workspace.cljs | 18 +- frontend/src/app/main/refs.cljs | 14 +- frontend/src/app/main/ui.cljs | 37 +- frontend/src/app/main/ui/handoff.cljs | 133 ------ frontend/src/app/main/ui/share_link.cljs | 232 +++++++++++ frontend/src/app/main/ui/viewer.cljs | 349 +++++----------- frontend/src/app/main/ui/viewer/comments.cljs | 158 +++++++ frontend/src/app/main/ui/viewer/handoff.cljs | 68 +++ .../ui/{ => viewer}/handoff/attributes.cljs | 20 +- .../{ => viewer}/handoff/attributes/blur.cljs | 2 +- .../handoff/attributes/common.cljs | 2 +- .../{ => viewer}/handoff/attributes/fill.cljs | 4 +- .../handoff/attributes/image.cljs | 2 +- .../handoff/attributes/layout.cljs | 2 +- .../handoff/attributes/shadow.cljs | 4 +- .../handoff/attributes/stroke.cljs | 4 +- .../{ => viewer}/handoff/attributes/svg.cljs | 2 +- .../{ => viewer}/handoff/attributes/text.cljs | 4 +- .../main/ui/{ => viewer}/handoff/code.cljs | 2 +- .../main/ui/{ => viewer}/handoff/exports.cljs | 2 +- .../ui/{ => viewer}/handoff/left_sidebar.cljs | 20 +- .../main/ui/{ => viewer}/handoff/render.cljs | 84 ++-- .../{ => viewer}/handoff/right_sidebar.cljs | 47 +-- .../handoff/selection_feedback.cljs | 57 +-- frontend/src/app/main/ui/viewer/header.cljs | 391 ++++++------------ .../src/app/main/ui/viewer/interactions.cljs | 136 ++++++ .../src/app/main/ui/viewer/thumbnails.cljs | 68 ++- .../src/app/main/ui/workspace/header.cljs | 15 +- frontend/src/app/util/router.cljs | 20 +- frontend/translations/de.po | 2 +- frontend/translations/el.po | 2 +- frontend/translations/en.po | 60 ++- frontend/translations/es.po | 61 ++- frontend/translations/fr.po | 2 +- frontend/translations/ro.po | 2 +- frontend/translations/ru.po | 2 +- frontend/translations/tr.po | 2 +- frontend/translations/zh_CN.po | 2 +- 58 files changed, 1837 insertions(+), 1245 deletions(-) create mode 100644 backend/src/app/migrations/sql/0063-add-share-link-table.sql create mode 100644 backend/src/app/rpc/mutations/share_link.clj create mode 100644 frontend/resources/styles/main/partials/share-link.scss create mode 100644 frontend/src/app/main/data/common.cljs delete mode 100644 frontend/src/app/main/ui/handoff.cljs create mode 100644 frontend/src/app/main/ui/share_link.cljs create mode 100644 frontend/src/app/main/ui/viewer/comments.cljs create mode 100644 frontend/src/app/main/ui/viewer/handoff.cljs rename frontend/src/app/main/ui/{ => viewer}/handoff/attributes.cljs (70%) rename frontend/src/app/main/ui/{ => viewer}/handoff/attributes/blur.cljs (96%) rename frontend/src/app/main/ui/{ => viewer}/handoff/attributes/common.cljs (98%) rename frontend/src/app/main/ui/{ => viewer}/handoff/attributes/fill.cljs (93%) rename frontend/src/app/main/ui/{ => viewer}/handoff/attributes/image.cljs (97%) rename frontend/src/app/main/ui/{ => viewer}/handoff/attributes/layout.cljs (98%) rename frontend/src/app/main/ui/{ => viewer}/handoff/attributes/shadow.cljs (95%) rename frontend/src/app/main/ui/{ => viewer}/handoff/attributes/stroke.cljs (96%) rename frontend/src/app/main/ui/{ => viewer}/handoff/attributes/svg.cljs (97%) rename frontend/src/app/main/ui/{ => viewer}/handoff/attributes/text.cljs (98%) rename frontend/src/app/main/ui/{ => viewer}/handoff/code.cljs (98%) rename frontend/src/app/main/ui/{ => viewer}/handoff/exports.cljs (99%) rename frontend/src/app/main/ui/{ => viewer}/handoff/left_sidebar.cljs (89%) rename frontend/src/app/main/ui/{ => viewer}/handoff/render.cljs (75%) rename frontend/src/app/main/ui/{ => viewer}/handoff/right_sidebar.cljs (53%) rename frontend/src/app/main/ui/{ => viewer}/handoff/selection_feedback.cljs (55%) create mode 100644 frontend/src/app/main/ui/viewer/interactions.cljs diff --git a/backend/src/app/db.clj b/backend/src/app/db.clj index 6e2e086bb..dd23992f7 100644 --- a/backend/src/app/db.clj +++ b/backend/src/app/db.clj @@ -231,9 +231,9 @@ (defn get-by-params ([ds table params] (get-by-params ds table params nil)) - ([ds table params {:keys [uncheked] :or {uncheked false} :as opts}] + ([ds table params {:keys [check-not-found] :or {check-not-found true} :as opts}] (let [res (exec-one! ds (sql/select table params opts))] - (when (and (not uncheked) (or (not res) (is-deleted? res))) + (when (and check-not-found (or (not res) (is-deleted? res))) (ex/raise :type :not-found :table table :hint "database object not found")) @@ -267,13 +267,28 @@ (instance? PGpoint v)) (defn pgarray? - [v] - (instance? PgArray v)) + ([v] (instance? PgArray v)) + ([v type] + (and (instance? PgArray v) + (= type (.getBaseTypeName ^PgArray v))))) (defn pgarray-of-uuid? [v] (and (pgarray? v) (= "uuid" (.getBaseTypeName ^PgArray v)))) +(defn decode-pgarray + ([v] (into [] (.getArray ^PgArray v))) + ([v in] (into in (.getArray ^PgArray v))) + ([v in xf] (into in xf (.getArray ^PgArray v)))) + +(defn pgarray->set + [v] + (set (.getArray ^PgArray v))) + +(defn pgarray->vector + [v] + (vec (.getArray ^PgArray v))) + (defn pgpoint [p] (PGpoint. (:x p) (:y p))) @@ -369,15 +384,6 @@ (.setType "jsonb") (.setValue (json/encode-str data)))) -(defn pgarray->set - [v] - (set (.getArray ^PgArray v))) - -(defn pgarray->vector - [v] - (vec (.getArray ^PgArray v))) - - ;; --- Locks (defn- xact-check-param diff --git a/backend/src/app/http/middleware.clj b/backend/src/app/http/middleware.clj index 42babf3dc..4a9929ec0 100644 --- a/backend/src/app/http/middleware.clj +++ b/backend/src/app/http/middleware.clj @@ -81,6 +81,9 @@ (try (let [tw (t/writer output-stream opts)] (t/write! tw data)) + (catch Throwable e + (l/error :hint "exception on writting data to response" + :cause e)) (finally (.close ^java.io.OutputStream output-stream)))))) diff --git a/backend/src/app/migrations.clj b/backend/src/app/migrations.clj index c0f62cf3f..831213383 100644 --- a/backend/src/app/migrations.clj +++ b/backend/src/app/migrations.clj @@ -193,6 +193,12 @@ {:name "0061-mod-file-table" :fn (mg/resource "app/migrations/sql/0061-mod-file-table.sql")} + + {:name "0062-fix-metadata-media" + :fn (mg/resource "app/migrations/sql/0062-fix-metadata-media.sql")} + + {:name "0063-add-share-link-table" + :fn (mg/resource "app/migrations/sql/0063-add-share-link-table.sql")} ]) diff --git a/backend/src/app/migrations/sql/0063-add-share-link-table.sql b/backend/src/app/migrations/sql/0063-add-share-link-table.sql new file mode 100644 index 000000000..6c2790045 --- /dev/null +++ b/backend/src/app/migrations/sql/0063-add-share-link-table.sql @@ -0,0 +1,12 @@ +CREATE TABLE share_link ( + id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + file_id uuid NOT NULL REFERENCES file(id) ON DELETE CASCADE DEFERRABLE, + owner_id uuid NULL REFERENCES profile(id) ON DELETE SET NULL DEFERRABLE, + created_at timestamptz NOT NULL DEFAULT clock_timestamp(), + + pages uuid[], + flags text[] +); + +CREATE INDEX share_link_file_id_idx ON share_link(file_id); +CREATE INDEX share_link_owner_id_idx ON share_link(owner_id); diff --git a/backend/src/app/rpc.clj b/backend/src/app/rpc.clj index 09a2a636a..eae7ef33c 100644 --- a/backend/src/app/rpc.clj +++ b/backend/src/app/rpc.clj @@ -175,6 +175,7 @@ 'app.rpc.mutations.management 'app.rpc.mutations.ldap 'app.rpc.mutations.fonts + 'app.rpc.mutations.share-link 'app.rpc.mutations.verify-token) (map (partial process-method cfg)) (into {})))) diff --git a/backend/src/app/rpc/mutations/share_link.clj b/backend/src/app/rpc/mutations/share_link.clj new file mode 100644 index 000000000..0e366957f --- /dev/null +++ b/backend/src/app/rpc/mutations/share_link.clj @@ -0,0 +1,67 @@ +;; 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) UXBOX Labs SL + +(ns app.rpc.mutations.share-link + "Share link related rpc mutation methods." + (:require + [app.common.spec :as us] + [app.common.uuid :as uuid] + [app.db :as db] + [app.rpc.queries.files :as files] + [app.util.services :as sv] + [clojure.spec.alpha :as s])) + +;; --- Helpers & Specs + +(s/def ::id ::us/uuid) +(s/def ::profile-id ::us/uuid) +(s/def ::file-id ::us/uuid) +(s/def ::flags (s/every ::us/string :kind set?)) +(s/def ::pages (s/every ::us/uuid :kind set?)) + +;; --- Mutation: Create Share Link + +(declare create-share-link) + +(s/def ::create-share-link + (s/keys :req-un [::profile-id ::file-id ::flags] + :opt-un [::pages])) + +(sv/defmethod ::create-share-link + [{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}] + (db/with-atomic [conn pool] + (files/check-edition-permissions! conn profile-id file-id) + (create-share-link conn params))) + +(defn create-share-link + [conn {:keys [profile-id file-id pages flags]}] + (let [pages (db/create-array conn "uuid" pages) + flags (->> (map name flags) + (db/create-array conn "text")) + slink (db/insert! conn :share-link + {:id (uuid/next) + :file-id file-id + :flags flags + :pages pages + :owner-id profile-id})] + (-> slink + (update :pages db/decode-pgarray #{}) + (update :flags db/decode-pgarray #{})))) + +;; --- Mutation: Delete Share Link + +(declare delete-share-link) + +(s/def ::delete-share-link + (s/keys :req-un [::profile-id ::id])) + +(sv/defmethod ::delete-share-link + [{:keys [pool] :as cfg} {:keys [profile-id id] :as params}] + (db/with-atomic [conn pool] + (let [slink (db/get-by-id conn :share-link id)] + (files/check-edition-permissions! conn profile-id (:file-id slink)) + (db/delete! conn :share-link {:id id}) + nil))) diff --git a/backend/src/app/rpc/permissions.clj b/backend/src/app/rpc/permissions.clj index 221b88363..9481bcd57 100644 --- a/backend/src/app/rpc/permissions.clj +++ b/backend/src/app/rpc/permissions.clj @@ -37,6 +37,41 @@ :is-admin false :can-edit false))) +(defn make-edition-predicate-fn + "A simple factory for edition permission predicate functions." + [qfn] + (us/assert fn? qfn) + (fn [& args] + (let [rows (apply qfn args)] + (when-not (or (empty? rows) + (not (or (some :can-edit rows) + (some :is-admin rows) + (some :is-owner rows)))) + rows)))) + +(defn make-read-predicate-fn + "A simple factory for read permission predicate functions." + [qfn] + (us/assert fn? qfn) + (fn [& args] + (let [rows (apply qfn args)] + (when (seq rows) + rows)))) + +(defn make-check-fn + "Helper that converts a predicate permission function to a check + function (function that raises an exception)." + [pred] + (fn [& args] + (when-not (seq (apply pred args)) + (ex/raise :type :not-found + :code :object-not-found + :hint "not found")))) + + +;; TODO: the following functions are deprecated and replaced with the +;; new ones. Should not be used. + (defn make-edition-check-fn "A simple factory for edition permission check functions." [qfn] diff --git a/backend/src/app/rpc/queries/files.clj b/backend/src/app/rpc/queries/files.clj index 55f536d5f..032032216 100644 --- a/backend/src/app/rpc/queries/files.clj +++ b/backend/src/app/rpc/queries/files.clj @@ -61,16 +61,23 @@ (defn- retrieve-file-permissions [conn profile-id file-id] - (db/exec! conn [sql:file-permissions - file-id profile-id - file-id profile-id - file-id profile-id])) + (when (and profile-id file-id) + (db/exec! conn [sql:file-permissions + file-id profile-id + file-id profile-id + file-id profile-id]))) + +(def has-edit-permissions? + (perms/make-edition-predicate-fn retrieve-file-permissions)) + +(def has-read-permissions? + (perms/make-read-predicate-fn retrieve-file-permissions)) (def check-edition-permissions! - (perms/make-edition-check-fn retrieve-file-permissions)) + (perms/make-check-fn has-edit-permissions?)) (def check-read-permissions! - (perms/make-read-check-fn retrieve-file-permissions)) + (perms/make-check-fn has-read-permissions?)) ;; --- Query: Files search diff --git a/backend/src/app/rpc/queries/viewer.clj b/backend/src/app/rpc/queries/viewer.clj index dfe95314c..bddae363c 100644 --- a/backend/src/app/rpc/queries/viewer.clj +++ b/backend/src/app/rpc/queries/viewer.clj @@ -14,24 +14,97 @@ [app.util.services :as sv] [clojure.spec.alpha :as s])) +;; --- Query: View Only Bundle + +(defn- decode-share-link-row + [row] + (-> row + (update :flags db/decode-pgarray #{}) + (update :pages db/decode-pgarray #{}))) + +(defn- retrieve-project + [conn id] + (db/get-by-id conn :project id {:columns [:id :name :team-id]})) + +(defn- retrieve-share-link + [{:keys [conn]} file-id id] + (some-> (db/get-by-params conn :share-link + {:id id :file-id file-id} + {:check-not-found false}) + (decode-share-link-row))) + +(defn- retrieve-bundle + [{:keys [conn] :as cfg} file-id] + (let [file (files/retrieve-file cfg file-id) + project (retrieve-project conn (:project-id file)) + libs (files/retrieve-file-libraries cfg false file-id) + users (teams/retrieve-users conn (:team-id project)) + + links (->> (db/query conn :share-link {:file-id file-id}) + (mapv decode-share-link-row)) + + fonts (db/query conn :team-font-variant + {:team-id (:team-id project) + :deleted-at nil})] + {:file file + :users users + :fonts fonts + :project project + :share-links links + :libraries libs})) + +(s/def ::file-id ::us/uuid) +(s/def ::profile-id ::us/uuid) +(s/def ::share-id ::us/uuid) + +(s/def ::view-only-bundle + (s/keys :req-un [::file-id] :opt-un [::profile-id ::share-id])) + +(sv/defmethod ::view-only-bundle {:auth false} + [{:keys [pool] :as cfg} {:keys [profile-id file-id share-id] :as params}] + (db/with-atomic [conn pool] + (let [cfg (assoc cfg :conn conn) + bundle (retrieve-bundle cfg file-id) + slink (retrieve-share-link cfg file-id share-id)] + + ;; When we have neither profile nor share, we just return a not + ;; found response to the user. + (when (and (not profile-id) + (not slink)) + (ex/raise :type :not-found + :code :object-not-found)) + + ;; When we have only profile, we need to check read permissiones + ;; on file. + (when (and profile-id (not slink)) + (files/check-read-permissions! conn profile-id file-id)) + + (cond-> bundle + ;; If we have current profile, put + (some? profile-id) + (as-> $ (let [edit? (boolean (files/has-edit-permissions? conn profile-id file-id)) + read? (boolean (files/has-read-permissions? conn profile-id file-id))] + (-> (assoc $ :permissions {:read read? :edit edit?}) + (cond-> (not edit?) (dissoc :share-links))))) + + (some? slink) + (assoc :share slink) + + (not (contains? (:flags slink) "view-all-pages")) + (update-in [:file :data] (fn [data] + (let [allowed-pages (:pages slink)] + (-> data + (update :pages (fn [pages] (filterv #(contains? allowed-pages %) pages))) + (update :pages-index (fn [index] (select-keys index allowed-pages))))))))))) + ;; --- Query: Viewer Bundle (by Page ID) +;; DEPRECATED: should be removed in 1.9.x + (declare check-shared-token!) (declare retrieve-shared-token) -(def ^:private - sql:project - "select p.id, p.name, p.team_id - from project as p - where p.id = ? - and p.deleted_at is null") - -(defn- retrieve-project - [conn id] - (db/exec-one! conn [sql:project id])) - (s/def ::id ::us/uuid) -(s/def ::file-id ::us/uuid) (s/def ::page-id ::us/uuid) (s/def ::token ::us/string) @@ -81,6 +154,3 @@ [conn file-id page-id] (let [sql "select * from file_share_token where file_id=? and page_id=?"] (db/exec-one! conn [sql file-id page-id]))) - - - diff --git a/backend/src/app/tasks/delete_object.clj b/backend/src/app/tasks/delete_object.clj index 2a30a6324..b1b3e701b 100644 --- a/backend/src/app/tasks/delete_object.clj +++ b/backend/src/app/tasks/delete_object.clj @@ -60,7 +60,7 @@ (defmethod handle-deletion :team-font-variant [{:keys [conn storage]} {:keys [id] :as props}] - (let [font (db/get-by-id conn :team-font-variant id {:uncheked true}) + (let [font (db/get-by-id conn :team-font-variant id {:check-not-found false}) storage (assoc storage :conn conn)] (when (:deleted-at font) (db/delete! conn :team-font-variant {:id id}) diff --git a/backend/test/app/services_viewer_test.clj b/backend/test/app/services_viewer_test.clj index d3176f81a..1b9d7c013 100644 --- a/backend/test/app/services_viewer_test.clj +++ b/backend/test/app/services_viewer_test.clj @@ -16,18 +16,18 @@ (t/use-fixtures :each th/database-reset) (t/deftest retrieve-bundle - (let [prof (th/create-profile* 1 {:is-active true}) - prof2 (th/create-profile* 2 {:is-active true}) - team-id (:default-team-id prof) - proj-id (:default-project-id prof) + (let [prof (th/create-profile* 1 {:is-active true}) + prof2 (th/create-profile* 2 {:is-active true}) + team-id (:default-team-id prof) + proj-id (:default-project-id prof) - file (th/create-file* 1 {:profile-id (:id prof) - :project-id proj-id - :is-shared false}) - token (atom nil)] + file (th/create-file* 1 {:profile-id (:id prof) + :project-id proj-id + :is-shared false}) + share-id (atom nil)] (t/testing "authenticated with page-id" - (let [data {::th/type :viewer-bundle + (let [data {::th/type :view-only-bundle :profile-id (:id prof) :file-id (:id file) :page-id (get-in file [:data :pages 0])} @@ -38,64 +38,67 @@ (t/is (nil? (:error out))) (let [result (:result out)] - (t/is (contains? result :token)) - (t/is (contains? result :page)) + (t/is (contains? result :share-links)) + (t/is (contains? result :permissions)) + (t/is (contains? result :libraries)) (t/is (contains? result :file)) (t/is (contains? result :project))))) (t/testing "generate share token" - (let [data {::th/type :create-file-share-token + (let [data {::th/type :create-share-link :profile-id (:id prof) :file-id (:id file) - :page-id (get-in file [:data :pages 0])} + :pages #{(get-in file [:data :pages 0])} + :flags #{}} out (th/mutation! data)] ;; (th/print-result! out) (t/is (nil? (:error out))) (let [result (:result out)] - (t/is (string? (:token result))) - (reset! token (:token result))))) + (t/is (uuid? (:id result))) + (reset! share-id (:id result))))) (t/testing "not authenticated with page-id" - (let [data {::th/type :viewer-bundle + (let [data {::th/type :view-only-bundle :profile-id (:id prof2) :file-id (:id file) :page-id (get-in file [:data :pages 0])} out (th/query! data)] ;; (th/print-result! out) - (let [error (:error out) + (let [error (:error out) error-data (ex-data error)] (t/is (th/ex-info? error)) (t/is (= (:type error-data) :not-found)) (t/is (= (:code error-data) :object-not-found))))) - ;; (t/testing "authenticated with token & profile" - ;; (let [data {::sq/type :viewer-bundle - ;; :profile-id (:id prof2) - ;; :token @token - ;; :file-id (:id file) - ;; :page-id (get-in file [:data :pages 0])} - ;; out (th/try-on! (sq/handle data))] + (t/testing "authenticated with token & profile" + (let [data {::th/type :view-only-bundle + :profile-id (:id prof2) + :share-id @share-id + :file-id (:id file) + :page-id (get-in file [:data :pages 0])} + out (th/query! data)] - ;; ;; (th/print-result! out) + ;; (th/print-result! out) + (t/is (nil? (:error out))) - ;; (let [result (:result out)] - ;; (t/is (contains? result :page)) - ;; (t/is (contains? result :file)) - ;; (t/is (contains? result :project))))) + (let [result (:result out)] + (t/is (contains? result :share)) + (t/is (contains? result :file)) + (t/is (contains? result :project))))) - ;; (t/testing "authenticated with token" - ;; (let [data {::sq/type :viewer-bundle - ;; :token @token - ;; :file-id (:id file) - ;; :page-id (get-in file [:data :pages 0])} - ;; out (th/try-on! (sq/handle data))] + (t/testing "authenticated with token" + (let [data {::th/type :view-only-bundle + :share-id @share-id + :file-id (:id file) + :page-id (get-in file [:data :pages 0])} + out (th/query! data)] - ;; ;; (th/print-result! out) + ;; (th/print-result! out) + (let [result (:result out)] + (t/is (contains? result :file)) + (t/is (contains? result :share)) + (t/is (contains? result :project))))) - ;; (let [result (:result out)] - ;; (t/is (contains? result :page)) - ;; (t/is (contains? result :file)) - ;; (t/is (contains? result :project))))) )) diff --git a/frontend/resources/styles/common/framework.scss b/frontend/resources/styles/common/framework.scss index 6e0731217..3e76b8e62 100644 --- a/frontend/resources/styles/common/framework.scss +++ b/frontend/resources/styles/common/framework.scss @@ -131,6 +131,24 @@ } } +.btn-text-dark { + @extend %btn; + background: $color-gray-60; + color: $color-gray-20; + + svg { + fill: $color-gray-20; + } + &:hover { + background: $color-primary; + color: $color-gray-60; + svg { + fill: $color-gray-60; + } + } +} + + .btn-gray { @extend %btn; background: $color-gray-30; @@ -588,7 +606,6 @@ input.element-name { box-sizing: border-box; flex-shrink: 0; } - } &.column { diff --git a/frontend/resources/styles/main-default.scss b/frontend/resources/styles/main-default.scss index 27c7c3610..5801d9c0b 100644 --- a/frontend/resources/styles/main-default.scss +++ b/frontend/resources/styles/main-default.scss @@ -88,3 +88,4 @@ @import "main/partials/color-bullet"; @import "main/partials/handoff"; @import "main/partials/exception-page"; +@import "main/partials/share-link"; diff --git a/frontend/resources/styles/main/partials/dropdown.scss b/frontend/resources/styles/main/partials/dropdown.scss index 0802e04d2..27500670a 100644 --- a/frontend/resources/styles/main/partials/dropdown.scss +++ b/frontend/resources/styles/main/partials/dropdown.scss @@ -53,8 +53,8 @@ .icon { display: flex; align-items: center; - width: 25px; - height: 25px; + width: 20px; + height: 20px; margin-right: 7px; } } diff --git a/frontend/resources/styles/main/partials/share-link.scss b/frontend/resources/styles/main/partials/share-link.scss new file mode 100644 index 000000000..a632d8509 --- /dev/null +++ b/frontend/resources/styles/main/partials/share-link.scss @@ -0,0 +1,135 @@ +.share-link-dialog { + width: 475px; + + .modal-footer { + display: flex; + align-items: center; + justify-content: flex-end; + height: unset; + padding: 16px 26px; + + .btn-primary, + .btn-secondary, + .btn-warning { + width: 126px; + margin-bottom: 0px; + + &:not(:last-child) { + margin-right: 10px; + } + } + + .confirm-dialog { + display: flex; + flex-direction: column; + background-color: unset; + + .description { + font-size: $fs14; + + margin-bottom: 16px; + + } + .actions { + display: flex; + justify-content: flex-end; + } + } + } + + + .modal-content { + padding: 26px; + + .title { + display: flex; + justify-content: space-between; + + h2 { + font-size: $fs18; + color: $color-black; + } + + .modal-close-button { + margin-right: 0px; + } + } + + + .share-link-section { + margin-top: 12px; + label { + font-size: $fs11; + color: $color-black; + } + + .hint { + padding-top: 10px; + font-size: $fs11; + } + + .help-icon { + cursor: pointer; + } + } + + .view-mode, + .access-mode { + display: flex; + flex-direction: column; + + .title { + color: $color-black; + font-weight: 400; + } + + .items { + padding-left: 20px; + display: flex; + + > .input-checkbox, > .input-radio { + display: flex; + user-select: none; + + /* input { */ + /* appearance: checkbox; */ + /* } */ + + label { + display: flex; + align-items: center; + color: $color-black; + + .hint { + margin-left: 5px; + color: $color-gray-30; + } + } + + &.disabled { + label { + color: $color-gray-30; + } + } + } + } + } + + .pages-selection { + padding-left: 20px; + max-height: 200px; + overflow-y: scroll; + user-select: none; + + label { + color: $color-black; + } + } + + .custom-input { + input { + padding: 0 40px 0 15px; + } + } + } +} diff --git a/frontend/resources/styles/main/partials/viewer-header.scss b/frontend/resources/styles/main/partials/viewer-header.scss index 605a08ec3..7aa002565 100644 --- a/frontend/resources/styles/main/partials/viewer-header.scss +++ b/frontend/resources/styles/main/partials/viewer-header.scss @@ -42,56 +42,64 @@ } } - .view-options { - .icon { - align-items: center; - cursor: pointer; - display: flex; - justify-content: center; + .options-zone { + align-items: center; + display: flex; + // width: 384px; + justify-content: flex-end; + position: relative; - svg { - fill: $color-gray-30; - height: 30px; - width: 28px; - } + > * { + margin-left: $big; + } - &:hover { - > svg { - fill: $color-primary; - } + .btn-primary { + flex-shrink: 0; + } + + .zoom-widget { + .dropdown { + top: 45px; + left: 25px; } } - .dropdown { - min-width: 260px; - left: 0px; - top: 40px; - } - - .view-options-dropdown { + .view-options { align-items: center; cursor: pointer; display: flex; + width: 90px; - span { + > span { color: $color-gray-10; font-size: $fs13; margin-right: $x-small; } - svg { - fill: $color-gray-10; - height: 12px; - width: 12px; - } - } - } + > .icon { + align-items: center; + cursor: pointer; + display: flex; + justify-content: center; - .file-menu { - .dropdown { - min-width: 100px; - right: 0px; - top: 40px; + svg { + fill: $color-gray-10; + height: 12px; + width: 12px; + } + + &:hover { + > svg { + fill: $color-primary; + } + } + } + + .dropdown { + min-width: 260px; + top: 45px; + left: -25px; + } } } @@ -100,39 +108,46 @@ cursor: pointer; display: flex; padding: $x-small; + position: relative; - svg { - fill: $color-gray-20; - height: 20px; - margin-right: $small; - width: 20px; - } + .icon { + display: flex; + justify-content: center; + align-items: center; - span { - color: $color-gray-20; - margin-right: $x-small; - font-size: $fs14; - overflow-x: hidden; - text-overflow: ellipsis; - white-space: nowrap; - - &.frame-name { - color: $color-white; + svg { + fill: $color-gray-20; + height: 12px; + margin-right: $small; + width: 12px; } } - .show-thumbnails-button svg { - fill: $color-white; - height: 10px; - width: 10px; + .breadcrumb { + display: flex; + position: relative; + + > span { + color: $color-gray-20; + margin-right: $x-small; + font-size: $fs14; + overflow-x: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + > .dropdown { + top: 45px; + right: 10px; + } } - .page-name { - color: $color-white; - } - - .counters { - margin-left: $size-3; + .current-frame { + display: flex; + span { + color: $color-white; + margin-right: $x-small; + } } } @@ -166,133 +181,6 @@ } } - .options-zone { - align-items: center; - display: flex; - width: 384px; - justify-content: flex-end; - position: relative; - - > * { - margin-left: $big; - } - - .btn-share { - display: flex; - align-items: center; - justify-content: center; - width: 25px; - height: 25px; - cursor: pointer; - - svg { - fill: $color-gray-20; - width: 20px; - height: 20px; - } - } - - .btn-primary { - flex-shrink: 0; - } - } - - .share-link-dropdown { - background-color: $color-white; - border-radius: $br-small; - box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.25); - display: flex; - flex-direction: column; - left: -135px; - position: absolute; - padding: 1rem; - top: 45px; - width: 400px; - - .share-link-title { - color: $color-black; - font-size: $fs15; - padding-bottom: 1rem; - } - - .share-link-subtitle { - color: $color-gray-40; - padding-bottom: 1rem; - } - - .share-link-buttons { - display: flex; - justify-content: center; - align-items: center; - - .btn-warning, - .btn-primary { - width: 50%; - } - - } - - .share-link-input { - border: 1px solid $color-gray-20; - border-radius: 3px; - display: flex; - height: 40px; - justify-content: space-between; - margin-bottom: 1rem; - padding: 9px $small; - overflow: hidden; - - .link { - &:before { - content: ''; - position: absolute; - width: 50%; - background: linear-gradient(45deg, transparent, #ffffff); - height: 100%; - top: 0; - left: 0; - pointer-events: none; - margin-left: 50%; - } - overflow: hidden; - white-space: nowrap; - position: relative; - color: $color-gray-50; - line-height: 1.5; - user-select: all; - overflow: hidden; - } - - .link-button { - color: $color-primary-dark; - cursor: pointer; - flex-shrink: 0; - font-size: $fs15; - - &:hover { - color: $color-black; - } - } - } - - &:before { - background-color: $color-white; - content: ""; - height: 16px; - left: 53%; - position: absolute; - transform: rotate(45deg); - top: -5px; - width: 16px; - } - - } - - .zoom-dropdown { - left: 180px; - top: 40px; - } - .users-zone { align-items: center; cursor: pointer; diff --git a/frontend/resources/styles/main/partials/viewer-thumbnails.scss b/frontend/resources/styles/main/partials/viewer-thumbnails.scss index d630c8bfc..b60a92847 100644 --- a/frontend/resources/styles/main/partials/viewer-thumbnails.scss +++ b/frontend/resources/styles/main/partials/viewer-thumbnails.scss @@ -1,4 +1,3 @@ - .viewer-thumbnails { grid-row: 1 / span 1; grid-column: 1 / span 1; @@ -9,6 +8,11 @@ flex-direction: column; z-index: 12; + &.invisible { + visibility: hidden; + pointer-events: none; + } + &.expanded { grid-row: 1 / span 2; @@ -159,7 +163,7 @@ &:hover { border-color: $color-primary; - border-width: 2px; + outline: 2px solid $color-primary; } } diff --git a/frontend/resources/styles/main/partials/zoom-widget.scss b/frontend/resources/styles/main/partials/zoom-widget.scss index 4e68d8807..b25192321 100644 --- a/frontend/resources/styles/main/partials/zoom-widget.scss +++ b/frontend/resources/styles/main/partials/zoom-widget.scss @@ -8,13 +8,13 @@ margin-left: $x-small; } - .dropdown-button svg { + .icon svg { fill: $color-gray-10; height: 10px; width: 10px; } - .zoom-dropdown { + .dropdown { position: absolute; z-index: 12; width: 210px; diff --git a/frontend/src/app/main/data/common.cljs b/frontend/src/app/main/data/common.cljs new file mode 100644 index 000000000..f4a302650 --- /dev/null +++ b/frontend/src/app/main/data/common.cljs @@ -0,0 +1,46 @@ +;; 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) UXBOX Labs SL + +(ns app.main.data.common + "A general purpose events." + (:require + [app.main.repo :as rp] + [beicon.core :as rx] + [potok.core :as ptk])) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; SHARE LINK +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn share-link-created + [link] + (ptk/reify ::share-link-created + ptk/UpdateEvent + (update [_ state] + (update state :share-links (fnil conj []) link)))) + +(defn create-share-link + [params] + (ptk/reify ::create-share-link + ptk/WatchEvent + (watch [_ _ _] + (->> (rp/mutation! :create-share-link params) + (rx/map share-link-created))))) + +(defn delete-share-link + [{:keys [id] :as link}] + (ptk/reify ::delete-share-link + ptk/UpdateEvent + (update [_ state] + (update state :share-links + (fn [links] + (filterv #(not= id (:id %)) links)))) + + ptk/WatchEvent + (watch [_ _ _] + (->> (rp/mutation! :delete-share-link {:id id}) + (rx/ignore))))) + diff --git a/frontend/src/app/main/data/viewer.cljs b/frontend/src/app/main/data/viewer.cljs index c6dc46a37..d73b6f478 100644 --- a/frontend/src/app/main/data/viewer.cljs +++ b/frontend/src/app/main/data/viewer.cljs @@ -14,24 +14,12 @@ [app.main.data.comments :as dcm] [app.main.data.fonts :as df] [app.main.repo :as rp] + [app.util.globals :as ug] [app.util.router :as rt] [beicon.core :as rx] [cljs.spec.alpha :as s] [potok.core :as ptk])) -;; --- General Specs - -(s/def ::id ::us/uuid) -(s/def ::name ::us/string) - -(s/def ::project (s/keys :req-un [::id ::name])) -(s/def ::file (s/keys :req-un [::id ::name])) -(s/def ::page ::cp/page) - -(s/def ::bundle - (s/keys :req-un [::project ::file ::page])) - - ;; --- Local State Initialization (def ^:private @@ -49,25 +37,24 @@ (declare fetch-bundle) (declare bundle-fetched) -(s/def ::page-id ::us/uuid) (s/def ::file-id ::us/uuid) (s/def ::index ::us/integer) -(s/def ::token (s/nilable ::us/string)) +(s/def ::page-id (s/nilable ::us/uuid)) +(s/def ::share-id (s/nilable ::us/uuid)) (s/def ::section ::us/string) (s/def ::initialize-params - (s/keys :req-un [::page-id ::file-id] - :opt-un [::token])) + (s/keys :req-un [::file-id] + :opt-un [::share-id ::page-id])) (defn initialize - [{:keys [page-id file-id] :as params}] + [{:keys [file-id] :as params}] (us/assert ::initialize-params params) (ptk/reify ::initialize ptk/UpdateEvent (update [_ state] (-> state (assoc :current-file-id file-id) - (assoc :current-page-id page-id) (update :viewer-local (fn [lstate] (if (nil? lstate) @@ -77,55 +64,72 @@ ptk/WatchEvent (watch [_ _ _] (rx/of (fetch-bundle params) - (fetch-comment-threads params))))) + (fetch-comment-threads params))) -;; --- Data Fetching + ptk/EffectEvent + (effect [_ _ _] + ;; Set the window name, the window name is used on inter-tab + ;; navigation; in other words: when a user opens a tab with a + ;; name, if there are already opened tab with that name, the + ;; browser just focus the opened tab instead of creating new + ;; tab. + (let [name (str "viewer-" file-id)] + (unchecked-set ug/global "name" name))))) -(s/def ::fetch-bundle-params - (s/keys :req-un [::page-id ::file-id] - :opt-un [::token])) +(defn finalize + [_] + (ptk/reify ::finalize + ptk/UpdateEvent + (update [_ state] + (dissoc state :viewer)))) -(defn fetch-bundle - [{:keys [page-id file-id token] :as params}] - (us/assert ::fetch-bundle-params params) - (ptk/reify ::fetch-file - ptk/WatchEvent - (watch [_ _ _] - (let [params (cond-> {:page-id page-id - :file-id file-id} - (string? token) (assoc :token token))] - (->> (rp/query :viewer-bundle params) - (rx/mapcat - (fn [{:keys [fonts] :as bundle}] - (rx/of (df/fonts-fetched fonts) - (bundle-fetched bundle))))))))) - -(defn- extract-frames - [objects] +(defn select-frames + [{:keys [objects] :as page}] (let [root (get objects uuid/zero)] (into [] (comp (map #(get objects %)) (filter #(= :frame (:type %)))) (reverse (:shapes root))))) +;; --- Data Fetching + +(s/def ::fetch-bundle-params + (s/keys :req-un [::page-id ::file-id] + :opt-un [::share-id])) + +(defn fetch-bundle + [{:keys [file-id share-id] :as params}] + (us/assert ::fetch-bundle-params params) + (ptk/reify ::fetch-file + ptk/WatchEvent + (watch [_ _ _] + (let [params' (cond-> {:file-id file-id} + (uuid? share-id) (assoc :share-id share-id))] + (->> (rp/query :view-only-bundle params') + (rx/mapcat + (fn [{:keys [fonts] :as bundle}] + (rx/of (df/fonts-fetched fonts) + (bundle-fetched (merge bundle params)))))))))) + + (defn bundle-fetched - [{:keys [project file page share-token token libraries users] :as bundle}] - (us/verify ::bundle bundle) - (ptk/reify ::bundle-fetched - ptk/UpdateEvent - (update [_ state] - (let [objects (:objects page) - frames (extract-frames objects)] + [{:keys [project file share-links libraries users permissions] :as bundle}] + (let [pages (->> (get-in file [:data :pages]) + (map (fn [page-id] + (let [data (get-in file [:data :pages-index page-id])] + [page-id (assoc data :frames (select-frames data))]))) + (into {}))] + + (ptk/reify ::bundle-fetched + ptk/UpdateEvent + (update [_ state] (-> state - (assoc :viewer-libraries (d/index-by :id libraries)) - (update :viewer-data assoc - :project project - :objects objects - :users (d/index-by :id users) - :file file - :page page - :frames frames - :token token - :share-token share-token)))))) + (assoc :share-links share-links) + (assoc :viewer {:libraries (d/index-by :id libraries) + :users (d/index-by :id users) + :permissions permissions + :project project + :pages pages + :file file})))))) (defn fetch-comment-threads [{:keys [file-id page-id] :as params}] @@ -168,32 +172,6 @@ (->> (rp/query :comments {:thread-id thread-id}) (rx/map #(partial fetched %))))))) -(defn create-share-link - [] - (ptk/reify ::create-share-link - ptk/WatchEvent - (watch [_ state _] - (let [file-id (:current-file-id state) - page-id (:current-page-id state)] - (->> (rp/mutation! :create-file-share-token {:file-id file-id - :page-id page-id}) - (rx/map (fn [{:keys [token]}] - #(assoc-in % [:viewer-data :token] token)))))))) - -(defn delete-share-link - [] - (ptk/reify ::delete-share-link - ptk/WatchEvent - (watch [_ state _] - (let [file-id (:current-file-id state) - page-id (:current-page-id state) - token (get-in state [:viewer-data :token]) - params {:file-id file-id - :page-id page-id - :token token}] - (->> (rp/mutation :delete-file-share-token params) - (rx/map (fn [_] #(update % :viewer-data dissoc :token)))))))) - ;; --- Zoom Management (def increase-zoom @@ -245,29 +223,32 @@ ptk/WatchEvent (watch [_ state _] (let [route (:route state) - screen (-> route :data :name keyword) qparams (:query-params route) pparams (:path-params route) index (:index qparams)] (when (pos? index) (rx/of (dcm/close-thread) - (rt/nav screen pparams (assoc qparams :index (dec index))))))))) + (rt/nav :viewer pparams (assoc qparams :index (dec index))))))))) (def select-next-frame (ptk/reify ::select-prev-frame ptk/WatchEvent (watch [_ state _] + (prn "select-next-frame") (let [route (:route state) - screen (-> route :data :name keyword) - qparams (:query-params route) pparams (:path-params route) + qparams (:query-params route) + + page-id (:page-id pparams) index (:index qparams) - total (count (get-in state [:viewer-data :frames]))] + + total (count (get-in state [:viewer :pages page-id :frames]))] + (when (< index (dec total)) (rx/of (dcm/close-thread) - (rt/nav screen pparams (assoc qparams :index (inc index))))))))) + (rt/nav :viewer pparams (assoc qparams :index (inc index))))))))) (s/def ::interactions-mode #{:hide :show :show-on-click}) @@ -329,7 +310,6 @@ (when index (rx/of (go-to-frame-by-index index))))))) - (defn go-to-section [section] (ptk/reify ::go-to-section @@ -340,7 +320,6 @@ qparams (:query-params route)] (rx/of (rt/nav :viewer pparams (assoc qparams :section section))))))) - (defn set-current-frame [frame-id] (ptk/reify ::set-current-frame ptk/UpdateEvent @@ -405,18 +384,50 @@ (let [toggled? (contains? (get-in state [:viewer-local :collapsed]) id)] (update-in state [:viewer-local :collapsed] (if toggled? disj conj) id))))) -(defn hover-shape [id hover?] +(defn hover-shape + [id hover?] (ptk/reify ::hover-shape ptk/UpdateEvent (update [_ state] (assoc-in state [:viewer-local :hover] (when hover? id))))) +;; --- NAV (defn go-to-dashboard - ([] (go-to-dashboard nil)) - ([{:keys [team-id]}] - (ptk/reify ::go-to-dashboard - ptk/WatchEvent + [] + (ptk/reify ::go-to-dashboard + ptk/WatchEvent + (watch [_ state _] + (let [team-id (get-in state [:viewer :project :team-id]) + params {:team-id team-id}] + (rx/of (rt/nav :dashboard-projects params)))))) + +(defn go-to-page + [page-id] + (ptk/reify ::go-to-page + ptk/WatchEvent (watch [_ state _] - (let [team-id (or team-id (get-in state [:viewer-data :project :team-id]))] - (rx/of (rt/nav :dashboard-projects {:team-id team-id}))))))) + (let [route (:route state) + pparams (:path-params route) + qparams (-> (:query-params route) + (assoc :index 0) + (assoc :page-id page-id)) + rname (get-in route [:data :name])] + (rx/of (rt/nav rname pparams qparams)))))) + +(defn go-to-workspace + [page-id] + (ptk/reify ::go-to-workspace + ptk/WatchEvent + (watch [_ state _] + (let [project-id (get-in state [:viewer :project :id]) + file-id (get-in state [:viewer :file :id]) + pparams {:project-id project-id :file-id file-id} + qparams {:page-id page-id}] + (rx/of (rt/nav-new-window* + {:rname :workspace + :path-params pparams + :query-params qparams + :name (str "workspace-" file-id)})))))) + + diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 66a8aa5b3..0ea2d6805 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -37,6 +37,7 @@ [app.main.repo :as rp] [app.main.streams :as ms] [app.main.worker :as uw] + [app.util.globals :as ug] [app.util.http :as http] [app.util.i18n :as i18n] [app.util.router :as rt] @@ -171,7 +172,12 @@ (->> stream (rx/filter #(= ::dwc/index-initialized %)) (rx/first) - (rx/map #(file-initialized bundle))))))))))) + (rx/map #(file-initialized bundle))))))))) + + ptk/EffectEvent + (effect [_ _ _] + (let [name (str "workspace-" file-id)] + (unchecked-set ug/global "name" name))))) (defn- file-initialized [{:keys [file users project libraries] :as bundle}] @@ -1273,10 +1279,14 @@ ptk/WatchEvent (watch [_ state _] (let [{:keys [current-file-id current-page-id]} state - params {:file-id (or file-id current-file-id) - :page-id (or page-id current-page-id)}] + pparams {:file-id (or file-id current-file-id)} + qparams {:page-id (or page-id current-page-id) + :index 0}] (rx/of ::dwp/force-persist - (rt/nav-new-window :viewer params {:index 0}))))))) + (rt/nav-new-window* {:rname :viewer + :path-params pparams + :query-params qparams + :name (str "viewer-" (:file-id pparams))}))))))) (defn go-to-dashboard ([] (go-to-dashboard nil)) diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index 12ab7e553..4dcbac803 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -38,6 +38,9 @@ (def threads-ref (l/derived :comment-threads st/state)) +(def share-links + (l/derived :share-links st/state)) + ;; ---- Dashboard refs (def dashboard-local @@ -287,8 +290,17 @@ ;; ---- Viewer refs +(def viewer-file + (l/derived :viewer-file st/state)) + +(def viewer-project + (l/derived :viewer-file st/state)) + (def viewer-data - (l/derived :viewer-data st/state)) + (l/derived :viewer st/state)) + +(def viewer-state + (l/derived :viewer st/state)) (def viewer-local (l/derived :viewer-local st/state)) diff --git a/frontend/src/app/main/ui.cljs b/frontend/src/app/main/ui.cljs index aeafb377f..bfe2d5b25 100644 --- a/frontend/src/app/main/ui.cljs +++ b/frontend/src/app/main/ui.cljs @@ -20,7 +20,6 @@ [app.main.ui.context :as ctx] [app.main.ui.cursors :as c] [app.main.ui.dashboard :refer [dashboard]] - [app.main.ui.handoff :refer [handoff]] [app.main.ui.icons :as i] [app.main.ui.messages :as msgs] [app.main.ui.onboarding] @@ -41,16 +40,17 @@ (s/def ::page-id ::us/uuid) (s/def ::file-id ::us/uuid) -(s/def ::viewer-path-params - (s/keys :req-un [::file-id ::page-id])) - (s/def ::section ::us/keyword) (s/def ::index ::us/integer) -(s/def ::token (s/nilable ::us/string)) +(s/def ::token (s/nilable ::us/not-empty-string)) +(s/def ::share-id ::us/uuid) + +(s/def ::viewer-path-params + (s/keys :req-un [::file-id])) (s/def ::viewer-query-params (s/keys :req-un [::index] - :opt-un [::token ::section])) + :opt-un [::share-id ::section ::page-id])) (def routes [["/auth" @@ -71,7 +71,7 @@ ["/feedback" :settings-feedback] ["/options" :settings-options]] - ["/view/:file-id/:page-id" + ["/view/:file-id" {:name :viewer :conform {:path-params ::viewer-path-params @@ -147,22 +147,15 @@ [:& dashboard {:route route}]] :viewer - (let [index (get-in route [:query-params :index]) - token (get-in route [:query-params :token]) - section (get-in route [:query-params :section] :interactions) - file-id (get-in route [:path-params :file-id]) - page-id (get-in route [:path-params :page-id])] + (let [{:keys [query-params path-params]} route + {:keys [index share-id section page-id] :or {section :interactions}} query-params + {:keys [file-id]} path-params] [:& fs/fullscreen-wrapper {} - (if (= section :handoff) - [:& handoff {:page-id page-id - :file-id file-id - :index index - :token token}] - [:& viewer-page {:page-id page-id - :file-id file-id - :section section - :index index - :token token}])]) + [:& viewer-page {:page-id page-id + :file-id file-id + :section section + :index index + :share-id share-id}]]) :render-object (do diff --git a/frontend/src/app/main/ui/handoff.cljs b/frontend/src/app/main/ui/handoff.cljs deleted file mode 100644 index bee408f97..000000000 --- a/frontend/src/app/main/ui/handoff.cljs +++ /dev/null @@ -1,133 +0,0 @@ -;; 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) UXBOX Labs SL - -(ns app.main.ui.handoff - (:require - [app.main.data.viewer :as dv] - [app.main.data.viewer.shortcuts :as sc] - [app.main.refs :as refs] - [app.main.store :as st] - [app.main.ui.handoff.left-sidebar :refer [left-sidebar]] - [app.main.ui.handoff.render :refer [render-frame-svg]] - [app.main.ui.handoff.right-sidebar :refer [right-sidebar]] - [app.main.ui.hooks :as hooks] - [app.main.ui.viewer.header :refer [header]] - [app.main.ui.viewer.thumbnails :refer [thumbnails-panel]] - [app.util.dom :as dom] - [app.util.i18n :as i18n :refer [t tr]] - [app.util.keyboard :as kbd] - [goog.events :as events] - [rumext.alpha :as mf]) - (:import goog.events.EventType)) - -(defn handle-select-frame [frame] - #(do (dom/prevent-default %) - (dom/stop-propagation %) - (st/emit! (dv/select-shape (:id frame))))) - -(mf/defc render-panel - [{:keys [data state index page-id file-id]}] - (let [locale (mf/deref i18n/locale) - frames (:frames data []) - objects (:objects data) - frame (get frames index)] - - (mf/use-effect - (mf/deps index) - (fn [] - (st/emit! (dv/set-current-frame (:id frame)) - (dv/select-shape (:id frame))))) - - [:section.viewer-preview - (cond - (empty? frames) - [:section.empty-state - [:span (t locale "viewer.empty-state")]] - - (nil? frame) - [:section.empty-state - [:span (t locale "viewer.frame-not-found")]] - - :else - [:* - [:& left-sidebar {:frame frame}] - [:div.handoff-svg-wrapper {:on-click (handle-select-frame frame)} - [:div.handoff-svg-container - [:& render-frame-svg {:frame-id (:id frame) - :zoom (:zoom state) - :objects objects}]]] - [:& right-sidebar {:frame frame - :page-id page-id - :file-id file-id}]])])) - -(mf/defc handoff-content - [{:keys [data state index page-id file-id] :as props}] - (let [on-mouse-wheel - (mf/use-callback - (fn [event] - (when (or (kbd/ctrl? event) (kbd/meta? event)) - (dom/prevent-default event) - (let [event (.getBrowserEvent ^js event) - delta (+ (.-deltaY ^js event) - (.-deltaX ^js event))] - (if (pos? delta) - (st/emit! dv/decrease-zoom) - (st/emit! dv/increase-zoom)))))) - - on-mount - (fn [] - ;; bind with passive=false to allow the event to be cancelled - ;; https://stackoverflow.com/a/57582286/3219895 - (let [key1 (events/listen goog/global EventType.WHEEL - on-mouse-wheel #js {"passive" false})] - (fn [] - (events/unlistenByKey key1))))] - - (mf/use-effect on-mount) - (hooks/use-shortcuts ::handoff sc/shortcuts) - - [:div.handoff-layout {:class (dom/classnames :force-visible - (:show-thumbnails state))} - [:& header - {:data data - :state state - :index index - :section :handoff}] - [:div.viewer-content - (when (:show-thumbnails state) - [:& thumbnails-panel {:index index - :data data - :screen :handoff}]) - [:& render-panel {:data data - :state state - :index index - :page-id page-id - :file-id file-id}]]])) - -(mf/defc handoff - [{:keys [file-id page-id index token] :as props}] - - (mf/use-effect - (mf/deps file-id page-id token) - (fn [] - (st/emit! (dv/initialize props)))) - - (let [data (mf/deref refs/viewer-data) - state (mf/deref refs/viewer-local)] - - (mf/use-effect - (mf/deps (:file data)) - #(when (:file data) - (dom/set-html-title (tr "title.viewer" - (get-in data [:file :name]))))) - - (when (and data state) - [:& handoff-content - {:file-id file-id - :page-id page-id - :index index - :state state - :data data}]))) diff --git a/frontend/src/app/main/ui/share_link.cljs b/frontend/src/app/main/ui/share_link.cljs new file mode 100644 index 000000000..b9a84565b --- /dev/null +++ b/frontend/src/app/main/ui/share_link.cljs @@ -0,0 +1,232 @@ +;; 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) UXBOX Labs SL + +(ns app.main.ui.share-link + (:require + [app.common.data :as d] + [app.config :as cf] + [app.main.data.common :as dc] + [app.main.data.messages :as dm] + [app.main.data.modal :as modal] + [app.main.refs :as refs] + [app.main.store :as st] + [app.main.ui.icons :as i] + [app.util.dom :as dom] + [app.util.i18n :as i18n :refer [tr]] + [app.util.logging :as log] + [app.util.router :as rt] + [app.util.webapi :as wapi] + [rumext.alpha :as mf])) + +(log/set-level! :debug) + +(defn prepare-params + [{:keys [sections pages pages-mode]}] + {:pages pages + :flags (-> #{} + (into (map #(str "section-" %)) sections) + (into (map #(str "pages-" %)) [pages-mode]))}) + +(mf/defc share-link-dialog + {::mf/register modal/components + ::mf/register-as :share-link} + [{:keys [file page]}] + (let [slinks (mf/deref refs/share-links) + router (mf/deref refs/router) + route (mf/deref refs/route) + + link (mf/use-state nil) + confirm (mf/use-state false) + + opts (mf/use-state + {:sections #{"viewer"} + :pages-mode "current" + :pages #{(:id page)}}) + + close + (fn [event] + (dom/prevent-default event) + (st/emit! (modal/hide))) + + select-pages-mode + (fn [mode] + (reset! confirm false) + (swap! opts + (fn [state] + (-> state + (assoc :pages-mode mode) + (cond-> (= mode "current") (assoc :pages #{(:id page)})) + (cond-> (= mode "all") (assoc :pages (into #{} (get-in file [:data :pages])))))))) + + mark-checked-page + (fn [event id] + (let [target (dom/get-target event) + checked? (.-checked ^js target)] + (reset! confirm false) + (swap! opts update :pages + (fn [pages] + (if checked? + (conj pages id) + (disj pages id)))))) + + create-link + (fn [_] + (let [params (prepare-params @opts) + params (assoc params :file-id (:id file))] + (st/emit! (dc/create-share-link params)))) + + copy-link + (fn [_] + (wapi/write-to-clipboard @link) + (st/emit! (dm/show {:type :info + :content (tr "common.share-link.link-copied-success") + :timeout 3000}))) + + try-delete-link + (fn [_] + (reset! confirm true)) + + delete-link + (fn [_] + (let [params (prepare-params @opts) + slink (d/seek #(= (:flags %) (:flags params)) slinks)] + (reset! confirm false) + (st/emit! (dc/delete-share-link slink) + (dm/show {:type :info + :content (tr "common.share-link.link-deleted-success") + :timeout 3000})))) + ] + + (mf/use-effect + (mf/deps file slinks @opts) + (fn [] + (let [{:keys [flags pages] :as params} (prepare-params @opts) + slink (d/seek #(and (= (:flags %) flags) (= (:pages %) pages)) slinks) + href (when slink + (let [pparams (:path-params route) + qparams (-> (:query-params route) + (assoc :share-id (:id slink)) + (assoc :index "0")) + href (rt/resolve router :viewer pparams qparams)] + (assoc cf/public-uri :fragment href)))] + (reset! link (some-> href str))))) + + [:div.modal-overlay + [:div.modal-container.share-link-dialog + [:div.modal-content + [:div.title + [:h2 (tr "common.share-link.title")] + [:div.modal-close-button + {:on-click close + :title (tr "labels.close")} + i/close]] + + [:div.share-link-section + [:label (tr "labels.link")] + [:div.custom-input.with-icon + [:input {:type "text" :value (or @link "") :read-only true}] + [:div.help-icon {:title (tr "labels.copy") + :on-click copy-link} + i/copy]] + + [:div.hint (tr "common.share-link.permissions-hint")]]] + + [:div.modal-content + (let [sections (:sections @opts)] + [:div.access-mode + [:div.title (tr "common.share-link.permissions-can-access")] + [:div.items + [:div.input-checkbox.check-primary.disabled + [:input.check-primary.input-checkbox {:type "checkbox" :disabled true}] + [:label (tr "labels.workspace")]] + + [:div.input-checkbox.check-primary + [:input {:type "checkbox" + :default-checked (contains? sections "viewer")}] + [:label (tr "labels.viewer") + [:span.hint "(" (tr "labels.default") ")"]]] + + ;; [:div.input-checkbox.check-primary + ;; [:input.check-primary.input-checkbox {:type "checkbox"}] + ;; [:label "Handsoff" ]] + ]]) + + (let [mode (:pages-mode @opts)] + [:* + [:div.view-mode + [:div.title (tr "common.share-link.permissions-can-view")] + [:div.items + [:div.input-radio.radio-primary + [:input {:type "radio" + :id "view-all" + :checked (= "all" mode) + :name "pages-mode" + :on-change #(select-pages-mode "all")}] + [:label {:for "view-all"} (tr "common.share-link.view-all-pages")]] + + [:div.input-radio.radio-primary + [:input {:type "radio" + :id "view-current" + :name "pages-mode" + :checked (= "current" mode) + :on-change #(select-pages-mode "current")}] + [:label {:for "view-current"} (tr "common.share-link.view-current-page")]] + + [:div.input-radio.radio-primary + [:input {:type "radio" + :id "view-selected" + :name "pages-mode" + :checked (= "selected" mode) + :on-change #(select-pages-mode "selected")}] + [:label {:for "view-selected"} (tr "common.share-link.view-selected-pages")]]]] + + (when (= "selected" mode) + (let [pages (->> (get-in file [:data :pages]) + (map #(get-in file [:data :pages-index %]))) + selected (:pages @opts)] + [:ul.pages-selection + (for [page pages] + [:li.input-checkbox.check-primary {:key (str (:id page))} + [:input {:type "checkbox" + :id (str "page-" (:id page)) + :on-change #(mark-checked-page % (:id page)) + :checked (contains? selected (:id page))}] + [:label {:for (str "page-" (:id page))} (:name page)]])]))])] + + [:div.modal-footer + (cond + (true? @confirm) + [:div.confirm-dialog + [:div.description (tr "common.share-link.confirm-deletion-link-description")] + [:div.actions + [:input.btn-secondary + {:type "button" + :on-click #(reset! confirm false) + :value (tr "labels.cancel")}] + [:input.btn-warning + {:type "button" + :on-click delete-link + :value (tr "common.share-link.remove-link") + }]]] + + (some? @link) + [:input.btn-secondary + {:type "button" + :class "primary" + :on-click try-delete-link + :value (tr "common.share-link.remove-link")}] + + :else + [:input.btn-primary + {:type "button" + :class "primary" + :on-click create-link + :value (tr "common.share-link.get-link")}])] + + ]])) + + + diff --git a/frontend/src/app/main/ui/viewer.cljs b/frontend/src/app/main/ui/viewer.cljs index 212301613..187951266 100644 --- a/frontend/src/app/main/ui/viewer.cljs +++ b/frontend/src/app/main/ui/viewer.cljs @@ -6,276 +6,147 @@ (ns app.main.ui.viewer (:require - [app.common.data :as d] - [app.common.geom.matrix :as gmt] - [app.common.geom.point :as gpt] - [app.common.geom.shapes :as geom] - [app.common.pages :as cp] [app.main.data.comments :as dcm] [app.main.data.viewer :as dv] [app.main.data.viewer.shortcuts :as sc] [app.main.refs :as refs] [app.main.store :as st] - [app.main.ui.comments :as cmt] [app.main.ui.hooks :as hooks] + [app.main.ui.share-link] + [app.main.ui.viewer.comments :refer [comments-layer]] + [app.main.ui.viewer.handoff :as handoff] [app.main.ui.viewer.header :refer [header]] - [app.main.ui.viewer.shapes :as shapes] + [app.main.ui.viewer.interactions :as interactions] [app.main.ui.viewer.thumbnails :refer [thumbnails-panel]] [app.util.dom :as dom] - [app.util.i18n :as i18n :refer [t tr]] - [app.util.keyboard :as kbd] + [app.util.i18n :as i18n :refer [tr]] [goog.events :as events] - [okulary.core :as l] [rumext.alpha :as mf])) -(defn- frame-contains? - [{:keys [x y width height]} {px :x py :y}] - (let [x2 (+ x width) - y2 (+ y height)] - (and (<= x px x2) - (<= y py y2)))) +(defn- calculate-size + [frame zoom] + {:width (* (:width frame) zoom) + :height (* (:height frame) zoom) + :vbox (str "0 0 " (:width frame 0) " " (:height frame 0))}) -(def threads-ref - (l/derived :comment-threads st/state)) +(mf/defc viewer + [{:keys [params data]}] -(def comments-local-ref - (l/derived :comments-local st/state)) + (let [{:keys [page-id section index]} params -(mf/defc comments-layer - [{:keys [zoom frame data] :as props}] - (let [profile (mf/deref refs/profile) + local (mf/deref refs/viewer-local) - modifier1 (-> (gpt/point (:x frame) (:y frame)) - (gpt/negate) - (gmt/translate-matrix)) + file (:file data) + users (:users data) + project (:project data) + perms (:permissions data) - modifier2 (-> (gpt/point (:x frame) (:y frame)) - (gmt/translate-matrix)) + page-id (or page-id (-> file :data :pages first)) - threads-map (->> (mf/deref threads-ref) - (d/mapm #(update %2 :position gpt/transform modifier1))) + page (mf/use-memo + (mf/deps data page-id) + (fn [] + (get-in data [:pages page-id]))) - cstate (mf/deref refs/comments-local) + zoom (:zoom local) + frames (:frames page) + frame (get frames index) - mframe (geom/transform-shape frame) - threads (->> (vals threads-map) - (dcm/apply-filters cstate profile) - (filter (fn [{:keys [position]}] - (frame-contains? mframe position)))) - - on-bubble-click - (mf/use-callback - (mf/deps cstate) - (fn [thread] - (if (= (:open cstate) (:id thread)) - (st/emit! (dcm/close-thread)) - (st/emit! (dcm/open-thread thread))))) + size (mf/use-memo + (mf/deps frame zoom) + (fn [] (calculate-size frame zoom))) on-click (mf/use-callback - (mf/deps cstate data frame) - (fn [event] - (dom/stop-propagation event) - (if (some? (:open cstate)) - (st/emit! (dcm/close-thread)) - (let [event (.-nativeEvent ^js event) - position (-> (dom/get-offset-position event) - (gpt/transform modifier2)) - params {:position position - :page-id (get-in data [:page :id]) - :file-id (get-in data [:file :id])}] - (st/emit! (dcm/create-draft params)))))) + (mf/deps section) + (fn [_] + (when (= section :comments) + (st/emit! (dcm/close-thread)))))] - on-draft-cancel - (mf/use-callback - (mf/deps cstate) - (st/emitf (dcm/close-thread))) - - on-draft-submit - (mf/use-callback - (mf/deps frame) - (fn [draft] - (let [params (update draft :position gpt/transform modifier2)] - (st/emit! (dcm/create-thread params) - (dcm/close-thread)))))] - - [:div.comments-section {:on-click on-click} - [:div.viewer-comments-container - [:div.threads - (for [item threads] - [:& cmt/thread-bubble {:thread item - :zoom zoom - :on-click on-bubble-click - :open? (= (:id item) (:open cstate)) - :key (:seqn item)}]) - - (when-let [id (:open cstate)] - (when-let [thread (get threads-map id)] - [:& cmt/thread-comments {:thread thread - :users (:users data) - :zoom zoom}])) - - (when-let [draft (:draft cstate)] - [:& cmt/draft-thread {:draft (update draft :position gpt/transform modifier1) - :on-cancel on-draft-cancel - :on-submit on-draft-submit - :zoom zoom}])]]])) - - - -(mf/defc viewport - {::mf/wrap [mf/memo]} - [{:keys [state data index section] :as props}] - (let [zoom (:zoom state) - objects (:objects data) - - frame (get-in data [:frames index]) - frame-id (:id frame) - - modifier (-> (gpt/point (:x frame) (:y frame)) - (gpt/negate) - (gmt/translate-matrix)) - - update-fn #(d/update-when %1 %2 assoc-in [:modifiers :displacement] modifier) - - objects (->> (d/concat [frame-id] (cp/get-children frame-id objects)) - (reduce update-fn objects)) - - interactions? (:interactions-show? state) - wrapper (mf/use-memo (mf/deps objects) #(shapes/frame-container-factory objects interactions?)) - - ;; Retrieve frame again with correct modifier - frame (get objects frame-id) - - width (* (:width frame) zoom) - height (* (:height frame) zoom) - vbox (str "0 0 " (:width frame 0) " " (:height frame 0))] - - [:div.viewport-container - {:style {:width width - :height height - :state state - :position "relative"}} - - (when (= section :comments) - [:& comments-layer {:width width - :height height - :frame frame - :data data - :zoom zoom}]) - - [:svg {:view-box vbox - :width width - :height height - :version "1.1" - :xmlnsXlink "http://www.w3.org/1999/xlink" - :xmlns "http://www.w3.org/2000/svg"} - [:& wrapper {:shape frame - :show-interactions? interactions? - :view-box vbox}]]])) - -(mf/defc main-panel - [{:keys [data state index section]}] - (let [locale (mf/deref i18n/locale) - frames (:frames data) - frame (get frames index)] - [:section.viewer-preview - (cond - (empty? frames) - [:section.empty-state - [:span (t locale "viewer.empty-state")]] - - (nil? frame) - [:section.empty-state - [:span (t locale "viewer.frame-not-found")]] - - (some? state) - [:& viewport - {:data data - :section section - :index index - :state state - }])])) - -(mf/defc viewer-content - [{:keys [data state index section] :as props}] - (let [on-click - (fn [event] - (dom/stop-propagation event) - (st/emit! (dcm/close-thread)) - (let [mode (get state :interactions-mode)] - (when (= mode :show-on-click) - (st/emit! dv/flash-interactions)))) - - on-mouse-wheel - (fn [event] - (when (or (kbd/ctrl? event) (kbd/meta? event)) - (dom/prevent-default event) - (let [event (.getBrowserEvent ^js event) - delta (+ (.-deltaY ^js event) (.-deltaX ^js event))] - (if (pos? delta) - (st/emit! dv/decrease-zoom) - (st/emit! dv/increase-zoom))))) - - on-key-down - (fn [event] - (when (kbd/esc? event) - (st/emit! (dcm/close-thread)))) - - on-mount - (fn [] - ;; bind with passive=false to allow the event to be cancelled - ;; https://stackoverflow.com/a/57582286/3219895 - (let [key1 (events/listen goog/global "wheel" on-mouse-wheel #js {"passive" false}) - key2 (events/listen js/window "keydown" on-key-down) - key3 (events/listen js/window "click" on-click)] - (fn [] - (events/unlistenByKey key1) - (events/unlistenByKey key2) - (events/unlistenByKey key3))))] - - (mf/use-effect on-mount) (hooks/use-shortcuts ::viewer sc/shortcuts) - [:div.viewer-layout {:class (dom/classnames :force-visible - (:show-thumbnails state))} - [:& header - {:data data - :state state - :section section - :index index}] + ;; Set the page title + (mf/use-effect + (mf/deps (:name file)) + (fn [] + (let [name (:name file)] + (dom/set-html-title (str "\u25b6 " (tr "title.viewer" name)))))) - [:div.viewer-content {:on-click on-click} - (when (:show-thumbnails state) - [:& thumbnails-panel {:screen :viewer - :index index - :data data}]) - [:& main-panel {:data data - :section section - :state state - :index index}]]])) + (mf/use-effect + (fn [] + (let [key1 (events/listen js/window "click" on-click)] + (fn [] + (events/unlistenByKey key1))))) + [:div {:class (dom/classnames + :force-visible (:show-thumbnails local) + :viewer-layout (not= section :handoff) + :handoff-layout (= section :handoff))} + + [:& header {:project project + :file file + :page page + :frame frame + :permissions perms + :zoom (:zoom local) + :section section}] + + [:div.viewer-content + [:& thumbnails-panel {:frames frames + :show? (:show-thumbnails local false) + :page page + :index index}] + [:section.viewer-preview + (cond + (empty? frames) + [:section.empty-state + [:span (tr "viewer.empty-state")]] + + (nil? frame) + [:section.empty-state + [:span (tr "viewer.frame-not-found")]] + + (some? frame) + (if (= :handoff section) + [:& handoff/viewport + {:frame frame + :page page + :file file + :section section + :local local}] + + + [:div.viewport-container + {:style {:width (:width size) + :height (:height size) + :position "relative"}} + + (when (= section :comments) + [:& comments-layer {:file file + :users users + :frame frame + :page page + :zoom zoom}]) + + [:& interactions/viewport + {:frame frame + :size size + :page page + :file file + :users users + :local local}]]))]]])) ;; --- Component: Viewer Page (mf/defc viewer-page - [{:keys [file-id page-id index token section] :as props}] - (let [data (mf/deref refs/viewer-data) - state (mf/deref refs/viewer-local)] - - (mf/use-effect - (mf/deps file-id page-id token) + [{:keys [file-id] :as props}] + (mf/use-effect + (mf/deps file-id) + (fn [] + (st/emit! (dv/initialize props)) (fn [] - (st/emit! (dv/initialize props)))) + (st/emit! (dv/finalize props))))) - (mf/use-effect - (mf/deps (:file data)) - #(when-let [name (get-in data [:file :name])] - (dom/set-html-title (str "\u25b6 " (tr "title.viewer" name))))) - - (when (and data state) - [:& viewer-content - {:index index - :section section - :state state - :data data}]))) + (when-let [data (mf/deref refs/viewer-data)] + (let [key (str (get-in data [:file :id]))] + [:& viewer {:params props :data data :key key}]))) diff --git a/frontend/src/app/main/ui/viewer/comments.cljs b/frontend/src/app/main/ui/viewer/comments.cljs new file mode 100644 index 000000000..bce399c69 --- /dev/null +++ b/frontend/src/app/main/ui/viewer/comments.cljs @@ -0,0 +1,158 @@ +;; 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) UXBOX Labs SL + +(ns app.main.ui.viewer.comments + (:require + [app.common.data :as d] + [app.common.geom.matrix :as gmt] + [app.common.geom.point :as gpt] + [app.common.geom.shapes :as geom] + [app.main.data.comments :as dcm] + [app.main.refs :as refs] + [app.main.store :as st] + [app.main.ui.comments :as cmt] + [app.main.ui.components.dropdown :refer [dropdown]] + [app.main.ui.icons :as i] + [app.util.dom :as dom] + [app.util.i18n :as i18n :refer [tr]] + [okulary.core :as l] + [rumext.alpha :as mf])) + + +(mf/defc comments-menu + [] + (let [{cmode :mode cshow :show} (mf/deref refs/comments-local) + + show-dropdown? (mf/use-state false) + toggle-dropdown (mf/use-fn #(swap! show-dropdown? not)) + hide-dropdown (mf/use-fn #(reset! show-dropdown? false)) + + update-mode + (mf/use-callback + (fn [mode] + (st/emit! (dcm/update-filters {:mode mode})))) + + update-show + (mf/use-callback + (fn [mode] + (st/emit! (dcm/update-filters {:show mode}))))] + + [:div.view-options {:on-click toggle-dropdown} + [:span.label (tr "labels.comments")] + [:span.icon i/arrow-down] + [:& dropdown {:show @show-dropdown? + :on-close hide-dropdown} + [:ul.dropdown.with-check + [:li {:class (dom/classnames :selected (= :all cmode)) + :on-click #(update-mode :all)} + [:span.icon i/tick] + [:span.label (tr "labels.show-all-comments")]] + + [:li {:class (dom/classnames :selected (= :yours cmode)) + :on-click #(update-mode :yours)} + [:span.icon i/tick] + [:span.label (tr "labels.show-your-comments")]] + + [:hr] + + [:li {:class (dom/classnames :selected (= :pending cshow)) + :on-click #(update-show (if (= :pending cshow) :all :pending))} + [:span.icon i/tick] + [:span.label (tr "labels.hide-resolved-comments")]]]]])) + + +(defn- frame-contains? + [{:keys [x y width height]} {px :x py :y}] + (let [x2 (+ x width) + y2 (+ y height)] + (and (<= x px x2) + (<= y py y2)))) + +(def threads-ref + (l/derived :comment-threads st/state)) + +(def comments-local-ref + (l/derived :comments-local st/state)) + +(mf/defc comments-layer + [{:keys [zoom file users frame page] :as props}] + (let [profile (mf/deref refs/profile) + + modifier1 (-> (gpt/point (:x frame) (:y frame)) + (gpt/negate) + (gmt/translate-matrix)) + + modifier2 (-> (gpt/point (:x frame) (:y frame)) + (gmt/translate-matrix)) + + threads-map (->> (mf/deref threads-ref) + (d/mapm #(update %2 :position gpt/transform modifier1))) + + cstate (mf/deref refs/comments-local) + + mframe (geom/transform-shape frame) + threads (->> (vals threads-map) + (dcm/apply-filters cstate profile) + (filter (fn [{:keys [position]}] + (frame-contains? mframe position)))) + + on-bubble-click + (mf/use-callback + (mf/deps cstate) + (fn [thread] + (if (= (:open cstate) (:id thread)) + (st/emit! (dcm/close-thread)) + (st/emit! (dcm/open-thread thread))))) + + on-click + (mf/use-callback + (mf/deps cstate frame page file) + (fn [event] + (dom/stop-propagation event) + (if (some? (:open cstate)) + (st/emit! (dcm/close-thread)) + (let [event (.-nativeEvent ^js event) + position (-> (dom/get-offset-position event) + (gpt/transform modifier2)) + params {:position position + :page-id (:id page) + :file-id (:id file)}] + (st/emit! (dcm/create-draft params)))))) + + on-draft-cancel + (mf/use-callback + (mf/deps cstate) + (st/emitf (dcm/close-thread))) + + on-draft-submit + (mf/use-callback + (mf/deps frame) + (fn [draft] + (let [params (update draft :position gpt/transform modifier2)] + (st/emit! (dcm/create-thread params) + (dcm/close-thread)))))] + + [:div.comments-section {:on-click on-click} + [:div.viewer-comments-container + [:div.threads + (for [item threads] + [:& cmt/thread-bubble {:thread item + :zoom zoom + :on-click on-bubble-click + :open? (= (:id item) (:open cstate)) + :key (:seqn item)}]) + + (when-let [id (:open cstate)] + (when-let [thread (get threads-map id)] + [:& cmt/thread-comments {:thread thread + :users users + :zoom zoom}])) + + (when-let [draft (:draft cstate)] + [:& cmt/draft-thread {:draft (update draft :position gpt/transform modifier1) + :on-cancel on-draft-cancel + :on-submit on-draft-submit + :zoom zoom}])]]])) diff --git a/frontend/src/app/main/ui/viewer/handoff.cljs b/frontend/src/app/main/ui/viewer/handoff.cljs new file mode 100644 index 000000000..1fa925678 --- /dev/null +++ b/frontend/src/app/main/ui/viewer/handoff.cljs @@ -0,0 +1,68 @@ +;; 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) UXBOX Labs SL + +(ns app.main.ui.viewer.handoff + (:require + [app.main.data.viewer :as dv] + [app.main.store :as st] + [app.main.ui.viewer.handoff.left-sidebar :refer [left-sidebar]] + [app.main.ui.viewer.handoff.render :refer [render-frame-svg]] + [app.main.ui.viewer.handoff.right-sidebar :refer [right-sidebar]] + [app.util.dom :as dom] + [app.util.keyboard :as kbd] + [goog.events :as events] + [rumext.alpha :as mf]) + (:import goog.events.EventType)) + +(defn handle-select-frame + [frame] + (fn [event] + (dom/prevent-default event) + (dom/stop-propagation event) + (st/emit! (dv/select-shape (:id frame))))) + +(mf/defc viewport + [{:keys [local file page frame]}] + (let [on-mouse-wheel + (fn [event] + (when (or (kbd/ctrl? event) (kbd/meta? event)) + (dom/prevent-default event) + (let [event (.getBrowserEvent ^js event) + delta (+ (.-deltaY ^js event) + (.-deltaX ^js event))] + (if (pos? delta) + (st/emit! dv/decrease-zoom) + (st/emit! dv/increase-zoom))))) + + on-mount + (fn [] + ;; bind with passive=false to allow the event to be cancelled + ;; https://stackoverflow.com/a/57582286/3219895 + (let [key1 (events/listen goog/global EventType.WHEEL + on-mouse-wheel #js {"passive" false})] + (fn [] + (events/unlistenByKey key1))))] + + (mf/use-effect on-mount) + + (mf/use-effect + (mf/deps (:id frame)) + (fn [] + (st/emit! (dv/set-current-frame (:id frame)) + (dv/select-shape (:id frame))))) + + [:* + [:& left-sidebar {:frame frame + :local local + :page page}] + [:div.handoff-svg-wrapper {:on-click (handle-select-frame frame)} + [:div.handoff-svg-container + [:& render-frame-svg {:frame frame :page page :local local}]]] + + [:& right-sidebar {:frame frame + :selected (:selected local) + :page page + :file file}]])) diff --git a/frontend/src/app/main/ui/handoff/attributes.cljs b/frontend/src/app/main/ui/viewer/handoff/attributes.cljs similarity index 70% rename from frontend/src/app/main/ui/handoff/attributes.cljs rename to frontend/src/app/main/ui/viewer/handoff/attributes.cljs index cdce9e76d..2792ae1fe 100644 --- a/frontend/src/app/main/ui/handoff/attributes.cljs +++ b/frontend/src/app/main/ui/viewer/handoff/attributes.cljs @@ -4,18 +4,18 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.main.ui.handoff.attributes +(ns app.main.ui.viewer.handoff.attributes (:require [app.common.geom.shapes :as gsh] - [app.main.ui.handoff.attributes.blur :refer [blur-panel]] - [app.main.ui.handoff.attributes.fill :refer [fill-panel]] - [app.main.ui.handoff.attributes.image :refer [image-panel]] - [app.main.ui.handoff.attributes.layout :refer [layout-panel]] - [app.main.ui.handoff.attributes.shadow :refer [shadow-panel]] - [app.main.ui.handoff.attributes.stroke :refer [stroke-panel]] - [app.main.ui.handoff.attributes.svg :refer [svg-panel]] - [app.main.ui.handoff.attributes.text :refer [text-panel]] - [app.main.ui.handoff.exports :refer [exports]] + [app.main.ui.viewer.handoff.attributes.blur :refer [blur-panel]] + [app.main.ui.viewer.handoff.attributes.fill :refer [fill-panel]] + [app.main.ui.viewer.handoff.attributes.image :refer [image-panel]] + [app.main.ui.viewer.handoff.attributes.layout :refer [layout-panel]] + [app.main.ui.viewer.handoff.attributes.shadow :refer [shadow-panel]] + [app.main.ui.viewer.handoff.attributes.stroke :refer [stroke-panel]] + [app.main.ui.viewer.handoff.attributes.svg :refer [svg-panel]] + [app.main.ui.viewer.handoff.attributes.text :refer [text-panel]] + [app.main.ui.viewer.handoff.exports :refer [exports]] [app.util.i18n :as i18n] [rumext.alpha :as mf])) diff --git a/frontend/src/app/main/ui/handoff/attributes/blur.cljs b/frontend/src/app/main/ui/viewer/handoff/attributes/blur.cljs similarity index 96% rename from frontend/src/app/main/ui/handoff/attributes/blur.cljs rename to frontend/src/app/main/ui/viewer/handoff/attributes/blur.cljs index 67f3b9194..fc9f382ea 100644 --- a/frontend/src/app/main/ui/handoff/attributes/blur.cljs +++ b/frontend/src/app/main/ui/viewer/handoff/attributes/blur.cljs @@ -4,7 +4,7 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.main.ui.handoff.attributes.blur +(ns app.main.ui.viewer.handoff.attributes.blur (:require [app.main.ui.components.copy-button :refer [copy-button]] [app.util.code-gen :as cg] diff --git a/frontend/src/app/main/ui/handoff/attributes/common.cljs b/frontend/src/app/main/ui/viewer/handoff/attributes/common.cljs similarity index 98% rename from frontend/src/app/main/ui/handoff/attributes/common.cljs rename to frontend/src/app/main/ui/viewer/handoff/attributes/common.cljs index 2171979a5..c13617692 100644 --- a/frontend/src/app/main/ui/handoff/attributes/common.cljs +++ b/frontend/src/app/main/ui/viewer/handoff/attributes/common.cljs @@ -4,7 +4,7 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.main.ui.handoff.attributes.common +(ns app.main.ui.viewer.handoff.attributes.common (:require [app.common.math :as mth] [app.main.store :as st] diff --git a/frontend/src/app/main/ui/handoff/attributes/fill.cljs b/frontend/src/app/main/ui/viewer/handoff/attributes/fill.cljs similarity index 93% rename from frontend/src/app/main/ui/handoff/attributes/fill.cljs rename to frontend/src/app/main/ui/viewer/handoff/attributes/fill.cljs index a2b6301dc..c1ee2269f 100644 --- a/frontend/src/app/main/ui/handoff/attributes/fill.cljs +++ b/frontend/src/app/main/ui/viewer/handoff/attributes/fill.cljs @@ -4,10 +4,10 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.main.ui.handoff.attributes.fill +(ns app.main.ui.viewer.handoff.attributes.fill (:require [app.main.ui.components.copy-button :refer [copy-button]] - [app.main.ui.handoff.attributes.common :refer [color-row]] + [app.main.ui.viewer.handoff.attributes.common :refer [color-row]] [app.util.code-gen :as cg] [app.util.color :as uc] [app.util.i18n :refer [tr]] diff --git a/frontend/src/app/main/ui/handoff/attributes/image.cljs b/frontend/src/app/main/ui/viewer/handoff/attributes/image.cljs similarity index 97% rename from frontend/src/app/main/ui/handoff/attributes/image.cljs rename to frontend/src/app/main/ui/viewer/handoff/attributes/image.cljs index e79818868..00691e236 100644 --- a/frontend/src/app/main/ui/handoff/attributes/image.cljs +++ b/frontend/src/app/main/ui/viewer/handoff/attributes/image.cljs @@ -4,7 +4,7 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.main.ui.handoff.attributes.image +(ns app.main.ui.viewer.handoff.attributes.image (:require [app.config :as cfg] [app.main.ui.components.copy-button :refer [copy-button]] diff --git a/frontend/src/app/main/ui/handoff/attributes/layout.cljs b/frontend/src/app/main/ui/viewer/handoff/attributes/layout.cljs similarity index 98% rename from frontend/src/app/main/ui/handoff/attributes/layout.cljs rename to frontend/src/app/main/ui/viewer/handoff/attributes/layout.cljs index 4704bbb0a..ea7ca7b52 100644 --- a/frontend/src/app/main/ui/handoff/attributes/layout.cljs +++ b/frontend/src/app/main/ui/viewer/handoff/attributes/layout.cljs @@ -4,7 +4,7 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.main.ui.handoff.attributes.layout +(ns app.main.ui.viewer.handoff.attributes.layout (:require [app.common.math :as mth] [app.main.ui.components.copy-button :refer [copy-button]] diff --git a/frontend/src/app/main/ui/handoff/attributes/shadow.cljs b/frontend/src/app/main/ui/viewer/handoff/attributes/shadow.cljs similarity index 95% rename from frontend/src/app/main/ui/handoff/attributes/shadow.cljs rename to frontend/src/app/main/ui/viewer/handoff/attributes/shadow.cljs index 892f46679..67a934f5c 100644 --- a/frontend/src/app/main/ui/handoff/attributes/shadow.cljs +++ b/frontend/src/app/main/ui/viewer/handoff/attributes/shadow.cljs @@ -4,11 +4,11 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.main.ui.handoff.attributes.shadow +(ns app.main.ui.viewer.handoff.attributes.shadow (:require [app.common.data :as d] [app.main.ui.components.copy-button :refer [copy-button]] - [app.main.ui.handoff.attributes.common :refer [color-row]] + [app.main.ui.viewer.handoff.attributes.common :refer [color-row]] [app.util.code-gen :as cg] [app.util.i18n :refer [tr]] [cuerdas.core :as str] diff --git a/frontend/src/app/main/ui/handoff/attributes/stroke.cljs b/frontend/src/app/main/ui/viewer/handoff/attributes/stroke.cljs similarity index 96% rename from frontend/src/app/main/ui/handoff/attributes/stroke.cljs rename to frontend/src/app/main/ui/viewer/handoff/attributes/stroke.cljs index ddd5a3263..a41d11ad5 100644 --- a/frontend/src/app/main/ui/handoff/attributes/stroke.cljs +++ b/frontend/src/app/main/ui/viewer/handoff/attributes/stroke.cljs @@ -4,12 +4,12 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.main.ui.handoff.attributes.stroke +(ns app.main.ui.viewer.handoff.attributes.stroke (:require [app.common.data :as d] [app.common.math :as mth] [app.main.ui.components.copy-button :refer [copy-button]] - [app.main.ui.handoff.attributes.common :refer [color-row]] + [app.main.ui.viewer.handoff.attributes.common :refer [color-row]] [app.util.code-gen :as cg] [app.util.color :as uc] [app.util.i18n :refer [t]] diff --git a/frontend/src/app/main/ui/handoff/attributes/svg.cljs b/frontend/src/app/main/ui/viewer/handoff/attributes/svg.cljs similarity index 97% rename from frontend/src/app/main/ui/handoff/attributes/svg.cljs rename to frontend/src/app/main/ui/viewer/handoff/attributes/svg.cljs index a2308c598..8e126e014 100644 --- a/frontend/src/app/main/ui/handoff/attributes/svg.cljs +++ b/frontend/src/app/main/ui/viewer/handoff/attributes/svg.cljs @@ -4,7 +4,7 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.main.ui.handoff.attributes.svg +(ns app.main.ui.viewer.handoff.attributes.svg (:require #_[app.common.math :as mth] #_[app.main.ui.icons :as i] diff --git a/frontend/src/app/main/ui/handoff/attributes/text.cljs b/frontend/src/app/main/ui/viewer/handoff/attributes/text.cljs similarity index 98% rename from frontend/src/app/main/ui/handoff/attributes/text.cljs rename to frontend/src/app/main/ui/viewer/handoff/attributes/text.cljs index 694c3525b..9ed9c189d 100644 --- a/frontend/src/app/main/ui/handoff/attributes/text.cljs +++ b/frontend/src/app/main/ui/viewer/handoff/attributes/text.cljs @@ -4,13 +4,13 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.main.ui.handoff.attributes.text +(ns app.main.ui.viewer.handoff.attributes.text (:require [app.common.text :as txt] [app.main.fonts :as fonts] [app.main.store :as st] [app.main.ui.components.copy-button :refer [copy-button]] - [app.main.ui.handoff.attributes.common :refer [color-row]] + [app.main.ui.viewer.handoff.attributes.common :refer [color-row]] [app.util.code-gen :as cg] [app.util.color :as uc] [app.util.i18n :refer [tr]] diff --git a/frontend/src/app/main/ui/handoff/code.cljs b/frontend/src/app/main/ui/viewer/handoff/code.cljs similarity index 98% rename from frontend/src/app/main/ui/handoff/code.cljs rename to frontend/src/app/main/ui/viewer/handoff/code.cljs index b6eab0744..db82e5e70 100644 --- a/frontend/src/app/main/ui/handoff/code.cljs +++ b/frontend/src/app/main/ui/viewer/handoff/code.cljs @@ -4,7 +4,7 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.main.ui.handoff.code +(ns app.main.ui.viewer.handoff.code (:require ["js-beautify" :as beautify] [app.common.geom.shapes :as gsh] diff --git a/frontend/src/app/main/ui/handoff/exports.cljs b/frontend/src/app/main/ui/viewer/handoff/exports.cljs similarity index 99% rename from frontend/src/app/main/ui/handoff/exports.cljs rename to frontend/src/app/main/ui/viewer/handoff/exports.cljs index 670a8f312..e866617f7 100644 --- a/frontend/src/app/main/ui/handoff/exports.cljs +++ b/frontend/src/app/main/ui/viewer/handoff/exports.cljs @@ -4,7 +4,7 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.main.ui.handoff.exports +(ns app.main.ui.viewer.handoff.exports (:require [app.common.data :as d] [app.main.data.messages :as dm] diff --git a/frontend/src/app/main/ui/handoff/left_sidebar.cljs b/frontend/src/app/main/ui/viewer/handoff/left_sidebar.cljs similarity index 89% rename from frontend/src/app/main/ui/handoff/left_sidebar.cljs rename to frontend/src/app/main/ui/viewer/handoff/left_sidebar.cljs index 12d4ea031..48aa1a5aa 100644 --- a/frontend/src/app/main/ui/handoff/left_sidebar.cljs +++ b/frontend/src/app/main/ui/viewer/handoff/left_sidebar.cljs @@ -4,7 +4,7 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.main.ui.handoff.left-sidebar +(ns app.main.ui.viewer.handoff.left-sidebar (:require [app.common.data :as d] [app.main.data.viewer :as dv] @@ -16,12 +16,6 @@ [okulary.core :as l] [rumext.alpha :as mf])) -(def selected-shapes - (l/derived (comp :selected :viewer-local) st/state)) - -(def page-ref - (l/derived (comp :page :viewer-data) st/state)) - (defn- make-collapsed-iref [id] #(-> (l/in [:viewer-local :collapsed id]) @@ -31,7 +25,9 @@ [{:keys [item selected objects disable-collapse?] :as props}] (let [id (:id item) selected? (contains? selected id) - item-ref (mf/use-ref nil) + item-ref (mf/use-ref nil) + + collapsed-iref (mf/use-memo (mf/deps id) (make-collapsed-iref id)) @@ -94,10 +90,10 @@ :objects objects :key (:id item)}]))])])) -(mf/defc left-sidebar [{:keys [frame]}] - (let [page (mf/deref page-ref) - selected (mf/deref selected-shapes) - objects (:objects page)] +(mf/defc left-sidebar + [{:keys [frame page local]}] + (let [selected (:selected local) + objects (:objects page)] [:aside.settings-bar.settings-bar-left [:div.settings-bar-inside diff --git a/frontend/src/app/main/ui/handoff/render.cljs b/frontend/src/app/main/ui/viewer/handoff/render.cljs similarity index 75% rename from frontend/src/app/main/ui/handoff/render.cljs rename to frontend/src/app/main/ui/viewer/handoff/render.cljs index d4d8a89e4..093cfc846 100644 --- a/frontend/src/app/main/ui/handoff/render.cljs +++ b/frontend/src/app/main/ui/viewer/handoff/render.cljs @@ -4,17 +4,12 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.main.ui.handoff.render +(ns app.main.ui.viewer.handoff.render "The main container for a frame in handoff mode" (:require - [app.common.data :as d] - [app.common.geom.matrix :as gmt] - [app.common.geom.point :as gpt] [app.common.geom.shapes :as geom] - [app.common.pages :as cp] [app.main.data.viewer :as dv] [app.main.store :as st] - [app.main.ui.handoff.selection-feedback :refer [selection-feedback]] [app.main.ui.shapes.circle :as circle] [app.main.ui.shapes.frame :as frame] [app.main.ui.shapes.group :as group] @@ -24,17 +19,21 @@ [app.main.ui.shapes.shape :refer [shape-container]] [app.main.ui.shapes.svg-raw :as svg-raw] [app.main.ui.shapes.text :as text] + [app.main.ui.viewer.handoff.selection-feedback :refer [selection-feedback]] + [app.main.ui.viewer.interactions :refer [prepare-objects]] [app.util.dom :as dom] [app.util.object :as obj] [rumext.alpha :as mf])) (declare shape-container-factory) -(defn handle-hover-shape [{:keys [type id]} hover?] - #(when-not (#{:group :frame} type) - (dom/prevent-default %) - (dom/stop-propagation %) - (st/emit! (dv/hover-shape id hover?)))) +(defn handle-hover-shape + [{:keys [type id]} hover?] + (fn [event] + (when-not (#{:group :frame} type) + (dom/prevent-default event) + (dom/stop-propagation event) + (st/emit! (dv/hover-shape id hover?))))) (defn select-shape [{:keys [type id]}] (fn [event] @@ -42,7 +41,7 @@ (dom/stop-propagation event) (dom/prevent-default event) (cond - (.-shiftKey event) + (.-shiftKey ^js event) (st/emit! (dv/toggle-selection id)) :else @@ -154,42 +153,37 @@ :group [:> group-container opts] :svg-raw [:> svg-raw-container opts]))))))) -(defn adjust-frame-position [frame-id objects] - (let [frame (get objects frame-id) - modifier (-> (gpt/point (:x frame) (:y frame)) - (gpt/negate) - (gmt/translate-matrix)) - - update-fn #(assoc-in %1 [%2 :modifiers :displacement] modifier) - modifier-ids (d/concat [frame-id] (cp/get-children frame-id objects))] - (reduce update-fn objects modifier-ids))) - -(defn make-vbox [frame] - (str "0 0 " (:width frame 0) " " (:height frame 0))) - (mf/defc render-frame-svg - {::mf/wrap [mf/memo]} - [{:keys [objects frame-id zoom] :or {zoom 1} :as props}] + [{:keys [page frame local]}] + (let [objects (mf/use-memo + (mf/deps page frame) + (prepare-objects page frame)) - (let [objects (adjust-frame-position frame-id objects) - frame (get objects frame-id) - width (* (:width frame) zoom) - height (* (:height frame) zoom) - vbox (make-vbox frame) - render-frame (mf/use-memo - (mf/deps objects) - #(frame-container-factory objects))] - [:svg {:id "svg-frame" - :view-box vbox - :width width - :height height - :version "1.1" - :xmlnsXlink "http://www.w3.org/1999/xlink" - :xmlns "http://www.w3.org/2000/svg"} + ;; Retrieve frame again with correct modifier + frame (get objects (:id frame)) - [:& render-frame {:shape frame - :view-box vbox}] + zoom (:zoom local 1) + width (* (:width frame) zoom) + height (* (:height frame) zoom) + vbox (str "0 0 " (:width frame 0) " " (:height frame 0)) - [:& selection-feedback {:frame frame}]])) + render (mf/use-memo + (mf/deps objects) + #(frame-container-factory objects))] + + [:svg + {:id "svg-frame" + :view-box vbox + :width width + :height height + :version "1.1" + :xmlnsXlink "http://www.w3.org/1999/xlink" + :xmlns "http://www.w3.org/2000/svg"} + + [:& render {:shape frame :view-box vbox}] + [:& selection-feedback + {:frame frame + :objects objects + :local local}]])) diff --git a/frontend/src/app/main/ui/handoff/right_sidebar.cljs b/frontend/src/app/main/ui/viewer/handoff/right_sidebar.cljs similarity index 53% rename from frontend/src/app/main/ui/handoff/right_sidebar.cljs rename to frontend/src/app/main/ui/viewer/handoff/right_sidebar.cljs index 390aeaf42..2169bab30 100644 --- a/frontend/src/app/main/ui/handoff/right_sidebar.cljs +++ b/frontend/src/app/main/ui/viewer/handoff/right_sidebar.cljs @@ -4,37 +4,26 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.main.ui.handoff.right-sidebar +(ns app.main.ui.viewer.handoff.right-sidebar (:require [app.common.data :as d] - [app.main.store :as st] [app.main.ui.components.tab-container :refer [tab-container tab-element]] - [app.main.ui.handoff.attributes :refer [attributes]] - [app.main.ui.handoff.code :refer [code]] [app.main.ui.icons :as i] + [app.main.ui.viewer.handoff.attributes :refer [attributes]] + [app.main.ui.viewer.handoff.code :refer [code]] + [app.main.ui.viewer.handoff.selection-feedback :refer [resolve-shapes]] [app.main.ui.workspace.sidebar.layers :refer [element-icon]] - [app.util.i18n :refer [t] :as i18n] - [okulary.core :as l] + [app.util.i18n :refer [tr]] [rumext.alpha :as mf])) -(defn make-selected-shapes-iref - [] - (let [selected->shapes - (fn [state] - (let [selected (get-in state [:viewer-local :selected]) - objects (get-in state [:viewer-data :page :objects]) - resolve-shape #(get objects %)] - (mapv resolve-shape selected)))] - #(l/derived selected->shapes st/state))) - (mf/defc right-sidebar - [{:keys [frame page-id file-id]}] - (let [expanded (mf/use-state false) - locale (mf/deref i18n/locale) - section (mf/use-state :info #_:code) - selected-ref (mf/use-memo (make-selected-shapes-iref)) - shapes (mf/deref selected-ref) - selected-type (-> shapes first (:type :not-found))] + [{:keys [frame page file selected]}] + (let [expanded (mf/use-state false) + section (mf/use-state :info #_:code) + + shapes (resolve-shapes (:objects page) selected) + selected-type (or (-> shapes first :type) :not-found)] + [:aside.settings-bar.settings-bar-right {:class (when @expanded "expanded")} [:div.settings-bar-inside (when (seq shapes) @@ -43,24 +32,24 @@ (if (> (count shapes) 1) [:* [:span.tool-window-bar-icon i/layers] - [:span.tool-window-bar-title (t locale "handoff.tabs.code.selected.multiple" (count shapes))]] + [:span.tool-window-bar-title (tr "handoff.tabs.code.selected.multiple" (count shapes))]] [:* [:span.tool-window-bar-icon [:& element-icon {:shape (-> shapes first)}]] - [:span.tool-window-bar-title (->> selected-type d/name (str "handoff.tabs.code.selected.") (t locale))]]) + [:span.tool-window-bar-title (->> selected-type d/name (str "handoff.tabs.code.selected.") (tr))]]) ] [:div.tool-window-content [:& tab-container {:on-change-tab #(do (reset! expanded false) (reset! section %)) :selected @section} - [:& tab-element {:id :info :title (t locale "handoff.tabs.info")} - [:& attributes {:page-id page-id - :file-id file-id + [:& tab-element {:id :info :title (tr "handoff.tabs.info")} + [:& attributes {:page-id (:id page) + :file-id (:id file) :frame frame :shapes shapes}]] - [:& tab-element {:id :code :title (t locale "handoff.tabs.code")} + [:& tab-element {:id :code :title (tr "handoff.tabs.code")} [:& code {:frame frame :shapes shapes :on-expand #(swap! expanded not)}]]]]])]])) diff --git a/frontend/src/app/main/ui/handoff/selection_feedback.cljs b/frontend/src/app/main/ui/viewer/handoff/selection_feedback.cljs similarity index 55% rename from frontend/src/app/main/ui/handoff/selection_feedback.cljs rename to frontend/src/app/main/ui/viewer/handoff/selection_feedback.cljs index 70df26923..51fb68ce4 100644 --- a/frontend/src/app/main/ui/handoff/selection_feedback.cljs +++ b/frontend/src/app/main/ui/viewer/handoff/selection_feedback.cljs @@ -4,12 +4,10 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.main.ui.handoff.selection-feedback +(ns app.main.ui.viewer.handoff.selection-feedback (:require [app.common.geom.shapes :as gsh] - [app.main.store :as st] [app.main.ui.measurements :refer [selection-guides size-display measurement]] - [okulary.core :as l] [rumext.alpha :as mf])) ;; ------------------------------------------------ @@ -21,33 +19,12 @@ (def select-guide-width 1) (def select-guide-dasharray 5) -;; ------------------------------------------------ -;; LENSES -;; ------------------------------------------------ - -(defn make-selected-shapes-iref - "Creates a lens to the current selected shapes" - [] - (let [selected->shapes - (fn [state] - (let [selected (get-in state [:viewer-local :selected]) - objects (get-in state [:viewer-data :page :objects]) - resolve-shape #(get objects %)] - (->> selected (map resolve-shape) (filterv (comp not nil?)))))] - #(l/derived selected->shapes st/state))) - -(defn make-hover-shapes-iref - "Creates a lens to the shapes the user is making hover" - [] - (let [hover->shapes - (fn [state] - (let [hover (get-in state [:viewer-local :hover]) - objects (get-in state [:viewer-data :page :objects])] - (get objects hover)))] - #(l/derived hover->shapes st/state))) - -(def selected-zoom - (l/derived (l/in [:viewer-local :zoom]) st/state)) +(defn resolve-shapes + [objects ids] + (let [resolve-shape #(get objects %)] + (into [] (comp (map resolve-shape) + (filter some?)) + ids))) ;; ------------------------------------------------ ;; HELPERS @@ -75,19 +52,17 @@ :stroke select-color :stroke-width selection-rect-width}}]])) -(mf/defc selection-feedback [{:keys [frame]}] - (let [zoom (mf/deref selected-zoom) - - hover-shapes-ref (mf/use-memo (make-hover-shapes-iref)) - hover-shape (-> (or (mf/deref hover-shapes-ref) frame) - (gsh/translate-to-frame frame)) - - selected-shapes-ref (mf/use-memo (make-selected-shapes-iref)) - selected-shapes (->> (mf/deref selected-shapes-ref) +(mf/defc selection-feedback + [{:keys [frame local objects]}] + (let [{:keys [hover selected zoom]} local + hover-shape (-> (or (first (resolve-shapes objects [hover])) frame) + (gsh/translate-to-frame frame)) + selected-shapes (->> (resolve-shapes objects selected) (map #(gsh/translate-to-frame % frame))) - selrect (gsh/selection-rect selected-shapes) - bounds (frame->bounds frame)] + selrect (gsh/selection-rect selected-shapes) + bounds (frame->bounds frame)] + (when (seq selected-shapes) [:g.selection-feedback {:pointer-events "none"} diff --git a/frontend/src/app/main/ui/viewer/header.cljs b/frontend/src/app/main/ui/viewer/header.cljs index 061afd0f1..52377fa4c 100644 --- a/frontend/src/app/main/ui/viewer/header.cljs +++ b/frontend/src/app/main/ui/viewer/header.cljs @@ -6,304 +6,155 @@ (ns app.main.ui.viewer.header (:require - [app.common.math :as mth] - [app.common.uuid :as uuid] - [app.config :as cfg] - [app.main.data.comments :as dcm] - [app.main.data.messages :as dm] + [app.main.data.modal :as modal] [app.main.data.viewer :as dv] - [app.main.data.viewer.shortcuts :as sc] - [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.components.fullscreen :as fs] [app.main.ui.icons :as i] + [app.main.ui.viewer.comments :refer [comments-menu]] + [app.main.ui.viewer.interactions :refer [interactions-menu]] + [app.main.ui.workspace.header :refer [zoom-widget]] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] - [app.util.router :as rt] - [app.util.webapi :as wapi] [rumext.alpha :as mf])) -(mf/defc zoom-widget - {:wrap [mf/memo]} - [{:keys [zoom - on-increase - on-decrease - on-zoom-to-50 - on-zoom-to-100 - on-zoom-to-200 - on-fullscreen] - :as props}] - (let [show-dropdown? (mf/use-state false)] - [:div.zoom-widget {:on-click #(reset! show-dropdown? true)} - [:span {} (str (mth/round (* 100 zoom)) "%")] - [:span.dropdown-button i/arrow-down] - [:& dropdown {:show @show-dropdown? - :on-close #(reset! show-dropdown? false)} - [:ul.dropdown.zoom-dropdown - [:li {:on-click on-increase} - "Zoom in" [:span (sc/get-tooltip :increase-zoom)]] - [:li {:on-click on-decrease} - "Zoom out" [:span (sc/get-tooltip :decrease-zoom)]] - [:li {:on-click on-zoom-to-50} - "Zoom to 50%" [:span (sc/get-tooltip :zoom-50)]] - [:li {:on-click on-zoom-to-100} - "Zoom to 100%" [:span (sc/get-tooltip :reset-zoom)]] - [:li {:on-click on-zoom-to-200} - "Zoom to 200%" [:span (sc/get-tooltip :zoom-200)]] - [:li {:on-click on-fullscreen} - "Full screen"]]]])) - ;; "Full screen" [:span (sc/get-tooltip :full-screen)]]]]])) - -(mf/defc share-link - [{:keys [token] :as props}] - (let [show-dropdown? (mf/use-state false) - dropdown-ref (mf/use-ref) - create (st/emitf (dv/create-share-link)) - delete (st/emitf (dv/delete-share-link)) - - router (mf/deref refs/router) - route (mf/deref refs/route) - link (rt/resolve router - :viewer - (:path-params route) - {:token token :index "0"}) - link (assoc cfg/public-uri :fragment link) - - copy-link - (fn [_] - (wapi/write-to-clipboard (str link)) - (st/emit! (dm/show {:type :info - :content "Link copied successfuly!" - :timeout 3000})))] - [:* - [:span.btn-primary.btn-small - {:alt (tr "viewer.header.share.title") - :on-click #(swap! show-dropdown? not)} - (tr "viewer.header.share.title")] - - [:& dropdown {:show @show-dropdown? - :on-close #(swap! show-dropdown? not) - :container dropdown-ref} - [:div.dropdown.share-link-dropdown {:ref dropdown-ref} - [:span.share-link-title (tr "viewer.header.share.title")] - [:div.share-link-input - (if (string? token) - [:* - [:span.link (str link)] - [:span.link-button {:on-click copy-link} - (tr "viewer.header.share.copy-link")]] - [:span.link-placeholder (tr "viewer.header.share.placeholder")])] - - [:span.share-link-subtitle (tr "viewer.header.share.subtitle")] - [:div.share-link-buttons - (if (string? token) - [:button.btn-warning {:on-click delete} - (tr "viewer.header.share.remove-link")] - [:button.btn-primary {:on-click create} - (tr "viewer.header.share.create-link")])]]]])) - -(mf/defc interactions-menu - [{:keys [state] :as props}] - (let [imode (:interactions-mode state) - - show-dropdown? (mf/use-state false) - toggle-dropdown (mf/use-fn #(swap! show-dropdown? not)) - hide-dropdown (mf/use-fn #(reset! show-dropdown? false)) - - select-mode - (mf/use-callback - (fn [mode] - (st/emit! (dv/set-interactions-mode mode))))] - - [:div.view-options - [:div.view-options-dropdown {:on-click toggle-dropdown} - [:span (tr "viewer.header.interactions")] - i/arrow-down] - [:& dropdown {:show @show-dropdown? - :on-close hide-dropdown} - [:ul.dropdown.with-check - [:li {:class (dom/classnames :selected (= imode :hide)) - :on-click #(select-mode :hide)} - [:span.icon i/tick] - [:span.label (tr "viewer.header.dont-show-interactions")]] - - [:li {:class (dom/classnames :selected (= imode :show)) - :on-click #(select-mode :show)} - [:span.icon i/tick] - [:span.label (tr "viewer.header.show-interactions")]] - - [:li {:class (dom/classnames :selected (= imode :show-on-click)) - :on-click #(select-mode :show-on-click)} - [:span.icon i/tick] - [:span.label (tr "viewer.header.show-interactions-on-click")]]]]])) - -(mf/defc comments-menu - [] - (let [{cmode :mode cshow :show} (mf/deref refs/comments-local) - - show-dropdown? (mf/use-state false) - toggle-dropdown (mf/use-fn #(swap! show-dropdown? not)) - hide-dropdown (mf/use-fn #(reset! show-dropdown? false)) - - update-mode - (mf/use-callback - (fn [mode] - (st/emit! (dcm/update-filters {:mode mode})))) - - update-show - (mf/use-callback - (fn [mode] - (st/emit! (dcm/update-filters {:show mode}))))] - - [:div.view-options - [:div.icon {:on-click toggle-dropdown} i/eye] - [:& dropdown {:show @show-dropdown? - :on-close hide-dropdown} - [:ul.dropdown.with-check - [:li {:class (dom/classnames :selected (= :all cmode)) - :on-click #(update-mode :all)} - [:span.icon i/tick] - [:span.label (tr "labels.show-all-comments")]] - - [:li {:class (dom/classnames :selected (= :yours cmode)) - :on-click #(update-mode :yours)} - [:span.icon i/tick] - [:span.label (tr "labels.show-your-comments")]] - - [:hr] - - [:li {:class (dom/classnames :selected (= :pending cshow)) - :on-click #(update-show (if (= :pending cshow) :all :pending))} - [:span.icon i/tick] - [:span.label (tr "labels.hide-resolved-comments")]]]]])) - -(mf/defc file-menu - [{:keys [project-id file-id page-id] :as props}] - (let [show-dropdown? (mf/use-state false) - toggle-dropdown (mf/use-fn #(swap! show-dropdown? not)) - hide-dropdown (mf/use-fn #(reset! show-dropdown? false)) - - on-edit - (mf/use-callback - (mf/deps project-id file-id page-id) - (st/emitf (rt/nav :workspace - {:project-id project-id - :file-id file-id} - {:page-id page-id})))] - [:div.file-menu - [:span.btn-icon-dark.btn-small {:on-click toggle-dropdown} - i/actions - [:& dropdown {:show @show-dropdown? - :on-close hide-dropdown} - [:ul.dropdown - [:li {:on-click on-edit} - [:span.label (tr "viewer.header.edit-file")]]]]]])) - -(mf/defc header - [{:keys [data index section state] :as props}] - (let [{:keys [project file page frames]} data - - fullscreen (mf/use-ctx fs/fullscreen-context) - - total (count frames) - profile (mf/deref refs/profile) - teams (mf/deref refs/teams) - - team-id (get-in data [:project :team-id]) - - has-permission? (and (not= uuid/zero (:id profile)) - (contains? teams team-id)) - - project-id (get-in data [:project :id]) - file-id (get-in data [:file :id]) - page-id (get-in data [:page :id]) - - on-click - (mf/use-callback - (st/emitf dv/toggle-thumbnails-panel)) - - on-goback - (mf/use-callback - (mf/deps project) - (st/emitf (dv/go-to-dashboard project))) - - navigate - (mf/use-callback - (mf/deps file-id page-id) - (fn [section] - (st/emit! (dv/go-to-section section)))) +(mf/defc header-options + [{:keys [section zoom page file permissions]}] + (let [fullscreen (mf/use-ctx fs/fullscreen-context) toggle-fullscreen (mf/use-callback - (mf/deps fullscreen) - (fn [] - (if @fullscreen (fullscreen false) (fullscreen true))))] + (mf/deps fullscreen) + (fn [] + (if @fullscreen (fullscreen false) (fullscreen true)))) + + go-to-workspace + (mf/use-callback + (mf/deps page) + (fn [] + (st/emit! (dv/go-to-workspace (:id page))))) + + open-share-dialog + (mf/use-callback + (mf/deps page) + (fn [] + (modal/show! :share-link {:page page :file file})))] + + [:div.options-zone + (case section + :interactions [:& interactions-menu] + :comments [:& comments-menu] + + [:div.view-options]) + + [:& zoom-widget + {:zoom zoom + :on-increase (st/emitf dv/increase-zoom) + :on-decrease (st/emitf dv/decrease-zoom) + :on-zoom-to-50 (st/emitf dv/zoom-to-50) + :on-zoom-to-100 (st/emitf dv/reset-zoom) + :on-zoom-to-200 (st/emitf dv/zoom-to-200) + :on-fullscreen toggle-fullscreen}] + + [:span.btn-icon-dark.btn-small.tooltip.tooltip-bottom-left + {:alt (tr "viewer.header.fullscreen") + :on-click toggle-fullscreen} + (if @fullscreen + i/full-screen-off + i/full-screen)] + + (when (:edit permissions) + [:span.btn-primary {:on-click open-share-dialog} (tr "labels.share-prototype")]) + + (when (:edit permissions) + [:span.btn-text-dark {:on-click go-to-workspace} (tr "labels.edit-file")])])) + +(mf/defc header-sitemap + [{:keys [project file page frame] :as props}] + (let [project-name (:name project) + file-name (:name file) + page-name (:name page) + frame-name (:name frame) + + toggle-thumbnails + (fn [] + (st/emit! dv/toggle-thumbnails-panel)) + + show-dropdown? (mf/use-state false) + + navigate-to + (fn [page-id] + (st/emit! (dv/go-to-page page-id)) + (reset! show-dropdown? false)) + ] + + [:div.sitemap-zone {:alt (tr "viewer.header.sitemap")} + [:div.breadcrumb + {:on-click #(swap! show-dropdown? not)} + [:span.project-name project-name] + [:span "/"] + [:span.file-name file-name] + [:span "/"] + [:span.page-name page-name] + [:span.icon i/arrow-down] + + [:& dropdown {:show @show-dropdown? + :on-close #(swap! show-dropdown? not)} + [:ul.dropdown + (for [id (get-in file [:data :pages])] + [:li {:id (str id) + :on-click (partial navigate-to id)} + (get-in file [:data :pages-index id :name])])]]] + + [:div.current-frame + {:on-click toggle-thumbnails} + [:span.label "/"] + [:span.label frame-name] + [:span.icon i/arrow-down]]])) + +(mf/defc header + [{:keys [project file page frame zoom section permissions]}] + (let [go-to-dashboard + (st/emitf (dv/go-to-dashboard)) + + navigate + (fn [section] + (st/emit! (dv/go-to-section section)))] + [:header.viewer-header [:div.main-icon - [:a {:on-click on-goback + [:a {:on-click go-to-dashboard ;; If the user doesn't have permission we disable the link - :style {:pointer-events (when-not has-permission? "none")}} i/logo-icon]] + :style {:pointer-events (when-not permissions "none")}} i/logo-icon]] - [:div.sitemap-zone {:alt (tr "viewer.header.sitemap") - :on-click on-click} - [:span.project-name (:name project)] - [:span "/"] - [:span.file-name (:name file)] - [:span "/"] - [:span.page-name (:name page)] - [:span.show-thumbnails-button i/arrow-down] - [:span.counters (str (inc index) " / " total)]] + [:& header-sitemap {:project project :file file :page page :frame frame}] [:div.mode-zone [:button.mode-zone-button.tooltip.tooltip-bottom {:on-click #(navigate :interactions) :class (dom/classnames :active (= section :interactions)) - :alt "View mode"} + :alt (tr "viewer.header.interactions-section")} i/play] - (when has-permission? + (when (:edit permissions) [:button.mode-zone-button.tooltip.tooltip-bottom {:on-click #(navigate :comments) :class (dom/classnames :active (= section :comments)) - :alt "Comments"} + :alt (tr "viewer.header.comments-section")} i/chat]) - [:button.mode-zone-button.tooltip.tooltip-bottom - {:on-click #(navigate :handoff) - :class (dom/classnames :active (= section :handoff)) - :alt "Code mode"} - i/code]] + (when (:read permissions) + [:button.mode-zone-button.tooltip.tooltip-bottom + {:on-click #(navigate :handoff) + :class (dom/classnames :active (= section :handoff)) + :alt (tr "viewer.header.handsoff-section")} + i/code])] - [:div.options-zone - (case section - :interactions [:& interactions-menu {:state state}] - :comments [:& comments-menu] - nil) - - (when has-permission? - [:& share-link {:token (:token data) - :page (:page data)}]) - - [:& zoom-widget - {:zoom (:zoom state) - :on-increase (st/emitf dv/increase-zoom) - :on-decrease (st/emitf dv/decrease-zoom) - :on-zoom-to-50 (st/emitf dv/zoom-to-50) - :on-zoom-to-100 (st/emitf dv/reset-zoom) - :on-zoom-to-200 (st/emitf dv/zoom-to-200) - :on-fullscreen toggle-fullscreen}] - - [:span.btn-icon-basic.btn-small.tooltip.tooltip-bottom-left - {:alt (tr "viewer.header.fullscreen") - :on-click toggle-fullscreen} - (if @fullscreen - i/full-screen-off - i/full-screen)] - - (when has-permission? - [:& file-menu {:project-id project-id - :file-id file-id - :page-id page-id}])]])) + [:& header-options {:section section + :permissions permissions + :page page + :file file + :zoom zoom}]])) diff --git a/frontend/src/app/main/ui/viewer/interactions.cljs b/frontend/src/app/main/ui/viewer/interactions.cljs new file mode 100644 index 000000000..d5eeb6379 --- /dev/null +++ b/frontend/src/app/main/ui/viewer/interactions.cljs @@ -0,0 +1,136 @@ +;; 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) UXBOX Labs SL + +(ns app.main.ui.viewer.interactions + (:require + [app.common.data :as d] + [app.common.geom.matrix :as gmt] + [app.common.geom.point :as gpt] + [app.common.pages :as cp] + [app.main.data.comments :as dcm] + [app.main.data.viewer :as dv] + [app.main.refs :as refs] + [app.main.store :as st] + [app.main.ui.components.dropdown :refer [dropdown]] + [app.main.ui.icons :as i] + [app.main.ui.viewer.shapes :as shapes] + [app.util.dom :as dom] + [app.util.i18n :as i18n :refer [tr]] + [app.util.keyboard :as kbd] + [goog.events :as events] + [rumext.alpha :as mf])) + +(defn prepare-objects + [page frame] + (fn [] + (let [objects (:objects page) + frame-id (:id frame) + modifier (-> (gpt/point (:x frame) (:y frame)) + (gpt/negate) + (gmt/translate-matrix)) + + update-fn #(d/update-when %1 %2 assoc-in [:modifiers :displacement] modifier)] + + (->> (cp/get-children frame-id objects) + (d/concat [frame-id]) + (reduce update-fn objects))))) + + +(mf/defc viewport + {::mf/wrap [mf/memo]} + [{:keys [local page frame size]}] + (let [interactions? (:interactions-show? local) + + objects (mf/use-memo + (mf/deps page frame) + (prepare-objects page frame)) + + wrapper (mf/use-memo + (mf/deps objects) + #(shapes/frame-container-factory objects interactions?)) + + ;; Retrieve frame again with correct modifier + frame (get objects (:id frame)) + + on-click + (fn [_] + (let [mode (:interactions-mode local)] + (when (= mode :show-on-click) + (st/emit! dv/flash-interactions)))) + + on-mouse-wheel + (fn [event] + (when (or (kbd/ctrl? event) (kbd/meta? event)) + (dom/prevent-default event) + (let [event (.getBrowserEvent ^js event) + delta (+ (.-deltaY ^js event) (.-deltaX ^js event))] + (if (pos? delta) + (st/emit! dv/decrease-zoom) + (st/emit! dv/increase-zoom))))) + + on-key-down + (fn [event] + (when (kbd/esc? event) + (st/emit! (dcm/close-thread))))] + + (mf/use-effect + (fn [] + ;; bind with passive=false to allow the event to be cancelled + ;; https://stackoverflow.com/a/57582286/3219895 + (let [key1 (events/listen goog/global "wheel" on-mouse-wheel #js {"passive" false}) + key2 (events/listen js/window "keydown" on-key-down) + key3 (events/listen js/window "click" on-click)] + (fn [] + (events/unlistenByKey key1) + (events/unlistenByKey key2) + (events/unlistenByKey key3))))) + + [:svg {:view-box (:vbox size) + :width (:width size) + :height (:height size) + :version "1.1" + :xmlnsXlink "http://www.w3.org/1999/xlink" + :xmlns "http://www.w3.org/2000/svg"} + [:& wrapper {:shape frame + :show-interactions? interactions? + :view-box (:vbox size)}]])) + + +(mf/defc interactions-menu + [] + (let [local (mf/deref refs/viewer-local) + mode (:interactions-mode local) + + show-dropdown? (mf/use-state false) + toggle-dropdown (mf/use-fn #(swap! show-dropdown? not)) + hide-dropdown (mf/use-fn #(reset! show-dropdown? false)) + + select-mode + (mf/use-callback + (fn [mode] + (st/emit! (dv/set-interactions-mode mode))))] + + [:div.view-options {:on-click toggle-dropdown} + [:span.label (tr "viewer.header.interactions")] + [:span.icon i/arrow-down] + [:& dropdown {:show @show-dropdown? + :on-close hide-dropdown} + [:ul.dropdown.with-check + [:li {:class (dom/classnames :selected (= mode :hide)) + :on-click #(select-mode :hide)} + [:span.icon i/tick] + [:span.label (tr "viewer.header.dont-show-interactions")]] + + [:li {:class (dom/classnames :selected (= mode :show)) + :on-click #(select-mode :show)} + [:span.icon i/tick] + [:span.label (tr "viewer.header.show-interactions")]] + + [:li {:class (dom/classnames :selected (= mode :show-on-click)) + :on-click #(select-mode :show-on-click)} + [:span.icon i/tick] + [:span.label (tr "viewer.header.show-interactions-on-click")]]]]])) + diff --git a/frontend/src/app/main/ui/viewer/thumbnails.cljs b/frontend/src/app/main/ui/viewer/thumbnails.cljs index 26397302e..52082e516 100644 --- a/frontend/src/app/main/ui/viewer/thumbnails.cljs +++ b/frontend/src/app/main/ui/viewer/thumbnails.cljs @@ -10,11 +10,11 @@ [app.main.data.viewer :as dv] [app.main.exports :as exports] [app.main.store :as st] - [app.main.ui.components.dropdown :refer [dropdown']] [app.main.ui.icons :as i] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] - [goog.object :as gobj] + [app.util.object :as obj] + [app.util.timers :as ts] [rumext.alpha :as mf])) (mf/defc thumbnails-content @@ -50,7 +50,7 @@ on-mount (fn [] (let [dom (mf/ref-val container)] - (reset! width (gobj/get dom "clientWidth"))))] + (reset! width (obj/get dom "clientWidth"))))] (mf/use-effect on-mount) (if expanded? @@ -72,7 +72,8 @@ [:span.btn-close {:on-click on-close} i/close]]]) (mf/defc thumbnail-item - [{:keys [selected? frame on-click index objects] :as props}] + {::mf/wrap [mf/memo #(mf/deferred % ts/idle-then-raf)]} + [{:keys [selected? frame on-click index objects]}] [:div.thumbnail-item {:on-click #(on-click % index)} [:div.thumbnail-preview {:class (dom/classnames :selected selected?)} @@ -81,42 +82,39 @@ [:span.name {:title (:name frame)} (:name frame)]]]) (mf/defc thumbnails-panel - [{:keys [data index] :as props}] + [{:keys [frames page index show?] :as props}] (let [expanded? (mf/use-state false) container (mf/use-ref) - on-close #(st/emit! dv/toggle-thumbnails-panel) - selected (mf/use-var false) + objects (:objects page) - on-mouse-leave - (fn [_] - (when @selected - (on-close))) + on-close #(st/emit! dv/toggle-thumbnails-panel) + selected (mf/use-var false) on-item-click - (fn [_ index] - (compare-and-set! selected false true) - (st/emit! (dv/go-to-frame-by-index index)) - (when @expanded? - (on-close)))] + (mf/use-callback + (mf/deps @expanded?) + (fn [_ index] + (compare-and-set! selected false true) + (st/emit! (dv/go-to-frame-by-index index)) + (when @expanded? + (on-close))))] - [:& dropdown' {:on-close on-close - :container container - :show true} - [:section.viewer-thumbnails - {:class (dom/classnames :expanded @expanded?) - :ref container - :on-mouse-leave on-mouse-leave} + [:section.viewer-thumbnails + {:class (dom/classnames :expanded @expanded? + :invisible (not show?)) - [:& thumbnails-summary {:on-toggle-expand #(swap! expanded? not) - :on-close on-close - :total (count (:frames data))}] - [:& thumbnails-content {:expanded? @expanded? - :total (count (:frames data))} - (for [[i frame] (d/enumerate (:frames data))] - [:& thumbnail-item {:key i - :index i - :frame frame - :objects (:objects data) - :on-click on-item-click - :selected? (= i index)}])]]])) + :ref container + } + + [:& thumbnails-summary {:on-toggle-expand #(swap! expanded? not) + :on-close on-close + :total (count frames)}] + [:& thumbnails-content {:expanded? @expanded? + :total (count frames)} + (for [[i frame] (d/enumerate frames)] + [:& thumbnail-item {:index i + :frame frame + :objects objects + :on-click on-item-click + :selected? (= i index)}])]])) diff --git a/frontend/src/app/main/ui/workspace/header.cljs b/frontend/src/app/main/ui/workspace/header.cljs index e95822115..bc1856e5c 100644 --- a/frontend/src/app/main/ui/workspace/header.cljs +++ b/frontend/src/app/main/ui/workspace/header.cljs @@ -64,15 +64,16 @@ on-decrease on-zoom-reset on-zoom-fit - on-zoom-selected] + on-zoom-selected + on-fullscreen] :as props}] (let [show-dropdown? (mf/use-state false)] [:div.zoom-widget {:on-click #(reset! show-dropdown? true)} - [:span {} (str (mth/round (* 100 zoom)) "%")] - [:span.dropdown-button i/arrow-down] + [:span.label {} (str (mth/round (* 100 zoom)) "%")] + [:span.icon i/arrow-down] [:& dropdown {:show @show-dropdown? :on-close #(reset! show-dropdown? false)} - [:ul.zoom-dropdown + [:ul.dropdown [:li {:on-click on-increase} "Zoom in" [:span (sc/get-tooltip :increase-zoom)]] [:li {:on-click on-decrease} @@ -82,7 +83,11 @@ [:li {:on-click on-zoom-fit} "Zoom to fit all" [:span (sc/get-tooltip :fit-all)]] [:li {:on-click on-zoom-selected} - "Zoom to selected" [:span (sc/get-tooltip :zoom-selected)]]]]])) + "Zoom to selected" [:span (sc/get-tooltip :zoom-selected)]] + (when on-fullscreen + [:li {:on-click on-fullscreen} + "Full screen"])]]])) + ;; --- Header Users diff --git a/frontend/src/app/util/router.cljs b/frontend/src/app/util/router.cljs index dd3078c75..2329af2b6 100644 --- a/frontend/src/app/util/router.cljs +++ b/frontend/src/app/util/router.cljs @@ -108,14 +108,30 @@ (let [router (:router state) path (resolve router id params qparams) uri (-> (u/uri cfg/public-uri) - (assoc :fragment path))] - (js/window.open (str uri) "_blank")))) + (assoc :fragment path)) + name (str (name id) "-" (:file-id params))] + (js/window.open (str uri) name)))) (defn nav-new-window ([id] (nav-new-window id nil nil)) ([id params] (nav-new-window id params nil)) ([id params qparams] (NavigateNewWindow. id params qparams))) + +(defn nav-new-window* + [{:keys [rname path-params query-params name]}] + (ptk/reify ::nav-new-window + ptk/EffectEvent + (effect [_ state _] + (let [router (:router state) + path (resolve router rname path-params query-params) + uri (-> (u/uri cfg/public-uri) + (assoc :fragment path))] + + + + (js/window.open (str uri) name))))) + ;; --- History API (defn initialize-history diff --git a/frontend/translations/de.po b/frontend/translations/de.po index 476375201..216acd571 100644 --- a/frontend/translations/de.po +++ b/frontend/translations/de.po @@ -1330,7 +1330,7 @@ msgid "viewer.header.share.subtitle" msgstr "Jeder mit dem Link hat Zugriff" #: src/app/main/ui/viewer/header.cljs, src/app/main/ui/viewer/header.cljs, src/app/main/ui/viewer/header.cljs -msgid "viewer.header.share.title" +msgid "labels.share-prototype" msgstr "Prototyp teilen" #: src/app/main/ui/viewer/header.cljs diff --git a/frontend/translations/el.po b/frontend/translations/el.po index b8a58c011..5f03095a7 100644 --- a/frontend/translations/el.po +++ b/frontend/translations/el.po @@ -1280,7 +1280,7 @@ msgid "viewer.header.share.subtitle" msgstr "Όποιος έχει τον link θα έχει πρόσβαση" #: src/app/main/ui/viewer/header.cljs, src/app/main/ui/viewer/header.cljs, src/app/main/ui/viewer/header.cljs -msgid "viewer.header.share.title" +msgid "labels.share-prototype" msgstr "Μοιραστείτε το link" #: src/app/main/ui/viewer/header.cljs diff --git a/frontend/translations/en.po b/frontend/translations/en.po index f86d710d2..2b54ff944 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -1524,8 +1524,7 @@ msgstr "Artboard not found." msgid "viewer.header.dont-show-interactions" msgstr "Don't show interactions" -#: src/app/main/ui/viewer/header.cljs -msgid "viewer.header.edit-file" +msgid "labels.edit-file" msgstr "Edit file" #: src/app/main/ui/viewer/header.cljs @@ -1556,8 +1555,7 @@ msgstr "Remove link" msgid "viewer.header.share.subtitle" msgstr "Anyone with the link will have access" -#: src/app/main/ui/viewer/header.cljs, src/app/main/ui/viewer/header.cljs, src/app/main/ui/viewer/header.cljs -msgid "viewer.header.share.title" +msgid "labels.share-prototype" msgstr "Share prototype" #: src/app/main/ui/viewer/header.cljs @@ -2762,3 +2760,57 @@ msgstr "Update" msgid "workspace.viewport.click-to-close-path" msgstr "Click to close the path" + +msgid "viewer.header.interactions-section" +msgstr "Interactions" + +msgid "viewer.header.comments-section" +msgstr "Comments" + +msgid "viewer.header.handsoff-section" +msgstr "Handsoff" + +msgid "common.share-link.title" +msgstr "Share prototypes" + +msgid "common.share-link.permissions-hint" +msgstr "Anyone with link will have access" + +msgid "common.share-link.permissions-can-access" +msgstr "Can access" + +msgid "common.share-link.permissions-can-view" +msgstr "Can view" + +msgid "common.share-link.view-all-pages" +msgstr "All pages" + +msgid "common.share-link.view-selected-pages" +msgstr "Selected pages" + +msgid "common.share-link.view-current-page" +msgstr "Only this page" + +msgid "common.share-link.confirm-deletion-link-description" +msgstr "Are you sure you want to remove this link? If you do it, it's no longer be available for anyone" + +msgid "common.share-link.remove-link" +msgstr "Remove link" + +msgid "common.share-link.get-link" +msgstr "Get link" + +msgid "common.share-link.link-copied-success" +msgstr "Link copied successfully" + +msgid "common.share-link.link-deleted-success" +msgstr "Link deleted successfully" + +msgid "labels.workspace" +msgstr "Workspace" + +msgid "labels.default" +msgstr "default" + +msgid "labels.link" +msgstr "Link" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 12d57eb64..0fa15cde3 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -1517,7 +1517,7 @@ msgid "viewer.header.dont-show-interactions" msgstr "No mostrar interacciones" #: src/app/main/ui/viewer/header.cljs -msgid "viewer.header.edit-file" +msgid "labels.edit-file" msgstr "Editar archivo" #: src/app/main/ui/viewer/header.cljs @@ -1549,7 +1549,7 @@ msgid "viewer.header.share.subtitle" msgstr "Cualquiera con el enlace podrá acceder" #: src/app/main/ui/viewer/header.cljs, src/app/main/ui/viewer/header.cljs, src/app/main/ui/viewer/header.cljs -msgid "viewer.header.share.title" +msgid "labels.share-prototype" msgstr "Compartir prototipo" #: src/app/main/ui/viewer/header.cljs @@ -2758,3 +2758,60 @@ msgstr "Actualizar" msgid "workspace.viewport.click-to-close-path" msgstr "Pulsar para cerrar la ruta" + + + + +msgid "viewer.header.interactions-section" +msgstr "Interacciones" + +msgid "viewer.header.comments-section" +msgstr "Comentarios" + +msgid "viewer.header.handsoff-section" +msgstr "Handsoff" + +msgid "common.share-link.title" +msgstr "Compartir prototipos" + +msgid "common.share-link.permissions-hint" +msgstr "Cualquiera con el enlace puede acceder" + +msgid "common.share-link.permissions-can-access" +msgstr "Puede acceder a" + +msgid "common.share-link.permissions-can-view" +msgstr "Puede ver" + +msgid "common.share-link.view-all-pages" +msgstr "Todas las paginas" + +msgid "common.share-link.view-selected-pages" +msgstr "Paginas seleccionadas" + +msgid "common.share-link.view-current-page" +msgstr "Solo esta pagina" + +msgid "common.share-link.confirm-deletion-link-description" +msgstr "¿Estas seguro que quieres eliminar el enlace? Si lo haces, el enlace dejara de funcionar para todos" + +msgid "common.share-link.remove-link" +msgstr "Eliminar enlace" + +msgid "common.share-link.get-link" +msgstr "Obtener enlace" + +msgid "common.share-link.link-copied-success" +msgstr "Enlace copiado satisfactoriamente" + +msgid "common.share-link.link-deleted-success" +msgstr "Enlace eliminado correctamente" + +msgid "labels.workspace" +msgstr "Espacio de trabajo" + +msgid "labels.default" +msgstr "por defecto" + +msgid "labels.link" +msgstr "Enlace" diff --git a/frontend/translations/fr.po b/frontend/translations/fr.po index b43da1080..d5090089f 100644 --- a/frontend/translations/fr.po +++ b/frontend/translations/fr.po @@ -1226,7 +1226,7 @@ msgid "viewer.header.share.subtitle" msgstr "Toute personne disposant du lien aura accès" #: src/app/main/ui/viewer/header.cljs, src/app/main/ui/viewer/header.cljs, src/app/main/ui/viewer/header.cljs -msgid "viewer.header.share.title" +msgid "labels.share-prototype" msgstr "Partager le prototype" #: src/app/main/ui/viewer/header.cljs diff --git a/frontend/translations/ro.po b/frontend/translations/ro.po index f12369e26..221142f6a 100644 --- a/frontend/translations/ro.po +++ b/frontend/translations/ro.po @@ -1465,7 +1465,7 @@ msgid "viewer.header.share.subtitle" msgstr "Prin acest link se permite accesul public" #: src/app/main/ui/viewer/header.cljs, src/app/main/ui/viewer/header.cljs, src/app/main/ui/viewer/header.cljs -msgid "viewer.header.share.title" +msgid "labels.share-prototype" msgstr "Distribuie link" #: src/app/main/ui/viewer/header.cljs diff --git a/frontend/translations/ru.po b/frontend/translations/ru.po index 4d681bf2b..2229bc7b7 100644 --- a/frontend/translations/ru.po +++ b/frontend/translations/ru.po @@ -599,7 +599,7 @@ msgid "viewer.header.share.subtitle" msgstr "Любой, у кого есть ссылка будет иметь доступ" #: src/app/main/ui/viewer/header.cljs, src/app/main/ui/viewer/header.cljs, src/app/main/ui/viewer/header.cljs -msgid "viewer.header.share.title" +msgid "labels.share-prototype" msgstr "Поделиться ссылкой" #: src/app/main/ui/viewer/header.cljs diff --git a/frontend/translations/tr.po b/frontend/translations/tr.po index 55e5587d0..d7488b00e 100644 --- a/frontend/translations/tr.po +++ b/frontend/translations/tr.po @@ -1560,7 +1560,7 @@ msgid "viewer.header.show-interactions" msgstr "Etkileşimleri göster" #: src/app/main/ui/viewer/header.cljs, src/app/main/ui/viewer/header.cljs, src/app/main/ui/viewer/header.cljs -msgid "viewer.header.share.title" +msgid "labels.share-prototype" msgstr "Bağlantıyı paylaş" #: src/app/main/ui/viewer/header.cljs diff --git a/frontend/translations/zh_CN.po b/frontend/translations/zh_CN.po index 37b1d7005..ed9661902 100644 --- a/frontend/translations/zh_CN.po +++ b/frontend/translations/zh_CN.po @@ -1223,7 +1223,7 @@ msgid "viewer.header.share.subtitle" msgstr "任何人都可以通过本链接访问" #: src/app/main/ui/viewer/header.cljs, src/app/main/ui/viewer/header.cljs, src/app/main/ui/viewer/header.cljs -msgid "viewer.header.share.title" +msgid "labels.share-prototype" msgstr "分享链接" #: src/app/main/ui/viewer/header.cljs