Merge remote-tracking branch 'origin/develop'
34
CHANGES.md
@@ -4,11 +4,45 @@
|
||||
## :rocket: Next
|
||||
|
||||
### :boom: Breaking changes
|
||||
|
||||
- The initial project / data mechanism (not documented) has been
|
||||
disabled. Is the mechanism used for creating initial project on user
|
||||
signup. With the new onboarding approach, this subsystem is no
|
||||
longer needed and is disabled.
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
- Enhance corner radius behavior [Taiga #2190](https://tree.taiga.io/project/penpot/issue/2190).
|
||||
- Allow preserve scroll position in interactions [Taiga #2250](https://tree.taiga.io/project/penpot/us/2250).
|
||||
- Add new onboarding modals.
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix problem with exporting before the document is saved [Taiga #2189](https://tree.taiga.io/project/penpot/issue/2189).
|
||||
- Fix undo stacking when changing color from color-picker [Taiga #2191](https://tree.taiga.io/project/penpot/issue/2191).
|
||||
- Fix pages dropdown in viewer [Taiga #2087](https://tree.taiga.io/project/penpot/issue/2087).
|
||||
- Fix problem when exporting texts with gradients or opacity [Taiga #2200](https://tree.taiga.io/project/penpot/issue/2200).
|
||||
- Fix problem with view mode comments [Taiga #2226](https://tree.taiga.io/project/penpot/issue/2226).
|
||||
- Disallow to create a component when already has one [Taiga #2237](https://tree.taiga.io/project/penpot/issue/2237).
|
||||
- Add ellipsis in long labels for input fields [Taiga #2224](https://tree.taiga.io/project/penpot/issue/2224)
|
||||
- Fix problem with text rendering on export [Taiga #2223](https://tree.taiga.io/project/penpot/issue/2223)
|
||||
- Fix problem when flattening booleans losing styles [Taiga #2217](https://tree.taiga.io/project/penpot/issue/2217)
|
||||
- Add shortcuts to boolean icons popups [Taiga #2220](https://tree.taiga.io/project/penpot/issue/2220)
|
||||
- Fix a worker error when transforming a rectangle into path
|
||||
- Fix max/min values for opacity fields [Taiga #2183](https://tree.taiga.io/project/penpot/issue/2183)
|
||||
- Fix viewer comment position when zoom applied [Taiga #2240](https://tree.taiga.io/project/penpot/issue/2240)
|
||||
- Remove change style on hover for options [Taiga #2172](https://tree.taiga.io/project/penpot/issue/2172)
|
||||
- Fix problem in viewer with dropdowns when comments active [#1303](https://github.com/penpot/penpot/issues/1303)
|
||||
- Add placeholder to create shareable link
|
||||
- Fix project files count not refreshing correctly after import [Taiga #2216](https://tree.taiga.io/project/penpot/issue/2216)
|
||||
- Remove button after import process finish [Taiga #2215](https://tree.taiga.io/project/penpot/issue/2215)
|
||||
|
||||
### :arrow_up: Deps updates
|
||||
### :heart: Community contributions by (Thank you!)
|
||||
|
||||
- To the translation community for the hard work on making penpot
|
||||
available on so many languages.
|
||||
|
||||
|
||||
## 1.9.0-alpha
|
||||
|
||||
|
||||
@@ -68,10 +68,6 @@
|
||||
mockery/mockery {:mvn/version "RELEASE"}}
|
||||
:extra-paths ["test" "dev"]}
|
||||
|
||||
:fn-fixtures
|
||||
{:exec-fn app.cli.fixtures/run
|
||||
:args {}}
|
||||
|
||||
:kaocha
|
||||
{:extra-deps {lambdaisland/kaocha {:mvn/version "1.0.887"}}
|
||||
:main-opts ["-m" "kaocha.runner"]}
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
#!/bin/sh
|
||||
#!/usr/bin/env bash
|
||||
|
||||
export PENPOT_ASSERTS_ENABLED=true
|
||||
export PENPOT_FLAGS="$PENPOT_FLAGS enable-asserts"
|
||||
|
||||
set -ex
|
||||
|
||||
if [ ! -e ~/.fixtures-loaded ]; then
|
||||
echo "Loading fixtures..."
|
||||
clojure -Adev -X:fn-fixtures
|
||||
touch ~/.fixtures-loaded
|
||||
fi
|
||||
|
||||
if [ "$1" = "--watch" ]; then
|
||||
echo "Start Watch..."
|
||||
|
||||
@@ -27,6 +21,3 @@ if [ "$1" = "--watch" ]; then
|
||||
else
|
||||
clojure -A:dev -M -m app.main
|
||||
fi
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,258 +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.cli.fixtures
|
||||
"A initial fixtures."
|
||||
(:require
|
||||
[app.common.logging :as l]
|
||||
[app.common.pages :as cp]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.db :as db]
|
||||
[app.main :as main]
|
||||
[app.rpc.mutations.profile :as profile]
|
||||
[app.util.blob :as blob]
|
||||
[buddy.hashers :as hashers]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
(defn- mk-uuid
|
||||
[prefix & args]
|
||||
(uuid/namespaced uuid/zero (apply str prefix (interpose "-" args))))
|
||||
|
||||
;; --- Profiles creation
|
||||
|
||||
(def password (hashers/derive "123123"))
|
||||
|
||||
(def preset-small
|
||||
{:num-teams 5
|
||||
:num-profiles 5
|
||||
:num-profiles-per-team 5
|
||||
:num-projects-per-team 5
|
||||
:num-files-per-project 5
|
||||
:num-draft-files-per-profile 10})
|
||||
|
||||
(defn- rng-ids
|
||||
[rng n max]
|
||||
(let [stream (->> (.longs rng 0 max)
|
||||
(.iterator)
|
||||
(iterator-seq))]
|
||||
(reduce (fn [acc item]
|
||||
(if (= (count acc) n)
|
||||
(reduced acc)
|
||||
(conj acc item)))
|
||||
#{}
|
||||
stream)))
|
||||
|
||||
(defn- rng-vec
|
||||
[rng vdata n]
|
||||
(let [ids (rng-ids rng n (count vdata))]
|
||||
(mapv #(nth vdata %) ids)))
|
||||
|
||||
(defn- rng-nth
|
||||
[rng vdata]
|
||||
(let [stream (->> (.longs rng 0 (count vdata))
|
||||
(.iterator)
|
||||
(iterator-seq))]
|
||||
(nth vdata (first stream))))
|
||||
|
||||
(defn- collect
|
||||
[f items]
|
||||
(reduce #(conj %1 (f %2)) [] items))
|
||||
|
||||
(defn- register-profile
|
||||
[conn params]
|
||||
(->> (#'profile/create-profile conn params)
|
||||
(#'profile/create-profile-relations conn)))
|
||||
|
||||
(defn impl-run
|
||||
[pool opts]
|
||||
(let [rng (java.util.Random. 1)]
|
||||
(letfn [(create-profile [conn index]
|
||||
(let [id (mk-uuid "profile" index)
|
||||
_ (l/info :action "create profile"
|
||||
:index index
|
||||
:id id)
|
||||
|
||||
prof (register-profile conn
|
||||
{:id id
|
||||
:fullname (str "Profile " index)
|
||||
:password "123123"
|
||||
:is-demo true
|
||||
:email (str "profile" index "@example.com")})
|
||||
team-id (:default-team-id prof)
|
||||
owner-id id]
|
||||
(let [project-ids (collect (partial create-project conn team-id owner-id)
|
||||
(range (:num-projects-per-team opts)))]
|
||||
(run! (partial create-files conn owner-id) project-ids))
|
||||
prof))
|
||||
|
||||
(create-profiles [conn]
|
||||
(l/info :action "create profiles")
|
||||
(collect (partial create-profile conn)
|
||||
(range (:num-profiles opts))))
|
||||
|
||||
(create-team [conn index]
|
||||
(let [id (mk-uuid "team" index)
|
||||
name (str "Team" index)]
|
||||
(l/info :action "create team"
|
||||
:index index
|
||||
:id id)
|
||||
(db/insert! conn :team {:id id
|
||||
:name name})
|
||||
id))
|
||||
|
||||
(create-teams [conn]
|
||||
(l/info :action "create teams")
|
||||
(collect (partial create-team conn)
|
||||
(range (:num-teams opts))))
|
||||
|
||||
(create-file [conn owner-id project-id index]
|
||||
(let [id (mk-uuid "file" project-id index)
|
||||
name (str "file" index)
|
||||
data (cp/make-file-data id)]
|
||||
(l/info :action "create file"
|
||||
:index index
|
||||
:id id)
|
||||
(db/insert! conn :file
|
||||
{:id id
|
||||
:data (blob/encode data)
|
||||
:project-id project-id
|
||||
:name name})
|
||||
(db/insert! conn :file-profile-rel
|
||||
{:file-id id
|
||||
:profile-id owner-id
|
||||
:is-owner true
|
||||
:is-admin true
|
||||
:can-edit true})
|
||||
id))
|
||||
|
||||
(create-files [conn owner-id project-id]
|
||||
(l/info :action "create files")
|
||||
(run! (partial create-file conn owner-id project-id)
|
||||
(range (:num-files-per-project opts))))
|
||||
|
||||
(create-project [conn team-id owner-id index]
|
||||
(let [id (if index
|
||||
(mk-uuid "project" team-id index)
|
||||
(mk-uuid "project" team-id))
|
||||
name (if index
|
||||
(str "project " index)
|
||||
"Drafts")
|
||||
is-default (nil? index)]
|
||||
(l/info :action "create project"
|
||||
:index index
|
||||
:id id)
|
||||
(db/insert! conn :project
|
||||
{:id id
|
||||
:team-id team-id
|
||||
:is-default is-default
|
||||
:name name})
|
||||
(db/insert! conn :project-profile-rel
|
||||
{:project-id id
|
||||
:profile-id owner-id
|
||||
:is-owner true
|
||||
:is-admin true
|
||||
:can-edit true})
|
||||
id))
|
||||
|
||||
(create-projects [conn team-id profile-ids]
|
||||
(l/info :action "create projects")
|
||||
(let [owner-id (rng-nth rng profile-ids)
|
||||
project-ids (conj
|
||||
(collect (partial create-project conn team-id owner-id)
|
||||
(range (:num-projects-per-team opts)))
|
||||
(create-project conn team-id owner-id nil))]
|
||||
(run! (partial create-files conn owner-id) project-ids)))
|
||||
|
||||
(assign-profile-to-team [conn team-id owner? profile-id]
|
||||
(db/insert! conn :team-profile-rel
|
||||
{:team-id team-id
|
||||
:profile-id profile-id
|
||||
:is-owner owner?
|
||||
:is-admin true
|
||||
:can-edit true}))
|
||||
|
||||
(setup-team [conn team-id profile-ids]
|
||||
(l/info :action "setup team"
|
||||
:team-id team-id
|
||||
:profile-ids (pr-str profile-ids))
|
||||
(assign-profile-to-team conn team-id true (first profile-ids))
|
||||
(run! (partial assign-profile-to-team conn team-id false)
|
||||
(rest profile-ids))
|
||||
(create-projects conn team-id profile-ids))
|
||||
|
||||
(assign-teams-and-profiles [conn teams profiles]
|
||||
(l/info :action "assign teams and profiles")
|
||||
(loop [team-id (first teams)
|
||||
teams (rest teams)]
|
||||
(when-not (nil? team-id)
|
||||
(let [n-profiles-team (:num-profiles-per-team opts)
|
||||
selected-profiles (rng-vec rng profiles n-profiles-team)]
|
||||
(setup-team conn team-id selected-profiles)
|
||||
(recur (first teams)
|
||||
(rest teams))))))
|
||||
|
||||
(create-draft-file [conn owner index]
|
||||
(let [owner-id (:id owner)
|
||||
id (mk-uuid "file" "draft" owner-id index)
|
||||
name (str "file" index)
|
||||
project-id (:default-project-id owner)
|
||||
data (cp/make-file-data id)]
|
||||
|
||||
(l/info :action "create draft file"
|
||||
:index index
|
||||
:id id)
|
||||
(db/insert! conn :file
|
||||
{:id id
|
||||
:data (blob/encode data)
|
||||
:project-id project-id
|
||||
:name name})
|
||||
(db/insert! conn :file-profile-rel
|
||||
{:file-id id
|
||||
:profile-id owner-id
|
||||
:is-owner true
|
||||
:is-admin true
|
||||
:can-edit true})
|
||||
id))
|
||||
|
||||
(create-draft-files [conn profile]
|
||||
(run! (partial create-draft-file conn profile)
|
||||
(range (:num-draft-files-per-profile opts))))
|
||||
]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [profiles (create-profiles conn)
|
||||
teams (create-teams conn)]
|
||||
(assign-teams-and-profiles conn teams (map :id profiles))
|
||||
(run! (partial create-draft-files conn) profiles))))))
|
||||
|
||||
(defn run-in-system
|
||||
[system preset]
|
||||
(let [pool (:app.db/pool system)
|
||||
preset (if (map? preset)
|
||||
preset
|
||||
(case preset
|
||||
(nil "small" :small) preset-small
|
||||
;; "medium" preset-medium
|
||||
;; "big" preset-big
|
||||
preset-small))]
|
||||
(impl-run pool preset)))
|
||||
|
||||
(defn run
|
||||
[{:keys [preset] :or {preset :small}}]
|
||||
(let [config (select-keys main/system-config
|
||||
[:app.db/pool
|
||||
:app.telemetry/migrations
|
||||
:app.migrations/migrations
|
||||
:app.migrations/all
|
||||
:app.metrics/metrics])
|
||||
_ (ig/load-namespaces config)
|
||||
system (-> (ig/prep config)
|
||||
(ig/init))]
|
||||
(try
|
||||
(run-in-system system preset)
|
||||
(catch Exception e
|
||||
(l/error :hint "unhandled exception" :cause e))
|
||||
(finally
|
||||
(ig/halt! system)))))
|
||||
@@ -85,7 +85,7 @@
|
||||
;; a server prop key where initial project is stored.
|
||||
:initial-project-skey "initial-project"})
|
||||
|
||||
(s/def ::flags ::us/words)
|
||||
(s/def ::flags ::us/set-of-keywords)
|
||||
|
||||
;; DEPRECATED PROPERTIES: should be removed in 1.10
|
||||
(s/def ::registration-enabled ::us/boolean)
|
||||
@@ -268,10 +268,16 @@
|
||||
::telemetry-with-taiga
|
||||
::tenant]))
|
||||
|
||||
(def default-flags
|
||||
[:enable-backend-asserts
|
||||
:enable-backend-api-doc
|
||||
:enable-secure-session-cookies])
|
||||
|
||||
(defn- parse-flags
|
||||
[config]
|
||||
(-> (:flags config)
|
||||
(flags/parse flags/default)))
|
||||
(flags/parse flags/default
|
||||
default-flags
|
||||
(:flags config)))
|
||||
|
||||
(defn read-env
|
||||
[prefix]
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
(defn handler
|
||||
[rpc]
|
||||
(let [context (prepare-context rpc)]
|
||||
(if (contains? cf/flags :api-doc)
|
||||
(if (contains? cf/flags :backend-api-doc)
|
||||
(fn [_]
|
||||
{:status 200
|
||||
:body (-> (io/resource "api-doc.tmpl")
|
||||
|
||||
@@ -203,6 +203,7 @@
|
||||
(sxf request)))
|
||||
(let [info (assoc info
|
||||
:iss :prepared-register
|
||||
:is-active true
|
||||
:exp (dt/in-future {:hours 48}))
|
||||
token (tokens :generate info)
|
||||
params (d/without-nils
|
||||
|
||||
@@ -53,12 +53,13 @@
|
||||
|
||||
(defn- add-cookies
|
||||
[response {:keys [id] :as session}]
|
||||
(let [cors? (contains? cfg/flags :cors)]
|
||||
(let [cors? (contains? cfg/flags :cors)
|
||||
secure? (contains? cfg/flags :secure-session-cookies)]
|
||||
(assoc response :cookies {cookie-name {:path "/"
|
||||
:http-only true
|
||||
:value id
|
||||
:same-site (if cors? :none :strict)
|
||||
:secure true}})))
|
||||
:secure secure?}})))
|
||||
|
||||
(defn- clear-cookies
|
||||
[response]
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
[app.loggers.audit :as audit]
|
||||
[app.metrics :as mtx]
|
||||
[app.rlimits :as rlm]
|
||||
[app.util.retry :as retry]
|
||||
[app.util.services :as sv]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
@@ -92,6 +93,7 @@
|
||||
(defn- wrap-impl
|
||||
[{:keys [audit] :as cfg} f mdata]
|
||||
(let [f (wrap-with-rlimits cfg f mdata)
|
||||
f (retry/wrap-retry cfg f mdata)
|
||||
f (wrap-with-metrics cfg f mdata)
|
||||
spec (or (::sv/spec mdata) (s/spec any?))
|
||||
auth? (:auth mdata true)]
|
||||
@@ -99,7 +101,6 @@
|
||||
(l/trace :action "register" :name (::sv/name mdata))
|
||||
(with-meta
|
||||
(fn [params]
|
||||
|
||||
;; Raise authentication error when rpc method requires auth but
|
||||
;; no profile-id is found in the request.
|
||||
(when (and auth? (not (uuid? (:profile-id params))))
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
[app.rpc.queries.comments :as comments]
|
||||
[app.rpc.queries.files :as files]
|
||||
[app.util.blob :as blob]
|
||||
#_:clj-kondo/ignore
|
||||
[app.util.retry :as retry]
|
||||
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[clojure.spec.alpha :as s]))
|
||||
@@ -32,6 +35,9 @@
|
||||
(s/keys :req-un [::profile-id ::file-id ::position ::content ::page-id]))
|
||||
|
||||
(sv/defmethod ::create-comment-thread
|
||||
{::retry/enabled true
|
||||
::retry/max-retries 3
|
||||
::retry/matches retry/conflict-db-insert?}
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(files/check-read-permissions! conn profile-id file-id)
|
||||
@@ -43,7 +49,7 @@
|
||||
res (db/exec-one! conn [sql file-id])]
|
||||
(:next-seqn res)))
|
||||
|
||||
(defn- create-comment-thread*
|
||||
(defn- create-comment-thread
|
||||
[conn {:keys [profile-id file-id page-id position content] :as params}]
|
||||
(let [seqn (retrieve-next-seqn conn file-id)
|
||||
now (dt/now)
|
||||
@@ -78,24 +84,6 @@
|
||||
|
||||
(select-keys thread [:id :file-id :page-id])))
|
||||
|
||||
(defn- create-comment-thread
|
||||
[conn params]
|
||||
(loop [sp (db/savepoint conn)
|
||||
rc 0]
|
||||
(let [res (ex/try (create-comment-thread* conn params))]
|
||||
(cond
|
||||
(and (instance? Throwable res)
|
||||
(< rc 3))
|
||||
(do
|
||||
(db/rollback! conn sp)
|
||||
(recur (db/savepoint conn)
|
||||
(inc rc)))
|
||||
|
||||
(instance? Throwable res)
|
||||
(throw res)
|
||||
|
||||
:else res))))
|
||||
|
||||
(defn- retrieve-page-name
|
||||
[conn {:keys [file-id page-id]}]
|
||||
(let [{:keys [data]} (db/get-by-id conn :file file-id)
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
params {:id id
|
||||
:email email
|
||||
:fullname fullname
|
||||
:is-demo true
|
||||
:is-active true
|
||||
:deleted-at (dt/in-future cf/deletion-delay)
|
||||
:password password
|
||||
:props {:onboarding-viewed true}}]
|
||||
@@ -46,8 +46,7 @@
|
||||
|
||||
(db/with-atomic [conn pool]
|
||||
(->> (#'profile/create-profile conn params)
|
||||
(#'profile/create-profile-relations conn)
|
||||
(sid/load-initial-project! conn))
|
||||
(#'profile/create-profile-relations conn))
|
||||
|
||||
(with-meta {:email email
|
||||
:password password}
|
||||
|
||||
@@ -16,10 +16,8 @@
|
||||
[app.loggers.audit :as audit]
|
||||
[app.media :as media]
|
||||
[app.metrics :as mtx]
|
||||
[app.rpc.mutations.projects :as projects]
|
||||
[app.rpc.mutations.teams :as teams]
|
||||
[app.rpc.queries.profile :as profile]
|
||||
[app.setup.initial-data :as sid]
|
||||
[app.storage :as sto]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
@@ -126,9 +124,7 @@
|
||||
|
||||
;; --- MUTATION: Register Profile
|
||||
|
||||
(s/def ::accept-terms-and-privacy ::us/boolean)
|
||||
(s/def ::token ::us/not-empty-string)
|
||||
|
||||
(s/def ::register-profile
|
||||
(s/keys :req-un [::token ::fullname]))
|
||||
|
||||
@@ -150,14 +146,15 @@
|
||||
[{:keys [conn tokens session metrics] :as cfg} {:keys [token] :as params}]
|
||||
(let [claims (tokens :verify {:token token :iss :prepared-register})
|
||||
params (merge params claims)]
|
||||
|
||||
(check-profile-existence! conn params)
|
||||
(let [profile (->> params
|
||||
|
||||
(let [is-active (or (:is-active params)
|
||||
(contains? cf/flags :insecure-register))
|
||||
profile (->> (assoc params :is-active is-active)
|
||||
(create-profile conn)
|
||||
(create-profile-relations conn)
|
||||
(decode-profile-row))]
|
||||
|
||||
(sid/load-initial-project! conn profile)
|
||||
|
||||
(cond
|
||||
;; If invitation token comes in params, this is because the
|
||||
;; user comes from team-invitation process; in this case,
|
||||
@@ -187,6 +184,15 @@
|
||||
::audit/props (audit/profile->props profile)
|
||||
::audit/profile-id (:id profile)})
|
||||
|
||||
;; If the `:enable-insecure-register` flag is set, we proceed
|
||||
;; to sign in the user directly, without email verification.
|
||||
(true? is-active)
|
||||
(with-meta (profile/strip-private-attrs profile)
|
||||
{:transform-response ((:create session) (:id profile))
|
||||
:before-complete (annotate-profile-register metrics)
|
||||
::audit/props (audit/profile->props profile)
|
||||
::audit/profile-id (:id profile)})
|
||||
|
||||
;; In all other cases, send a verification email.
|
||||
:else
|
||||
(let [vtoken (tokens :generate
|
||||
@@ -231,7 +237,7 @@
|
||||
backend (:backend params "penpot")
|
||||
is-demo (:is-demo params false)
|
||||
is-muted (:is-muted params false)
|
||||
is-active (:is-active params (or (not= "penpot" backend) is-demo))
|
||||
is-active (:is-active params false)
|
||||
email (str/lower (:email params))
|
||||
|
||||
params {:id id
|
||||
@@ -256,28 +262,15 @@
|
||||
:code :email-already-exists
|
||||
:cause e)))))))
|
||||
|
||||
|
||||
(defn create-profile-relations
|
||||
[conn profile]
|
||||
(let [team (teams/create-team conn {:profile-id (:id profile)
|
||||
:name "Default"
|
||||
:is-default true})
|
||||
project (projects/create-project conn {:profile-id (:id profile)
|
||||
:team-id (:id team)
|
||||
:name "Drafts"
|
||||
:is-default true})
|
||||
params {:team-id (:id team)
|
||||
:profile-id (:id profile)
|
||||
:project-id (:id project)
|
||||
:role :owner}]
|
||||
|
||||
(teams/create-team-role conn params)
|
||||
(projects/create-project-role conn params)
|
||||
|
||||
:is-default true})]
|
||||
(-> profile
|
||||
(profile/strip-private-attrs)
|
||||
(assoc :default-team-id (:id team))
|
||||
(assoc :default-project-id (:id project)))))
|
||||
(assoc :default-project-id (:default-project-id team)))))
|
||||
|
||||
;; --- MUTATION: Login
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
;; --- Mutation: Create Team
|
||||
|
||||
(declare create-team)
|
||||
(declare create-team-entry)
|
||||
(declare create-team-role)
|
||||
(declare create-team-default-project)
|
||||
|
||||
@@ -42,15 +43,21 @@
|
||||
(sv/defmethod ::create-team
|
||||
[{:keys [pool] :as cfg} params]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [team (create-team conn params)
|
||||
params (assoc params
|
||||
:team-id (:id team)
|
||||
:role :owner)]
|
||||
(create-team-role conn params)
|
||||
(create-team-default-project conn params)
|
||||
team)))
|
||||
(create-team conn params)))
|
||||
|
||||
(defn create-team
|
||||
"This is a complete team creation process, it creates the team
|
||||
object and all related objects (default role and default project)."
|
||||
[conn params]
|
||||
(let [team (create-team-entry conn params)
|
||||
params (assoc params
|
||||
:team-id (:id team)
|
||||
:role :owner)
|
||||
project (create-team-default-project conn params)]
|
||||
(create-team-role conn params)
|
||||
(assoc team :default-project-id (:id project))))
|
||||
|
||||
(defn- create-team-entry
|
||||
[conn {:keys [id name is-default] :as params}]
|
||||
(let [id (or id (uuid/next))
|
||||
is-default (if (boolean? is-default) is-default false)]
|
||||
@@ -59,23 +66,24 @@
|
||||
:name name
|
||||
:is-default is-default})))
|
||||
|
||||
(defn create-team-role
|
||||
(defn- create-team-role
|
||||
[conn {:keys [team-id profile-id role] :as params}]
|
||||
(let [params {:team-id team-id
|
||||
:profile-id profile-id}]
|
||||
(->> (perms/assign-role-flags params role)
|
||||
(db/insert! conn :team-profile-rel))))
|
||||
|
||||
(defn create-team-default-project
|
||||
(defn- create-team-default-project
|
||||
[conn {:keys [team-id profile-id] :as params}]
|
||||
(let [project {:id (uuid/next)
|
||||
:team-id team-id
|
||||
:name "Drafts"
|
||||
:is-default true}]
|
||||
(projects/create-project conn project)
|
||||
:is-default true}
|
||||
project (projects/create-project conn project)]
|
||||
(projects/create-project-role conn {:project-id (:id project)
|
||||
:profile-id profile-id
|
||||
:role :owner})))
|
||||
:role :owner})
|
||||
project))
|
||||
|
||||
;; --- Mutation: Update Team
|
||||
|
||||
@@ -293,28 +301,18 @@
|
||||
|
||||
;; --- Mutation: Invite Member
|
||||
|
||||
(declare create-team-invitation)
|
||||
|
||||
(s/def ::email ::us/email)
|
||||
(s/def ::invite-team-member
|
||||
(s/keys :req-un [::profile-id ::team-id ::email ::role]))
|
||||
|
||||
(sv/defmethod ::invite-team-member
|
||||
[{:keys [pool tokens] :as cfg} {:keys [profile-id team-id email role] :as params}]
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id team-id email role] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [perms (teams/get-permissions conn profile-id team-id)
|
||||
profile (db/get-by-id conn :profile profile-id)
|
||||
member (profile/retrieve-profile-data-by-email conn email)
|
||||
team (db/get-by-id conn :team team-id)
|
||||
itoken (tokens :generate
|
||||
{:iss :team-invitation
|
||||
:exp (dt/in-future "48h")
|
||||
:profile-id (:id profile)
|
||||
:role role
|
||||
:team-id team-id
|
||||
:member-email (:email member email)
|
||||
:member-id (:id member)})
|
||||
ptoken (tokens :generate-predefined
|
||||
{:iss :profile-identity
|
||||
:profile-id (:id profile)})]
|
||||
team (db/get-by-id conn :team team-id)]
|
||||
|
||||
(when-not (:is-admin perms)
|
||||
(ex/raise :type :validation
|
||||
@@ -326,6 +324,30 @@
|
||||
:code :profile-is-muted
|
||||
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces"))
|
||||
|
||||
(create-team-invitation
|
||||
(assoc cfg
|
||||
:email email
|
||||
:conn conn
|
||||
:team team
|
||||
:profile profile
|
||||
:role role))
|
||||
nil)))
|
||||
|
||||
(defn- create-team-invitation
|
||||
[{:keys [conn tokens team profile role email] :as cfg}]
|
||||
(let [member (profile/retrieve-profile-data-by-email conn email)
|
||||
itoken (tokens :generate
|
||||
{:iss :team-invitation
|
||||
:exp (dt/in-future "48h")
|
||||
:profile-id (:id profile)
|
||||
:role role
|
||||
:team-id (:id team)
|
||||
:member-email (:email member email)
|
||||
:member-id (:id member)})
|
||||
ptoken (tokens :generate-predefined
|
||||
{:iss :profile-identity
|
||||
:profile-id (:id profile)})]
|
||||
|
||||
(when (and member (not (eml/allow-send-emails? conn member)))
|
||||
(ex/raise :type :validation
|
||||
:code :member-is-muted
|
||||
@@ -345,5 +367,28 @@
|
||||
:invited-by (:fullname profile)
|
||||
:team (:name team)
|
||||
:token itoken
|
||||
:extra-data ptoken})
|
||||
nil)))
|
||||
:extra-data ptoken})))
|
||||
|
||||
|
||||
;; --- Mutation: Create Team & Invite Members
|
||||
|
||||
(s/def ::emails ::us/set-of-emails)
|
||||
(s/def ::create-team-and-invite-members
|
||||
(s/and ::create-team (s/keys :req-un [::emails ::role])))
|
||||
|
||||
(sv/defmethod ::create-team-and-invite-members
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id emails role] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [team (create-team conn params)
|
||||
profile (db/get-by-id conn :profile profile-id)]
|
||||
|
||||
;; Create invitations for all provided emails.
|
||||
(doseq [email emails]
|
||||
(create-team-invitation
|
||||
(assoc cfg
|
||||
:conn conn
|
||||
:team team
|
||||
:profile profile
|
||||
:email email
|
||||
:role role)))
|
||||
team)))
|
||||
|
||||
@@ -34,10 +34,15 @@
|
||||
(when (profile/retrieve-profile-data-by-email conn email)
|
||||
(ex/raise :type :validation
|
||||
:code :email-already-exists))
|
||||
|
||||
(db/update! conn :profile
|
||||
{:email email}
|
||||
{:id profile-id})
|
||||
claims)
|
||||
|
||||
(with-meta claims
|
||||
{::audit/name "update-profile-email"
|
||||
::audit/props {:email email}
|
||||
::audit/profile-id profile-id}))
|
||||
|
||||
(defn- annotate-profile-activation
|
||||
"A helper for properly increase the profile-activation metric once the
|
||||
|
||||
@@ -87,13 +87,9 @@
|
||||
|
||||
(defn retrieve-profile
|
||||
[conn id]
|
||||
(let [profile (some->> (retrieve-profile-data conn id)
|
||||
(let [profile (->> (retrieve-profile-data conn id)
|
||||
(strip-private-attrs)
|
||||
(populate-additional-data conn))]
|
||||
(when (nil? profile)
|
||||
(ex/raise :type :not-found
|
||||
:hint "Object doest not exists."))
|
||||
|
||||
(update profile :props filter-profile-props)))
|
||||
|
||||
(def ^:private sql:profile-by-email
|
||||
|
||||
@@ -79,12 +79,14 @@
|
||||
where f.project_id = p.id
|
||||
and deleted_at is null) as count
|
||||
from project as p
|
||||
inner join team as t on (t.id = p.team_id)
|
||||
left join team_project_profile_rel as tpp
|
||||
on (tpp.project_id = p.id and
|
||||
tpp.team_id = p.team_id and
|
||||
tpp.profile_id = ?)
|
||||
where p.team_id = ?
|
||||
and p.deleted_at is null
|
||||
and t.deleted_at is null
|
||||
order by p.modified_at desc")
|
||||
|
||||
(defn retrieve-projects
|
||||
@@ -108,26 +110,26 @@
|
||||
(def sql:all-projects
|
||||
"select p1.*, t.name as team_name, t.is_default as is_default_team
|
||||
from project as p1
|
||||
inner join team as t
|
||||
on t.id = p1.team_id
|
||||
inner join team as t on (t.id = p1.team_id)
|
||||
where t.id in (select team_id
|
||||
from team_profile_rel as tpr
|
||||
where tpr.profile_id = ?
|
||||
and (tpr.can_edit = true or
|
||||
tpr.is_owner = true or
|
||||
tpr.is_admin = true))
|
||||
and t.deleted_at is null
|
||||
and p1.deleted_at is null
|
||||
union
|
||||
select p2.*, t.name as team_name, t.is_default as is_default_team
|
||||
from project as p2
|
||||
inner join team as t
|
||||
on t.id = p2.team_id
|
||||
inner join team as t on (t.id = p2.team_id)
|
||||
where p2.id in (select project_id
|
||||
from project_profile_rel as ppr
|
||||
where ppr.profile_id = ?
|
||||
and (ppr.can_edit = true or
|
||||
ppr.is_owner = true or
|
||||
ppr.is_admin = true))
|
||||
and t.deleted_at is null
|
||||
and p2.deleted_at is null
|
||||
order by team_name, name;")
|
||||
|
||||
|
||||
43
backend/src/app/util/retry.clj
Normal file
@@ -0,0 +1,43 @@
|
||||
;; 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.util.retry
|
||||
"A fault tolerance helpers. Allow retry some operations that we know
|
||||
we can retry."
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.util.async :as aa]
|
||||
[app.util.services :as sv]))
|
||||
|
||||
(defn conflict-db-insert?
|
||||
"Check if exception matches a insertion conflict on postgresql."
|
||||
[e]
|
||||
(and (instance? org.postgresql.util.PSQLException e)
|
||||
(= "23505" (.getSQLState e))))
|
||||
|
||||
(defn wrap-retry
|
||||
[_ f {:keys [::max-retries ::matches ::sv/name]
|
||||
:or {max-retries 3
|
||||
matches (constantly false)}
|
||||
:as mdata}]
|
||||
(when (::enabled mdata)
|
||||
(l/debug :hint "wrapping retry" :name name))
|
||||
(if (::enabled mdata)
|
||||
(fn [cfg params]
|
||||
(loop [retry 1]
|
||||
(when (> retry 1)
|
||||
(l/debug :hint "retrying controlled function" :retry retry :name name))
|
||||
(let [res (ex/try (f cfg params))]
|
||||
(if (ex/exception? res)
|
||||
(if (and (matches res) (< retry max-retries))
|
||||
(do
|
||||
(aa/thread-sleep (* 100 retry))
|
||||
(recur (inc retry)))
|
||||
(throw res))
|
||||
res))))
|
||||
f))
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
|
||||
(t/is (nil? (:error out)))
|
||||
(let [result (:result out)]
|
||||
(t/is (= 1 (count result)))
|
||||
(t/is (= 2 (count result)))
|
||||
(t/is project-id (get-in result [0 :id]))
|
||||
(t/is (= "test project" (get-in result [0 :name])))))
|
||||
|
||||
@@ -55,15 +55,15 @@
|
||||
|
||||
(t/is (nil? (:error out)))
|
||||
(let [result (:result out)]
|
||||
(t/is (= 2 (count result)))
|
||||
(t/is (= 3 (count result)))
|
||||
(t/is (not= project-id (get-in result [0 :id])))
|
||||
(t/is (= "Drafts" (get-in result [0 :name])))
|
||||
(t/is (= "Default" (get-in result [0 :team-name])))
|
||||
(t/is (= true (get-in result [0 :is-default-team])))
|
||||
(t/is project-id (get-in result [1 :id]))
|
||||
(t/is (= "test project" (get-in result [1 :name])))
|
||||
(t/is (= "team1" (get-in result [1 :team-name])))
|
||||
(t/is (= false (get-in result [1 :is-default-team])))))
|
||||
(t/is project-id (get-in result [2 :id]))
|
||||
(t/is (= "test project" (get-in result [2 :name])))
|
||||
(t/is (= "team1" (get-in result [2 :team-name])))
|
||||
(t/is (= false (get-in result [2 :is-default-team])))))
|
||||
|
||||
;; rename project
|
||||
(let [data {::th/type :rename-project
|
||||
@@ -95,7 +95,7 @@
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (nil? (:result out))))
|
||||
|
||||
;; query a list of projects after delete"
|
||||
;; query a list of projects after delete
|
||||
(let [data {::th/type :projects
|
||||
:team-id (:id team)
|
||||
:profile-id (:id profile)}
|
||||
@@ -103,7 +103,7 @@
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? (:error out)))
|
||||
(let [result (:result out)]
|
||||
(t/is (= 0 (count result)))))
|
||||
(t/is (= 1 (count result)))))
|
||||
))
|
||||
|
||||
(t/deftest permissions-checks-create-project
|
||||
|
||||
@@ -130,7 +130,7 @@
|
||||
(let [result (task {:max-age (dt/duration {:minutes 1})})]
|
||||
(t/is (nil? result)))
|
||||
|
||||
;; query the list of projects of a after hard deletion
|
||||
;; query the list of projects after hard deletion
|
||||
(let [data {::th/type :projects
|
||||
:team-id (:id team)
|
||||
:profile-id (:id profile1)}
|
||||
|
||||
@@ -126,7 +126,8 @@
|
||||
:password "123123"
|
||||
:is-demo false}
|
||||
params)]
|
||||
(->> (#'profile/create-profile conn params)
|
||||
(->> params
|
||||
(#'profile/create-profile conn)
|
||||
(#'profile/create-profile-relations conn)))))
|
||||
|
||||
(defn create-project*
|
||||
@@ -159,15 +160,10 @@
|
||||
([i params] (create-team* *pool* i params))
|
||||
([conn i {:keys [profile-id] :as params}]
|
||||
(us/assert uuid? profile-id)
|
||||
(let [id (mk-uuid "team" i)
|
||||
team (#'teams/create-team conn {:id id
|
||||
(let [id (mk-uuid "team" i)]
|
||||
(teams/create-team conn {:id id
|
||||
:profile-id profile-id
|
||||
:name (str "team" i)})]
|
||||
(#'teams/create-team-role conn
|
||||
{:team-id id
|
||||
:profile-id profile-id
|
||||
:role :owner})
|
||||
team)))
|
||||
:name (str "team" i)}))))
|
||||
|
||||
(defn create-file-media-object*
|
||||
([params] (create-file-media-object* *pool* params))
|
||||
@@ -350,3 +346,11 @@
|
||||
(defn reset-mock!
|
||||
[m]
|
||||
(reset! m @(mk/make-mock {})))
|
||||
|
||||
(defn pause
|
||||
[]
|
||||
(let [^java.io.Console cnsl (System/console)]
|
||||
(println "[waiting RETURN]")
|
||||
(.readLine cnsl)
|
||||
nil))
|
||||
|
||||
|
||||
@@ -392,7 +392,8 @@
|
||||
|
||||
(defmethod read-action-opts :navigate
|
||||
[interaction-src]
|
||||
(select-keys interaction-src [:destination]))
|
||||
(select-keys interaction-src [:destination
|
||||
:preserve-scroll]))
|
||||
|
||||
(defmethod read-action-opts :open-overlay
|
||||
[interaction-src]
|
||||
@@ -430,7 +431,8 @@
|
||||
(let [{:keys [event-type action-type]} (read-classifier interaction-src)
|
||||
{:keys [delay]} (read-event-opts interaction-src)
|
||||
{:keys [destination overlay-pos-type overlay-position url
|
||||
close-click-outside background-overlay]} (read-action-opts interaction-src)
|
||||
close-click-outside background-overlay preserve-scroll]}
|
||||
(read-action-opts interaction-src)
|
||||
|
||||
interactions (-> (lookup-shape file from-id)
|
||||
:interactions
|
||||
@@ -443,7 +445,8 @@
|
||||
:overlay-position overlay-position
|
||||
:url url
|
||||
:close-click-outside close-click-outside
|
||||
:background-overlay background-overlay})))]
|
||||
:background-overlay background-overlay
|
||||
:preserve-scroll preserve-scroll})))]
|
||||
(commit-change
|
||||
file
|
||||
{:type :mod-obj
|
||||
|
||||
@@ -10,16 +10,14 @@
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(def default
|
||||
#{:backend-asserts
|
||||
:api-doc
|
||||
:registration
|
||||
:demo-users})
|
||||
"A common flags that affects both: backend and frontend."
|
||||
[:enable-registration
|
||||
:enable-demo-users])
|
||||
|
||||
(defn parse
|
||||
([flags] (parse flags #{}))
|
||||
([flags default]
|
||||
(loop [flags (seq flags)
|
||||
result default]
|
||||
[& flags]
|
||||
(loop [flags (apply concat flags)
|
||||
result #{}]
|
||||
(let [item (first flags)]
|
||||
(if (nil? item)
|
||||
result
|
||||
@@ -34,6 +32,6 @@
|
||||
(disj result (keyword (subs sname 8))))
|
||||
|
||||
:else
|
||||
(recur (rest flags) result))))))))
|
||||
(recur (rest flags) result)))))))
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
(ns app.common.geom.shapes.intersect
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes.path :as gpp]
|
||||
@@ -172,7 +173,8 @@
|
||||
"Checks if the given rect overlaps with the path in any point"
|
||||
[shape rect]
|
||||
|
||||
(let [;; If paths are too complex the intersection is too expensive
|
||||
(when (d/not-empty? (:content shape))
|
||||
(let [ ;; If paths are too complex the intersection is too expensive
|
||||
;; we fallback to check its bounding box otherwise the performance penalty
|
||||
;; is too big
|
||||
;; TODO: Look for ways to optimize this operation
|
||||
@@ -187,7 +189,7 @@
|
||||
|
||||
(or (is-point-inside-nonzero? (first rect-points) path-lines)
|
||||
(is-point-inside-nonzero? start-point rect-lines)
|
||||
(intersects-lines? rect-lines path-lines))))
|
||||
(intersects-lines? rect-lines path-lines)))))
|
||||
|
||||
(defn is-point-inside-ellipse?
|
||||
"checks if a point is inside an ellipse"
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
[app.common.spec :as us]
|
||||
[app.common.types.interactions :as cti]
|
||||
[app.common.types.page-options :as cto]
|
||||
[app.common.types.radius :as ctr]
|
||||
[app.common.uuid :as uuid]
|
||||
[clojure.set :as set]
|
||||
[clojure.spec.alpha :as s]))
|
||||
@@ -191,12 +192,6 @@
|
||||
(s/def :internal.shape/page-id uuid?)
|
||||
(s/def :internal.shape/proportion ::us/safe-number)
|
||||
(s/def :internal.shape/proportion-lock boolean?)
|
||||
(s/def :internal.shape/rx ::us/safe-number)
|
||||
(s/def :internal.shape/ry ::us/safe-number)
|
||||
(s/def :internal.shape/r1 ::us/safe-number)
|
||||
(s/def :internal.shape/r2 ::us/safe-number)
|
||||
(s/def :internal.shape/r3 ::us/safe-number)
|
||||
(s/def :internal.shape/r4 ::us/safe-number)
|
||||
(s/def :internal.shape/stroke-color string?)
|
||||
(s/def :internal.shape/stroke-color-gradient (s/nilable ::gradient))
|
||||
(s/def :internal.shape/stroke-color-ref-file (s/nilable uuid?))
|
||||
@@ -285,12 +280,12 @@
|
||||
:internal.shape/constraints-h
|
||||
:internal.shape/constraints-v
|
||||
:internal.shape/fixed-scroll
|
||||
:internal.shape/rx
|
||||
:internal.shape/ry
|
||||
:internal.shape/r1
|
||||
:internal.shape/r2
|
||||
:internal.shape/r3
|
||||
:internal.shape/r4
|
||||
::ctr/rx
|
||||
::ctr/ry
|
||||
::ctr/r1
|
||||
::ctr/r2
|
||||
::ctr/r3
|
||||
::ctr/r4
|
||||
:internal.shape/x
|
||||
:internal.shape/y
|
||||
:internal.shape/exports
|
||||
|
||||
@@ -177,18 +177,11 @@
|
||||
(map #(get objects %))
|
||||
(map #(convert-to-path % objects)))
|
||||
bool-type (:bool-type shape)
|
||||
head (if (= bool-type :difference) (first children) (last children))
|
||||
head (cond-> head
|
||||
(and (contains? head :svg-attrs) (nil? (:fill-color head)))
|
||||
(assoc :fill-color "#000000"))
|
||||
|
||||
head-data (select-keys head style-properties)
|
||||
content (pb/content-bool (:bool-type shape) (mapv :content children))]
|
||||
content (pb/content-bool bool-type (mapv :content children))]
|
||||
|
||||
(-> shape
|
||||
(assoc :type :path)
|
||||
(assoc :content content)
|
||||
(merge head-data)
|
||||
(d/without-keys dissoc-attrs))))
|
||||
|
||||
(defn convert-to-path
|
||||
|
||||
@@ -111,16 +111,6 @@
|
||||
(s/def ::point gpt/point?)
|
||||
(s/def ::id ::uuid)
|
||||
|
||||
(s/def ::words
|
||||
(s/conformer
|
||||
(fn [s]
|
||||
(cond
|
||||
(set? s) s
|
||||
(string? s) (into #{} (map keyword) (str/words s))
|
||||
:else ::s/invalid))
|
||||
(fn [s]
|
||||
(str/join " " (map name s)))))
|
||||
|
||||
(defn bytes?
|
||||
"Test if a first parameter is a byte
|
||||
array or not."
|
||||
@@ -134,7 +124,6 @@
|
||||
|
||||
(s/def ::bytes bytes?)
|
||||
|
||||
|
||||
(s/def ::safe-integer
|
||||
#(and
|
||||
(int? %)
|
||||
@@ -149,7 +138,27 @@
|
||||
(<= % max-safe-int)))
|
||||
|
||||
|
||||
;; --- SPEC: set of Keywords
|
||||
|
||||
(s/def ::set-of-keywords
|
||||
(s/conformer
|
||||
(fn [s]
|
||||
(let [xform (comp
|
||||
(map (fn [s]
|
||||
(cond
|
||||
(string? s) (keyword s)
|
||||
(keyword? s) s
|
||||
:else nil)))
|
||||
(filter identity))]
|
||||
(cond
|
||||
(set? s) (into #{} xform s)
|
||||
(string? s) (into #{} xform (str/words s))
|
||||
:else ::s/invalid)))
|
||||
(fn [s]
|
||||
(str/join " " (map name s)))))
|
||||
|
||||
;; --- SPEC: email
|
||||
|
||||
(def email-re #"[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+")
|
||||
|
||||
(s/def ::email
|
||||
@@ -162,6 +171,23 @@
|
||||
::s/invalid))
|
||||
str))
|
||||
|
||||
(s/def ::set-of-emails
|
||||
(s/conformer
|
||||
(fn [v]
|
||||
(cond
|
||||
(string? v)
|
||||
(into #{} (re-seq email-re v))
|
||||
|
||||
(or (set? v) (sequential? v))
|
||||
(->> (str/join " " v)
|
||||
(re-seq email-re)
|
||||
(into #{}))
|
||||
|
||||
:else ::s/invalid))
|
||||
|
||||
(fn [v]
|
||||
(str/join " " v))))
|
||||
|
||||
;; --- SPEC: set-of-str
|
||||
|
||||
(s/def ::set-of-str
|
||||
|
||||
@@ -64,11 +64,13 @@
|
||||
(s/def ::url ::us/string)
|
||||
(s/def ::close-click-outside ::us/boolean)
|
||||
(s/def ::background-overlay ::us/boolean)
|
||||
(s/def ::preserve-scroll ::us/boolean)
|
||||
|
||||
(defmulti action-opts-spec :action-type)
|
||||
|
||||
(defmethod action-opts-spec :navigate [_]
|
||||
(s/keys :req-un [::destination]))
|
||||
(s/keys :req-un [::destination]
|
||||
:opt-un [::preserve-scroll]))
|
||||
|
||||
(defmethod action-opts-spec :open-overlay [_]
|
||||
(s/keys :req-un [::destination
|
||||
@@ -151,7 +153,8 @@
|
||||
:navigate
|
||||
(assoc interaction
|
||||
:action-type action-type
|
||||
:destination (get interaction :destination))
|
||||
:destination (get interaction :destination)
|
||||
:preserve-scroll false)
|
||||
|
||||
(:open-overlay :toggle-overlay)
|
||||
(let [overlay-pos-type (get interaction :overlay-pos-type :center)
|
||||
@@ -196,6 +199,10 @@
|
||||
(and (has-destination interaction)
|
||||
(some? (:destination interaction))))
|
||||
|
||||
(defn has-preserve-scroll
|
||||
[interaction]
|
||||
(= (:action-type interaction) :navigate))
|
||||
|
||||
(defn set-destination
|
||||
[interaction destination]
|
||||
(us/verify ::interaction interaction)
|
||||
@@ -210,6 +217,13 @@
|
||||
(assoc :overlay-pos-type :center
|
||||
:overlay-position (gpt/point 0 0))))
|
||||
|
||||
(defn set-preserve-scroll
|
||||
[interaction preserve-scroll]
|
||||
(us/verify ::interaction interaction)
|
||||
(us/verify ::us/boolean preserve-scroll)
|
||||
(assert (has-preserve-scroll interaction))
|
||||
(assoc interaction :preserve-scroll preserve-scroll))
|
||||
|
||||
(defn has-url
|
||||
[interaction]
|
||||
(= (:action-type interaction) :open-url))
|
||||
|
||||
90
common/src/app/common/types/radius.cljc
Normal file
@@ -0,0 +1,90 @@
|
||||
;; 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.common.types.radius
|
||||
(:require
|
||||
[app.common.spec :as us]
|
||||
[clojure.spec.alpha :as s]))
|
||||
|
||||
(s/def ::rx ::us/safe-number)
|
||||
(s/def ::ry ::us/safe-number)
|
||||
(s/def ::r1 ::us/safe-number)
|
||||
(s/def ::r2 ::us/safe-number)
|
||||
(s/def ::r3 ::us/safe-number)
|
||||
(s/def ::r4 ::us/safe-number)
|
||||
|
||||
;; Rectangle shapes may define the radius of the corners in two modes:
|
||||
;; - radius-1 all corners have the same radius (although we store two
|
||||
;; values :rx and :ry because svg uses it this way).
|
||||
;; - radius-4 each corner (top-left, top-right, bottom-right, bottom-left)
|
||||
;; has an independent value. SVG does not allow this directly, so we
|
||||
;; emulate it with paths.
|
||||
|
||||
;; A shape never will have both :rx and :r1 simultaneously
|
||||
|
||||
;; All operations take into account that the shape may not be a rectangle, and so
|
||||
;; it hasn't :rx nor :r1. In this case operations must leave shape untouched.
|
||||
|
||||
(defn radius-mode
|
||||
[shape]
|
||||
(cond (:rx shape) :radius-1
|
||||
(:r1 shape) :radius-4
|
||||
:else nil))
|
||||
|
||||
(defn radius-1?
|
||||
[shape]
|
||||
(and (:rx shape) (not= (:rx shape) 0)))
|
||||
|
||||
(defn radius-4?
|
||||
[shape]
|
||||
(and (:r1 shape)
|
||||
(or (not= (:r1 shape) 0)
|
||||
(not= (:r2 shape) 0)
|
||||
(not= (:r3 shape) 0)
|
||||
(not= (:r4 shape) 0))))
|
||||
|
||||
(defn all-equal?
|
||||
[shape]
|
||||
(= (:r1 shape) (:r2 shape) (:r3 shape) (:r4 shape)))
|
||||
|
||||
(defn switch-to-radius-1
|
||||
[shape]
|
||||
(let [r (if (all-equal? shape) (:r1 shape) 0)]
|
||||
(cond-> shape
|
||||
(:r1 shape)
|
||||
(-> (assoc :rx r :ry r)
|
||||
(dissoc :r1 :r2 :r3 :r4)))))
|
||||
|
||||
(defn switch-to-radius-4
|
||||
[shape]
|
||||
(cond-> shape
|
||||
(:rx shape)
|
||||
(-> (assoc :r1 (:rx shape)
|
||||
:r2 (:rx shape)
|
||||
:r3 (:rx shape)
|
||||
:r4 (:rx shape))
|
||||
(dissoc :rx :ry))))
|
||||
|
||||
(defn set-radius-1
|
||||
[shape value]
|
||||
(cond-> shape
|
||||
(:r1 shape)
|
||||
(-> (dissoc :r1 :r2 :r3 :r4)
|
||||
(assoc :rx 0 :ry 0))
|
||||
|
||||
(:rx shape)
|
||||
(assoc :rx value :ry value)))
|
||||
|
||||
(defn set-radius-4
|
||||
[shape attr value]
|
||||
(cond-> shape
|
||||
(:rx shape)
|
||||
(-> (dissoc :rx :rx)
|
||||
(assoc :r1 0 :r2 0 :r3 0 :r4 0))
|
||||
|
||||
(attr shape)
|
||||
(assoc attr value)))
|
||||
|
||||
@@ -5,7 +5,7 @@ ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
ENV NODE_VERSION=v14.17.6 \
|
||||
CLOJURE_VERSION=1.10.3.967 \
|
||||
CLJKONDO_VERSION=2021.09.15 \
|
||||
CLJKONDO_VERSION=2021.10.19 \
|
||||
BABASHKA_VERSION=0.6.1 \
|
||||
LANG=en_US.UTF-8 \
|
||||
LC_ALL=en_US.UTF-8
|
||||
|
||||
@@ -50,7 +50,7 @@ services:
|
||||
- PENPOT_SMTP_PASSWORD=
|
||||
- PENPOT_SMTP_SSL=false
|
||||
- PENPOT_SMTP_TLS=false
|
||||
- PENPOT_FLAGS="enable-cors"
|
||||
- PENPOT_FLAGS="enable-cors enable-insecure-register enable-terms-and-privacy-checkbox"
|
||||
|
||||
# LDAP setup
|
||||
- PENPOT_LDAP_HOST=ldap
|
||||
|
||||
@@ -89,6 +89,16 @@ http {
|
||||
error_page 301 302 307 = @handle_redirect;
|
||||
}
|
||||
|
||||
location ~ ^/github/penpot-files/(?<template_file>[a-zA-Z0-9\-\_\.]+) {
|
||||
proxy_pass https://raw.githubusercontent.com/penpot/penpot-files/main/$template_file;
|
||||
proxy_hide_header Access-Control-Allow-Origin;
|
||||
proxy_set_header User-Agent "curl/7.74.0";
|
||||
proxy_set_header Host "raw.githubusercontent.com";
|
||||
proxy_set_header Accept "*/*";
|
||||
add_header Access-Control-Allow-Origin $http_origin;
|
||||
proxy_buffering off;
|
||||
}
|
||||
|
||||
location /internal/assets {
|
||||
internal;
|
||||
alias /home/penpot/penpot/backend/assets;
|
||||
|
||||
@@ -104,7 +104,7 @@
|
||||
(def browser-pool-factory
|
||||
(letfn [(create []
|
||||
(let [path (cf/get :browser-executable-path "/usr/bin/google-chrome")]
|
||||
(-> (pp/launch #js {:executablePath path :args #js ["--no-sandbox"]})
|
||||
(-> (pp/launch #js {:executablePath path :args #js ["--no-sandbox" "--font-render-hinting=none"]})
|
||||
(p/then (fn [browser]
|
||||
(let [id (deref pool-browser-id)]
|
||||
(log/info :origin "factory" :action "create" :browser-id id)
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
[app.renderer.bitmap :refer [create-cookie]]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(log/set-level "app.http.export-svg" :trace)
|
||||
(log/set-level "app.renderer.svg" :trace)
|
||||
|
||||
(defn- xml->clj
|
||||
[data]
|
||||
@@ -146,15 +146,61 @@
|
||||
{:color color
|
||||
:svgdata data}))))))
|
||||
|
||||
(join-color-layers [{:keys [x y width height] :as node} layers]
|
||||
(log/trace :fn :join-color-layers)
|
||||
(set-path-color [id color mapping node]
|
||||
(let [color-mapping (get mapping color)]
|
||||
(cond
|
||||
(and (some? color-mapping)
|
||||
(= "transparent" (get color-mapping "type")))
|
||||
(update node "attributes" assoc
|
||||
"fill" (get color-mapping "hex")
|
||||
"fill-opacity" (get color-mapping "opacity"))
|
||||
|
||||
(and (some? color-mapping)
|
||||
(= "gradient" (get color-mapping "type")))
|
||||
(update node "attributes" assoc
|
||||
"fill" (str "url(#gradient-" id "-" (subs color 1) ")"))
|
||||
|
||||
:else
|
||||
(update node "attributes" assoc "fill" color))))
|
||||
|
||||
(get-stops [data]
|
||||
(->> (get-in data ["gradient" "stops"])
|
||||
(mapv (fn [stop-data]
|
||||
{"type" "element"
|
||||
"name" "stop"
|
||||
"attributes" {"offset" (get stop-data "offset")
|
||||
"stop-color" (get stop-data "color")
|
||||
"stop-opacity" (get stop-data "opacity")}}))))
|
||||
|
||||
(data->gradient-def [id [color data]]
|
||||
(let [id (str "gradient-" id "-" (subs color 1))]
|
||||
(if (= type "linear")
|
||||
{"type" "element"
|
||||
"name" "linearGradient"
|
||||
"attributes" {"id" id "x1" "0.5" "y1" "1" "x2" "0.5" "y2" "0"}
|
||||
"elements" (get-stops data)}
|
||||
|
||||
{"type" "element"
|
||||
"name" "radialGradient"
|
||||
"attributes" {"id" id "cx" "0.5" "cy" "0.5" "r" "0.5"}
|
||||
"elements" (get-stops data)}
|
||||
)))
|
||||
|
||||
(get-gradients [id mapping]
|
||||
(->> mapping
|
||||
(filter (fn [[color data]]
|
||||
(= (get data "type") "gradient")))
|
||||
(mapv (partial data->gradient-def id))))
|
||||
|
||||
(join-color-layers [{:keys [id x y width height mapping] :as node} layers]
|
||||
(log/trace :fn :join-color-layers :mapping mapping)
|
||||
(loop [result (-> (:svgdata (first layers))
|
||||
(assoc "elements" []))
|
||||
layers (seq layers)]
|
||||
(if-let [{:keys [color svgdata]} (first layers)]
|
||||
(recur (->> (get svgdata "elements")
|
||||
(filter #(= (get % "name") "g"))
|
||||
(map #(update % "attributes" assoc "fill" color))
|
||||
(map (partial set-path-color id color mapping))
|
||||
(update result "elements" d/concat))
|
||||
(rest layers))
|
||||
|
||||
@@ -166,11 +212,12 @@
|
||||
(parse-viewbox))
|
||||
transform (str/fmt "translate(%s, %s) scale(%s, %s)" x y
|
||||
(/ width (:width vbox))
|
||||
(/ height (:height vbox)))]
|
||||
(-> result
|
||||
(assoc "name" "g")
|
||||
(assoc "attributes" {})
|
||||
(update "elements" (fn [elements]
|
||||
(/ height (:height vbox)))
|
||||
|
||||
gradient-defs (get-gradients id mapping)
|
||||
|
||||
elements
|
||||
(->> (get result "elements")
|
||||
(mapv (fn [group]
|
||||
(let [paths (get group "elements")]
|
||||
(if (= 1 (count paths))
|
||||
@@ -180,8 +227,18 @@
|
||||
(-> attrs
|
||||
(d/merge (get group "attributes"))
|
||||
(update "transform" #(str transform " " %))))))
|
||||
(update-in group ["attributes" "transform"] #(str transform " " %)))))
|
||||
elements))))))))
|
||||
(update-in group ["attributes" "transform"] #(str transform " " %)))))))
|
||||
|
||||
|
||||
elements (cond->> elements
|
||||
(not (empty? gradient-defs))
|
||||
(d/concat [{"type" "element" "name" "defs" "attributes" {}
|
||||
"elements" gradient-defs}]))]
|
||||
|
||||
(-> result
|
||||
(assoc "name" "g")
|
||||
(assoc "attributes" {})
|
||||
(assoc "elements" elements))))))
|
||||
|
||||
(convert-to-svg [ppmpath {:keys [colors] :as node}]
|
||||
(log/trace :fn :convert-to-svg :ppmpath ppmpath :colors colors)
|
||||
@@ -202,13 +259,15 @@
|
||||
|
||||
(extract-element-attrs [^js element]
|
||||
(let [^js attrs (.. element -attributes)
|
||||
^js colors (.. element -dataset -colors)]
|
||||
^js colors (.. element -dataset -colors)
|
||||
^js mapping (.. element -dataset -mapping)]
|
||||
#js {:id (.. attrs -id -value)
|
||||
:x (.. attrs -x -value)
|
||||
:y (.. attrs -y -value)
|
||||
:width (.. attrs -width -value)
|
||||
:height (.. attrs -height -value)
|
||||
:colors (.split colors ",")}))
|
||||
:colors (.split colors ",")
|
||||
:mapping (js/JSON.parse mapping)}))
|
||||
|
||||
(extract-single-node [[shot node]]
|
||||
(log/trace :fn :extract-single-node)
|
||||
@@ -220,6 +279,7 @@
|
||||
:width (unchecked-get attrs "width")
|
||||
:height (unchecked-get attrs "height")
|
||||
:colors (vec (unchecked-get attrs "colors"))
|
||||
:mapping (js->clj (unchecked-get attrs "mapping"))
|
||||
:data shot}))
|
||||
|
||||
(resolve-text-node [page node]
|
||||
@@ -313,3 +373,4 @@
|
||||
".svg"))
|
||||
:length (alength content)
|
||||
:mime-type "image/svg+xml"}))
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
:dev
|
||||
{:extra-deps
|
||||
{thheller/shadow-cljs {:mvn/version "2.15.9"}
|
||||
{thheller/shadow-cljs {:mvn/version "2.15.12"}
|
||||
cider/cider-nrepl {:mvn/version "0.26.0"}}}
|
||||
|
||||
:shadow-cljs
|
||||
|
||||
@@ -25,6 +25,17 @@ paths.resources = "./resources/";
|
||||
paths.output = "./resources/public/";
|
||||
paths.dist = "./target/dist/";
|
||||
|
||||
/***********************************************
|
||||
* Marked Extensions
|
||||
***********************************************/
|
||||
|
||||
const renderer = {
|
||||
link(href, title, text) {
|
||||
return `<a href="${href}" target="_blank">${text}</a>`;
|
||||
}
|
||||
};
|
||||
|
||||
marked.use({renderer});
|
||||
|
||||
/***********************************************
|
||||
* Helpers
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"start": "npm-run-all --parallel watch-gulp watch-main"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^10.2.4",
|
||||
"autoprefixer": "^10.3.7",
|
||||
"gettext-parser": "^4.0.4",
|
||||
"gulp": "4.0.2",
|
||||
"gulp-concat": "^2.6.1",
|
||||
@@ -35,36 +35,36 @@
|
||||
"gulp-sourcemaps": "^3.0.0",
|
||||
"gulp-svg-sprite": "^1.5.0",
|
||||
"map-stream": "0.0.7",
|
||||
"marked": "^3.0.4",
|
||||
"marked": "^3.0.8",
|
||||
"mkdirp": "^1.0.4",
|
||||
"nodemon": "^2.0.13",
|
||||
"nodemon": "^2.0.14",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"postcss": "^8.3.5",
|
||||
"postcss": "^8.3.11",
|
||||
"postcss-clean": "^1.2.2",
|
||||
"rimraf": "^3.0.0",
|
||||
"sass": "^1.35.1",
|
||||
"shadow-cljs": "2.15.9"
|
||||
"sass": "^1.43.4",
|
||||
"shadow-cljs": "2.15.12"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sentry/browser": "^6.12.0",
|
||||
"@sentry/tracing": "^6.12.0",
|
||||
"date-fns": "^2.22.1",
|
||||
"@sentry/browser": "^6.13.3",
|
||||
"@sentry/tracing": "^6.13.3",
|
||||
"date-fns": "^2.25.0",
|
||||
"draft-js": "^0.11.7",
|
||||
"highlight.js": "^11.0.1",
|
||||
"highlight.js": "^11.3.1",
|
||||
"js-beautify": "^1.14.0",
|
||||
"jszip": "^3.6.0",
|
||||
"luxon": "^2.0.2",
|
||||
"mousetrap": "^1.6.5",
|
||||
"opentype.js": "^1.3.3",
|
||||
"opentype.js": "^1.3.4",
|
||||
"randomcolor": "^0.6.2",
|
||||
"react": "~17.0.2",
|
||||
"react-dom": "~17.0.2",
|
||||
"react-virtualized": "^9.22.3",
|
||||
"rxjs": "~7.2.0",
|
||||
"rxjs": "~7.4.0",
|
||||
"sax": "^1.2.4",
|
||||
"source-map-support": "^0.5.16",
|
||||
"tdigest": "^0.1.1",
|
||||
"ua-parser-js": "^0.7.28",
|
||||
"ua-parser-js": "^1.0.2",
|
||||
"xregexp": "^5.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
1
frontend/resources/images/on-solo-hover.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="24.47353642154394" xmlns="http://www.w3.org/2000/svg" id="screenshot" version="1.1" viewBox="10.276508697997087 -10.666460420698968 24.47353642154394 45.833972589785844" height="45.833972589785844" style="-webkit-print-color-adjust: exact;"><g id="shape-68c61310-2040-11ec-9082-1597698bcafa" width="133" height="70" fill="none"><g id="shape-68c61311-2040-11ec-9082-1597698bcafa"><path d="M34.15111509349208,-6.042576477324019C32.728545198285246,-6.330271155262835,31.92939323677365,-3.376021339875024,31.573755956590503,-2.4511143742279273C31.03134089353898,-4.792719923592813,32.95730466396617,-10.268928805500309,31.43334627304739,-10.431571776022338C29.909377991244583,-10.594250374600506,28.25067219538414,-7.913146659964696,27.49821990591863,-6.170389430908017C26.74573283532027,-4.427587759031667,25.891378375591557,-1.7644073640558418,25.713212854031553,-2.2272258928269366C25.535081233357232,-2.6900095615483224,27.15263561710435,-6.757552580227184,25.941143250794084,-7.1652527066157745C24.72968451746874,-7.572893837298125,23.954342086512952,-5.090038220419956,23.25052490786311,-3.268314742871553C21.417817942427064,1.4754114884185583,10.659070475242515,0.4358613513782075,10.659070475242515,0.4358613513782075L10.492289692115264,34.903869918353394C10.950455754343238,34.90895547471155,28.014759237585167,30.129601494621056,32.24939430527593,19.872789654186818C35.25119864070166,13.145361698193483,32.406657583041124,6.514941655593702,33.309134176787666,1.1371823352346837C33.78537439733827,-1.58885192629441,35.57368117762144,-5.754847357929975,34.15111509349208,-6.042576477324019ZZ" style="fill: rgb(255, 255, 255); fill-opacity: 1;"/></g><g id="shape-68c61312-2040-11ec-9082-1597698bcafa"><path d="M22.85523475939408,17.21695965203071C22.427431361919844,16.01479714589459,23.05111296555242,14.69085378511636,24.24829081777807,14.259819026323385C25.445434386402667,13.828783886987821,26.762725789724755,14.45390600122937,27.19052918719808,15.656068507365035C27.618298301069444,16.85823063295993,26.994616697437777,18.18217399373725,25.79747312881318,18.61320913307327C24.60029527658753,19.044243891866245,23.283003873265443,18.41912177762515,22.85523475939408,17.21695965203071ZZ" style="stroke-width: 1; stroke: rgb(49, 239, 184); stroke-opacity: 1;"/></g><g id="shape-68c61313-2040-11ec-9082-1597698bcafa"><path d="M17.110277419337763,8.332660542501344C14.855684713780647,9.317767677909615,11.627251872240777,12.863450146366631,13.47442592751213,18.054309980863763C15.321633118238424,23.24527363442894,19.819776888222805,23.267580687635473,22.247438584220617,22.768891367604738" style="stroke-width: 1; stroke: rgb(49, 239, 184); stroke-opacity: 1;"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
1
frontend/resources/images/on-solo.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="24.47353642154485" xmlns="http://www.w3.org/2000/svg" id="screenshot" version="1.1" viewBox="10.276508697995268 -10.66646042069624 24.47353642154485 45.833972589782206" height="45.833972589782206" style="-webkit-print-color-adjust: exact;"><g id="shape-b0db2ad1-203d-11ec-9082-1597698bcafa" width="133" height="70" fill="none"><g id="shape-b0db2ad2-203d-11ec-9082-1597698bcafa"><path d="M34.15111509349208,-6.042576477324474C32.728545198285246,-6.3302711552623805,31.92939323677365,-3.376021339874569,31.573755956590503,-2.4511143742274726C31.03134089353898,-4.792719923592813,32.95730466396617,-10.268928805500764,31.43334627304739,-10.431571776021883C29.909377991244583,-10.59425037460096,28.25067219538414,-7.913146659964696,27.49821990591863,-6.170389430908472C26.74573283532027,-4.427587759031667,25.891378375591557,-1.7644073640558418,25.713212854031553,-2.2272258928269366C25.535081233357232,-2.6900095615483224,27.15263561710435,-6.757552580227639,25.941143250794084,-7.165252706616229C24.72968451746874,-7.57289383729767,23.954342086512952,-5.090038220419956,23.25052490786311,-3.268314742871553C21.417817942427064,1.4754114884181035,10.659070475242515,0.4358613513777527,10.659070475242515,0.4358613513777527L10.492289692115264,34.90386991835294C10.950455754343238,34.90895547471155,28.014759237585167,30.129601494621056,32.24939430527593,19.872789654186818C35.25119864070166,13.145361698193483,32.406657583041124,6.514941655594157,33.309134176787666,1.1371823352346837C33.78537439733827,-1.58885192629441,35.57368117762144,-5.754847357929975,34.15111509349208,-6.042576477324474ZZ" style="fill: rgb(250, 181, 245);"/></g><g id="shape-b0db51e0-203d-11ec-9082-1597698bcafa"><path d="M22.85523475939408,17.21695965203071C22.427431361919844,16.01479714589459,23.05111296555242,14.690853785115905,24.24829081777807,14.259819026323385C25.445434386402667,13.828783886987367,26.762725789724755,14.45390600122937,27.19052918719808,15.65606850736549C27.618298301069444,16.85823063295993,26.994616697437777,18.182173993736797,25.79747312881318,18.613209133072814C24.60029527658753,19.044243891866245,23.283003873265443,18.41912177762515,22.85523475939408,17.21695965203071ZZ" style="stroke-width: 1; stroke: white;"/></g><g id="shape-b0db51e1-203d-11ec-9082-1597698bcafa"><path d="M17.110277419337763,8.33266054250089C14.855684713780647,9.317767677909615,11.627251872240777,12.863450146366631,13.47442592751213,18.054309980863763C15.321633118238424,23.245273634429395,19.819776888222805,23.267580687635927,22.247438584220617,22.768891367604738" style="stroke-width: 1; stroke: white;"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
1
frontend/resources/images/on-teamup-hover.svg
Normal file
|
After Width: | Height: | Size: 11 KiB |
1
frontend/resources/images/on-teamup.svg
Normal file
|
After Width: | Height: | Size: 11 KiB |
1
frontend/resources/images/ph-file.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="206.95680443620125" xmlns="http://www.w3.org/2000/svg" id="screenshot" version="1.1" viewBox="-1.9573043775739962 -2.7988168413976156 206.95680443620125 158.15679986166066" height="158.15679986166066" style="-webkit-print-color-adjust: exact;"><g id="shape-575c9093-25c2-11ec-9877-05429cda5971"><g id="shape-575c9094-25c2-11ec-9877-05429cda5971"><defs><mask id="outer-stroke-ddd0b3b0-3bbe-11ec-880d-a73a7db30a35" x="-1.9573043775739962" y="-2.7988168413976156" width="206.95680443620125" height="158.15679986166066" maskUnits="userSpaceOnUse"><use xlink:href="#stroke-shape-ddd0b3b0-3bbe-11ec-880d-a73a7db30a35" style="fill: none; stroke: white; stroke-width: 4;"/><use xlink:href="#stroke-shape-ddd0b3b0-3bbe-11ec-880d-a73a7db30a35" style="fill: black;"/></mask></defs><g class="outer-stroke-shape"><defs><rect width="201" height="152" x="1" id="stroke-shape-ddd0b3b0-3bbe-11ec-880d-a73a7db30a35" transform="matrix(0.9999999978661108,-1.745329248269963e-8,1.7453292541199837e-7,1.0000000006480119,-0.000013047912574393195,0.0000017222602934907627)" ry="3" rx="3" y="0" data-style="fill:none;stroke-width:2;stroke:#E3E3E3;stroke-opacity:1;stroke-dasharray:"/></defs><use xlink:href="#stroke-shape-ddd0b3b0-3bbe-11ec-880d-a73a7db30a35" mask="url(#outer-stroke-ddd0b3b0-3bbe-11ec-880d-a73a7db30a35)" style="stroke-width: 4; stroke: rgb(227, 227, 227); stroke-opacity: 1; fill: none;"/><use xlink:href="#stroke-shape-ddd0b3b0-3bbe-11ec-880d-a73a7db30a35" style="fill: none;"/></g></g><g id="shape-575c9095-25c2-11ec-9877-05429cda5971"><path d="M0,91 h202 a0,0 0 0 1 0,0 v59 a3,3 0 0 1 -3,3 h-196 a3,3 0 0 1 -3,-3 v-59 a0,0 0 0 1 0,0 z" x="0" y="91" transform="matrix(1.0000000024760176,-1.0471975568525078e-7,3.490658523073093e-8,1.0000000103484785,-0.000004508681172410434,0.000009314180957176177)" width="202" height="62" style="fill: none; stroke-width: 2; stroke: rgb(227, 227, 227); stroke-opacity: 1;"/></g><g id="shape-575c9096-25c2-11ec-9877-05429cda5971"><rect rx="3" ry="3" x="10" y="103" transform="matrix(0.9999999995870766,-4.810081334728633e-16,-4.810081268554315e-16,1.000000030940994,3.1795153176972235e-8,-0.0000034653912877047333)" width="134" height="18" style="fill: rgb(227, 227, 227); fill-opacity: 1;"/></g><g id="shape-575c9097-25c2-11ec-9877-05429cda5971"><rect rx="3" ry="3" x="10" y="131" transform="matrix(1.0000000022017677,6.948317137937551e-23,6.617444834426139e-23,1.0000000667740845,-2.234794180822064e-7,-0.000009114662532283546)" width="183" height="11" style="fill: rgb(227, 227, 227); fill-opacity: 1;"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
1
frontend/resources/images/ph-left.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="81.5877720738863" xmlns="http://www.w3.org/2000/svg" id="screenshot" version="1.1" viewBox="-0.43500565160593396 0.14985215967590193 81.5877720738863 128.8502825397186" height="128.8502825397186" style="-webkit-print-color-adjust: exact;"><g id="shape-305b39c3-25c6-11ec-9877-05429cda5971" width="90" height="140" fill="none"><g id="shape-305b39c4-25c6-11ec-9877-05429cda5971"><path d="M18.180074555850297,34.13571661820788C16.328443710368447,37.802933917027985,15.363633970810042,41.85364142381468,15.363541785663983,45.96187246179579C15.363449600517924,49.407753224720636,16.04230101673238,52.81989422415609,17.360917347203213,56.00341606115035C18.679625862816465,59.187030083290665,20.612379636906553,62.07970778419531,23.049017419827578,64.51634556711724C25.485655202748603,66.95298335003963,28.378332903652336,68.88573712412926,31.561946925794473,70.20444563974297C34.74556094793297,71.52306197021062,38.157794132515846,72.20182120128084,41.603674895442964,72.20182120128084C48.56282376312447,72.20172901613478,55.23693615892262,69.43709648322647,60.15777926017472,64.51616119682512C65.07871454658016,59.59522591042378,67.84316270919953,52.92102132947775,67.8430705240462,45.96187246179579C67.84316270919953,41.85373360896074,66.87872171022173,37.802933917027985,65.027275235032,34.13571661820788L18.180074555850297,34.13571661820788ZZ" style="fill: rgb(227, 227, 227); fill-opacity: 1;"/></g><g id="shape-305b39c5-25c6-11ec-9877-05429cda5971"><path d="M40.358622311596264,76.81790020937888C29.539497004501754,76.81799239452494,19.163413509784732,80.76582127821257,11.513152601448382,87.79300278375013C3.862873256086459,94.81972336355693,-0.4349826275210944,104.35074562355658,-0.43500383010177757,114.28922622951995C-0.4349070356984157,119.34558149560962,0.6793145450319571,124.34939122839569,2.840650607504358,129.00013185144462L77.87714712656452,129.00013185144462C80.03842787794929,124.34939122839569,81.15276192455713,119.34558149560962,81.15276192455713,114.28922622951995C81.15276192455713,109.36838312826512,80.09761074177368,104.49547630300549,78.04750527664146,99.94890489510135C75.99739981150924,95.40325533865871,72.99244060259662,91.27234675472573,69.20436857717868,87.79281841345801C65.41629655176075,84.31329007218983,60.919228567337996,81.55317461144296,55.96990025592095,79.67010863112546C51.02047975936148,77.78704265080796,45.715777709559916,76.81790020937888,40.358622311596264,76.81790020937888ZZ" style="fill: rgb(227, 227, 227); fill-opacity: 1;"/></g><g id="shape-305b39c6-25c6-11ec-9877-05429cda5971"><path d="M22.63741074228892,55.77654841648564C22.63741074228892,55.77654841648564,28.038261899520876,66.05546876716517,39.71092947477882,65.88123884095012C51.38359705003313,65.70700891473462,57.30695361562539,56.473468121347196,57.30695361562539,56.473468121347196" style="stroke-width: 2; stroke: white;"/></g><g id="shape-305b39c7-25c6-11ec-9877-05429cda5971"><path d="M33.141447219852125,53.60300704066367C36.667529059923254,53.60300704066367,39.52591388646397,50.77716357071449,39.52591388646397,47.29136663950885C39.52591388646397,43.805569708302755,36.667529059923254,40.97972623835358,33.141447219852125,40.97972623835358C29.615457564927056,40.97972623835358,26.75698055324392,43.805569708302755,26.75698055324392,47.29136663950885C26.75698055324392,50.77716357071449,29.615457564927056,53.60300704066367,33.141447219852125,53.60300704066367ZZ" style="fill: rgb(227, 227, 227); fill-opacity: 1; stroke-width: 2; stroke: white;"/></g><g id="shape-305b39c8-25c6-11ec-9877-05429cda5971"><path d="M52.22921139562823,5.9902455968044706C47.897615748537646,2.9522656685448965,42.82503589672706,1.0223696339853632,37.493231413951435,0.383799439228369C32.16142693117581,-0.2547744429339218,26.74730111289682,0.4191478335183092,21.76469396372704,2.341603604932061L64.9989743951628,32.66471824116161C65.42634473269572,27.547336408323645,64.47748302341643,22.41059569479512,62.24254634026147,17.742063338540447C60.007609657106514,13.073623167432288,56.56080704272608,9.028299273180437,52.22921139562823,5.9902455968044706ZZ" style="fill: rgb(227, 227, 227); fill-opacity: 1;"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
1
frontend/resources/images/ph-right.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="102.12322708597458" xmlns="http://www.w3.org/2000/svg" id="screenshot" version="1.1" viewBox="-0.2830548330293823 0.00013327541864782688 102.12322708597458 54.02153523420793" height="54.02153523420793" style="-webkit-print-color-adjust: exact;"><g id="shape-305b39ca-25c6-11ec-9877-05429cda5971" width="133" height="70" fill="none"><g id="shape-305b39cb-25c6-11ec-9877-05429cda5971"><path d="M92.06001883709541,52.780900512351764C92.66931429164106,49.56773051235177,86.02942338254798,47.840623239624165,83.94800520073477,47.06227823962445C89.18168701891409,45.781289603260575,101.50092247346038,49.98474823962442,101.82747470073446,46.54621233053331C102.1541065189158,43.10765323962414,96.1049138370945,39.43658051235161,92.18095065527814,37.78441233053354C88.2568870189134,36.13216687598788,82.26786429164349,34.273371421442334,83.30053701891302,33.860271421442576C84.33313247346086,33.4472486941695,93.48809610982426,36.99089869416957,94.37155520073429,34.2504214214423C95.25488292800583,31.510021421442616,89.671905200732,29.82478051235148,85.57220065527872,28.284194148715414C74.89666429164208,24.27258051235185,76.95853247345804,0.00013505780589184724,76.95853247345804,0.00013505780589184724L-0.28305389017623384,0.486489603260452C-0.28305389017623384,1.519162330533618,10.851173382550769,39.85655778507862,33.94041883709724,49.143364148715136C49.09020065527875,55.740090512351344,63.87718701891572,49.16363278507879,75.95035520072997,51.06297323962417C82.07081883709907,52.068059603260735,91.45064610982081,55.99406278507877,92.06001883709541,52.780900512351764ZZ" style="fill: rgb(227, 227, 227); fill-opacity: 1;"/></g><g id="shape-305b39cc-25c6-11ec-9877-05429cda5971"><path d="M39.658059746187064,27.905789603260928C42.341277928004274,26.911598694169697,45.32354156436486,28.28403960326068,46.31920065527811,30.971275966897338C47.314859746184084,33.658435057805946,45.946823382550065,36.64278505780612,43.263605200732854,37.6369759668969C40.580387018912006,38.631089603260534,37.59812338255142,37.258648694170006,36.60246429163817,34.57148960326049C35.6068052007322,31.884253239624286,36.974841564369854,28.89990323962411,39.658059746187064,27.905789603260928ZZ" style="stroke-width: 2; stroke: white;"/></g><g id="shape-305b39cd-25c6-11ec-9877-05429cda5971"><path d="M59.423496109822736,14.73643051235149C57.15994610982307,9.680012330533373,49.1343233825537,2.492953239624512,37.54835974618618,6.785685057806404C25.962164291642694,11.078494148715436,26.02405974618887,21.21628960326052,27.201927928006626,26.674912330533516" style="stroke-width: 2; stroke: white;"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
@@ -396,6 +396,11 @@ ul.slider-dots {
|
||||
text-align: right;
|
||||
top: 26%;
|
||||
width: 18px;
|
||||
|
||||
pointer-events: none;
|
||||
max-width: 4rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.after {
|
||||
|
||||
@@ -46,7 +46,7 @@ $width-settings-bar: 16rem;
|
||||
}
|
||||
|
||||
.handoff-layout {
|
||||
.viewer-preview {
|
||||
.viewer-section {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
.settings-bar {
|
||||
|
||||
@@ -336,7 +336,7 @@
|
||||
.viewer-comments-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1000;
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
|
||||
@@ -335,6 +335,20 @@
|
||||
padding: 3rem;
|
||||
justify-content: center;
|
||||
|
||||
&.drafts {
|
||||
background-image: url("/images/ph-left.svg"), url("/images/ph-right.svg");
|
||||
background-position: 15% bottom, 85% top;
|
||||
background-repeat: no-repeat;
|
||||
.text {
|
||||
p {
|
||||
max-width: 360px;
|
||||
text-align: center;
|
||||
font-size: $fs16;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
svg {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
@@ -346,5 +360,10 @@
|
||||
color: $color-gray-30;
|
||||
font-size: $fs16;
|
||||
}
|
||||
|
||||
img.ph-files {
|
||||
height: 150px;
|
||||
margin-right: calc(100% - 148px);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 448px;
|
||||
background-color: $color-dashboard;
|
||||
background-color: $color-white;
|
||||
|
||||
.modal-header {
|
||||
align-items: center;
|
||||
@@ -705,7 +705,7 @@
|
||||
background-color: $color-white;
|
||||
box-shadow: 0 10px 10px rgba(0,0,0,.2);
|
||||
display: flex;
|
||||
min-height: 370px;
|
||||
min-height: 420px;
|
||||
flex-direction: row;
|
||||
font-family: "sourcesanspro", sans-serif;
|
||||
min-width: 620px;
|
||||
@@ -824,21 +824,93 @@
|
||||
}
|
||||
|
||||
&.final {
|
||||
// TODO: Juan revisa TODA esta parte
|
||||
|
||||
padding: $size-5 0 0 0;
|
||||
flex-direction: column;
|
||||
|
||||
.modal-top {
|
||||
padding-top: 40px;
|
||||
color: $color-gray-60;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
h1 {
|
||||
font-family: 'worksans', sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 27px;
|
||||
margin-bottom: $size-3;
|
||||
}
|
||||
p {
|
||||
font-family: 'worksans', sans-serif;
|
||||
font-weight: 500;
|
||||
font-size: $fs18;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.modal-columns {
|
||||
display: flex;
|
||||
margin: 17px;
|
||||
|
||||
.modal-left {
|
||||
background-image: url("/images/on-solo.svg");
|
||||
background-position: left top;
|
||||
background-size: 11%;
|
||||
}
|
||||
|
||||
.modal-left:hover {
|
||||
background-image: url("/images/on-solo-hover.svg");
|
||||
background-size: 15%;
|
||||
}
|
||||
|
||||
.modal-right {
|
||||
background-image: url("/images/on-teamup.svg");
|
||||
background-position: right top;
|
||||
background-size: 28%;
|
||||
}
|
||||
|
||||
.modal-right:hover {
|
||||
background-image: url("/images/on-teamup-hover.svg");
|
||||
background-size: 32%;
|
||||
}
|
||||
|
||||
.modal-right,
|
||||
.modal-left {
|
||||
background-repeat: no-repeat;
|
||||
border-radius: $br-medium;
|
||||
transition: all ease .3s;
|
||||
&:hover {
|
||||
background-color: $color-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-left {
|
||||
margin-right: 35px;
|
||||
}
|
||||
|
||||
.modal-left,
|
||||
.modal-right {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: $color-white;
|
||||
color: $color-black;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
overflow: visible;
|
||||
padding: $size-6 40px;
|
||||
// overflow: visible;
|
||||
// padding: $size-6 40px;
|
||||
text-align: center;
|
||||
|
||||
border: 1px solid $color-gray-10;
|
||||
border-radius: 2px;
|
||||
min-height: 180px;
|
||||
width: 233px;
|
||||
cursor: pointer;
|
||||
|
||||
h2 {
|
||||
font-weight: 900;
|
||||
font-weight: 700;
|
||||
margin-bottom: $size-5;
|
||||
font-size: $fs24;
|
||||
}
|
||||
@@ -847,12 +919,6 @@
|
||||
font-size: $fs14;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
margin-bottom: 0;
|
||||
margin-top: auto;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
img {
|
||||
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.25);
|
||||
border-radius: $br-medium;
|
||||
@@ -861,26 +927,6 @@
|
||||
width: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-left {
|
||||
border-right: 1px solid $color-gray-10;
|
||||
|
||||
form {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: auto;
|
||||
|
||||
.custom-input {
|
||||
margin-bottom: $size-4;
|
||||
|
||||
input {
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -899,3 +945,193 @@
|
||||
.relnotes .onboarding {
|
||||
height: 420px;
|
||||
}
|
||||
|
||||
.onboarding-templates {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 348px;
|
||||
height: 100vh;
|
||||
|
||||
.modal-close-button {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
margin-right: 13px;
|
||||
margin-top: 13px;
|
||||
svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
height: unset;
|
||||
border-radius: unset;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
border: 0px;
|
||||
padding: 0px 25px;
|
||||
background-color: $color-white;
|
||||
flex-grow: 1;
|
||||
|
||||
p, h3 {
|
||||
color: $color-gray-60;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: $fs18;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: $fs16;
|
||||
}
|
||||
|
||||
|
||||
.templates {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-top: 8%;
|
||||
}
|
||||
|
||||
.template-item {
|
||||
width: 275px;
|
||||
border: 1px solid $color-gray-10;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: left;
|
||||
border-radius: $br-small;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
.template-item-content {
|
||||
// height: 144px;
|
||||
flex-grow: 1;
|
||||
|
||||
img {
|
||||
border-radius: $br-small $br-small 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.template-item-title {
|
||||
padding: 6px 12px;
|
||||
height: 64px;
|
||||
border-top: 1px solid $color-gray-10;
|
||||
|
||||
.label {
|
||||
color: $color-black;
|
||||
padding: 0px 4px;
|
||||
font-size: $fs16;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.action {
|
||||
color: $color-primary-dark;
|
||||
cursor: pointer;
|
||||
font-size: $fs14;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: $size-2;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.onboarding-team {
|
||||
display: flex;
|
||||
min-width: 620px;
|
||||
min-height: 420px;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 408px;
|
||||
|
||||
color: $color-gray-60;
|
||||
h2 {
|
||||
font-weight: 700;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
p {
|
||||
text-align: center;
|
||||
font-size: $fs18;
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: $size-6;
|
||||
|
||||
.buttons {
|
||||
margin-top: 30px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
> *:not(:last-child) {
|
||||
margin-right: 13px;
|
||||
}
|
||||
|
||||
input { margin-bottom: unset; }
|
||||
input[type=submit] {
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
width: 117px;
|
||||
}
|
||||
}
|
||||
|
||||
.team-row {
|
||||
.custom-input {
|
||||
width: 459px;
|
||||
}
|
||||
}
|
||||
|
||||
.invite-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
> *:not(:last-child) {
|
||||
margin-right: 13px;
|
||||
}
|
||||
|
||||
.custom-input {
|
||||
width: 321px;
|
||||
}
|
||||
|
||||
.custom-select {
|
||||
width: 118px;
|
||||
}
|
||||
}
|
||||
|
||||
.skip-action {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 15px;
|
||||
.action {
|
||||
color: $color-primary-dark;
|
||||
font-weight: 500;
|
||||
font-size: $fs16;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,12 +70,6 @@
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.element-set-title {
|
||||
color: $color-gray-10;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.element-list {
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
height: 48px;
|
||||
padding: 0 $size-4 0 55px;
|
||||
position: relative;
|
||||
z-index: 12;
|
||||
justify-content: space-between;
|
||||
|
||||
a {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 12;
|
||||
z-index: 10;
|
||||
|
||||
&.invisible {
|
||||
visibility: hidden;
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.viewer-preview {
|
||||
height: calc(100vh - 40px);
|
||||
.viewer-section {
|
||||
height: calc(100vh - 48px);
|
||||
|
||||
grid-row: 1 / span 2;
|
||||
grid-column: 1 / span 1;
|
||||
|
||||
@@ -57,8 +57,8 @@
|
||||
(defn- parse-flags
|
||||
[global]
|
||||
(let [flags (obj/get global "penpotFlags" "")
|
||||
flags (into #{} (map keyword) (str/words flags))]
|
||||
(flags/parse flags flags/default)))
|
||||
flags (sequence (map keyword) (str/words flags))]
|
||||
(flags/parse flags/default flags)))
|
||||
|
||||
(defn- parse-version
|
||||
[global]
|
||||
|
||||
@@ -187,10 +187,12 @@
|
||||
(ptk/reify ::files-fetched
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update state :dashboard-files
|
||||
(-> state
|
||||
(update :dashboard-files
|
||||
(fn [state]
|
||||
(let [state (remove-project-files state)]
|
||||
(reduce #(assoc %1 (:id %2) %2) state files))))))))
|
||||
(reduce #(assoc %1 (:id %2) %2) state files))))
|
||||
(assoc-in [:dashboard-projects project-id :count] (count files)))))))
|
||||
|
||||
(defn fetch-files
|
||||
[{:keys [project-id] :as params}]
|
||||
@@ -300,6 +302,28 @@
|
||||
(rx/map team-created)
|
||||
(rx/catch on-error))))))
|
||||
|
||||
;; --- EVENT: create-team-with-invitations
|
||||
|
||||
;; NOTE: right now, it only handles a single email, in a near future
|
||||
;; this will be changed to the ability to specify multiple emails.
|
||||
|
||||
(defn create-team-with-invitations
|
||||
[{:keys [name email role] :as params}]
|
||||
(us/assert string? name)
|
||||
(ptk/reify ::create-team-with-invitations
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(let [{:keys [on-success on-error]
|
||||
:or {on-success identity
|
||||
on-error rx/throw}} (meta params)
|
||||
params {:name name
|
||||
:emails #{email}
|
||||
:role role}]
|
||||
(->> (rp/mutation! :create-team-and-invite-members params)
|
||||
(rx/tap on-success)
|
||||
(rx/map team-created)
|
||||
(rx/catch on-error))))))
|
||||
|
||||
;; --- EVENT: update-team
|
||||
|
||||
(defn update-team
|
||||
|
||||
@@ -302,6 +302,21 @@
|
||||
(update [_ state]
|
||||
(assoc-in state [:viewer-local :interactions-show?] false))))
|
||||
|
||||
(defn set-nav-scroll
|
||||
[scroll]
|
||||
(ptk/reify ::set-nav-scroll
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:viewer-local :nav-scroll] scroll))))
|
||||
|
||||
(defn reset-nav-scroll
|
||||
[]
|
||||
(ptk/reify ::reset-nav-scroll
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(d/dissoc-in state [:viewer-local :nav-scroll]))))
|
||||
|
||||
|
||||
;; --- Navigation inside page
|
||||
|
||||
(defn go-to-frame-by-index
|
||||
|
||||
@@ -125,6 +125,9 @@
|
||||
create a group that contains all ids. Then, make a component with it,
|
||||
and link all shapes to their corresponding one in the component."
|
||||
[shapes objects page-id file-id]
|
||||
(if (and (= (count shapes) 1)
|
||||
(:component-id (first shapes)))
|
||||
empty-changes
|
||||
(let [[group rchanges uchanges]
|
||||
(if (and (= (count shapes) 1)
|
||||
(= (:type (first shapes)) :group))
|
||||
@@ -188,7 +191,7 @@
|
||||
:attr :touched
|
||||
:val (:touched original-shape)}]}))
|
||||
updated-shapes))]
|
||||
[group rchanges uchanges]))
|
||||
[group rchanges uchanges])))
|
||||
|
||||
(defn duplicate-component
|
||||
"Clone the root shape of the component and all children. Generate new
|
||||
|
||||
@@ -201,6 +201,9 @@
|
||||
(s/def ::shapes-changes-persisted
|
||||
(s/keys :req-un [::revn ::cp/changes]))
|
||||
|
||||
(defn shapes-persited-event? [event]
|
||||
(= (ptk/type event) ::changes-persisted))
|
||||
|
||||
(defn shapes-changes-persisted
|
||||
[file-id {:keys [revn changes] :as params}]
|
||||
(us/verify ::us/uuid file-id)
|
||||
|
||||
@@ -61,12 +61,6 @@
|
||||
(def dashboard-search-result
|
||||
(l/derived :dashboard-search-result st/state))
|
||||
|
||||
(def dashboard-team
|
||||
(l/derived (fn [state]
|
||||
(let [team-id (:current-team-id state)]
|
||||
(get-in state [:teams team-id])))
|
||||
st/state))
|
||||
|
||||
(def dashboard-team-stats
|
||||
(l/derived :dashboard-team-stats st/state))
|
||||
|
||||
|
||||
@@ -72,7 +72,10 @@
|
||||
:dashboard-team-settings)
|
||||
[:*
|
||||
#_[:div.modal-wrapper
|
||||
[:& app.main.ui.onboarding/release-notes-modal {:version "1.8"}]]
|
||||
#_[:& app.main.ui.onboarding/onboarding-templates-modal]
|
||||
#_[:& app.main.ui.onboarding/onboarding-modal]
|
||||
#_[:& app.main.ui.onboarding/onboarding-team-modal]
|
||||
]
|
||||
[:& dashboard {:route route}]]
|
||||
|
||||
:viewer
|
||||
|
||||
@@ -169,7 +169,9 @@
|
||||
(let [token (:invitation-token data)]
|
||||
(st/emit! (rt/nav :auth-verify-token {} {:token token})))
|
||||
|
||||
(not= "penpot" (:auth-backend data))
|
||||
;; The :is-active flag is true, when insecure-register is enabled
|
||||
;; or the user used external auth provider.
|
||||
(:is-active data)
|
||||
(st/emit! (du/login-from-register))
|
||||
|
||||
:else
|
||||
@@ -178,9 +180,14 @@
|
||||
(s/def ::accept-terms-and-privacy (s/and ::us/boolean true?))
|
||||
(s/def ::accept-newsletter-subscription ::us/boolean)
|
||||
|
||||
(s/def ::register-validate-form
|
||||
(if (contains? @cf/flags :terms-and-privacy-checkbox)
|
||||
(s/def ::register-validate-form
|
||||
(s/keys :req-un [::token ::fullname ::accept-terms-and-privacy]
|
||||
:opt-un [::accept-newsletter-subscription]))
|
||||
(s/def ::register-validate-form
|
||||
(s/keys :req-un [::token ::fullname]
|
||||
:opt-un [::accept-terms-and-privacy
|
||||
::accept-newsletter-subscription])))
|
||||
|
||||
(mf/defc register-validate-form
|
||||
[{:keys [params] :as props}]
|
||||
@@ -207,6 +214,7 @@
|
||||
:label (tr "auth.fullname")
|
||||
:type "text"}]]
|
||||
|
||||
(when (contains? @cf/flags :terms-and-privacy-checkbox)
|
||||
[:div.fields-row
|
||||
[:& fm/input {:name :accept-terms-and-privacy
|
||||
:class "check-primary"
|
||||
@@ -216,14 +224,7 @@
|
||||
[:div
|
||||
[:a {:href "https://penpot.app/terms.html" :target "_blank"} (tr "auth.terms-of-service")]
|
||||
[:span ",\u00A0"]
|
||||
[:a {:href "https://penpot.app/privacy.html" :target "_blank"} (tr "auth.privacy-policy")]]]]]
|
||||
|
||||
;; (when (contains? @cf/flags :newsletter-registration-check)
|
||||
;; [:div.fields-row
|
||||
;; [:& fm/input {:name :accept-newsletter-subscription
|
||||
;; :class "check-primary"
|
||||
;; :label (tr "auth.newsletter-subscription")
|
||||
;; :type "checkbox"}]])
|
||||
[:a {:href "https://penpot.app/privacy.html" :target "_blank"} (tr "auth.privacy-policy")]]]]])
|
||||
|
||||
[:& fm/submit-button
|
||||
{:label (tr "auth.register-submit")
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
max-val-str (obj/get props "max")
|
||||
wrap-value? (obj/get props "data-wrap")
|
||||
on-change (obj/get props "onChange")
|
||||
title (obj/get props "title")
|
||||
|
||||
;; We need a ref pointing to the input dom element, but the user
|
||||
;; of this component may provide one (that is forwarded here).
|
||||
@@ -33,9 +34,18 @@
|
||||
|
||||
value (d/parse-integer value-str 0)
|
||||
|
||||
min-val (when (string? min-val-str)
|
||||
min-val (cond
|
||||
(number? min-val-str)
|
||||
min-val-str
|
||||
|
||||
(string? min-val-str)
|
||||
(d/parse-integer min-val-str))
|
||||
max-val (when (string? max-val-str)
|
||||
|
||||
max-val (cond
|
||||
(number? max-val-str)
|
||||
max-val-str
|
||||
|
||||
(string? max-val-str)
|
||||
(d/parse-integer max-val-str))
|
||||
|
||||
num? (fn [val] (and (number? val)
|
||||
@@ -144,6 +154,7 @@
|
||||
(obj/set! "type" "text")
|
||||
(obj/set! "ref" ref)
|
||||
(obj/set! "defaultValue" value-str)
|
||||
(obj/set! "title" title)
|
||||
(obj/set! "onWheel" handle-mouse-wheel)
|
||||
(obj/set! "onKeyDown" handle-key-down)
|
||||
(obj/set! "onBlur" handle-blur))]
|
||||
|
||||
@@ -11,6 +11,10 @@
|
||||
(def render-ctx (mf/create-context nil))
|
||||
(def def-ctx (mf/create-context false))
|
||||
|
||||
;; This content is used to replace complex colors to simple ones
|
||||
;; for text shapes in the export process
|
||||
(def text-plain-colors-ctx (mf/create-context false))
|
||||
|
||||
(def current-route (mf/create-context nil))
|
||||
(def current-team-id (mf/create-context nil))
|
||||
(def current-project-id (mf/create-context nil))
|
||||
|
||||
@@ -104,11 +104,11 @@
|
||||
(when (and (:onboarding-viewed props)
|
||||
(not= version (:main @cf/version))
|
||||
(not= "0.0" (:main @cf/version)))
|
||||
(tm/schedule 1000 #(st/emit! (modal/show {:type :release-notes :version (:main @cf/version)})))))))
|
||||
(tm/schedule 1000 #(st/emit! (modal/show {:type :release-notes
|
||||
:version (:main @cf/version)})))))))
|
||||
|
||||
[:& (mf/provider ctx/current-team-id) {:value team-id}
|
||||
[:& (mf/provider ctx/current-project-id) {:value project-id}
|
||||
|
||||
;; NOTE: dashboard events and other related functions assumes
|
||||
;; that the team is a implicit context variable that is
|
||||
;; available using react context or accessing
|
||||
|
||||
@@ -120,6 +120,6 @@
|
||||
[:*
|
||||
[:& header {:team team :project project}]
|
||||
[:section.dashboard-container
|
||||
[:& grid {:project-id (:id project)
|
||||
[:& grid {:project project
|
||||
:files files}]]]))
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
[app.main.ui.dashboard.file-menu :refer [file-menu]]
|
||||
[app.main.ui.dashboard.import :refer [use-import-file]]
|
||||
[app.main.ui.dashboard.inline-edition :refer [inline-edition]]
|
||||
[app.main.ui.dashboard.placeholder :refer [empty-placeholder loading-placeholder]]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.main.worker :as wrk]
|
||||
[app.util.dom :as dom]
|
||||
@@ -195,24 +196,10 @@
|
||||
:on-edit on-edit
|
||||
:on-menu-close on-menu-close}])]]]))
|
||||
|
||||
(mf/defc empty-placeholder
|
||||
[{:keys [dragging?] :as props}]
|
||||
(if-not dragging?
|
||||
[:div.grid-empty-placeholder
|
||||
[:div.icon i/file-html]
|
||||
[:div.text (tr "dashboard.empty-files")]]
|
||||
[:div.grid-row.no-wrap
|
||||
[:div.grid-item]]))
|
||||
|
||||
(mf/defc loading-placeholder
|
||||
[]
|
||||
[:div.grid-empty-placeholder
|
||||
[:div.icon i/loader]
|
||||
[:div.text (tr "dashboard.loading-files")]])
|
||||
|
||||
(mf/defc grid
|
||||
[{:keys [files project-id] :as props}]
|
||||
[{:keys [files project] :as props}]
|
||||
(let [dragging? (mf/use-state false)
|
||||
project-id (:id project)
|
||||
|
||||
on-finish-import
|
||||
(mf/use-callback
|
||||
@@ -272,7 +259,7 @@
|
||||
:navigate? true}])]
|
||||
|
||||
:else
|
||||
[:& empty-placeholder])]))
|
||||
[:& empty-placeholder {:default? (:is-default project)}])]))
|
||||
|
||||
(mf/defc line-grid-row
|
||||
[{:keys [files selected-files on-load-more dragging?] :as props}]
|
||||
@@ -330,8 +317,11 @@
|
||||
(tr "dashboard.show-all-files")]])]))
|
||||
|
||||
(mf/defc line-grid
|
||||
[{:keys [project-id team-id files on-load-more] :as props}]
|
||||
[{:keys [project team files on-load-more] :as props}]
|
||||
(let [dragging? (mf/use-state false)
|
||||
project-id (:id project)
|
||||
team-id (:id team)
|
||||
|
||||
selected-files (mf/deref refs/dashboard-selected-files)
|
||||
selected-project (mf/deref refs/dashboard-selected-project)
|
||||
|
||||
@@ -413,5 +403,6 @@
|
||||
:dragging? @dragging?}]
|
||||
|
||||
:else
|
||||
[:& empty-placeholder {:dragging? @dragging?}])]))
|
||||
[:& empty-placeholder {:dragging? @dragging?
|
||||
:default? (:is-default project)}])]))
|
||||
|
||||
|
||||
@@ -331,10 +331,11 @@
|
||||
|
||||
[:div.modal-footer
|
||||
[:div.action-buttons
|
||||
(when (or (= :analyzing (:status @state)) pending-import?)
|
||||
[:input.cancel-button
|
||||
{:type "button"
|
||||
:value (tr "labels.cancel")
|
||||
:on-click handle-cancel}]
|
||||
:on-click handle-cancel}])
|
||||
|
||||
(when (= :analyzing (:status @state))
|
||||
[:input.accept-button
|
||||
|
||||
34
frontend/src/app/main/ui/dashboard/placeholder.cljs
Normal file
@@ -0,0 +1,34 @@
|
||||
;; 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.dashboard.placeholder
|
||||
(:require
|
||||
[app.main.ui.icons :as i]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(mf/defc empty-placeholder
|
||||
[{:keys [dragging? default?] :as props}]
|
||||
(cond
|
||||
(true? dragging?)
|
||||
[:div.grid-row.no-wrap
|
||||
[:div.grid-item]]
|
||||
|
||||
(true? default?)
|
||||
[:div.grid-empty-placeholder.drafts
|
||||
[:div.text
|
||||
[:& i18n/tr-html {:label "dashboard.empty-placeholder-drafts"}]]]
|
||||
|
||||
:else
|
||||
[:div.grid-empty-placeholder
|
||||
[:img.ph-files {:src "images/ph-file.svg"}]]))
|
||||
|
||||
(mf/defc loading-placeholder
|
||||
[]
|
||||
[:div.grid-empty-placeholder
|
||||
[:div.icon i/loader]
|
||||
[:div.text (tr "dashboard.loading-files")]])
|
||||
|
||||
@@ -73,7 +73,6 @@
|
||||
:accept-label (tr "modals.delete-project-confirm.accept")
|
||||
:on-accept delete-fn}))
|
||||
|
||||
|
||||
file-input (mf/use-ref nil)
|
||||
|
||||
on-import-files
|
||||
|
||||
@@ -33,10 +33,8 @@
|
||||
(tr "dashboard.new-project")]]))
|
||||
|
||||
(mf/defc project-item
|
||||
[{:keys [project first? files] :as props}]
|
||||
[{:keys [project first? team files] :as props}]
|
||||
(let [locale (mf/deref i18n/locale)
|
||||
|
||||
team-id (:team-id project)
|
||||
file-count (or (:count project) 0)
|
||||
|
||||
dstate (mf/deref refs/dashboard-local)
|
||||
@@ -100,7 +98,8 @@
|
||||
on-import
|
||||
(mf/use-callback
|
||||
(fn []
|
||||
(st/emit! (dd/fetch-recent-files)
|
||||
(st/emit! (dd/fetch-files {:project-id (:id project)})
|
||||
(dd/fetch-recent-files)
|
||||
(dd/clear-selected-files))))]
|
||||
|
||||
[:div.dashboard-project-row {:class (when first? "first")}
|
||||
@@ -145,9 +144,8 @@
|
||||
i/actions]]
|
||||
|
||||
[:& line-grid
|
||||
{:project-id (:id project)
|
||||
:project project
|
||||
:team-id team-id
|
||||
{:project project
|
||||
:team team
|
||||
:on-load-more on-nav
|
||||
:files files}]]))
|
||||
|
||||
@@ -186,6 +184,7 @@
|
||||
(filterv #(= id (:project-id %)))
|
||||
(sort-by :modified-at #(compare %2 %1))))]
|
||||
[:& project-item {:project project
|
||||
:team team
|
||||
:files files
|
||||
:first? (= project (first projects))
|
||||
:key (:id project)}]))]])))
|
||||
|
||||
@@ -72,8 +72,9 @@
|
||||
#(doseq [key keys]
|
||||
(events/unlistenByKey key)))))
|
||||
|
||||
(when-let [component (get components (:type data))]
|
||||
[:div.modal-wrapper {:ref wrapper-ref}
|
||||
(mf/element (get components (:type data)) (:props data))]))
|
||||
(mf/element component (:props data))])))
|
||||
|
||||
|
||||
(def modal-ref
|
||||
|
||||
@@ -12,8 +12,10 @@
|
||||
[app.main.data.messages :as dm]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.users :as du]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.forms :as fm]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.main.ui.releases.common :as rc]
|
||||
[app.main.ui.releases.v1-4]
|
||||
[app.main.ui.releases.v1-5]
|
||||
@@ -21,10 +23,13 @@
|
||||
[app.main.ui.releases.v1-7]
|
||||
[app.main.ui.releases.v1-8]
|
||||
[app.main.ui.releases.v1-9]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.http :as http]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.object :as obj]
|
||||
[app.util.router :as rt]
|
||||
[app.util.timers :as tm]
|
||||
[beicon.core :as rx]
|
||||
[cljs.spec.alpha :as s]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
@@ -38,10 +43,11 @@
|
||||
[:div.modal-right
|
||||
[:div.modal-title
|
||||
[:h2 (tr "onboarding.welcome.title")]]
|
||||
[:span.release "Alpha version " (:main @cf/version)]
|
||||
[:span.release "Beta version " (:main @cf/version)]
|
||||
[:div.modal-content
|
||||
[:p (tr "onboarding.welcome.desc1")]
|
||||
[:p (tr "onboarding.welcome.desc2")]]
|
||||
[:p (tr "onboarding.welcome.desc2")]
|
||||
[:p (tr "onboarding.welcome.desc3")]]
|
||||
[:div.modal-navigation
|
||||
[:button.btn-secondary {:on-click next} (tr "labels.continue")]]]
|
||||
[:img.deco {:src "images/deco-left.png" :border "0"}]
|
||||
@@ -159,7 +165,7 @@
|
||||
skip
|
||||
(mf/use-callback
|
||||
(st/emitf (modal/hide)
|
||||
(modal/show {:type :onboarding-team})
|
||||
(modal/show {:type :onboarding-choice})
|
||||
(du/mark-onboarding-as-viewed)))]
|
||||
|
||||
(mf/use-layout-effect
|
||||
@@ -187,57 +193,233 @@
|
||||
(s/def ::team-form
|
||||
(s/keys :req-un [::name]))
|
||||
|
||||
(mf/defc onboarding-choice-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :onboarding-choice}
|
||||
[]
|
||||
(let [;; When user choices the option of `fly solo`, we proceed to show
|
||||
;; the onboarding templates modal.
|
||||
on-fly-solo
|
||||
(fn []
|
||||
(tm/schedule 400 #(st/emit! (modal/show {:type :onboarding-templates}))))
|
||||
|
||||
;; When user choices the option of `team up`, we proceed to show
|
||||
;; the team creation modal.
|
||||
on-team-up
|
||||
(fn []
|
||||
(st/emit! (modal/show {:type :onboarding-team})))
|
||||
]
|
||||
|
||||
[:div.modal-overlay
|
||||
[:div.modal-container.onboarding.final.animated.fadeInUp
|
||||
[:div.modal-top
|
||||
[:h1 (tr "onboarding.choice.title")]
|
||||
[:p (tr "onboarding.choice.desc")]]
|
||||
[:div.modal-columns
|
||||
[:div.modal-left
|
||||
[:div.content-button {:on-click on-fly-solo}
|
||||
[:h2 (tr "onboarding.choice.fly-solo")]
|
||||
[:p (tr "onboarding.choice.fly-solo-desc")]]]
|
||||
[:div.modal-right
|
||||
[:div.content-button {:on-click on-team-up}
|
||||
[:h2 (tr "onboarding.choice.team-up")]
|
||||
[:p (tr "onboarding.choice.team-up-desc")]]]]
|
||||
[:img.deco {:src "images/deco-left.png" :border "0"}]
|
||||
[:img.deco.right {:src "images/deco-right.png" :border "0"}]]]))
|
||||
|
||||
(mf/defc onboarding-team-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :onboarding-team}
|
||||
[]
|
||||
(let [close (mf/use-fn (st/emitf (modal/hide)))
|
||||
form (fm/use-form :spec ::team-form
|
||||
(let [form (fm/use-form :spec ::team-form
|
||||
:initial {})
|
||||
on-submit
|
||||
(mf/use-callback
|
||||
(fn [form _]
|
||||
(let [tname (get-in @form [:clean-data :name])]
|
||||
(st/emit! (modal/show {:type :onboarding-team-invitations :name tname})))))]
|
||||
|
||||
[:div.modal-overlay
|
||||
[:div.modal-container.onboarding-team
|
||||
[:div.title
|
||||
[:h2 (tr "onboarding.choice.team-up")]
|
||||
[:p (tr "onboarding.choice.team-up-desc")]]
|
||||
|
||||
[:& fm/form {:form form
|
||||
:on-submit on-submit}
|
||||
|
||||
[:div.team-row
|
||||
[:& fm/input {:type "text"
|
||||
:name :name
|
||||
:label (tr "onboarding.team-input-placeholder")}]]
|
||||
|
||||
[:div.buttons
|
||||
[:button.btn-secondary.btn-large
|
||||
{:on-click #(st/emit! (modal/show {:type :onboarding-choice}))}
|
||||
(tr "labels.cancel")]
|
||||
[:& fm/submit-button
|
||||
{:label (tr "labels.next")}]]]
|
||||
|
||||
[:img.deco {:src "images/deco-left.png" :border "0"}]
|
||||
[:img.deco.right {:src "images/deco-right.png" :border "0"}]]]))
|
||||
|
||||
(defn get-available-roles
|
||||
[]
|
||||
[{:value "editor" :label (tr "labels.editor")}
|
||||
{:value "admin" :label (tr "labels.admin")}])
|
||||
|
||||
(s/def ::email ::us/email)
|
||||
(s/def ::role ::us/keyword)
|
||||
(s/def ::invite-form
|
||||
(s/keys :req-un [::role ::email]))
|
||||
|
||||
;; This is the final step of team creation, consists in provide a
|
||||
;; shortcut for invite users.
|
||||
|
||||
(mf/defc onboarding-team-invitations-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :onboarding-team-invitations}
|
||||
[{:keys [name] :as props}]
|
||||
(let [initial (mf/use-memo (constantly
|
||||
{:role "editor"
|
||||
:name name}))
|
||||
form (fm/use-form :spec ::invite-form
|
||||
:initial initial)
|
||||
|
||||
roles (mf/use-memo #(get-available-roles))
|
||||
|
||||
on-success
|
||||
(mf/use-callback
|
||||
(fn [_form response]
|
||||
(st/emit! (modal/hide)
|
||||
(rt/nav :dashboard-projects {:team-id (:id response)}))))
|
||||
(let [project-id (:default-project-id response)
|
||||
team-id (:id response)]
|
||||
(st/emit!
|
||||
(modal/hide)
|
||||
(rt/nav :dashboard-projects {:team-id team-id}))
|
||||
(tm/schedule 400 #(st/emit!
|
||||
(modal/show {:type :onboarding-templates
|
||||
:project-id project-id}))))))
|
||||
|
||||
on-error
|
||||
(mf/use-callback
|
||||
(fn [_form _response]
|
||||
(st/emit! (dm/error "Error on creating team."))))
|
||||
|
||||
on-submit
|
||||
;; The SKIP branch only creates the team, without invitations
|
||||
on-skip
|
||||
(mf/use-callback
|
||||
(fn [form _event]
|
||||
(fn [_]
|
||||
(let [mdata {:on-success (partial on-success form)
|
||||
:on-error (partial on-error form)}
|
||||
params {:name (get-in @form [:clean-data :name])}]
|
||||
(st/emit! (dd/create-team (with-meta params mdata))))))]
|
||||
params {:name name}]
|
||||
(st/emit! (dd/create-team (with-meta params mdata))))))
|
||||
|
||||
;; The SUBMIT branch creates the team with the invitations
|
||||
on-submit
|
||||
(mf/use-callback
|
||||
(fn [form _]
|
||||
(let [mdata {:on-success (partial on-success form)
|
||||
:on-error (partial on-error form)}
|
||||
params (:clean-data @form)]
|
||||
(st/emit! (dd/create-team-with-invitations (with-meta params mdata))))))]
|
||||
|
||||
[:div.modal-overlay
|
||||
[:div.modal-container.onboarding.final.animated.fadeInUp
|
||||
[:div.modal-left
|
||||
[:img {:src "images/onboarding-team.jpg" :border "0" :alt (tr "onboarding.team.create.title")}]
|
||||
[:h2 (tr "onboarding.team.create.title")]
|
||||
[:p (tr "onboarding.team.create.desc1")]
|
||||
[:div.modal-container.onboarding-team
|
||||
[:div.title
|
||||
[:h2 (tr "onboarding.choice.team-up")]
|
||||
[:p (tr "onboarding.choice.team-up-desc")]]
|
||||
|
||||
[:& fm/form {:form form
|
||||
:on-submit on-submit}
|
||||
[:& fm/input {:type "text"
|
||||
:name :name
|
||||
:label (tr "onboarding.team.create.input-placeholder")}]
|
||||
|
||||
[:div.invite-row
|
||||
[:& fm/input {:name :email
|
||||
:label (tr "labels.email")}]
|
||||
[:& fm/select {:name :role
|
||||
:options roles}]]
|
||||
|
||||
[:div.buttons
|
||||
[:button.btn-secondary.btn-large
|
||||
{:on-click #(st/emit! (modal/show {:type :onboarding-choice}))}
|
||||
(tr "labels.cancel")]
|
||||
[:& fm/submit-button
|
||||
{:label (tr "onboarding.team.create.button")}]]]
|
||||
|
||||
[:div.modal-right
|
||||
[:img {:src "images/onboarding-start.jpg" :border "0" :alt (tr "onboarding.team.start.title")}]
|
||||
[:h2 (tr "onboarding.team.start.title")]
|
||||
[:p (tr "onboarding.team.start.desc1")]
|
||||
[:button.btn-primary.btn-large {:on-click close} (tr "onboarding.team.start.button")]]
|
||||
|
||||
|
||||
{:label (tr "labels.create")}]]
|
||||
[:div.skip-action
|
||||
{:on-click on-skip}
|
||||
[:div.action "Skip and invite later"]]]
|
||||
[:img.deco {:src "images/deco-left.png" :border "0"}]
|
||||
[:img.deco.right {:src "images/deco-right.png" :border "0"}]]]))
|
||||
|
||||
(mf/defc template-item
|
||||
[{:keys [name path image project-id]}]
|
||||
(let [downloading? (mf/use-state false)
|
||||
link (str (assoc cf/public-uri :path path))
|
||||
|
||||
on-finish-import
|
||||
(fn []
|
||||
(st/emit! (dd/fetch-files {:project-id project-id})
|
||||
(dd/fetch-recent-files)
|
||||
(dd/clear-selected-files)))
|
||||
|
||||
open-import-modal
|
||||
(fn [file]
|
||||
(st/emit! (modal/show
|
||||
{:type :import
|
||||
:project-id project-id
|
||||
:files [file]
|
||||
:on-finish-import on-finish-import})))
|
||||
on-click
|
||||
(fn []
|
||||
(reset! downloading? true)
|
||||
(->> (http/send! {:method :get :uri link :response-type :blob :mode :no-cors})
|
||||
(rx/subs (fn [{:keys [body] :as response}]
|
||||
(open-import-modal {:name name :uri (dom/create-uri body)}))
|
||||
(fn [error]
|
||||
(js/console.log "error" error))
|
||||
(fn []
|
||||
(reset! downloading? false)))))
|
||||
]
|
||||
|
||||
[:div.template-item
|
||||
[:div.template-item-content
|
||||
[:img {:src image}]]
|
||||
[:div.template-item-title
|
||||
[:div.label name]
|
||||
(if @downloading?
|
||||
[:div.action "Fetching..."]
|
||||
[:div.action {:on-click on-click} "+ Add to drafts"])]]))
|
||||
|
||||
(mf/defc onboarding-templates-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :onboarding-templates}
|
||||
;; NOTE: the project usually comes empty, it only comes fullfilled
|
||||
;; when a user creates a new team just after signup.
|
||||
[{:keys [project-id] :as props}]
|
||||
(let [close-fn (mf/use-callback #(st/emit! (modal/hide)))
|
||||
profile (mf/deref refs/profile)
|
||||
project-id (or project-id (:default-project-id profile))]
|
||||
[:div.modal-overlay
|
||||
[:div.modal-container.onboarding-templates
|
||||
[:div.modal-header
|
||||
[:div.modal-close-button
|
||||
{:on-click close-fn} i/close]]
|
||||
|
||||
[:div.modal-content
|
||||
[:h3 (tr "onboarding.templates.title")]
|
||||
[:p (tr "onboarding.templates.subtitle")]
|
||||
|
||||
[:div.templates
|
||||
[:& template-item
|
||||
{:path "/github/penpot-files/Penpot-Design-system.penpot"
|
||||
:image "https://penpot.app/images/libraries/cover-ds-penpot.jpg"
|
||||
:name "Penpot Design System"
|
||||
:project-id project-id}]
|
||||
[:& template-item
|
||||
{:path "/github/penpot-files/Material-Design-Kit.penpot"
|
||||
:image "https://penpot.app/images/libraries/cover-material.jpg"
|
||||
:name "Material Design Kit"
|
||||
:project-id project-id}]]]]]))
|
||||
|
||||
|
||||
;;; --- RELEASE NOTES MODAL
|
||||
|
||||
@@ -299,5 +481,4 @@
|
||||
|
||||
(defmethod rc/render-release-notes "0.0"
|
||||
[params]
|
||||
(rc/render-release-notes (assoc params :version "1.9")))
|
||||
|
||||
(rc/render-release-notes (assoc params :version "1.10")))
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
[app.main.exports :as exports]
|
||||
[app.main.repo :as repo]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.context :as muc]
|
||||
[app.main.ui.shapes.embed :as embed]
|
||||
[app.main.ui.shapes.export :as ed]
|
||||
[app.main.ui.shapes.filters :as filters]
|
||||
@@ -124,6 +125,7 @@
|
||||
;; Auxiliary SVG for rendering text-shapes
|
||||
(when render-texts?
|
||||
(for [object text-shapes]
|
||||
[:& (mf/provider muc/text-plain-colors-ctx) {:value true}
|
||||
[:svg {:id (str "screenshot-text-" (:id object))
|
||||
:view-box (str "0 0 " (:width object) " " (:height object))
|
||||
:width (:width object)
|
||||
@@ -131,7 +133,7 @@
|
||||
:version "1.1"
|
||||
:xmlns "http://www.w3.org/2000/svg"
|
||||
:xmlnsXlink "http://www.w3.org/1999/xlink"}
|
||||
[:& shape-wrapper {:shape (-> object (assoc :x 0 :y 0))}]]))]))
|
||||
[:& shape-wrapper {:shape (-> object (assoc :x 0 :y 0))}]]]))]))
|
||||
|
||||
(defn- adapt-root-frame
|
||||
[objects object-id]
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
(ns app.main.ui.shapes.attrs
|
||||
(:require
|
||||
[app.common.pages.spec :as spec]
|
||||
[app.common.types.radius :as ctr]
|
||||
[app.main.ui.context :as muc]
|
||||
[app.util.object :as obj]
|
||||
[app.util.svg :as usvg]
|
||||
@@ -53,7 +54,13 @@
|
||||
(min r-bottom-left r-left-bottom)]))
|
||||
|
||||
(defn add-border-radius [attrs shape]
|
||||
(if (or (:r1 shape) (:r2 shape) (:r3 shape) (:r4 shape))
|
||||
(case (ctr/radius-mode shape)
|
||||
|
||||
:radius-1
|
||||
(obj/merge! attrs #js {:rx (:rx shape)
|
||||
:ry (:ry shape)})
|
||||
|
||||
:radius-4
|
||||
(let [[r1 r2 r3 r4] (truncate-radius shape)
|
||||
top (- (:width shape) r1 r2)
|
||||
right (- (:height shape) r2 r3)
|
||||
@@ -69,10 +76,7 @@
|
||||
"v" (- left) " "
|
||||
"a" r1 "," r1 " 0 0 1 " r1 "," (- r1) " "
|
||||
"z")}))
|
||||
(if (or (:rx shape) (:ry shape))
|
||||
(obj/merge! attrs #js {:rx (:rx shape)
|
||||
:ry (:ry shape)})
|
||||
attrs)))
|
||||
attrs))
|
||||
|
||||
(defn add-fill [attrs shape render-id]
|
||||
(let [fill-attrs (cond
|
||||
|
||||
@@ -242,7 +242,8 @@
|
||||
:penpot:overlay-position-y ((d/nilf get-in) interaction [:overlay-position :y])
|
||||
:penpot:url (:url interaction)
|
||||
:penpot:close-click-outside ((d/nilf str) (:close-click-outside interaction))
|
||||
:penpot:background-overlay ((d/nilf str) (:background-overlay interaction))}])]))
|
||||
:penpot:background-overlay ((d/nilf str) (:background-overlay interaction))
|
||||
:penpot:preserve-scroll ((d/nilf str) (:preserve-scroll interaction))}])]))
|
||||
|
||||
(mf/defc export-data
|
||||
[{:keys [shape]}]
|
||||
|
||||
@@ -10,24 +10,10 @@
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.main.ui.context :as muc]
|
||||
[app.main.ui.shapes.export :as ed]
|
||||
[app.util.object :as obj]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(mf/defc linear-gradient [{:keys [id gradient shape]}]
|
||||
(let [transform (when (= :path (:type shape)) (gsh/transform-matrix shape nil (gpt/point 0.5 0.5)))]
|
||||
[:> :linearGradient #js {:id id
|
||||
:x1 (:start-x gradient)
|
||||
:y1 (:start-y gradient)
|
||||
:x2 (:end-x gradient)
|
||||
:y2 (:end-y gradient)
|
||||
:gradientTransform transform
|
||||
:penpot:gradient "true"}
|
||||
(for [{:keys [offset color opacity]} (:stops gradient)]
|
||||
[:stop {:key (str id "-stop-" offset)
|
||||
:offset (or offset 0)
|
||||
:stop-color color
|
||||
:stop-opacity opacity}])]))
|
||||
|
||||
(defn add-metadata [props gradient]
|
||||
(-> props
|
||||
(obj/set! "penpot:gradient" "true")
|
||||
@@ -38,6 +24,30 @@
|
||||
(obj/set! "penpot:end-y" (:end-y gradient))
|
||||
(obj/set! "penpot:width" (:width gradient))))
|
||||
|
||||
(mf/defc linear-gradient [{:keys [id gradient shape]}]
|
||||
(let [transform (when (= :path (:type shape)) (gsh/transform-matrix shape nil (gpt/point 0.5 0.5)))
|
||||
base-props #js {:id id
|
||||
:x1 (:start-x gradient)
|
||||
:y1 (:start-y gradient)
|
||||
:x2 (:end-x gradient)
|
||||
:y2 (:end-y gradient)
|
||||
:gradientTransform transform}
|
||||
|
||||
include-metadata? (mf/use-ctx ed/include-metadata-ctx)
|
||||
|
||||
props (cond-> base-props
|
||||
include-metadata?
|
||||
(add-metadata gradient))]
|
||||
|
||||
[:> :linearGradient props
|
||||
(for [{:keys [offset color opacity]} (:stops gradient)]
|
||||
[:stop {:key (str id "-stop-" offset)
|
||||
:offset (or offset 0)
|
||||
:stop-color color
|
||||
:stop-opacity opacity}])]))
|
||||
|
||||
|
||||
|
||||
(mf/defc radial-gradient [{:keys [id gradient shape]}]
|
||||
(let [{:keys [x y width height]} (:selrect shape)
|
||||
transform (if (= :path (:type shape))
|
||||
@@ -73,7 +83,11 @@
|
||||
:gradientUnits "userSpaceOnUse"
|
||||
:gradientTransform transform}
|
||||
|
||||
props (-> base-props (add-metadata gradient))]
|
||||
include-metadata? (mf/use-ctx ed/include-metadata-ctx)
|
||||
|
||||
props (cond-> base-props
|
||||
include-metadata?
|
||||
(add-metadata gradient))]
|
||||
[:> :radialGradient props
|
||||
(for [{:keys [offset color opacity]} (:stops gradient)]
|
||||
[:stop {:key (str id "-stop-" offset)
|
||||
|
||||
@@ -8,9 +8,12 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.shapes :as geom]
|
||||
[app.main.ui.context :as muc]
|
||||
[app.main.ui.shapes.attrs :as attrs]
|
||||
[app.main.ui.shapes.text.styles :as sts]
|
||||
[app.util.color :as uc]
|
||||
[app.util.object :as obj]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(mf/defc render-text
|
||||
@@ -73,12 +76,111 @@
|
||||
(obj/set! "key" index))]
|
||||
[:> render-node props]))])))))
|
||||
|
||||
(defn- next-color
|
||||
"Given a set of colors try to get a color not yet used"
|
||||
[colors]
|
||||
(assert (set? colors))
|
||||
(loop [current-rgb [0 0 0]]
|
||||
(let [current-hex (uc/rgb->hex current-rgb)]
|
||||
(if (contains? colors current-hex)
|
||||
(recur (uc/next-rgb current-rgb))
|
||||
current-hex))))
|
||||
|
||||
(defn- remap-colors
|
||||
"Returns a new content replacing the original colors by their mapped 'simple color'"
|
||||
[content color-mapping]
|
||||
|
||||
(cond-> content
|
||||
(and (:fill-opacity content) (< (:fill-opacity content) 1.0))
|
||||
(-> (assoc :fill-color (get color-mapping [(:fill-color content) (:fill-opacity content)]))
|
||||
(assoc :fill-opacity 1.0))
|
||||
|
||||
(some? (:fill-color-gradient content))
|
||||
(-> (assoc :fill-color (get color-mapping (:fill-color-gradient content)))
|
||||
(assoc :fill-opacity 1.0)
|
||||
(dissoc :fill-color-gradient))
|
||||
|
||||
(contains? content :children)
|
||||
(update :children #(mapv (fn [node] (remap-colors node color-mapping)) %))))
|
||||
|
||||
(defn- fill->color
|
||||
"Given a content node returns the information about that node fill color"
|
||||
[{:keys [fill-color fill-opacity fill-color-gradient]}]
|
||||
|
||||
(cond
|
||||
(some? fill-color-gradient)
|
||||
{:type :gradient
|
||||
:gradient fill-color-gradient}
|
||||
|
||||
(and (string? fill-color) (some? fill-opacity) (not= fill-opacity 1))
|
||||
{:type :transparent
|
||||
:hex fill-color
|
||||
:opacity fill-opacity}
|
||||
|
||||
(string? fill-color)
|
||||
{:type :solid
|
||||
:hex fill-color
|
||||
:map-to fill-color}))
|
||||
|
||||
(defn- retrieve-colors
|
||||
"Given a text shape returns a triple with the values:
|
||||
- colors used as fills
|
||||
- a mapping from simple solid colors to complex ones (transparents/gradients)
|
||||
- the inverse of the previous mapping (to restore the value in the SVG)"
|
||||
[shape]
|
||||
(let [colors (->> (:content shape)
|
||||
(let [color-data
|
||||
(->> (:content shape)
|
||||
(tree-seq map? :children)
|
||||
(into #{"#000000"} (comp (map :fill-color) (filter string?))))]
|
||||
(apply str (interpose "," colors))))
|
||||
(map fill->color)
|
||||
(filter some?))
|
||||
|
||||
colors (->> color-data
|
||||
(into #{"#000000"}
|
||||
(comp (filter #(= :solid (:type %)))
|
||||
(map :hex))))
|
||||
|
||||
[colors color-data]
|
||||
(loop [colors colors
|
||||
head (first color-data)
|
||||
tail (rest color-data)
|
||||
result []]
|
||||
|
||||
(if (nil? head)
|
||||
[colors result]
|
||||
|
||||
(if (= :solid (:type head))
|
||||
(recur colors
|
||||
(first tail)
|
||||
(rest tail)
|
||||
(conj result head))
|
||||
|
||||
(let [next-color (next-color colors)
|
||||
head (assoc head :map-to next-color)
|
||||
colors (conj colors next-color)]
|
||||
(recur colors
|
||||
(first tail)
|
||||
(rest tail)
|
||||
(conj result head))))))
|
||||
|
||||
color-mapping-inverse
|
||||
(->> color-data
|
||||
(remove #(= :solid (:type %)))
|
||||
(group-by :map-to)
|
||||
(d/mapm #(first %2)))
|
||||
|
||||
color-mapping
|
||||
(merge
|
||||
(->> color-data
|
||||
(filter #(= :transparent (:type %)))
|
||||
(map #(vector [(:hex %) (:opacity %)] (:map-to %)))
|
||||
(into {}))
|
||||
|
||||
(->> color-data
|
||||
(filter #(= :gradient (:type %)))
|
||||
(map #(vector (:gradient %) (:map-to %)))
|
||||
(into {})))]
|
||||
|
||||
[colors color-mapping color-mapping-inverse]))
|
||||
|
||||
(mf/defc text-shape
|
||||
{::mf/wrap-props false
|
||||
@@ -88,11 +190,19 @@
|
||||
grow-type (obj/get props "grow-type") ;; This is only needed in workspace
|
||||
;; We add 8px to add a padding for the exporter
|
||||
;; width (+ width 8)
|
||||
]
|
||||
[colors color-mapping color-mapping-inverse] (retrieve-colors shape)
|
||||
|
||||
plain-colors? (mf/use-ctx muc/text-plain-colors-ctx)
|
||||
|
||||
content (cond-> content
|
||||
plain-colors?
|
||||
(remap-colors color-mapping))]
|
||||
|
||||
[:foreignObject {:x x
|
||||
:y y
|
||||
:id id
|
||||
:data-colors (retrieve-colors shape)
|
||||
:data-colors (->> colors (str/join ","))
|
||||
:data-mapping (-> color-mapping-inverse (clj->js) (js/JSON.stringify))
|
||||
:transform (geom/transform-matrix shape)
|
||||
:width (if (#{:auto-width} grow-type) 100000 width)
|
||||
:height (if (#{:auto-height :auto-width} grow-type) 100000 height)
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
|
||||
(defn generate-paragraph-styles
|
||||
[shape data]
|
||||
(let [line-height (:line-height data)
|
||||
(let [line-height (:line-height data 1.2)
|
||||
text-align (:text-align data "start")
|
||||
grow-type (:grow-type shape)
|
||||
|
||||
@@ -60,10 +60,10 @@
|
||||
|
||||
(defn generate-text-styles
|
||||
[data]
|
||||
(let [letter-spacing (:letter-spacing data)
|
||||
(let [letter-spacing (:letter-spacing data 0)
|
||||
text-decoration (:text-decoration data)
|
||||
text-transform (:text-transform data)
|
||||
line-height (:line-height data)
|
||||
line-height (:line-height data 1.2)
|
||||
|
||||
font-id (:font-id data (:font-id txt/default-text-attrs))
|
||||
font-variant-id (:font-variant-id data)
|
||||
|
||||
@@ -127,10 +127,14 @@
|
||||
[:div.share-link-section
|
||||
[:label (tr "labels.link")]
|
||||
[:div.custom-input.with-icon
|
||||
[:input {:type "text" :value (or @link "") :read-only true}]
|
||||
[:input {:type "text"
|
||||
:value (or @link "")
|
||||
:placeholder (tr "common.share-link.placeholder")
|
||||
:read-only true}]
|
||||
(when (some? @link)
|
||||
[:div.help-icon {:title (tr "labels.copy")
|
||||
:on-click copy-link}
|
||||
i/copy]]
|
||||
i/copy])]
|
||||
|
||||
[:div.hint (tr "common.share-link.permissions-hint")]]]
|
||||
|
||||
|
||||
@@ -43,6 +43,8 @@
|
||||
|
||||
local (mf/deref refs/viewer-local)
|
||||
|
||||
nav-scroll (:nav-scroll local)
|
||||
|
||||
page-id (or page-id (-> file :data :pages first))
|
||||
|
||||
page (mf/use-memo
|
||||
@@ -91,6 +93,14 @@
|
||||
(fn []
|
||||
(events/unlistenByKey key1)))))
|
||||
|
||||
(mf/use-layout-effect
|
||||
(mf/deps nav-scroll)
|
||||
(fn []
|
||||
(when (number? nav-scroll)
|
||||
(let [viewer-section (dom/get-element "viewer-section")]
|
||||
(st/emit! (dv/reset-nav-scroll))
|
||||
(dom/set-scroll-pos! viewer-section nav-scroll)))))
|
||||
|
||||
[:div {:class (dom/classnames
|
||||
:force-visible (:show-thumbnails local)
|
||||
:viewer-layout (not= section :handoff)
|
||||
@@ -110,7 +120,7 @@
|
||||
:show? (:show-thumbnails local false)
|
||||
:page page
|
||||
:index index}]
|
||||
[:section.viewer-preview
|
||||
[:section.viewer-section {:id "viewer-section"}
|
||||
(cond
|
||||
(empty? frames)
|
||||
[:section.empty-state
|
||||
|
||||
@@ -6,10 +6,8 @@
|
||||
|
||||
(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.data.events :as ev]
|
||||
[app.main.refs :as refs]
|
||||
@@ -80,6 +78,7 @@
|
||||
(mf/defc comments-layer
|
||||
[{:keys [zoom file users frame page] :as props}]
|
||||
(let [profile (mf/deref refs/profile)
|
||||
threads-map (mf/deref threads-ref)
|
||||
|
||||
modifier1 (-> (gpt/point (:x frame) (:y frame))
|
||||
(gpt/negate)
|
||||
@@ -88,16 +87,12 @@
|
||||
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))))
|
||||
(frame-contains? frame position))))
|
||||
|
||||
on-bubble-click
|
||||
(mf/use-callback
|
||||
@@ -110,14 +105,15 @@
|
||||
|
||||
on-click
|
||||
(mf/use-callback
|
||||
(mf/deps cstate frame page file)
|
||||
(mf/deps cstate frame page file zoom)
|
||||
(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))
|
||||
viewport-point (dom/get-offset-position event)
|
||||
viewport-point (-> viewport-point (update :x #(/ % zoom)) (update :y #(/ % zoom)))
|
||||
position (gpt/transform viewport-point modifier2)
|
||||
params {:position position
|
||||
:page-id (:id page)
|
||||
:file-id (:id file)}]
|
||||
@@ -140,14 +136,16 @@
|
||||
[:div.viewer-comments-container
|
||||
[:div.threads
|
||||
(for [item threads]
|
||||
(let [item (update item :position gpt/transform modifier1)]
|
||||
[:& cmt/thread-bubble {:thread item
|
||||
:zoom zoom
|
||||
:on-click on-bubble-click
|
||||
:open? (= (:id item) (:open cstate))
|
||||
:key (:seqn item)}])
|
||||
:key (:seqn item)}]))
|
||||
|
||||
(when-let [id (:open cstate)]
|
||||
(when-let [thread (get threads-map id)]
|
||||
(when-let [thread (-> (get threads-map id)
|
||||
(update :position gpt/transform modifier1))]
|
||||
[:& cmt/thread-comments {:thread thread
|
||||
:users users
|
||||
:zoom zoom}]))
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
(ns app.main.ui.viewer.handoff.attributes.layout
|
||||
(:require
|
||||
[app.common.math :as mth]
|
||||
[app.common.types.radius :as ctr]
|
||||
[app.main.ui.components.copy-button :refer [copy-button]]
|
||||
[app.util.code-gen :as cg]
|
||||
[app.util.i18n :refer [t]]
|
||||
@@ -58,20 +59,17 @@
|
||||
[:div.attributes-value (mth/precision y 2) "px"]
|
||||
[:& copy-button {:data (copy-data selrect :y)}]])
|
||||
|
||||
(when (and (:rx shape) (not= (:rx shape) 0))
|
||||
(when (ctr/radius-1? shape)
|
||||
[:div.attributes-unit-row
|
||||
[:div.attributes-label (t locale "handoff.attributes.layout.radius")]
|
||||
[:div.attributes-value (mth/precision (:rx shape) 2) "px"]
|
||||
[:& copy-button {:data (copy-data shape :rx)}]])
|
||||
|
||||
(when (and (:r1 shape)
|
||||
(or (not= (:r1 shape) 0)
|
||||
(not= (:r2 shape) 0)
|
||||
(not= (:r3 shape) 0)
|
||||
(not= (:r4 shape) 0)))
|
||||
(when (ctr/radius-4? shape)
|
||||
[:div.attributes-unit-row
|
||||
[:div.attributes-label (t locale "handoff.attributes.layout.radius")]
|
||||
[:div.attributes-value (mth/precision (:r1 shape) 2) ", "
|
||||
[:div.attributes-value
|
||||
(mth/precision (:r1 shape) 2) ", "
|
||||
(mth/precision (:r2 shape) 2) ", "
|
||||
(mth/precision (:r3 shape) 2) ", "
|
||||
(mth/precision (:r4 shape) 2) "px"]
|
||||
|
||||
@@ -80,26 +80,28 @@
|
||||
page-name (:name page)
|
||||
frame-name (:name frame)
|
||||
total (count (:frames page))
|
||||
|
||||
toggle-thumbnails
|
||||
(fn []
|
||||
(st/emit! dv/toggle-thumbnails-panel))
|
||||
|
||||
show-dropdown? (mf/use-state false)
|
||||
|
||||
open-dropdown
|
||||
toggle-thumbnails
|
||||
(mf/use-callback
|
||||
(fn []
|
||||
(reset! show-dropdown? true)
|
||||
(st/emit! dv/close-thumbnails-panel))
|
||||
(st/emit! dv/toggle-thumbnails-panel)))
|
||||
|
||||
open-dropdown
|
||||
(mf/use-callback
|
||||
(fn []
|
||||
(reset! show-dropdown? true)))
|
||||
|
||||
close-dropdown
|
||||
(mf/use-callback
|
||||
(fn []
|
||||
(reset! show-dropdown? false))
|
||||
(reset! show-dropdown? false)))
|
||||
|
||||
navigate-to
|
||||
(mf/use-callback
|
||||
(fn [page-id]
|
||||
(st/emit! (dv/go-to-page page-id))
|
||||
(reset! show-dropdown? false))]
|
||||
(reset! show-dropdown? false)))]
|
||||
|
||||
[:div.sitemap-zone {:alt (tr "viewer.header.sitemap")}
|
||||
[:div.breadcrumb
|
||||
@@ -108,6 +110,7 @@
|
||||
[:span "/"]
|
||||
[:span.file-name file-name]
|
||||
[:span "/"]
|
||||
|
||||
[:span.page-name page-name]
|
||||
[:span.icon i/arrow-down]
|
||||
|
||||
|
||||
@@ -44,7 +44,12 @@
|
||||
(case (:action-type interaction)
|
||||
:navigate
|
||||
(when-let [frame-id (:destination interaction)]
|
||||
(st/emit! (dv/go-to-frame frame-id)))
|
||||
(let [viewer-section (dom/get-element "viewer-section")
|
||||
scroll (if (:preserve-scroll interaction)
|
||||
(dom/get-scroll-pos viewer-section)
|
||||
0)]
|
||||
(st/emit! (dv/set-nav-scroll scroll)
|
||||
(dv/go-to-frame frame-id))))
|
||||
|
||||
:open-overlay
|
||||
(let [dest-frame-id (:destination interaction)
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.workspace.colors :as dc]
|
||||
[app.main.data.workspace.libraries :as dwl]
|
||||
[app.main.data.workspace.undo :as dwu]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.icons :as i]
|
||||
@@ -137,7 +138,8 @@
|
||||
editing-stop (update-in [:stops editing-stop] merge changes)))
|
||||
(reset! dirty? true)))
|
||||
|
||||
handle-click-picker (fn []
|
||||
handle-click-picker
|
||||
(fn []
|
||||
(if picking-color?
|
||||
(do (modal/disallow-click-outside!)
|
||||
(st/emit! (dc/stop-picker)))
|
||||
@@ -307,13 +309,19 @@
|
||||
(case @active-tab
|
||||
:ramp [:& ramp-selector {:color current-color
|
||||
:disable-opacity disable-opacity
|
||||
:on-change handle-change-color}]
|
||||
:on-change handle-change-color
|
||||
:on-start-drag #(st/emit! (dwu/start-undo-transaction))
|
||||
:on-finish-drag #(st/emit! (dwu/commit-undo-transaction))}]
|
||||
:harmony [:& harmony-selector {:color current-color
|
||||
:disable-opacity disable-opacity
|
||||
:on-change handle-change-color}]
|
||||
:on-change handle-change-color
|
||||
:on-start-drag #(st/emit! (dwu/start-undo-transaction))
|
||||
:on-finish-drag #(st/emit! (dwu/commit-undo-transaction))}]
|
||||
:hsva [:& hsva-selector {:color current-color
|
||||
:disable-opacity disable-opacity
|
||||
:on-change handle-change-color}]
|
||||
:on-change handle-change-color
|
||||
:on-start-drag #(st/emit! (dwu/start-undo-transaction))
|
||||
:on-finish-drag #(st/emit! (dwu/commit-undo-transaction))}]
|
||||
nil))
|
||||
|
||||
[:& color-inputs {:type (if (= @active-tab :hsva) :hsv :rgb)
|
||||
@@ -363,7 +371,7 @@
|
||||
position (or position :left)
|
||||
style (calculate-position vport position x y)
|
||||
|
||||
handle-change (fn [new-data _shift-clicked?]
|
||||
handle-change (fn [new-data]
|
||||
(reset! dirty? (not= data new-data))
|
||||
(reset! last-change new-data)
|
||||
(when on-change
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
y (+ (/ canvas-side 2) (* comp-y (/ canvas-side 2)))]
|
||||
(gpt/point x y)))
|
||||
|
||||
(mf/defc harmony-selector [{:keys [color disable-opacity on-change]}]
|
||||
(mf/defc harmony-selector [{:keys [color disable-opacity on-change on-start-drag on-finish-drag]}]
|
||||
(let [canvas-ref (mf/use-ref nil)
|
||||
{hue :h saturation :s value :v alpha :alpha} color
|
||||
|
||||
@@ -84,6 +84,24 @@
|
||||
:h new-hue
|
||||
:s new-saturation})))
|
||||
|
||||
handle-start-drag
|
||||
(mf/use-callback
|
||||
(mf/deps on-start-drag)
|
||||
(fn [event]
|
||||
(dom/capture-pointer event)
|
||||
(reset! dragging? true)
|
||||
(when on-start-drag
|
||||
(on-start-drag))))
|
||||
|
||||
handle-stop-drag
|
||||
(mf/use-callback
|
||||
(mf/deps on-finish-drag)
|
||||
(fn [event]
|
||||
(dom/release-pointer event)
|
||||
(reset! dragging? false)
|
||||
(when on-finish-drag
|
||||
(on-finish-drag))))
|
||||
|
||||
on-change-value (fn [new-value]
|
||||
(let [hex (uc/hsv->hex [hue saturation new-value])
|
||||
[r g b] (uc/hex->rgb hex)]
|
||||
@@ -112,11 +130,8 @@
|
||||
{:ref canvas-ref
|
||||
:width canvas-side
|
||||
:height canvas-side
|
||||
:on-mouse-down #(reset! dragging? true)
|
||||
:on-mouse-up #(reset! dragging? false)
|
||||
:on-pointer-down (partial dom/capture-pointer)
|
||||
:on-lost-pointer-capture #(do (dom/release-pointer %)
|
||||
(reset! dragging? false))
|
||||
:on-pointer-down handle-start-drag
|
||||
:on-lost-pointer-capture handle-stop-drag
|
||||
:on-click calculate-pos
|
||||
:on-mouse-move #(when @dragging? (calculate-pos %))}]
|
||||
[:div.handler {:style {:pointer-events "none"
|
||||
@@ -133,11 +148,15 @@
|
||||
:value value
|
||||
:max-value 255
|
||||
:vertical true
|
||||
:on-change on-change-value}]
|
||||
:on-change on-change-value
|
||||
:on-start-drag on-start-drag
|
||||
:on-finish-drag on-finish-drag}]
|
||||
(when (not disable-opacity)
|
||||
[:& slider-selector {:class "opacity"
|
||||
:vertical? true
|
||||
:value alpha
|
||||
:max-value 1
|
||||
:vertical true
|
||||
:on-change on-change-opacity}])]]))
|
||||
:on-change on-change-opacity
|
||||
:on-start-drag on-start-drag
|
||||
:on-finish-drag on-finish-drag}])]]))
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
[app.util.color :as uc]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(mf/defc hsva-selector [{:keys [color disable-opacity on-change]}]
|
||||
(mf/defc hsva-selector [{:keys [color disable-opacity on-change on-start-drag on-finish-drag]}]
|
||||
(let [{hue :h saturation :s value :v alpha :alpha} color
|
||||
handle-change-slider (fn [key]
|
||||
(fn [new-value]
|
||||
@@ -25,18 +25,39 @@
|
||||
[:div.hsva-selector
|
||||
[:span.hsva-selector-label "H"]
|
||||
[:& slider-selector
|
||||
{:class "hue" :max-value 360 :value hue :on-change (handle-change-slider :h)}]
|
||||
{:class "hue"
|
||||
:max-value 360
|
||||
:value hue
|
||||
:on-change (handle-change-slider :h)
|
||||
:on-start-drag on-start-drag
|
||||
:on-finish-drag on-finish-drag}]
|
||||
|
||||
[:span.hsva-selector-label "S"]
|
||||
[:& slider-selector
|
||||
{:class "saturation" :max-value 1 :value saturation :on-change (handle-change-slider :s)}]
|
||||
{:class "saturation"
|
||||
:max-value 1
|
||||
:value saturation
|
||||
:on-change (handle-change-slider :s)
|
||||
:on-start-drag on-start-drag
|
||||
:on-finish-drag on-finish-drag}]
|
||||
|
||||
[:span.hsva-selector-label "V"]
|
||||
[:& slider-selector
|
||||
{:class "value" :reverse? true :max-value 255 :value value :on-change (handle-change-slider :v)}]
|
||||
{:class "value"
|
||||
:reverse? true
|
||||
:max-value 255
|
||||
:value value
|
||||
:on-change (handle-change-slider :v)
|
||||
:on-start-drag on-start-drag
|
||||
:on-finish-drag on-finish-drag}]
|
||||
|
||||
(when (not disable-opacity)
|
||||
[:*
|
||||
[:span.hsva-selector-label "A"]
|
||||
[:& slider-selector
|
||||
{:class "opacity" :max-value 1 :value alpha :on-change on-change-opacity}]])]))
|
||||
{:class "opacity"
|
||||
:max-value 1
|
||||
:value alpha
|
||||
:on-change on-change-opacity
|
||||
:on-start-drag on-start-drag
|
||||
:on-finish-drag on-finish-drag}]])]))
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
[app.util.dom :as dom]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(mf/defc value-saturation-selector [{:keys [saturation value on-change]}]
|
||||
(mf/defc value-saturation-selector [{:keys [saturation value on-change on-start-drag on-finish-drag]}]
|
||||
(let [dragging? (mf/use-state false)
|
||||
calculate-pos
|
||||
(fn [ev]
|
||||
@@ -21,13 +21,27 @@
|
||||
{:keys [x y]} (-> ev dom/get-client-position)
|
||||
px (math/clamp (/ (- x left) (- right left)) 0 1)
|
||||
py (* 255 (- 1 (math/clamp (/ (- y top) (- bottom top)) 0 1)))]
|
||||
(on-change px py)))]
|
||||
(on-change px py)))
|
||||
|
||||
handle-start-drag
|
||||
(mf/use-callback
|
||||
(mf/deps on-start-drag)
|
||||
(fn [event]
|
||||
(dom/capture-pointer event)
|
||||
(reset! dragging? true)
|
||||
(on-start-drag)))
|
||||
|
||||
handle-stop-drag
|
||||
(mf/use-callback
|
||||
(mf/deps on-finish-drag)
|
||||
(fn [event]
|
||||
(dom/release-pointer event)
|
||||
(reset! dragging? false)
|
||||
(on-finish-drag)))
|
||||
]
|
||||
[:div.value-saturation-selector
|
||||
{:on-mouse-down #(reset! dragging? true)
|
||||
:on-mouse-up #(reset! dragging? false)
|
||||
:on-pointer-down (partial dom/capture-pointer)
|
||||
:on-lost-pointer-capture #(do (dom/release-pointer %)
|
||||
(reset! dragging? false))
|
||||
{:on-pointer-down handle-start-drag
|
||||
:on-lost-pointer-capture handle-stop-drag
|
||||
:on-click calculate-pos
|
||||
:on-mouse-move #(when @dragging? (calculate-pos %))}
|
||||
[:div.handler {:style {:pointer-events "none"
|
||||
@@ -35,7 +49,7 @@
|
||||
:top (str (* 100 (- 1 (/ value 255))) "%")}}]]))
|
||||
|
||||
|
||||
(mf/defc ramp-selector [{:keys [color disable-opacity on-change]}]
|
||||
(mf/defc ramp-selector [{:keys [color disable-opacity on-change on-start-drag on-finish-drag]}]
|
||||
(let [{hex :hex
|
||||
hue :h saturation :s value :v alpha :alpha} color
|
||||
|
||||
@@ -64,7 +78,9 @@
|
||||
{:hue hue
|
||||
:saturation saturation
|
||||
:value value
|
||||
:on-change on-change-value-saturation}]
|
||||
:on-change on-change-value-saturation
|
||||
:on-start-drag on-start-drag
|
||||
:on-finish-drag on-finish-drag}]
|
||||
|
||||
[:div.shade-selector
|
||||
[:& color-bullet {:color {:color hex
|
||||
@@ -72,10 +88,14 @@
|
||||
[:& slider-selector {:class "hue"
|
||||
:max-value 360
|
||||
:value hue
|
||||
:on-change on-change-hue}]
|
||||
:on-change on-change-hue
|
||||
:on-start-drag on-start-drag
|
||||
:on-finish-drag on-finish-drag}]
|
||||
|
||||
(when (not disable-opacity)
|
||||
[:& slider-selector {:class "opacity"
|
||||
:max-value 1
|
||||
:value alpha
|
||||
:on-change on-change-opacity}])]]))
|
||||
:on-change on-change-opacity
|
||||
:on-start-drag on-start-drag
|
||||
:on-finish-drag on-finish-drag}])]]))
|
||||
|
||||
@@ -12,10 +12,29 @@
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(mf/defc slider-selector
|
||||
[{:keys [value class min-value max-value vertical? reverse? on-change]}]
|
||||
[{:keys [value class min-value max-value vertical? reverse? on-change on-start-drag on-finish-drag]}]
|
||||
(let [min-value (or min-value 0)
|
||||
max-value (or max-value 1)
|
||||
dragging? (mf/use-state false)
|
||||
|
||||
handle-start-drag
|
||||
(mf/use-callback
|
||||
(mf/deps on-start-drag)
|
||||
(fn [event]
|
||||
(dom/capture-pointer event)
|
||||
(reset! dragging? true)
|
||||
(when on-start-drag
|
||||
(on-start-drag))))
|
||||
|
||||
handle-stop-drag
|
||||
(mf/use-callback
|
||||
(mf/deps on-finish-drag)
|
||||
(fn [event]
|
||||
(dom/release-pointer event)
|
||||
(reset! dragging? false)
|
||||
(when on-finish-drag
|
||||
(on-finish-drag))))
|
||||
|
||||
calculate-pos
|
||||
(fn [ev]
|
||||
(when on-change
|
||||
@@ -32,11 +51,8 @@
|
||||
|
||||
[:div.slider-selector
|
||||
{:class (str (if vertical? "vertical " "") class)
|
||||
:on-mouse-down #(reset! dragging? true)
|
||||
:on-mouse-up #(reset! dragging? false)
|
||||
:on-pointer-down (partial dom/capture-pointer)
|
||||
:on-lost-pointer-capture #(do (dom/release-pointer %)
|
||||
(reset! dragging? false))
|
||||
:on-pointer-down handle-start-drag
|
||||
:on-lost-pointer-capture handle-stop-drag
|
||||
:on-click calculate-pos
|
||||
:on-mouse-move #(when @dragging? (calculate-pos %))}
|
||||
|
||||
|
||||
@@ -281,7 +281,8 @@
|
||||
[:& menu-entry {:title (tr "workspace.shape.menu.delete-flow-start")
|
||||
:on-click (do-remove-flow flow)}])))
|
||||
|
||||
(when (not= (:type shape) :frame)
|
||||
(when (and (not= (:type shape) :frame)
|
||||
(or multiple? (nil? (:component-id shape))))
|
||||
[:*
|
||||
[:& menu-separator]
|
||||
[:& menu-entry {:title (tr "workspace.shape.menu.create-component")
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
(ns app.main.ui.workspace.sidebar.options.menus.booleans
|
||||
(:require
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.shortcuts :as sc]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.icons :as i]
|
||||
@@ -51,28 +52,28 @@
|
||||
[:div.align-options
|
||||
[:div.align-group
|
||||
[:div.align-button.tooltip.tooltip-bottom
|
||||
{:alt (tr "workspace.shape.menu.union")
|
||||
{:alt (str (tr "workspace.shape.menu.union") " (" (sc/get-tooltip :boolean-union) ")")
|
||||
:class (dom/classnames :disabled disabled-bool-btns
|
||||
:selected (= head-bool-type :union))
|
||||
:on-click (set-bool :union)}
|
||||
i/boolean-union]
|
||||
|
||||
[:div.align-button.tooltip.tooltip-bottom
|
||||
{:alt (tr "workspace.shape.menu.difference")
|
||||
{:alt (str (tr "workspace.shape.menu.difference") " (" (sc/get-tooltip :boolean-difference) ")")
|
||||
:class (dom/classnames :disabled disabled-bool-btns
|
||||
:selected (= head-bool-type :difference))
|
||||
:on-click (set-bool :difference)}
|
||||
i/boolean-difference]
|
||||
|
||||
[:div.align-button.tooltip.tooltip-bottom
|
||||
{:alt (tr "workspace.shape.menu.intersection")
|
||||
{:alt (str (tr "workspace.shape.menu.intersection") " (" (sc/get-tooltip :boolean-intersection) ")")
|
||||
:class (dom/classnames :disabled disabled-bool-btns
|
||||
:selected (= head-bool-type :intersection))
|
||||
:on-click (set-bool :intersection)}
|
||||
i/boolean-intersection]
|
||||
|
||||
[:div.align-button.tooltip.tooltip-bottom
|
||||
{:alt (tr "workspace.shape.menu.exclude")
|
||||
{:alt (str (tr "workspace.shape.menu.exclude") " (" (sc/get-tooltip :boolean-exclude) ")")
|
||||
:class (dom/classnames :disabled disabled-bool-btns
|
||||
:selected (= head-bool-type :exclude))
|
||||
:on-click (set-bool :exclude)}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
[app.common.data :as d]
|
||||
[app.main.data.messages :as dm]
|
||||
[app.main.data.workspace :as udw]
|
||||
[app.main.data.workspace.persistence :as dwp]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.icons :as i]
|
||||
@@ -19,7 +20,10 @@
|
||||
|
||||
(defn request-export
|
||||
[shape exports]
|
||||
(rp/query! :export
|
||||
;; Force a persist before exporting otherwise the exported shape could be outdated
|
||||
(st/emit! ::dwp/force-persist)
|
||||
(rp/query!
|
||||
:export
|
||||
{:page-id (:page-id shape)
|
||||
:file-id (:file-id shape)
|
||||
:object-id (:id shape)
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
(:require
|
||||
[app.common.pages :as cp]
|
||||
[app.main.data.workspace.colors :as dc]
|
||||
[app.main.data.workspace.undo :as dwu]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row]]
|
||||
@@ -70,17 +69,7 @@
|
||||
(assoc :id nil :file-id nil))]
|
||||
(st/emit! (dc/change-fill ids color)))))
|
||||
|
||||
on-open-picker
|
||||
(mf/use-callback
|
||||
(mf/deps ids)
|
||||
(fn [_value _opacity _id _file-id]
|
||||
(st/emit! (dwu/start-undo-transaction))))
|
||||
|
||||
on-close-picker
|
||||
(mf/use-callback
|
||||
(mf/deps ids)
|
||||
(fn [_value _opacity _id _file-id]
|
||||
(st/emit! (dwu/commit-undo-transaction))))]
|
||||
]
|
||||
|
||||
(if show?
|
||||
[:div.element-set
|
||||
@@ -90,10 +79,9 @@
|
||||
|
||||
[:div.element-set-content
|
||||
[:& color-row {:color color
|
||||
:title (tr "workspace.options.fill")
|
||||
:on-change on-change
|
||||
:on-detach on-detach
|
||||
:on-open on-open-picker
|
||||
:on-close on-close-picker}]]]
|
||||
:on-detach on-detach}]]]
|
||||
|
||||
[:div.element-set
|
||||
[:div.element-set-title
|
||||
|
||||
@@ -130,7 +130,7 @@
|
||||
:on-change handle-change-type}]
|
||||
|
||||
(if (= type :square)
|
||||
[:div.input-element.pixels
|
||||
[:div.input-element.pixels {:title (tr "workspace.options.size")}
|
||||
[:> numeric-input {:min 1
|
||||
:no-validate true
|
||||
:value (:size params)
|
||||
@@ -214,6 +214,7 @@
|
||||
:on-change (handle-change :params :margin)}]])
|
||||
|
||||
[:& color-row {:color (:color params)
|
||||
:title (tr "workspace.options.grid.params.color")
|
||||
:disable-gradient true
|
||||
:on-change handle-change-color
|
||||
:on-detach handle-detach-color}]
|
||||
|
||||
@@ -169,6 +169,7 @@
|
||||
overlay-pos-type (:overlay-pos-type interaction)
|
||||
close-click-outside? (:close-click-outside interaction false)
|
||||
background-overlay? (:background-overlay interaction false)
|
||||
preserve-scroll? (:preserve-scroll interaction false)
|
||||
|
||||
extended-open? (mf/use-state false)
|
||||
|
||||
@@ -197,6 +198,11 @@
|
||||
value (when (not= value "") (uuid/uuid value))]
|
||||
(update-interaction index #(cti/set-destination % value))))
|
||||
|
||||
change-preserve-scroll
|
||||
(fn [event]
|
||||
(let [value (-> event dom/get-target dom/checked?)]
|
||||
(update-interaction index #(cti/set-preserve-scroll % value))))
|
||||
|
||||
change-url
|
||||
(fn [event]
|
||||
(let [target (dom/get-target event)
|
||||
@@ -264,11 +270,12 @@
|
||||
(when (cti/has-delay interaction)
|
||||
[:div.interactions-element
|
||||
[:span.element-set-subtitle.wide (tr "workspace.options.interaction-delay")]
|
||||
[:div.input-element
|
||||
[:div.input-element {:title (tr "workspace.options.interaction-ms")}
|
||||
[:> numeric-input {:ref ext-delay-ref
|
||||
:on-click (select-text ext-delay-ref)
|
||||
:on-change change-delay
|
||||
:value (:delay interaction)}]
|
||||
:value (:delay interaction)
|
||||
:title (tr "workspace.options.interaction-ms")}]
|
||||
[:span.after (tr "workspace.options.interaction-ms")]]])
|
||||
|
||||
; Action select
|
||||
@@ -295,6 +302,17 @@
|
||||
(not= (:id frame) (:frame-id shape))) ; nor a shape to its container frame
|
||||
[:option {:value (str (:id frame))} (:name frame)]))]])
|
||||
|
||||
; Preserve scroll
|
||||
(when (cti/has-preserve-scroll interaction)
|
||||
[:div.interactions-element
|
||||
[:div.input-checkbox
|
||||
[:input {:type "checkbox"
|
||||
:id (str "preserve-" index)
|
||||
:checked preserve-scroll?
|
||||
:on-change change-preserve-scroll}]
|
||||
[:label {:for (str "preserve-" index)}
|
||||
(tr "workspace.options.interaction-preserve-scroll")]]])
|
||||
|
||||
; URL
|
||||
(when (cti/has-url interaction)
|
||||
[:div.interactions-element
|
||||
|
||||
@@ -112,8 +112,7 @@
|
||||
[:option {:value "color"} (tr "workspace.options.layer-options.blend-mode.color")]
|
||||
[:option {:value "luminosity"} (tr "workspace.options.layer-options.blend-mode.luminosity")]]
|
||||
|
||||
[:div.input-element
|
||||
{:class "percentail"}
|
||||
[:div.input-element {:title (tr "workspace.options.opacity") :class "percentail"}
|
||||
[:> numeric-input {:value (-> values :opacity opacity->string)
|
||||
:placeholder (tr "settings.multiple")
|
||||
:on-click select-all
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.math :as math]
|
||||
[app.common.types.radius :as ctr]
|
||||
[app.main.data.workspace :as udw]
|
||||
[app.main.data.workspace.changes :as dch]
|
||||
[app.main.refs :as refs]
|
||||
@@ -61,6 +62,11 @@
|
||||
|
||||
proportion-lock (:proportion-lock values)
|
||||
|
||||
radius-mode (ctr/radius-mode values)
|
||||
all-equal? (ctr/all-equal? values)
|
||||
radius-multi? (mf/use-state nil)
|
||||
radius-input-ref (mf/use-ref nil)
|
||||
|
||||
on-size-change
|
||||
(mf/use-callback
|
||||
(mf/deps ids)
|
||||
@@ -97,57 +103,38 @@
|
||||
(mf/use-callback
|
||||
(mf/deps ids)
|
||||
(fn [_value]
|
||||
(let [radius-update
|
||||
(fn [shape]
|
||||
(cond-> shape
|
||||
(:r1 shape)
|
||||
(-> (assoc :rx 0 :ry 0)
|
||||
(dissoc :r1 :r2 :r3 :r4))))]
|
||||
(st/emit! (dch/update-shapes ids-with-children radius-update)))))
|
||||
(if all-equal?
|
||||
(st/emit! (dch/update-shapes ids-with-children ctr/switch-to-radius-1))
|
||||
(reset! radius-multi? true))))
|
||||
|
||||
on-switch-to-radius-4
|
||||
(mf/use-callback
|
||||
(mf/deps ids)
|
||||
(fn [_value]
|
||||
(let [radius-update
|
||||
(fn [shape]
|
||||
(cond-> shape
|
||||
(:rx shape)
|
||||
(-> (assoc :r1 0 :r2 0 :r3 0 :r4 0)
|
||||
(dissoc :rx :ry))))]
|
||||
(st/emit! (dch/update-shapes ids-with-children radius-update)))))
|
||||
(st/emit! (dch/update-shapes ids-with-children ctr/switch-to-radius-4))
|
||||
(reset! radius-multi? false)))
|
||||
|
||||
on-radius-1-change
|
||||
(mf/use-callback
|
||||
(mf/deps ids)
|
||||
(fn [value]
|
||||
(let [radius-update
|
||||
(fn [shape]
|
||||
(cond-> shape
|
||||
(:r1 shape)
|
||||
(-> (dissoc :r1 :r2 :r3 :r4)
|
||||
(assoc :rx 0 :ry 0))
|
||||
(st/emit! (dch/update-shapes ids-with-children #(ctr/set-radius-1 % value)))))
|
||||
|
||||
(or (:rx shape) (:r1 shape))
|
||||
(assoc :rx value :ry value)))]
|
||||
|
||||
(st/emit! (dch/update-shapes ids-with-children radius-update)))))
|
||||
on-radius-multi-change
|
||||
(mf/use-callback
|
||||
(mf/deps ids)
|
||||
(fn [event]
|
||||
(let [value (-> event dom/get-target dom/get-value d/parse-integer)]
|
||||
(when (some? value)
|
||||
(st/emit! (dch/update-shapes ids-with-children ctr/switch-to-radius-1)
|
||||
(dch/update-shapes ids-with-children #(ctr/set-radius-1 % value)))
|
||||
(reset! radius-multi? false)))))
|
||||
|
||||
on-radius-4-change
|
||||
(mf/use-callback
|
||||
(mf/deps ids)
|
||||
(fn [value attr]
|
||||
(let [radius-update
|
||||
(fn [shape]
|
||||
(cond-> shape
|
||||
(:rx shape)
|
||||
(-> (dissoc :rx :rx)
|
||||
(assoc :r1 0 :r2 0 :r3 0 :r4 0))
|
||||
|
||||
(attr shape)
|
||||
(assoc attr value)))]
|
||||
|
||||
(st/emit! (dch/update-shapes ids-with-children radius-update)))))
|
||||
(st/emit! (dch/update-shapes ids-with-children #(ctr/set-radius-4 % attr value)))))
|
||||
|
||||
on-width-change #(on-size-change % :width)
|
||||
on-height-change #(on-size-change % :height)
|
||||
@@ -160,6 +147,16 @@
|
||||
|
||||
select-all #(-> % (dom/get-target) (.select))]
|
||||
|
||||
(mf/use-layout-effect
|
||||
(mf/deps radius-mode @radius-multi?)
|
||||
(fn []
|
||||
(when (and (= radius-mode :radius-1)
|
||||
(= @radius-multi? false))
|
||||
;; when going back from radius-multi to normal radius-1,
|
||||
;; restore focus to the newly created numeric-input
|
||||
(let [radius-input (mf/ref-val radius-input-ref)]
|
||||
(dom/focus! radius-input)))))
|
||||
|
||||
[:*
|
||||
[:div.element-set
|
||||
[:div.element-set-content
|
||||
@@ -168,7 +165,7 @@
|
||||
(when (options :size)
|
||||
[:div.row-flex
|
||||
[:span.element-set-subtitle (tr "workspace.options.size")]
|
||||
[:div.input-element.width
|
||||
[:div.input-element.width {:title (tr "workspace.options.width")}
|
||||
[:> numeric-input {:min 1
|
||||
:no-validate true
|
||||
:placeholder "--"
|
||||
@@ -176,7 +173,7 @@
|
||||
:on-change on-width-change
|
||||
:value (attr->string :width values)}]]
|
||||
|
||||
[:div.input-element.height
|
||||
[:div.input-element.height {:title (tr "workspace.options.height")}
|
||||
[:> numeric-input {:min 1
|
||||
:no-validate true
|
||||
:placeholder "--"
|
||||
@@ -196,13 +193,13 @@
|
||||
(when (options :position)
|
||||
[:div.row-flex
|
||||
[:span.element-set-subtitle (tr "workspace.options.position")]
|
||||
[:div.input-element.Xaxis
|
||||
[:div.input-element.Xaxis {:title (tr "workspace.options.x")}
|
||||
[:> numeric-input {:no-validate true
|
||||
:placeholder "--"
|
||||
:on-click select-all
|
||||
:on-change on-pos-x-change
|
||||
:value (attr->string :x values)}]]
|
||||
[:div.input-element.Yaxis
|
||||
[:div.input-element.Yaxis {:title (tr "workspace.options.y")}
|
||||
[:> numeric-input {:no-validate true
|
||||
:placeholder "--"
|
||||
:on-click select-all
|
||||
@@ -213,7 +210,7 @@
|
||||
(when (options :rotation)
|
||||
[:div.row-flex
|
||||
[:span.element-set-subtitle (tr "workspace.options.rotation")]
|
||||
[:div.input-element.degrees
|
||||
[:div.input-element.degrees {:title (tr "workspace.options.rotation")}
|
||||
[:> numeric-input
|
||||
{:no-validate true
|
||||
:min 0
|
||||
@@ -233,60 +230,72 @@
|
||||
:value (attr->string :rotation values)}]])
|
||||
|
||||
;; RADIUS
|
||||
(let [radius-1? (some? (:rx values))
|
||||
radius-4? (some? (:r1 values))]
|
||||
(when (and (options :radius) (or radius-1? radius-4?))
|
||||
(when (and (options :radius) (some? radius-mode))
|
||||
[:div.row-flex
|
||||
[:div.radius-options
|
||||
[:div.radius-icon.tooltip.tooltip-bottom
|
||||
{:class (dom/classnames
|
||||
:selected
|
||||
(and radius-1? (not radius-4?)))
|
||||
:selected (or (= radius-mode :radius-1) @radius-multi?))
|
||||
:alt (tr "workspace.options.radius.all-corners")
|
||||
:on-click on-switch-to-radius-1}
|
||||
i/radius-1]
|
||||
[:div.radius-icon.tooltip.tooltip-bottom
|
||||
{:class (dom/classnames
|
||||
:selected
|
||||
(and radius-4? (not radius-1?)))
|
||||
:selected (and (= radius-mode :radius-4) (not @radius-multi?)))
|
||||
:alt (tr "workspace.options.radius.single-corners")
|
||||
:on-click on-switch-to-radius-4}
|
||||
i/radius-4]]
|
||||
(if radius-1?
|
||||
[:div.input-element.mini
|
||||
|
||||
(cond
|
||||
(= radius-mode :radius-1)
|
||||
[:div.input-element.mini {:title (tr "workspace.options.radius")}
|
||||
[:> numeric-input
|
||||
{:placeholder "--"
|
||||
:ref radius-input-ref
|
||||
:min 0
|
||||
:on-click select-all
|
||||
:on-change on-radius-1-change
|
||||
:value (attr->string :rx values)}]]
|
||||
|
||||
@radius-multi?
|
||||
[:div.input-element.mini {:title (tr "workspace.options.radius")}
|
||||
[:input.input-text
|
||||
{:type "number"
|
||||
:placeholder "--"
|
||||
:on-click select-all
|
||||
:on-change on-radius-multi-change
|
||||
:value ""}]]
|
||||
|
||||
(= radius-mode :radius-4)
|
||||
[:*
|
||||
[:div.input-element.mini
|
||||
[:div.input-element.mini {:title (tr "workspace.options.radius")}
|
||||
[:> numeric-input
|
||||
{:placeholder "--"
|
||||
:min 0
|
||||
:on-click select-all
|
||||
:on-change on-radius-r1-change
|
||||
:value (attr->string :r1 values)}]]
|
||||
[:div.input-element.mini
|
||||
|
||||
[:div.input-element.mini {:title (tr "workspace.options.radius")}
|
||||
[:> numeric-input
|
||||
{:placeholder "--"
|
||||
:min 0
|
||||
:on-click select-all
|
||||
:on-change on-radius-r2-change
|
||||
:value (attr->string :r2 values)}]]
|
||||
[:div.input-element.mini
|
||||
|
||||
[:div.input-element.mini {:title (tr "workspace.options.radius")}
|
||||
[:> numeric-input
|
||||
{:placeholder "--"
|
||||
:min 0
|
||||
:on-click select-all
|
||||
:on-change on-radius-r3-change
|
||||
:value (attr->string :r3 values)}]]
|
||||
[:div.input-element.mini
|
||||
|
||||
[:div.input-element.mini {:title (tr "workspace.options.radius")}
|
||||
[:> numeric-input
|
||||
{:placeholder "--"
|
||||
:min 0
|
||||
:on-click select-all
|
||||
:on-change on-radius-r4-change
|
||||
:value (attr->string :r4 values)}]]])]))]]]))
|
||||
:value (attr->string :r4 values)}]]])])]]]))
|
||||
|
||||
@@ -146,7 +146,7 @@
|
||||
[:option {:value ":inner-shadow"} (tr "workspace.options.shadow-options.inner-shadow")]]]
|
||||
|
||||
[:div.row-grid-2
|
||||
[:div.input-element
|
||||
[:div.input-element {:title (tr "workspace.options.shadow-options.offsetx")}
|
||||
[:> numeric-input {:ref adv-offset-x-ref
|
||||
:no-validate true
|
||||
:placeholder "--"
|
||||
@@ -155,7 +155,7 @@
|
||||
:value (:offset-x value)}]
|
||||
[:span.after (tr "workspace.options.shadow-options.offsetx")]]
|
||||
|
||||
[:div.input-element
|
||||
[:div.input-element {:title (tr "workspace.options.shadow-options.offsety")}
|
||||
[:> numeric-input {:ref adv-offset-y-ref
|
||||
:no-validate true
|
||||
:placeholder "--"
|
||||
@@ -165,7 +165,7 @@
|
||||
[:span.after (tr "workspace.options.shadow-options.offsety")]]]
|
||||
|
||||
[:div.row-grid-2
|
||||
[:div.input-element
|
||||
[:div.input-element {:title (tr "workspace.options.shadow-options.blur")}
|
||||
[:> numeric-input {:ref adv-blur-ref
|
||||
:no-validate true
|
||||
:placeholder "--"
|
||||
@@ -175,7 +175,7 @@
|
||||
:value (:blur value)}]
|
||||
[:span.after (tr "workspace.options.shadow-options.blur")]]
|
||||
|
||||
[:div.input-element
|
||||
[:div.input-element {:title (tr "workspace.options.shadow-options.spread")}
|
||||
[:> numeric-input {:ref adv-spread-ref
|
||||
:no-validate true
|
||||
:placeholder "--"
|
||||
@@ -190,6 +190,7 @@
|
||||
;; Support for old format colors
|
||||
{:color (:color value) :opacity (:opacity value)}
|
||||
(:color value))
|
||||
:title (tr "workspace.options.shadow-options.color")
|
||||
:disable-gradient true
|
||||
:on-change (update-color index)
|
||||
:on-detach (detach-color index)
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
[app.common.pages.spec :as spec]
|
||||
[app.main.data.workspace.changes :as dch]
|
||||
[app.main.data.workspace.colors :as dc]
|
||||
[app.main.data.workspace.undo :as dwu]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.dropdown :refer [dropdown]]
|
||||
[app.main.ui.icons :as i]
|
||||
@@ -188,19 +187,7 @@
|
||||
|
||||
on-del-stroke
|
||||
(fn [_]
|
||||
(st/emit! (dch/update-shapes ids #(assoc % :stroke-style :none))))
|
||||
|
||||
on-open-picker
|
||||
(mf/use-callback
|
||||
(mf/deps ids)
|
||||
(fn [_value _opacity _id _file-id]
|
||||
(st/emit! (dwu/start-undo-transaction))))
|
||||
|
||||
on-close-picker
|
||||
(mf/use-callback
|
||||
(mf/deps ids)
|
||||
(fn [_value _opacity _id _file-id]
|
||||
(st/emit! (dwu/commit-undo-transaction))))]
|
||||
(st/emit! (dch/update-shapes ids #(assoc % :stroke-style :none))))]
|
||||
|
||||
(if show-options
|
||||
[:div.element-set
|
||||
@@ -211,15 +198,15 @@
|
||||
[:div.element-set-content
|
||||
;; Stroke Color
|
||||
[:& color-row {:color current-stroke-color
|
||||
:title (tr "workspace.options.stroke-color")
|
||||
:on-change handle-change-stroke-color
|
||||
:on-detach handle-detach
|
||||
:on-open on-open-picker
|
||||
:on-close on-close-picker}]
|
||||
:on-detach handle-detach}]
|
||||
|
||||
;; Stroke Width, Alignment & Style
|
||||
[:div.row-flex
|
||||
[:div.input-element
|
||||
{:class (dom/classnames :pixels (not= (:stroke-width values) :multiple))}
|
||||
{:class (dom/classnames :pixels (not= (:stroke-width values) :multiple))
|
||||
:title (tr "workspace.options.stroke-width")}
|
||||
[:input.input-text {:type "number"
|
||||
:min "0"
|
||||
:value (-> (:stroke-width values) width->string)
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
[:div.element-set-content
|
||||
[:& color-row {:disable-gradient true
|
||||
:disable-opacity true
|
||||
:title (tr "workspace.options.canvas-background")
|
||||
:color {:color (get options :background "#E8E9EA")
|
||||
:opacity 1}
|
||||
:on-change on-change
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
(if (= v :multiple) nil v))
|
||||
|
||||
(mf/defc color-row
|
||||
[{:keys [color disable-gradient disable-opacity on-change on-detach on-open on-close]}]
|
||||
[{:keys [color disable-gradient disable-opacity on-change on-detach on-open on-close title]}]
|
||||
(let [current-file-id (mf/use-ctx ctx/current-file-id)
|
||||
file-colors (mf/deref refs/workspace-file-colors)
|
||||
shared-libs (mf/deref refs/workspace-libraries)
|
||||
@@ -123,7 +123,7 @@
|
||||
(when (not= prev-color color)
|
||||
(modal/update-props! :colorpicker {:data (parse-color color)}))))
|
||||
|
||||
[:div.row-flex.color-data
|
||||
[:div.row-flex.color-data {:title title}
|
||||
[:& cb/color-bullet {:color color
|
||||
:on-click handle-click-color}]
|
||||
|
||||
|
||||