🎉 Share link & pages on viewer.

This commit is contained in:
Andrey Antukh 2021-08-16 15:46:02 +02:00 committed by Andrés Moya
parent 3532263af4
commit c8102f4bff
58 changed files with 1837 additions and 1245 deletions

View file

@ -231,9 +231,9 @@
(defn get-by-params (defn get-by-params
([ds table params] ([ds table params]
(get-by-params ds table params nil)) (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))] (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 (ex/raise :type :not-found
:table table :table table
:hint "database object not found")) :hint "database object not found"))
@ -267,13 +267,28 @@
(instance? PGpoint v)) (instance? PGpoint v))
(defn pgarray? (defn pgarray?
[v] ([v] (instance? PgArray v))
(instance? PgArray v)) ([v type]
(and (instance? PgArray v)
(= type (.getBaseTypeName ^PgArray v)))))
(defn pgarray-of-uuid? (defn pgarray-of-uuid?
[v] [v]
(and (pgarray? v) (= "uuid" (.getBaseTypeName ^PgArray 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 (defn pgpoint
[p] [p]
(PGpoint. (:x p) (:y p))) (PGpoint. (:x p) (:y p)))
@ -369,15 +384,6 @@
(.setType "jsonb") (.setType "jsonb")
(.setValue (json/encode-str data)))) (.setValue (json/encode-str data))))
(defn pgarray->set
[v]
(set (.getArray ^PgArray v)))
(defn pgarray->vector
[v]
(vec (.getArray ^PgArray v)))
;; --- Locks ;; --- Locks
(defn- xact-check-param (defn- xact-check-param

View file

@ -81,6 +81,9 @@
(try (try
(let [tw (t/writer output-stream opts)] (let [tw (t/writer output-stream opts)]
(t/write! tw data)) (t/write! tw data))
(catch Throwable e
(l/error :hint "exception on writting data to response"
:cause e))
(finally (finally
(.close ^java.io.OutputStream output-stream)))))) (.close ^java.io.OutputStream output-stream))))))

View file

@ -193,6 +193,12 @@
{:name "0061-mod-file-table" {:name "0061-mod-file-table"
:fn (mg/resource "app/migrations/sql/0061-mod-file-table.sql")} :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")}
]) ])

View file

@ -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);

View file

@ -175,6 +175,7 @@
'app.rpc.mutations.management 'app.rpc.mutations.management
'app.rpc.mutations.ldap 'app.rpc.mutations.ldap
'app.rpc.mutations.fonts 'app.rpc.mutations.fonts
'app.rpc.mutations.share-link
'app.rpc.mutations.verify-token) 'app.rpc.mutations.verify-token)
(map (partial process-method cfg)) (map (partial process-method cfg))
(into {})))) (into {}))))

View file

@ -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)))

View file

@ -37,6 +37,41 @@
:is-admin false :is-admin false
:can-edit 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 (defn make-edition-check-fn
"A simple factory for edition permission check functions." "A simple factory for edition permission check functions."
[qfn] [qfn]

View file

@ -61,16 +61,23 @@
(defn- retrieve-file-permissions (defn- retrieve-file-permissions
[conn profile-id file-id] [conn profile-id file-id]
(when (and profile-id file-id)
(db/exec! conn [sql:file-permissions (db/exec! conn [sql:file-permissions
file-id profile-id file-id profile-id
file-id profile-id 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! (def check-edition-permissions!
(perms/make-edition-check-fn retrieve-file-permissions)) (perms/make-check-fn has-edit-permissions?))
(def check-read-permissions! (def check-read-permissions!
(perms/make-read-check-fn retrieve-file-permissions)) (perms/make-check-fn has-read-permissions?))
;; --- Query: Files search ;; --- Query: Files search

View file

@ -14,24 +14,97 @@
[app.util.services :as sv] [app.util.services :as sv]
[clojure.spec.alpha :as s])) [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) ;; --- Query: Viewer Bundle (by Page ID)
;; DEPRECATED: should be removed in 1.9.x
(declare check-shared-token!) (declare check-shared-token!)
(declare retrieve-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 ::id ::us/uuid)
(s/def ::file-id ::us/uuid)
(s/def ::page-id ::us/uuid) (s/def ::page-id ::us/uuid)
(s/def ::token ::us/string) (s/def ::token ::us/string)
@ -81,6 +154,3 @@
[conn file-id page-id] [conn file-id page-id]
(let [sql "select * from file_share_token where file_id=? and page_id=?"] (let [sql "select * from file_share_token where file_id=? and page_id=?"]
(db/exec-one! conn [sql file-id page-id]))) (db/exec-one! conn [sql file-id page-id])))

View file

@ -60,7 +60,7 @@
(defmethod handle-deletion :team-font-variant (defmethod handle-deletion :team-font-variant
[{:keys [conn storage]} {:keys [id] :as props}] [{: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)] storage (assoc storage :conn conn)]
(when (:deleted-at font) (when (:deleted-at font)
(db/delete! conn :team-font-variant {:id id}) (db/delete! conn :team-font-variant {:id id})

View file

@ -24,10 +24,10 @@
file (th/create-file* 1 {:profile-id (:id prof) file (th/create-file* 1 {:profile-id (:id prof)
:project-id proj-id :project-id proj-id
:is-shared false}) :is-shared false})
token (atom nil)] share-id (atom nil)]
(t/testing "authenticated with page-id" (t/testing "authenticated with page-id"
(let [data {::th/type :viewer-bundle (let [data {::th/type :view-only-bundle
:profile-id (:id prof) :profile-id (:id prof)
:file-id (:id file) :file-id (:id file)
:page-id (get-in file [:data :pages 0])} :page-id (get-in file [:data :pages 0])}
@ -38,26 +38,28 @@
(t/is (nil? (:error out))) (t/is (nil? (:error out)))
(let [result (:result out)] (let [result (:result out)]
(t/is (contains? result :token)) (t/is (contains? result :share-links))
(t/is (contains? result :page)) (t/is (contains? result :permissions))
(t/is (contains? result :libraries))
(t/is (contains? result :file)) (t/is (contains? result :file))
(t/is (contains? result :project))))) (t/is (contains? result :project)))))
(t/testing "generate share token" (t/testing "generate share token"
(let [data {::th/type :create-file-share-token (let [data {::th/type :create-share-link
:profile-id (:id prof) :profile-id (:id prof)
:file-id (:id file) :file-id (:id file)
:page-id (get-in file [:data :pages 0])} :pages #{(get-in file [:data :pages 0])}
:flags #{}}
out (th/mutation! data)] out (th/mutation! data)]
;; (th/print-result! out) ;; (th/print-result! out)
(t/is (nil? (:error out))) (t/is (nil? (:error out)))
(let [result (:result out)] (let [result (:result out)]
(t/is (string? (:token result))) (t/is (uuid? (:id result)))
(reset! token (:token result))))) (reset! share-id (:id result)))))
(t/testing "not authenticated with page-id" (t/testing "not authenticated with page-id"
(let [data {::th/type :viewer-bundle (let [data {::th/type :view-only-bundle
:profile-id (:id prof2) :profile-id (:id prof2)
:file-id (:id file) :file-id (:id file)
:page-id (get-in file [:data :pages 0])} :page-id (get-in file [:data :pages 0])}
@ -70,32 +72,33 @@
(t/is (= (:type error-data) :not-found)) (t/is (= (:type error-data) :not-found))
(t/is (= (:code error-data) :object-not-found))))) (t/is (= (:code error-data) :object-not-found)))))
;; (t/testing "authenticated with token & profile" (t/testing "authenticated with token & profile"
;; (let [data {::sq/type :viewer-bundle (let [data {::th/type :view-only-bundle
;; :profile-id (:id prof2) :profile-id (:id prof2)
;; :token @token :share-id @share-id
;; :file-id (:id file) :file-id (:id file)
;; :page-id (get-in file [:data :pages 0])} :page-id (get-in file [:data :pages 0])}
;; out (th/try-on! (sq/handle data))] out (th/query! data)]
;; ;; (th/print-result! out) ;; (th/print-result! out)
(t/is (nil? (:error out)))
;; (let [result (:result out)] (let [result (:result out)]
;; (t/is (contains? result :page)) (t/is (contains? result :share))
;; (t/is (contains? result :file)) (t/is (contains? result :file))
;; (t/is (contains? result :project))))) (t/is (contains? result :project)))))
;; (t/testing "authenticated with token" (t/testing "authenticated with token"
;; (let [data {::sq/type :viewer-bundle (let [data {::th/type :view-only-bundle
;; :token @token :share-id @share-id
;; :file-id (:id file) :file-id (:id file)
;; :page-id (get-in file [:data :pages 0])} :page-id (get-in file [:data :pages 0])}
;; out (th/try-on! (sq/handle data))] 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)))))
)) ))

View file

@ -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 { .btn-gray {
@extend %btn; @extend %btn;
background: $color-gray-30; background: $color-gray-30;
@ -588,7 +606,6 @@ input.element-name {
box-sizing: border-box; box-sizing: border-box;
flex-shrink: 0; flex-shrink: 0;
} }
} }
&.column { &.column {

View file

@ -88,3 +88,4 @@
@import "main/partials/color-bullet"; @import "main/partials/color-bullet";
@import "main/partials/handoff"; @import "main/partials/handoff";
@import "main/partials/exception-page"; @import "main/partials/exception-page";
@import "main/partials/share-link";

View file

@ -53,8 +53,8 @@
.icon { .icon {
display: flex; display: flex;
align-items: center; align-items: center;
width: 25px; width: 20px;
height: 25px; height: 20px;
margin-right: 7px; margin-right: 7px;
} }
} }

View file

@ -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;
}
}
}
}

View file

@ -42,17 +42,50 @@
} }
} }
.options-zone {
align-items: center;
display: flex;
// width: 384px;
justify-content: flex-end;
position: relative;
> * {
margin-left: $big;
}
.btn-primary {
flex-shrink: 0;
}
.zoom-widget {
.dropdown {
top: 45px;
left: 25px;
}
}
.view-options { .view-options {
.icon { align-items: center;
cursor: pointer;
display: flex;
width: 90px;
> span {
color: $color-gray-10;
font-size: $fs13;
margin-right: $x-small;
}
> .icon {
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
justify-content: center; justify-content: center;
svg { svg {
fill: $color-gray-30; fill: $color-gray-10;
height: 30px; height: 12px;
width: 28px; width: 12px;
} }
&:hover { &:hover {
@ -64,34 +97,9 @@
.dropdown { .dropdown {
min-width: 260px; min-width: 260px;
left: 0px; top: 45px;
top: 40px; left: -25px;
} }
.view-options-dropdown {
align-items: center;
cursor: pointer;
display: flex;
span {
color: $color-gray-10;
font-size: $fs13;
margin-right: $x-small;
}
svg {
fill: $color-gray-10;
height: 12px;
width: 12px;
}
}
}
.file-menu {
.dropdown {
min-width: 100px;
right: 0px;
top: 40px;
} }
} }
@ -100,40 +108,47 @@
cursor: pointer; cursor: pointer;
display: flex; display: flex;
padding: $x-small; padding: $x-small;
position: relative;
.icon {
display: flex;
justify-content: center;
align-items: center;
svg { svg {
fill: $color-gray-20; fill: $color-gray-20;
height: 20px; height: 12px;
margin-right: $small; margin-right: $small;
width: 20px; width: 12px;
}
} }
span { .breadcrumb {
display: flex;
position: relative;
> span {
color: $color-gray-20; color: $color-gray-20;
margin-right: $x-small; margin-right: $x-small;
font-size: $fs14; font-size: $fs14;
overflow-x: hidden; overflow-x: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
}
&.frame-name { > .dropdown {
top: 45px;
right: 10px;
}
}
.current-frame {
display: flex;
span {
color: $color-white; color: $color-white;
margin-right: $x-small;
} }
} }
.show-thumbnails-button svg {
fill: $color-white;
height: 10px;
width: 10px;
}
.page-name {
color: $color-white;
}
.counters {
margin-left: $size-3;
}
} }
.mode-zone { .mode-zone {
@ -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 { .users-zone {
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;

View file

@ -1,4 +1,3 @@
.viewer-thumbnails { .viewer-thumbnails {
grid-row: 1 / span 1; grid-row: 1 / span 1;
grid-column: 1 / span 1; grid-column: 1 / span 1;
@ -9,6 +8,11 @@
flex-direction: column; flex-direction: column;
z-index: 12; z-index: 12;
&.invisible {
visibility: hidden;
pointer-events: none;
}
&.expanded { &.expanded {
grid-row: 1 / span 2; grid-row: 1 / span 2;
@ -159,7 +163,7 @@
&:hover { &:hover {
border-color: $color-primary; border-color: $color-primary;
border-width: 2px; outline: 2px solid $color-primary;
} }
} }

View file

@ -8,13 +8,13 @@
margin-left: $x-small; margin-left: $x-small;
} }
.dropdown-button svg { .icon svg {
fill: $color-gray-10; fill: $color-gray-10;
height: 10px; height: 10px;
width: 10px; width: 10px;
} }
.zoom-dropdown { .dropdown {
position: absolute; position: absolute;
z-index: 12; z-index: 12;
width: 210px; width: 210px;

View file

@ -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)))))

View file

@ -14,24 +14,12 @@
[app.main.data.comments :as dcm] [app.main.data.comments :as dcm]
[app.main.data.fonts :as df] [app.main.data.fonts :as df]
[app.main.repo :as rp] [app.main.repo :as rp]
[app.util.globals :as ug]
[app.util.router :as rt] [app.util.router :as rt]
[beicon.core :as rx] [beicon.core :as rx]
[cljs.spec.alpha :as s] [cljs.spec.alpha :as s]
[potok.core :as ptk])) [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 ;; --- Local State Initialization
(def ^:private (def ^:private
@ -49,25 +37,24 @@
(declare fetch-bundle) (declare fetch-bundle)
(declare bundle-fetched) (declare bundle-fetched)
(s/def ::page-id ::us/uuid)
(s/def ::file-id ::us/uuid) (s/def ::file-id ::us/uuid)
(s/def ::index ::us/integer) (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 ::section ::us/string)
(s/def ::initialize-params (s/def ::initialize-params
(s/keys :req-un [::page-id ::file-id] (s/keys :req-un [::file-id]
:opt-un [::token])) :opt-un [::share-id ::page-id]))
(defn initialize (defn initialize
[{:keys [page-id file-id] :as params}] [{:keys [file-id] :as params}]
(us/assert ::initialize-params params) (us/assert ::initialize-params params)
(ptk/reify ::initialize (ptk/reify ::initialize
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(-> state (-> state
(assoc :current-file-id file-id) (assoc :current-file-id file-id)
(assoc :current-page-id page-id)
(update :viewer-local (update :viewer-local
(fn [lstate] (fn [lstate]
(if (nil? lstate) (if (nil? lstate)
@ -77,55 +64,72 @@
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ _ _]
(rx/of (fetch-bundle params) (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 (defn finalize
(s/keys :req-un [::page-id ::file-id] [_]
:opt-un [::token])) (ptk/reify ::finalize
ptk/UpdateEvent
(update [_ state]
(dissoc state :viewer))))
(defn fetch-bundle (defn select-frames
[{:keys [page-id file-id token] :as params}] [{:keys [objects] :as page}]
(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]
(let [root (get objects uuid/zero)] (let [root (get objects uuid/zero)]
(into [] (comp (map #(get objects %)) (into [] (comp (map #(get objects %))
(filter #(= :frame (:type %)))) (filter #(= :frame (:type %))))
(reverse (:shapes root))))) (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 (defn bundle-fetched
[{:keys [project file page share-token token libraries users] :as bundle}] [{:keys [project file share-links libraries users permissions] :as bundle}]
(us/verify ::bundle 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/reify ::bundle-fetched
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(let [objects (:objects page)
frames (extract-frames objects)]
(-> state (-> state
(assoc :viewer-libraries (d/index-by :id libraries)) (assoc :share-links share-links)
(update :viewer-data assoc (assoc :viewer {:libraries (d/index-by :id libraries)
:project project
:objects objects
:users (d/index-by :id users) :users (d/index-by :id users)
:file file :permissions permissions
:page page :project project
:frames frames :pages pages
:token token :file file}))))))
:share-token share-token))))))
(defn fetch-comment-threads (defn fetch-comment-threads
[{:keys [file-id page-id] :as params}] [{:keys [file-id page-id] :as params}]
@ -168,32 +172,6 @@
(->> (rp/query :comments {:thread-id thread-id}) (->> (rp/query :comments {:thread-id thread-id})
(rx/map #(partial fetched %))))))) (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 ;; --- Zoom Management
(def increase-zoom (def increase-zoom
@ -245,29 +223,32 @@
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(let [route (:route state) (let [route (:route state)
screen (-> route :data :name keyword)
qparams (:query-params route) qparams (:query-params route)
pparams (:path-params route) pparams (:path-params route)
index (:index qparams)] index (:index qparams)]
(when (pos? index) (when (pos? index)
(rx/of (rx/of
(dcm/close-thread) (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 (def select-next-frame
(ptk/reify ::select-prev-frame (ptk/reify ::select-prev-frame
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(prn "select-next-frame")
(let [route (:route state) (let [route (:route state)
screen (-> route :data :name keyword)
qparams (:query-params route)
pparams (:path-params route) pparams (:path-params route)
qparams (:query-params route)
page-id (:page-id pparams)
index (:index qparams) 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)) (when (< index (dec total))
(rx/of (rx/of
(dcm/close-thread) (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}) (s/def ::interactions-mode #{:hide :show :show-on-click})
@ -329,7 +310,6 @@
(when index (when index
(rx/of (go-to-frame-by-index index))))))) (rx/of (go-to-frame-by-index index)))))))
(defn go-to-section (defn go-to-section
[section] [section]
(ptk/reify ::go-to-section (ptk/reify ::go-to-section
@ -340,7 +320,6 @@
qparams (:query-params route)] qparams (:query-params route)]
(rx/of (rt/nav :viewer pparams (assoc qparams :section section))))))) (rx/of (rt/nav :viewer pparams (assoc qparams :section section)))))))
(defn set-current-frame [frame-id] (defn set-current-frame [frame-id]
(ptk/reify ::set-current-frame (ptk/reify ::set-current-frame
ptk/UpdateEvent ptk/UpdateEvent
@ -405,18 +384,50 @@
(let [toggled? (contains? (get-in state [:viewer-local :collapsed]) id)] (let [toggled? (contains? (get-in state [:viewer-local :collapsed]) id)]
(update-in state [:viewer-local :collapsed] (if toggled? disj conj) 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/reify ::hover-shape
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(assoc-in state [:viewer-local :hover] (when hover? id))))) (assoc-in state [:viewer-local :hover] (when hover? id)))))
;; --- NAV
(defn go-to-dashboard (defn go-to-dashboard
([] (go-to-dashboard nil)) []
([{:keys [team-id]}]
(ptk/reify ::go-to-dashboard (ptk/reify ::go-to-dashboard
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(let [team-id (or team-id (get-in state [:viewer-data :project :team-id]))] (let [team-id (get-in state [:viewer :project :team-id])
(rx/of (rt/nav :dashboard-projects {:team-id 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 [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)}))))))

View file

@ -37,6 +37,7 @@
[app.main.repo :as rp] [app.main.repo :as rp]
[app.main.streams :as ms] [app.main.streams :as ms]
[app.main.worker :as uw] [app.main.worker :as uw]
[app.util.globals :as ug]
[app.util.http :as http] [app.util.http :as http]
[app.util.i18n :as i18n] [app.util.i18n :as i18n]
[app.util.router :as rt] [app.util.router :as rt]
@ -171,7 +172,12 @@
(->> stream (->> stream
(rx/filter #(= ::dwc/index-initialized %)) (rx/filter #(= ::dwc/index-initialized %))
(rx/first) (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 (defn- file-initialized
[{:keys [file users project libraries] :as bundle}] [{:keys [file users project libraries] :as bundle}]
@ -1273,10 +1279,14 @@
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(let [{:keys [current-file-id current-page-id]} state (let [{:keys [current-file-id current-page-id]} state
params {:file-id (or file-id current-file-id) pparams {:file-id (or file-id current-file-id)}
:page-id (or page-id current-page-id)}] qparams {:page-id (or page-id current-page-id)
:index 0}]
(rx/of ::dwp/force-persist (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 (defn go-to-dashboard
([] (go-to-dashboard nil)) ([] (go-to-dashboard nil))

View file

@ -38,6 +38,9 @@
(def threads-ref (def threads-ref
(l/derived :comment-threads st/state)) (l/derived :comment-threads st/state))
(def share-links
(l/derived :share-links st/state))
;; ---- Dashboard refs ;; ---- Dashboard refs
(def dashboard-local (def dashboard-local
@ -287,8 +290,17 @@
;; ---- Viewer refs ;; ---- Viewer refs
(def viewer-file
(l/derived :viewer-file st/state))
(def viewer-project
(l/derived :viewer-file st/state))
(def viewer-data (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 (def viewer-local
(l/derived :viewer-local st/state)) (l/derived :viewer-local st/state))

View file

@ -20,7 +20,6 @@
[app.main.ui.context :as ctx] [app.main.ui.context :as ctx]
[app.main.ui.cursors :as c] [app.main.ui.cursors :as c]
[app.main.ui.dashboard :refer [dashboard]] [app.main.ui.dashboard :refer [dashboard]]
[app.main.ui.handoff :refer [handoff]]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.main.ui.messages :as msgs] [app.main.ui.messages :as msgs]
[app.main.ui.onboarding] [app.main.ui.onboarding]
@ -41,16 +40,17 @@
(s/def ::page-id ::us/uuid) (s/def ::page-id ::us/uuid)
(s/def ::file-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 ::section ::us/keyword)
(s/def ::index ::us/integer) (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/def ::viewer-query-params
(s/keys :req-un [::index] (s/keys :req-un [::index]
:opt-un [::token ::section])) :opt-un [::share-id ::section ::page-id]))
(def routes (def routes
[["/auth" [["/auth"
@ -71,7 +71,7 @@
["/feedback" :settings-feedback] ["/feedback" :settings-feedback]
["/options" :settings-options]] ["/options" :settings-options]]
["/view/:file-id/:page-id" ["/view/:file-id"
{:name :viewer {:name :viewer
:conform :conform
{:path-params ::viewer-path-params {:path-params ::viewer-path-params
@ -147,22 +147,15 @@
[:& dashboard {:route route}]] [:& dashboard {:route route}]]
:viewer :viewer
(let [index (get-in route [:query-params :index]) (let [{:keys [query-params path-params]} route
token (get-in route [:query-params :token]) {:keys [index share-id section page-id] :or {section :interactions}} query-params
section (get-in route [:query-params :section] :interactions) {:keys [file-id]} path-params]
file-id (get-in route [:path-params :file-id])
page-id (get-in route [:path-params :page-id])]
[:& fs/fullscreen-wrapper {} [:& 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 [:& viewer-page {:page-id page-id
:file-id file-id :file-id file-id
:section section :section section
:index index :index index
:token token}])]) :share-id share-id}]])
:render-object :render-object
(do (do

View file

@ -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}])))

View file

@ -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")}])]
]]))

View file

@ -6,276 +6,147 @@
(ns app.main.ui.viewer (ns app.main.ui.viewer
(:require (: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.comments :as dcm]
[app.main.data.viewer :as dv] [app.main.data.viewer :as dv]
[app.main.data.viewer.shortcuts :as sc] [app.main.data.viewer.shortcuts :as sc]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.comments :as cmt]
[app.main.ui.hooks :as hooks] [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.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.main.ui.viewer.thumbnails :refer [thumbnails-panel]]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :as i18n :refer [t tr]] [app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as kbd]
[goog.events :as events] [goog.events :as events]
[okulary.core :as l]
[rumext.alpha :as mf])) [rumext.alpha :as mf]))
(defn- frame-contains? (defn- calculate-size
[{:keys [x y width height]} {px :x py :y}] [frame zoom]
(let [x2 (+ x width) {:width (* (:width frame) zoom)
y2 (+ y height)] :height (* (:height frame) zoom)
(and (<= x px x2) :vbox (str "0 0 " (:width frame 0) " " (:height frame 0))})
(<= y py y2))))
(def threads-ref (mf/defc viewer
(l/derived :comment-threads st/state)) [{:keys [params data]}]
(def comments-local-ref (let [{:keys [page-id section index]} params
(l/derived :comments-local st/state))
(mf/defc comments-layer local (mf/deref refs/viewer-local)
[{:keys [zoom frame data] :as props}]
(let [profile (mf/deref refs/profile)
modifier1 (-> (gpt/point (:x frame) (:y frame)) file (:file data)
(gpt/negate) users (:users data)
(gmt/translate-matrix)) project (:project data)
perms (:permissions data)
modifier2 (-> (gpt/point (:x frame) (:y frame)) page-id (or page-id (-> file :data :pages first))
(gmt/translate-matrix))
threads-map (->> (mf/deref threads-ref) page (mf/use-memo
(d/mapm #(update %2 :position gpt/transform modifier1))) (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) size (mf/use-memo
threads (->> (vals threads-map) (mf/deps frame zoom)
(dcm/apply-filters cstate profile) (fn [] (calculate-size frame zoom)))
(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 on-click
(mf/use-callback (mf/use-callback
(mf/deps cstate data frame) (mf/deps section)
(fn [event] (fn [_]
(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))))))
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) (when (= section :comments)
[:& comments-layer {:width width (st/emit! (dcm/close-thread)))))]
:height height
(hooks/use-shortcuts ::viewer sc/shortcuts)
;; 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))))))
(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 :frame frame
:data data :permissions perms
:zoom zoom}]) :zoom (:zoom local)
:section section}]
[:svg {:view-box vbox [:div.viewer-content
:width width [:& thumbnails-panel {:frames frames
:height height :show? (:show-thumbnails local false)
:version "1.1" :page page
:xmlnsXlink "http://www.w3.org/1999/xlink" :index index}]
: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 [:section.viewer-preview
(cond (cond
(empty? frames) (empty? frames)
[:section.empty-state [:section.empty-state
[:span (t locale "viewer.empty-state")]] [:span (tr "viewer.empty-state")]]
(nil? frame) (nil? frame)
[:section.empty-state [:section.empty-state
[:span (t locale "viewer.frame-not-found")]] [:span (tr "viewer.frame-not-found")]]
(some? state) (some? frame)
[:& viewport (if (= :handoff section)
{:data data [:& handoff/viewport
{:frame frame
:page page
:file file
:section section :section section
:index index :local local}]
: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 [:div.viewport-container
(fn [event] {:style {:width (:width size)
(when (or (kbd/ctrl? event) (kbd/meta? event)) :height (:height size)
(dom/prevent-default event) :position "relative"}}
(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 (when (= section :comments)
(fn [event] [:& comments-layer {:file file
(when (kbd/esc? event) :users users
(st/emit! (dcm/close-thread)))) :frame frame
:page page
on-mount :zoom zoom}])
(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}]
[: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}]]]))
[:& interactions/viewport
{:frame frame
:size size
:page page
:file file
:users users
:local local}]]))]]]))
;; --- Component: Viewer Page ;; --- Component: Viewer Page
(mf/defc viewer-page (mf/defc viewer-page
[{:keys [file-id page-id index token section] :as props}] [{:keys [file-id] :as props}]
(let [data (mf/deref refs/viewer-data)
state (mf/deref refs/viewer-local)]
(mf/use-effect (mf/use-effect
(mf/deps file-id page-id token) (mf/deps file-id)
(fn [] (fn []
(st/emit! (dv/initialize props)))) (st/emit! (dv/initialize props))
(fn []
(st/emit! (dv/finalize props)))))
(mf/use-effect (when-let [data (mf/deref refs/viewer-data)]
(mf/deps (:file data)) (let [key (str (get-in data [:file :id]))]
#(when-let [name (get-in data [:file :name])] [:& viewer {:params props :data data :key key}])))
(dom/set-html-title (str "\u25b6 " (tr "title.viewer" name)))))
(when (and data state)
[:& viewer-content
{:index index
:section section
:state state
:data data}])))

View file

@ -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}])]]]))

View file

@ -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}]]))

View file

@ -4,18 +4,18 @@
;; ;;
;; Copyright (c) UXBOX Labs SL ;; Copyright (c) UXBOX Labs SL
(ns app.main.ui.handoff.attributes (ns app.main.ui.viewer.handoff.attributes
(:require (:require
[app.common.geom.shapes :as gsh] [app.common.geom.shapes :as gsh]
[app.main.ui.handoff.attributes.blur :refer [blur-panel]] [app.main.ui.viewer.handoff.attributes.blur :refer [blur-panel]]
[app.main.ui.handoff.attributes.fill :refer [fill-panel]] [app.main.ui.viewer.handoff.attributes.fill :refer [fill-panel]]
[app.main.ui.handoff.attributes.image :refer [image-panel]] [app.main.ui.viewer.handoff.attributes.image :refer [image-panel]]
[app.main.ui.handoff.attributes.layout :refer [layout-panel]] [app.main.ui.viewer.handoff.attributes.layout :refer [layout-panel]]
[app.main.ui.handoff.attributes.shadow :refer [shadow-panel]] [app.main.ui.viewer.handoff.attributes.shadow :refer [shadow-panel]]
[app.main.ui.handoff.attributes.stroke :refer [stroke-panel]] [app.main.ui.viewer.handoff.attributes.stroke :refer [stroke-panel]]
[app.main.ui.handoff.attributes.svg :refer [svg-panel]] [app.main.ui.viewer.handoff.attributes.svg :refer [svg-panel]]
[app.main.ui.handoff.attributes.text :refer [text-panel]] [app.main.ui.viewer.handoff.attributes.text :refer [text-panel]]
[app.main.ui.handoff.exports :refer [exports]] [app.main.ui.viewer.handoff.exports :refer [exports]]
[app.util.i18n :as i18n] [app.util.i18n :as i18n]
[rumext.alpha :as mf])) [rumext.alpha :as mf]))

View file

@ -4,7 +4,7 @@
;; ;;
;; Copyright (c) UXBOX Labs SL ;; Copyright (c) UXBOX Labs SL
(ns app.main.ui.handoff.attributes.blur (ns app.main.ui.viewer.handoff.attributes.blur
(:require (:require
[app.main.ui.components.copy-button :refer [copy-button]] [app.main.ui.components.copy-button :refer [copy-button]]
[app.util.code-gen :as cg] [app.util.code-gen :as cg]

View file

@ -4,7 +4,7 @@
;; ;;
;; Copyright (c) UXBOX Labs SL ;; Copyright (c) UXBOX Labs SL
(ns app.main.ui.handoff.attributes.common (ns app.main.ui.viewer.handoff.attributes.common
(:require (:require
[app.common.math :as mth] [app.common.math :as mth]
[app.main.store :as st] [app.main.store :as st]

View file

@ -4,10 +4,10 @@
;; ;;
;; Copyright (c) UXBOX Labs SL ;; Copyright (c) UXBOX Labs SL
(ns app.main.ui.handoff.attributes.fill (ns app.main.ui.viewer.handoff.attributes.fill
(:require (:require
[app.main.ui.components.copy-button :refer [copy-button]] [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.code-gen :as cg]
[app.util.color :as uc] [app.util.color :as uc]
[app.util.i18n :refer [tr]] [app.util.i18n :refer [tr]]

View file

@ -4,7 +4,7 @@
;; ;;
;; Copyright (c) UXBOX Labs SL ;; Copyright (c) UXBOX Labs SL
(ns app.main.ui.handoff.attributes.image (ns app.main.ui.viewer.handoff.attributes.image
(:require (:require
[app.config :as cfg] [app.config :as cfg]
[app.main.ui.components.copy-button :refer [copy-button]] [app.main.ui.components.copy-button :refer [copy-button]]

View file

@ -4,7 +4,7 @@
;; ;;
;; Copyright (c) UXBOX Labs SL ;; Copyright (c) UXBOX Labs SL
(ns app.main.ui.handoff.attributes.layout (ns app.main.ui.viewer.handoff.attributes.layout
(:require (:require
[app.common.math :as mth] [app.common.math :as mth]
[app.main.ui.components.copy-button :refer [copy-button]] [app.main.ui.components.copy-button :refer [copy-button]]

View file

@ -4,11 +4,11 @@
;; ;;
;; Copyright (c) UXBOX Labs SL ;; Copyright (c) UXBOX Labs SL
(ns app.main.ui.handoff.attributes.shadow (ns app.main.ui.viewer.handoff.attributes.shadow
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.main.ui.components.copy-button :refer [copy-button]] [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.code-gen :as cg]
[app.util.i18n :refer [tr]] [app.util.i18n :refer [tr]]
[cuerdas.core :as str] [cuerdas.core :as str]

View file

@ -4,12 +4,12 @@
;; ;;
;; Copyright (c) UXBOX Labs SL ;; Copyright (c) UXBOX Labs SL
(ns app.main.ui.handoff.attributes.stroke (ns app.main.ui.viewer.handoff.attributes.stroke
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.math :as mth] [app.common.math :as mth]
[app.main.ui.components.copy-button :refer [copy-button]] [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.code-gen :as cg]
[app.util.color :as uc] [app.util.color :as uc]
[app.util.i18n :refer [t]] [app.util.i18n :refer [t]]

View file

@ -4,7 +4,7 @@
;; ;;
;; Copyright (c) UXBOX Labs SL ;; Copyright (c) UXBOX Labs SL
(ns app.main.ui.handoff.attributes.svg (ns app.main.ui.viewer.handoff.attributes.svg
(:require (:require
#_[app.common.math :as mth] #_[app.common.math :as mth]
#_[app.main.ui.icons :as i] #_[app.main.ui.icons :as i]

View file

@ -4,13 +4,13 @@
;; ;;
;; Copyright (c) UXBOX Labs SL ;; Copyright (c) UXBOX Labs SL
(ns app.main.ui.handoff.attributes.text (ns app.main.ui.viewer.handoff.attributes.text
(:require (:require
[app.common.text :as txt] [app.common.text :as txt]
[app.main.fonts :as fonts] [app.main.fonts :as fonts]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.copy-button :refer [copy-button]] [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.code-gen :as cg]
[app.util.color :as uc] [app.util.color :as uc]
[app.util.i18n :refer [tr]] [app.util.i18n :refer [tr]]

View file

@ -4,7 +4,7 @@
;; ;;
;; Copyright (c) UXBOX Labs SL ;; Copyright (c) UXBOX Labs SL
(ns app.main.ui.handoff.code (ns app.main.ui.viewer.handoff.code
(:require (:require
["js-beautify" :as beautify] ["js-beautify" :as beautify]
[app.common.geom.shapes :as gsh] [app.common.geom.shapes :as gsh]

View file

@ -4,7 +4,7 @@
;; ;;
;; Copyright (c) UXBOX Labs SL ;; Copyright (c) UXBOX Labs SL
(ns app.main.ui.handoff.exports (ns app.main.ui.viewer.handoff.exports
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.main.data.messages :as dm] [app.main.data.messages :as dm]

View file

@ -4,7 +4,7 @@
;; ;;
;; Copyright (c) UXBOX Labs SL ;; Copyright (c) UXBOX Labs SL
(ns app.main.ui.handoff.left-sidebar (ns app.main.ui.viewer.handoff.left-sidebar
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.main.data.viewer :as dv] [app.main.data.viewer :as dv]
@ -16,12 +16,6 @@
[okulary.core :as l] [okulary.core :as l]
[rumext.alpha :as mf])) [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 (defn- make-collapsed-iref
[id] [id]
#(-> (l/in [:viewer-local :collapsed id]) #(-> (l/in [:viewer-local :collapsed id])
@ -32,6 +26,8 @@
(let [id (:id item) (let [id (:id item)
selected? (contains? selected id) selected? (contains? selected id)
item-ref (mf/use-ref nil) item-ref (mf/use-ref nil)
collapsed-iref (mf/use-memo collapsed-iref (mf/use-memo
(mf/deps id) (mf/deps id)
(make-collapsed-iref id)) (make-collapsed-iref id))
@ -94,9 +90,9 @@
:objects objects :objects objects
:key (:id item)}]))])])) :key (:id item)}]))])]))
(mf/defc left-sidebar [{:keys [frame]}] (mf/defc left-sidebar
(let [page (mf/deref page-ref) [{:keys [frame page local]}]
selected (mf/deref selected-shapes) (let [selected (:selected local)
objects (:objects page)] objects (:objects page)]
[:aside.settings-bar.settings-bar-left [:aside.settings-bar.settings-bar-left

View file

@ -4,17 +4,12 @@
;; ;;
;; Copyright (c) UXBOX Labs SL ;; 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" "The main container for a frame in handoff mode"
(:require (: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.geom.shapes :as geom]
[app.common.pages :as cp]
[app.main.data.viewer :as dv] [app.main.data.viewer :as dv]
[app.main.store :as st] [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.circle :as circle]
[app.main.ui.shapes.frame :as frame] [app.main.ui.shapes.frame :as frame]
[app.main.ui.shapes.group :as group] [app.main.ui.shapes.group :as group]
@ -24,17 +19,21 @@
[app.main.ui.shapes.shape :refer [shape-container]] [app.main.ui.shapes.shape :refer [shape-container]]
[app.main.ui.shapes.svg-raw :as svg-raw] [app.main.ui.shapes.svg-raw :as svg-raw]
[app.main.ui.shapes.text :as text] [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.dom :as dom]
[app.util.object :as obj] [app.util.object :as obj]
[rumext.alpha :as mf])) [rumext.alpha :as mf]))
(declare shape-container-factory) (declare shape-container-factory)
(defn handle-hover-shape [{:keys [type id]} hover?] (defn handle-hover-shape
#(when-not (#{:group :frame} type) [{:keys [type id]} hover?]
(dom/prevent-default %) (fn [event]
(dom/stop-propagation %) (when-not (#{:group :frame} type)
(st/emit! (dv/hover-shape id hover?)))) (dom/prevent-default event)
(dom/stop-propagation event)
(st/emit! (dv/hover-shape id hover?)))))
(defn select-shape [{:keys [type id]}] (defn select-shape [{:keys [type id]}]
(fn [event] (fn [event]
@ -42,7 +41,7 @@
(dom/stop-propagation event) (dom/stop-propagation event)
(dom/prevent-default event) (dom/prevent-default event)
(cond (cond
(.-shiftKey event) (.-shiftKey ^js event)
(st/emit! (dv/toggle-selection id)) (st/emit! (dv/toggle-selection id))
:else :else
@ -154,33 +153,27 @@
:group [:> group-container opts] :group [:> group-container opts]
:svg-raw [:> svg-raw-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/defc render-frame-svg
{::mf/wrap [mf/memo]} [{:keys [page frame local]}]
[{:keys [objects frame-id zoom] :or {zoom 1} :as props}] (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) ;; Retrieve frame again with correct modifier
frame (get objects (:id frame))
zoom (:zoom local 1)
width (* (:width frame) zoom) width (* (:width frame) zoom)
height (* (:height frame) zoom) height (* (:height frame) zoom)
vbox (make-vbox frame) vbox (str "0 0 " (:width frame 0) " " (:height frame 0))
render-frame (mf/use-memo
render (mf/use-memo
(mf/deps objects) (mf/deps objects)
#(frame-container-factory objects))] #(frame-container-factory objects))]
[:svg {:id "svg-frame" [:svg
{:id "svg-frame"
:view-box vbox :view-box vbox
:width width :width width
:height height :height height
@ -188,8 +181,9 @@
:xmlnsXlink "http://www.w3.org/1999/xlink" :xmlnsXlink "http://www.w3.org/1999/xlink"
:xmlns "http://www.w3.org/2000/svg"} :xmlns "http://www.w3.org/2000/svg"}
[:& render-frame {:shape frame [:& render {:shape frame :view-box vbox}]
:view-box vbox}] [:& selection-feedback
{:frame frame
[:& selection-feedback {:frame frame}]])) :objects objects
:local local}]]))

View file

@ -4,37 +4,26 @@
;; ;;
;; Copyright (c) UXBOX Labs SL ;; Copyright (c) UXBOX Labs SL
(ns app.main.ui.handoff.right-sidebar (ns app.main.ui.viewer.handoff.right-sidebar
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.main.store :as st]
[app.main.ui.components.tab-container :refer [tab-container tab-element]] [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.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.main.ui.workspace.sidebar.layers :refer [element-icon]]
[app.util.i18n :refer [t] :as i18n] [app.util.i18n :refer [tr]]
[okulary.core :as l]
[rumext.alpha :as mf])) [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 (mf/defc right-sidebar
[{:keys [frame page-id file-id]}] [{:keys [frame page file selected]}]
(let [expanded (mf/use-state false) (let [expanded (mf/use-state false)
locale (mf/deref i18n/locale)
section (mf/use-state :info #_:code) section (mf/use-state :info #_:code)
selected-ref (mf/use-memo (make-selected-shapes-iref))
shapes (mf/deref selected-ref) shapes (resolve-shapes (:objects page) selected)
selected-type (-> shapes first (:type :not-found))] selected-type (or (-> shapes first :type) :not-found)]
[:aside.settings-bar.settings-bar-right {:class (when @expanded "expanded")} [:aside.settings-bar.settings-bar-right {:class (when @expanded "expanded")}
[:div.settings-bar-inside [:div.settings-bar-inside
(when (seq shapes) (when (seq shapes)
@ -43,24 +32,24 @@
(if (> (count shapes) 1) (if (> (count shapes) 1)
[:* [:*
[:span.tool-window-bar-icon i/layers] [: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 [:span.tool-window-bar-icon
[:& element-icon {:shape (-> shapes first)}]] [:& 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 [:div.tool-window-content
[:& tab-container {:on-change-tab #(do [:& tab-container {:on-change-tab #(do
(reset! expanded false) (reset! expanded false)
(reset! section %)) (reset! section %))
:selected @section} :selected @section}
[:& tab-element {:id :info :title (t locale "handoff.tabs.info")} [:& tab-element {:id :info :title (tr "handoff.tabs.info")}
[:& attributes {:page-id page-id [:& attributes {:page-id (:id page)
:file-id file-id :file-id (:id file)
:frame frame :frame frame
:shapes shapes}]] :shapes shapes}]]
[:& tab-element {:id :code :title (t locale "handoff.tabs.code")} [:& tab-element {:id :code :title (tr "handoff.tabs.code")}
[:& code {:frame frame [:& code {:frame frame
:shapes shapes :shapes shapes
:on-expand #(swap! expanded not)}]]]]])]])) :on-expand #(swap! expanded not)}]]]]])]]))

View file

@ -4,12 +4,10 @@
;; ;;
;; Copyright (c) UXBOX Labs SL ;; Copyright (c) UXBOX Labs SL
(ns app.main.ui.handoff.selection-feedback (ns app.main.ui.viewer.handoff.selection-feedback
(:require (:require
[app.common.geom.shapes :as gsh] [app.common.geom.shapes :as gsh]
[app.main.store :as st]
[app.main.ui.measurements :refer [selection-guides size-display measurement]] [app.main.ui.measurements :refer [selection-guides size-display measurement]]
[okulary.core :as l]
[rumext.alpha :as mf])) [rumext.alpha :as mf]))
;; ------------------------------------------------ ;; ------------------------------------------------
@ -21,33 +19,12 @@
(def select-guide-width 1) (def select-guide-width 1)
(def select-guide-dasharray 5) (def select-guide-dasharray 5)
;; ------------------------------------------------ (defn resolve-shapes
;; LENSES [objects ids]
;; ------------------------------------------------ (let [resolve-shape #(get objects %)]
(into [] (comp (map resolve-shape)
(defn make-selected-shapes-iref (filter some?))
"Creates a lens to the current selected shapes" ids)))
[]
(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))
;; ------------------------------------------------ ;; ------------------------------------------------
;; HELPERS ;; HELPERS
@ -75,20 +52,18 @@
:stroke select-color :stroke select-color
:stroke-width selection-rect-width}}]])) :stroke-width selection-rect-width}}]]))
(mf/defc selection-feedback [{:keys [frame]}] (mf/defc selection-feedback
(let [zoom (mf/deref selected-zoom) [{:keys [frame local objects]}]
(let [{:keys [hover selected zoom]} local
hover-shapes-ref (mf/use-memo (make-hover-shapes-iref)) hover-shape (-> (or (first (resolve-shapes objects [hover])) frame)
hover-shape (-> (or (mf/deref hover-shapes-ref) frame)
(gsh/translate-to-frame frame)) (gsh/translate-to-frame frame))
selected-shapes (->> (resolve-shapes objects selected)
selected-shapes-ref (mf/use-memo (make-selected-shapes-iref))
selected-shapes (->> (mf/deref selected-shapes-ref)
(map #(gsh/translate-to-frame % frame))) (map #(gsh/translate-to-frame % frame)))
selrect (gsh/selection-rect selected-shapes) selrect (gsh/selection-rect selected-shapes)
bounds (frame->bounds frame)] bounds (frame->bounds frame)]
(when (seq selected-shapes) (when (seq selected-shapes)
[:g.selection-feedback {:pointer-events "none"} [:g.selection-feedback {:pointer-events "none"}
[:g.selected-shapes [:g.selected-shapes

View file

@ -6,288 +6,50 @@
(ns app.main.ui.viewer.header (ns app.main.ui.viewer.header
(:require (:require
[app.common.math :as mth] [app.main.data.modal :as modal]
[app.common.uuid :as uuid]
[app.config :as cfg]
[app.main.data.comments :as dcm]
[app.main.data.messages :as dm]
[app.main.data.viewer :as dv] [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.store :as st]
[app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.components.fullscreen :as fs] [app.main.ui.components.fullscreen :as fs]
[app.main.ui.icons :as i] [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.dom :as dom]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[app.util.router :as rt]
[app.util.webapi :as wapi]
[rumext.alpha :as mf])) [rumext.alpha :as mf]))
(mf/defc zoom-widget (mf/defc header-options
{:wrap [mf/memo]} [{:keys [section zoom page file permissions]}]
[{:keys [zoom (let [fullscreen (mf/use-ctx fs/fullscreen-context)
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))))
toggle-fullscreen toggle-fullscreen
(mf/use-callback (mf/use-callback
(mf/deps fullscreen) (mf/deps fullscreen)
(fn [] (fn []
(if @fullscreen (fullscreen false) (fullscreen true))))] (if @fullscreen (fullscreen false) (fullscreen true))))
[:header.viewer-header go-to-workspace
[:div.main-icon (mf/use-callback
[:a {:on-click on-goback (mf/deps page)
;; If the user doesn't have permission we disable the link (fn []
:style {:pointer-events (when-not has-permission? "none")}} i/logo-icon]] (st/emit! (dv/go-to-workspace (:id page)))))
[:div.sitemap-zone {:alt (tr "viewer.header.sitemap") open-share-dialog
:on-click on-click} (mf/use-callback
[:span.project-name (:name project)] (mf/deps page)
[:span "/"] (fn []
[:span.file-name (:name file)] (modal/show! :share-link {:page page :file file})))]
[:span "/"]
[:span.page-name (:name page)]
[:span.show-thumbnails-button i/arrow-down]
[:span.counters (str (inc index) " / " total)]]
[:div.mode-zone
[:button.mode-zone-button.tooltip.tooltip-bottom
{:on-click #(navigate :interactions)
:class (dom/classnames :active (= section :interactions))
:alt "View mode"}
i/play]
(when has-permission?
[:button.mode-zone-button.tooltip.tooltip-bottom
{:on-click #(navigate :comments)
:class (dom/classnames :active (= section :comments))
:alt "Comments"}
i/chat])
[:button.mode-zone-button.tooltip.tooltip-bottom
{:on-click #(navigate :handoff)
:class (dom/classnames :active (= section :handoff))
:alt "Code mode"}
i/code]]
[:div.options-zone [:div.options-zone
(case section (case section
:interactions [:& interactions-menu {:state state}] :interactions [:& interactions-menu]
:comments [:& comments-menu] :comments [:& comments-menu]
nil)
(when has-permission? [:div.view-options])
[:& share-link {:token (:token data)
:page (:page data)}])
[:& zoom-widget [:& zoom-widget
{:zoom (:zoom state) {:zoom zoom
:on-increase (st/emitf dv/increase-zoom) :on-increase (st/emitf dv/increase-zoom)
:on-decrease (st/emitf dv/decrease-zoom) :on-decrease (st/emitf dv/decrease-zoom)
:on-zoom-to-50 (st/emitf dv/zoom-to-50) :on-zoom-to-50 (st/emitf dv/zoom-to-50)
@ -295,15 +57,104 @@
:on-zoom-to-200 (st/emitf dv/zoom-to-200) :on-zoom-to-200 (st/emitf dv/zoom-to-200)
:on-fullscreen toggle-fullscreen}] :on-fullscreen toggle-fullscreen}]
[:span.btn-icon-basic.btn-small.tooltip.tooltip-bottom-left [:span.btn-icon-dark.btn-small.tooltip.tooltip-bottom-left
{:alt (tr "viewer.header.fullscreen") {:alt (tr "viewer.header.fullscreen")
:on-click toggle-fullscreen} :on-click toggle-fullscreen}
(if @fullscreen (if @fullscreen
i/full-screen-off i/full-screen-off
i/full-screen)] i/full-screen)]
(when has-permission? (when (:edit permissions)
[:& file-menu {:project-id project-id [:span.btn-primary {:on-click open-share-dialog} (tr "labels.share-prototype")])
:file-id file-id
:page-id page-id}])]])) (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 go-to-dashboard
;; If the user doesn't have permission we disable the link
:style {:pointer-events (when-not permissions "none")}} i/logo-icon]]
[:& 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 (tr "viewer.header.interactions-section")}
i/play]
(when (:edit permissions)
[:button.mode-zone-button.tooltip.tooltip-bottom
{:on-click #(navigate :comments)
:class (dom/classnames :active (= section :comments))
:alt (tr "viewer.header.comments-section")}
i/chat])
(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])]
[:& header-options {:section section
:permissions permissions
:page page
:file file
:zoom zoom}]]))

View file

@ -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")]]]]]))

View file

@ -10,11 +10,11 @@
[app.main.data.viewer :as dv] [app.main.data.viewer :as dv]
[app.main.exports :as exports] [app.main.exports :as exports]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.dropdown :refer [dropdown']]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]] [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])) [rumext.alpha :as mf]))
(mf/defc thumbnails-content (mf/defc thumbnails-content
@ -50,7 +50,7 @@
on-mount on-mount
(fn [] (fn []
(let [dom (mf/ref-val container)] (let [dom (mf/ref-val container)]
(reset! width (gobj/get dom "clientWidth"))))] (reset! width (obj/get dom "clientWidth"))))]
(mf/use-effect on-mount) (mf/use-effect on-mount)
(if expanded? (if expanded?
@ -72,7 +72,8 @@
[:span.btn-close {:on-click on-close} i/close]]]) [:span.btn-close {:on-click on-close} i/close]]])
(mf/defc thumbnail-item (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-item {:on-click #(on-click % index)}
[:div.thumbnail-preview [:div.thumbnail-preview
{:class (dom/classnames :selected selected?)} {:class (dom/classnames :selected selected?)}
@ -81,42 +82,39 @@
[:span.name {:title (:name frame)} (:name frame)]]]) [:span.name {:title (:name frame)} (:name frame)]]])
(mf/defc thumbnails-panel (mf/defc thumbnails-panel
[{:keys [data index] :as props}] [{:keys [frames page index show?] :as props}]
(let [expanded? (mf/use-state false) (let [expanded? (mf/use-state false)
container (mf/use-ref) container (mf/use-ref)
objects (:objects page)
on-close #(st/emit! dv/toggle-thumbnails-panel) on-close #(st/emit! dv/toggle-thumbnails-panel)
selected (mf/use-var false) selected (mf/use-var false)
on-mouse-leave
(fn [_]
(when @selected
(on-close)))
on-item-click on-item-click
(mf/use-callback
(mf/deps @expanded?)
(fn [_ index] (fn [_ index]
(compare-and-set! selected false true) (compare-and-set! selected false true)
(st/emit! (dv/go-to-frame-by-index index)) (st/emit! (dv/go-to-frame-by-index index))
(when @expanded? (when @expanded?
(on-close)))] (on-close))))]
[:& dropdown' {:on-close on-close
:container container
:show true}
[:section.viewer-thumbnails [:section.viewer-thumbnails
{:class (dom/classnames :expanded @expanded?) {:class (dom/classnames :expanded @expanded?
:invisible (not show?))
:ref container :ref container
:on-mouse-leave on-mouse-leave} }
[:& thumbnails-summary {:on-toggle-expand #(swap! expanded? not) [:& thumbnails-summary {:on-toggle-expand #(swap! expanded? not)
:on-close on-close :on-close on-close
:total (count (:frames data))}] :total (count frames)}]
[:& thumbnails-content {:expanded? @expanded? [:& thumbnails-content {:expanded? @expanded?
:total (count (:frames data))} :total (count frames)}
(for [[i frame] (d/enumerate (:frames data))] (for [[i frame] (d/enumerate frames)]
[:& thumbnail-item {:key i [:& thumbnail-item {:index i
:index i
:frame frame :frame frame
:objects (:objects data) :objects objects
:on-click on-item-click :on-click on-item-click
:selected? (= i index)}])]]])) :selected? (= i index)}])]]))

View file

@ -64,15 +64,16 @@
on-decrease on-decrease
on-zoom-reset on-zoom-reset
on-zoom-fit on-zoom-fit
on-zoom-selected] on-zoom-selected
on-fullscreen]
:as props}] :as props}]
(let [show-dropdown? (mf/use-state false)] (let [show-dropdown? (mf/use-state false)]
[:div.zoom-widget {:on-click #(reset! show-dropdown? true)} [:div.zoom-widget {:on-click #(reset! show-dropdown? true)}
[:span {} (str (mth/round (* 100 zoom)) "%")] [:span.label {} (str (mth/round (* 100 zoom)) "%")]
[:span.dropdown-button i/arrow-down] [:span.icon i/arrow-down]
[:& dropdown {:show @show-dropdown? [:& dropdown {:show @show-dropdown?
:on-close #(reset! show-dropdown? false)} :on-close #(reset! show-dropdown? false)}
[:ul.zoom-dropdown [:ul.dropdown
[:li {:on-click on-increase} [:li {:on-click on-increase}
"Zoom in" [:span (sc/get-tooltip :increase-zoom)]] "Zoom in" [:span (sc/get-tooltip :increase-zoom)]]
[:li {:on-click on-decrease} [:li {:on-click on-decrease}
@ -82,7 +83,11 @@
[:li {:on-click on-zoom-fit} [:li {:on-click on-zoom-fit}
"Zoom to fit all" [:span (sc/get-tooltip :fit-all)]] "Zoom to fit all" [:span (sc/get-tooltip :fit-all)]]
[:li {:on-click on-zoom-selected} [: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 ;; --- Header Users

View file

@ -108,14 +108,30 @@
(let [router (:router state) (let [router (:router state)
path (resolve router id params qparams) path (resolve router id params qparams)
uri (-> (u/uri cfg/public-uri) uri (-> (u/uri cfg/public-uri)
(assoc :fragment path))] (assoc :fragment path))
(js/window.open (str uri) "_blank")))) name (str (name id) "-" (:file-id params))]
(js/window.open (str uri) name))))
(defn nav-new-window (defn nav-new-window
([id] (nav-new-window id nil nil)) ([id] (nav-new-window id nil nil))
([id params] (nav-new-window id params nil)) ([id params] (nav-new-window id params nil))
([id params qparams] (NavigateNewWindow. id params qparams))) ([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 ;; --- History API
(defn initialize-history (defn initialize-history

View file

@ -1330,7 +1330,7 @@ msgid "viewer.header.share.subtitle"
msgstr "Jeder mit dem Link hat Zugriff" 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 #: 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" msgstr "Prototyp teilen"
#: src/app/main/ui/viewer/header.cljs #: src/app/main/ui/viewer/header.cljs

View file

@ -1280,7 +1280,7 @@ msgid "viewer.header.share.subtitle"
msgstr "Όποιος έχει τον link θα έχει πρόσβαση" msgstr "Όποιος έχει τον link θα έχει πρόσβαση"
#: src/app/main/ui/viewer/header.cljs, src/app/main/ui/viewer/header.cljs, src/app/main/ui/viewer/header.cljs #: 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" msgstr "Μοιραστείτε το link"
#: src/app/main/ui/viewer/header.cljs #: src/app/main/ui/viewer/header.cljs

View file

@ -1524,8 +1524,7 @@ msgstr "Artboard not found."
msgid "viewer.header.dont-show-interactions" msgid "viewer.header.dont-show-interactions"
msgstr "Don't show interactions" msgstr "Don't show interactions"
#: src/app/main/ui/viewer/header.cljs msgid "labels.edit-file"
msgid "viewer.header.edit-file"
msgstr "Edit file" msgstr "Edit file"
#: src/app/main/ui/viewer/header.cljs #: src/app/main/ui/viewer/header.cljs
@ -1556,8 +1555,7 @@ msgstr "Remove link"
msgid "viewer.header.share.subtitle" msgid "viewer.header.share.subtitle"
msgstr "Anyone with the link will have access" 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 "labels.share-prototype"
msgid "viewer.header.share.title"
msgstr "Share prototype" msgstr "Share prototype"
#: src/app/main/ui/viewer/header.cljs #: src/app/main/ui/viewer/header.cljs
@ -2762,3 +2760,57 @@ msgstr "Update"
msgid "workspace.viewport.click-to-close-path" msgid "workspace.viewport.click-to-close-path"
msgstr "Click to close the 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"

View file

@ -1517,7 +1517,7 @@ msgid "viewer.header.dont-show-interactions"
msgstr "No mostrar interacciones" msgstr "No mostrar interacciones"
#: src/app/main/ui/viewer/header.cljs #: src/app/main/ui/viewer/header.cljs
msgid "viewer.header.edit-file" msgid "labels.edit-file"
msgstr "Editar archivo" msgstr "Editar archivo"
#: src/app/main/ui/viewer/header.cljs #: src/app/main/ui/viewer/header.cljs
@ -1549,7 +1549,7 @@ msgid "viewer.header.share.subtitle"
msgstr "Cualquiera con el enlace podrá acceder" 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 #: 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" msgstr "Compartir prototipo"
#: src/app/main/ui/viewer/header.cljs #: src/app/main/ui/viewer/header.cljs
@ -2758,3 +2758,60 @@ msgstr "Actualizar"
msgid "workspace.viewport.click-to-close-path" msgid "workspace.viewport.click-to-close-path"
msgstr "Pulsar para cerrar la ruta" 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"

View file

@ -1226,7 +1226,7 @@ msgid "viewer.header.share.subtitle"
msgstr "Toute personne disposant du lien aura accès" 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 #: 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" msgstr "Partager le prototype"
#: src/app/main/ui/viewer/header.cljs #: src/app/main/ui/viewer/header.cljs

View file

@ -1465,7 +1465,7 @@ msgid "viewer.header.share.subtitle"
msgstr "Prin acest link se permite accesul public" 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 #: 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" msgstr "Distribuie link"
#: src/app/main/ui/viewer/header.cljs #: src/app/main/ui/viewer/header.cljs

View file

@ -599,7 +599,7 @@ msgid "viewer.header.share.subtitle"
msgstr "Любой, у кого есть ссылка будет иметь доступ" msgstr "Любой, у кого есть ссылка будет иметь доступ"
#: src/app/main/ui/viewer/header.cljs, src/app/main/ui/viewer/header.cljs, src/app/main/ui/viewer/header.cljs #: 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 "Поделиться ссылкой" msgstr "Поделиться ссылкой"
#: src/app/main/ui/viewer/header.cljs #: src/app/main/ui/viewer/header.cljs

View file

@ -1560,7 +1560,7 @@ msgid "viewer.header.show-interactions"
msgstr "Etkileşimleri göster" 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 #: 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ş" msgstr "Bağlantıyı paylaş"
#: src/app/main/ui/viewer/header.cljs #: src/app/main/ui/viewer/header.cljs

View file

@ -1223,7 +1223,7 @@ msgid "viewer.header.share.subtitle"
msgstr "任何人都可以通过本链接访问" msgstr "任何人都可以通过本链接访问"
#: src/app/main/ui/viewer/header.cljs, src/app/main/ui/viewer/header.cljs, src/app/main/ui/viewer/header.cljs #: 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 "分享链接" msgstr "分享链接"
#: src/app/main/ui/viewer/header.cljs #: src/app/main/ui/viewer/header.cljs