diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 9265e4c38f..55acb5a3cd 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,14 +1,14 @@
# Contributing Guide #
-Thank you for your interest in contributing to UXBox. This is a
-generic guide that details how to contribute to UXBox in a way that is
+Thank you for your interest in contributing to Penpot. This is a
+generic guide that details how to contribute to Penpot in a way that is
efficient for everyone. If you want a specific documentation for
different parts of the platform, please refer to `docs/` directory.
## Reporting Bugs ##
-We are using [GitHub Issues](https://github.com/uxbox/uxbox/issues)
+We are using [GitHub Issues](https://github.com/penpot/penpot/issues)
for our public bugs. We keep a close eye on this and try to make it
clear when we have an internal fix in progress. Before filing a new
task, try to make sure your problem doesn't already exist.
diff --git a/README.md b/README.md
index 9a050d668b..dff4de3838 100644
--- a/README.md
+++ b/README.md
@@ -6,14 +6,14 @@
[](https://tree.taiga.io/project/uxbox/ "Managed with Taiga.io")
-# UXBOX #
+# PENPOT #
-
+
## Introduction ##
-The open-source solution for design and prototyping. UXBOX is
+The open-source solution for design and prototyping. PENPOT is
currently at an early development stage but we are working hard to
bring you the beta version as soon as possible. Follow the project
progress in Twitter or Github and stay tuned!
@@ -23,8 +23,8 @@ progress in Twitter or Github and stay tuned!
## SVG based ##
-UXBOX works with SVG, a standard format, for all your designs and
-prototypes . This means that all your stuff in UXBOX is portable and
+Penpot works with SVG, a standard format, for all your designs and
+prototypes . This means that all your stuff in Penpot is portable and
editable in many other vector tools and easy to use on the web.
@@ -34,7 +34,7 @@ editable in many other vector tools and easy to use on the web.
We love the open source software community. Contributing is our
passion and because of this, we'll be glad if you want to participate
-and improve UXBOX. All your awesome ideas and code are welcome!
+and improve Penpot. All your awesome ideas and code are welcome!
Please refer to the [Contributing Guide](./CONTRIBUTING.md)
diff --git a/backend/resources/emails-mjml/invite-to-team/en.mjml b/backend/resources/emails-mjml/invite-to-team/en.mjml
new file mode 100644
index 0000000000..dbef7afe5f
--- /dev/null
+++ b/backend/resources/emails-mjml/invite-to-team/en.mjml
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hello!
+
+ {{invited-by}} has invited you to join the team “{{ team }}”.
+
+
+ Accept invite
+
+ Enjoy!
+ The UXBOX team.
+
+
+
+
+
+
+ UXBOX is the first Open Source prototyping platform that will be embraced by multidisciplinary teams.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ UXBOX © 2020 | Made with <3 and Open Source
+
+
+
+
+
+
diff --git a/backend/resources/emails/invite-to-team/en.html b/backend/resources/emails/invite-to-team/en.html
new file mode 100644
index 0000000000..3a77a7a6ec
--- /dev/null
+++ b/backend/resources/emails/invite-to-team/en.html
@@ -0,0 +1,468 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+ Hello!
+ |
+
+
+ |
+ {{invited-by}} has invited you to join the team “{{ team }}”.
+ |
+
+
+ |
+
+ |
+
+
+ |
+ Enjoy!
+ |
+
+
+ |
+ The UXBOX team.
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+ UXBOX is the first Open Source prototyping platform that will be embraced by multidisciplinary teams.
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+ UXBOX © 2020 | Made with <3 and Open Source
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/backend/resources/emails/invite-to-team/en.subj b/backend/resources/emails/invite-to-team/en.subj
new file mode 100644
index 0000000000..1ff46884ac
--- /dev/null
+++ b/backend/resources/emails/invite-to-team/en.subj
@@ -0,0 +1 @@
+Inviation to join {{team}}
\ No newline at end of file
diff --git a/backend/resources/emails/invite-to-team/en.txt b/backend/resources/emails/invite-to-team/en.txt
new file mode 100644
index 0000000000..c999019cc3
--- /dev/null
+++ b/backend/resources/emails/invite-to-team/en.txt
@@ -0,0 +1,10 @@
+Hello!
+
+{{invited-by}} has invited you to join the team “{{ team }}”.
+
+Accept invitation using this link:
+
+{{ public-uri }}/#/auth/verify-token?token={{token}}
+
+Enjoy!
+The UXBOX team.
diff --git a/backend/scripts/build.sh b/backend/scripts/build.sh
index 23f262aca7..8da54b34d4 100755
--- a/backend/scripts/build.sh
+++ b/backend/scripts/build.sh
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
CLASSPATH=`(clojure -Spath)`
-NEWCP="./resources:./app.jar"
+NEWCP="./resources:./main:./common"
rm -rf ./target/dist
mkdir -p ./target/dist/deps
@@ -15,10 +15,9 @@ for item in $(echo $CLASSPATH | tr ":" "\n"); do
done
cp ./resources/log4j2-bundle.xml ./target/dist/log4j2.xml
+cp -r ./src ./target/dist/main
+cp -r ../common ./target/dist/common
-clojure -Ajar
-
-cp ./target/app.jar ./target/dist/app.jar
echo $NEWCP > ./target/dist/classpath;
tee -a ./target/dist/run.sh >> /dev/null <
+;; This Source Code Form is "Incompatible With Secondary Licenses", as
+;; defined by the Mozilla Public License, v. 2.0.
+;;
+;; Copyright (c) 2020 UXBOX Labs SL
(ns app.db
(:require
- [clojure.spec.alpha :as s]
+ [app.common.exceptions :as ex]
+ [app.common.geom.point :as gpt]
+ [app.config :as cfg]
+ [app.metrics :as mtx]
+ [app.util.data :as data]
+ [app.util.time :as dt]
+ [app.util.transit :as t]
[clojure.data.json :as json]
+ [clojure.spec.alpha :as s]
[clojure.string :as str]
[clojure.tools.logging :as log]
[lambdaisland.uri :refer [uri]]
@@ -17,19 +27,17 @@
[next.jdbc.optional :as jdbc-opt]
[next.jdbc.result-set :as jdbc-rs]
[next.jdbc.sql :as jdbc-sql]
- [next.jdbc.sql.builder :as jdbc-bld]
- [app.common.exceptions :as ex]
- [app.config :as cfg]
- [app.metrics :as mtx]
- [app.util.time :as dt]
- [app.util.transit :as t]
- [app.util.data :as data])
+ [next.jdbc.sql.builder :as jdbc-bld])
(:import
- org.postgresql.util.PGobject
- org.postgresql.util.PGInterval
- com.zaxxer.hikari.metrics.prometheus.PrometheusMetricsTrackerFactory
com.zaxxer.hikari.HikariConfig
- com.zaxxer.hikari.HikariDataSource))
+ com.zaxxer.hikari.HikariDataSource
+ com.zaxxer.hikari.metrics.prometheus.PrometheusMetricsTrackerFactory
+ java.sql.Connection
+ java.sql.Savepoint
+ org.postgresql.jdbc.PgArray
+ org.postgresql.geometric.PGpoint
+ org.postgresql.util.PGInterval
+ org.postgresql.util.PGobject))
(def initsql
(str "SET statement_timeout = 10000;\n"
@@ -158,10 +166,42 @@
[v]
(instance? PGInterval v))
+(defn pgpoint?
+ [v]
+ (instance? PGpoint v))
+
+(defn pgarray?
+ [v]
+ (instance? PgArray v))
+
+(defn pgarray-of-uuid?
+ [v]
+ (and (pgarray? v) (= "uuid" (.getBaseTypeName ^PgArray v))))
+
+(defn pgpoint
+ [p]
+ (PGpoint. (:x p) (:y p)))
+
+(defn decode-pgpoint
+ [^PGpoint v]
+ (gpt/point (.-x v) (.-y v)))
+
(defn pginterval
[data]
(org.postgresql.util.PGInterval. ^String data))
+(defn savepoint
+ ([^Connection conn]
+ (.setSavepoint conn))
+ ([^Connection conn label]
+ (.setSavepoint conn (name label))))
+
+(defn rollback!
+ ([^Connection conn]
+ (.rollback conn))
+ ([^Connection conn ^Savepoint sp]
+ (.rollback conn sp)))
+
(defn interval
[data]
(cond
@@ -222,6 +262,14 @@
(.setType "jsonb")
(.setValue (json/write-str data))))
+(defn pgarray->set
+ [v]
+ (set (.getArray ^PgArray v)))
+
+(defn pgarray->vector
+ [v]
+ (vec (.getArray ^PgArray v)))
+
;; Instrumentation
(mtx/instrument-with-counter!
diff --git a/backend/src/app/emails.clj b/backend/src/app/emails.clj
index 95a96d13b6..9bef19b2ee 100644
--- a/backend/src/app/emails.clj
+++ b/backend/src/app/emails.clj
@@ -74,3 +74,16 @@
(def change-email
"Password change confirmation email"
(emails/build ::change-email default-context))
+
+(s/def :internal.emails.invite-to-team/invited-by ::us/string)
+(s/def :internal.emails.invite-to-team/team ::us/string)
+(s/def :internal.emails.invite-to-team/token ::us/string)
+
+(s/def ::invite-to-team
+ (s/keys :keys [:internal.emails.invite-to-team/invited-by
+ :internal.emails.invite-to-team/token
+ :internal.emails.invite-to-team/team]))
+
+(def invite-to-team
+ "Teams member invitation email."
+ (emails/build ::invite-to-team default-context))
diff --git a/backend/src/app/http/handlers.clj b/backend/src/app/http/handlers.clj
index f49e6da458..f859b7a362 100644
--- a/backend/src/app/http/handlers.clj
+++ b/backend/src/app/http/handlers.clj
@@ -20,7 +20,7 @@
#{:create-demo-profile
:logout
:profile
- :verify-profile-token
+ :verify-token
:recover-profile
:register-profile
:request-profile-recovery
@@ -34,8 +34,7 @@
{::sq/type type})
data (cond-> data
(:profile-id req) (assoc :profile-id (:profile-id req)))]
- (if (or (:profile-id req)
- (contains? unauthorized-services type))
+ (if (or (:profile-id req) (contains? unauthorized-services type))
{:status 200
:body (sq/handle (with-meta data {:req req}))}
{:status 403
@@ -51,18 +50,14 @@
{::sm/type type})
data (cond-> data
(:profile-id req) (assoc :profile-id (:profile-id req)))]
- (if (or (:profile-id req)
- (contains? unauthorized-services type))
- (let [body (sm/handle (with-meta data {:req req}))]
- (if (= type :delete-profile)
- (do
- (some-> (session/extract-auth-token req)
- (session/delete))
- {:status 204
- :cookies (session/cookies "" {:max-age -1})
- :body ""})
- {:status 200
- :body body}))
+ (if (or (:profile-id req) (contains? unauthorized-services type))
+ (let [result (sm/handle (with-meta data {:req req}))
+ mdata (meta result)
+ resp {:status (if (nil? (seq result)) 204 200)
+ :body result}]
+ (cond->> resp
+ (:transform-response mdata) ((:transform-response mdata) req)))
+
{:status 403
:body {:type :authentication
:code :unauthorized}})))
diff --git a/backend/src/app/http/session.clj b/backend/src/app/http/session.clj
index be3384364a..75fe605b50 100644
--- a/backend/src/app/http/session.clj
+++ b/backend/src/app/http/session.clj
@@ -7,6 +7,8 @@
;;
;; Copyright (c) 2020 UXBOX Labs SL
+;; TODO: move to services.
+
(ns app.http.session
(:require
[app.db :as db]
diff --git a/backend/src/app/media.clj b/backend/src/app/media.clj
index 6e0a77c368..91a59d611b 100644
--- a/backend/src/app/media.clj
+++ b/backend/src/app/media.clj
@@ -10,19 +10,19 @@
(ns app.media
"Media postprocessing."
(:require
+ [app.common.data :as d]
+ [app.common.exceptions :as ex]
+ [app.common.media :as cm]
+ [app.common.spec :as us]
+ [app.config :as cfg]
+ [app.media-storage :as mst]
+ [app.util.http :as http]
+ [app.util.storage :as ust]
[clojure.core.async :as a]
[clojure.java.io :as io]
[clojure.spec.alpha :as s]
[datoteka.core :as fs]
- [mount.core :refer [defstate]]
- [app.config :as cfg]
- [app.common.data :as d]
- [app.common.media :as cm]
- [app.common.exceptions :as ex]
- [app.common.spec :as us]
- [app.media-storage :as mst]
- [app.util.storage :as ust]
- [app.util.http :as http])
+ [mount.core :refer [defstate]])
(:import
java.io.ByteArrayInputStream
java.io.InputStream
@@ -34,6 +34,21 @@
(defstate semaphore
:start (Semaphore. (:image-process-max-threads cfg/config 1)))
+
+;; --- Generic specs
+
+(s/def :internal.http.upload/filename ::us/string)
+(s/def :internal.http.upload/size ::us/integer)
+(s/def :internal.http.upload/content-type cm/valid-media-types)
+(s/def :internal.http.upload/tempfile any?)
+
+(s/def ::upload
+ (s/keys :req-un [:internal.http.upload/filename
+ :internal.http.upload/size
+ :internal.http.upload/tempfile
+ :internal.http.upload/content-type]))
+
+
;; --- Thumbnails Generation
(s/def ::cmd keyword?)
diff --git a/backend/src/app/migrations.clj b/backend/src/app/migrations.clj
index 920ad7d277..54fa2a50da 100644
--- a/backend/src/app/migrations.clj
+++ b/backend/src/app/migrations.clj
@@ -98,6 +98,18 @@
{:name "0027-mod-file-table-ignore-sync"
:fn (mg/resource "app/migrations/sql/0027-mod-file-table-ignore-sync.sql")}
+
+ {:name "0028-add-team-project-profile-rel-table"
+ :fn (mg/resource "app/migrations/sql/0028-add-team-project-profile-rel-table.sql")}
+
+ {:name "0029-del-project-profile-rel-indexes"
+ :fn (mg/resource "app/migrations/sql/0029-del-project-profile-rel-indexes.sql")}
+
+ {:name "0030-mod-file-table-add-missing-index"
+ :fn (mg/resource "app/migrations/sql/0030-mod-file-table-add-missing-index.sql")}
+
+ {:name "0031-add-conversation-related-tables"
+ :fn (mg/resource "app/migrations/sql/0031-add-conversation-related-tables.sql")}
]})
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
diff --git a/backend/src/app/migrations/sql/0002-add-profile-tables.sql b/backend/src/app/migrations/sql/0002-add-profile-tables.sql
index 230a35a5b7..1e8e4d7491 100644
--- a/backend/src/app/migrations/sql/0002-add-profile-tables.sql
+++ b/backend/src/app/migrations/sql/0002-add-profile-tables.sql
@@ -78,7 +78,6 @@ VALUES ('00000000-0000-0000-0000-000000000000'::uuid,
true);
-
CREATE TABLE team_profile_rel (
team_id uuid NOT NULL REFERENCES team(id) ON DELETE CASCADE,
profile_id uuid NOT NULL REFERENCES profile(id) ON DELETE RESTRICT,
diff --git a/backend/src/app/migrations/sql/0003-add-project-tables.sql b/backend/src/app/migrations/sql/0003-add-project-tables.sql
index 9ba826010a..925f7c8b00 100644
--- a/backend/src/app/migrations/sql/0003-add-project-tables.sql
+++ b/backend/src/app/migrations/sql/0003-add-project-tables.sql
@@ -28,9 +28,6 @@ CREATE TABLE project_profile_rel (
PRIMARY KEY (profile_id, project_id)
);
-COMMENT ON TABLE project_profile_rel
- IS 'Relation between projects and profiles (NM)';
-
CREATE INDEX project_profile_rel__profile_id__idx
ON project_profile_rel(profile_id);
diff --git a/backend/src/app/migrations/sql/0028-add-team-project-profile-rel-table.sql b/backend/src/app/migrations/sql/0028-add-team-project-profile-rel-table.sql
new file mode 100644
index 0000000000..a2490b1cc4
--- /dev/null
+++ b/backend/src/app/migrations/sql/0028-add-team-project-profile-rel-table.sql
@@ -0,0 +1,12 @@
+CREATE TABLE team_project_profile_rel (
+ team_id uuid NOT NULL REFERENCES team(id) ON DELETE CASCADE,
+ profile_id uuid NOT NULL REFERENCES profile(id) ON DELETE CASCADE,
+ project_id uuid NOT NULL REFERENCES project(id) ON DELETE CASCADE,
+
+ created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
+ modified_at timestamptz NOT NULL DEFAULT clock_timestamp(),
+
+ is_pinned boolean NOT NULL DEFAULT false,
+
+ PRIMARY KEY (team_id, profile_id, project_id)
+);
diff --git a/backend/src/app/migrations/sql/0029-del-project-profile-rel-indexes.sql b/backend/src/app/migrations/sql/0029-del-project-profile-rel-indexes.sql
new file mode 100644
index 0000000000..7a35640aca
--- /dev/null
+++ b/backend/src/app/migrations/sql/0029-del-project-profile-rel-indexes.sql
@@ -0,0 +1,4 @@
+--- Drop duplicate indexes
+
+DROP INDEX IF EXISTS project_profile_rel__project_id__idx;
+DROP INDEX IF EXISTS project_profile_rel__profile_id__idx;
diff --git a/backend/src/app/migrations/sql/0030-mod-file-table-add-missing-index.sql b/backend/src/app/migrations/sql/0030-mod-file-table-add-missing-index.sql
new file mode 100644
index 0000000000..5c940bee7a
--- /dev/null
+++ b/backend/src/app/migrations/sql/0030-mod-file-table-add-missing-index.sql
@@ -0,0 +1 @@
+CREATE INDEX IF NOT EXISTS file__project_id__idx ON file (project_id);
diff --git a/backend/src/app/migrations/sql/0031-add-conversation-related-tables.sql b/backend/src/app/migrations/sql/0031-add-conversation-related-tables.sql
new file mode 100644
index 0000000000..0049f7fef1
--- /dev/null
+++ b/backend/src/app/migrations/sql/0031-add-conversation-related-tables.sql
@@ -0,0 +1,48 @@
+CREATE TABLE comment_thread (
+ id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
+ file_id uuid NOT NULL REFERENCES file(id) ON DELETE CASCADE,
+ owner_id uuid NOT NULL REFERENCES profile(id) ON DELETE CASCADE,
+
+ created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
+ modified_at timestamptz NOT NULL DEFAULT clock_timestamp(),
+
+ page_id uuid NOT NULL,
+
+ participants jsonb NOT NULL,
+ seqn integer NOT NULL DEFAULT 0,
+
+ position point NOT NULL,
+
+ is_resolved boolean NOT NULL DEFAULT false
+);
+
+CREATE INDEX comment_thread__owner_id__idx ON comment_thread(owner_id);
+CREATE UNIQUE INDEX comment_thread__file_id__seqn__idx ON comment_thread(file_id, seqn);
+
+CREATE TABLE comment_thread_status (
+ thread_id uuid NOT NULL REFERENCES comment_thread(id) ON DELETE CASCADE,
+ profile_id uuid NOT NULL REFERENCES profile(id) ON DELETE CASCADE,
+
+ created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
+ modified_at timestamptz NOT NULL DEFAULT clock_timestamp(),
+
+ PRIMARY KEY (thread_id, profile_id)
+);
+
+CREATE TABLE comment (
+ id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
+
+ thread_id uuid NOT NULL REFERENCES comment_thread(id) ON DELETE CASCADE,
+ owner_id uuid NOT NULL REFERENCES profile(id) ON DELETE CASCADE,
+
+ created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
+ modified_at timestamptz NOT NULL DEFAULT clock_timestamp(),
+
+ content text NOT NULL
+);
+
+CREATE INDEX comment__thread_id__idx ON comment(thread_id);
+CREATE INDEX comment__owner_id__idx ON comment(owner_id);
+
+ALTER TABLE file ADD COLUMN comment_thread_seqn integer DEFAULT 0;
+
diff --git a/backend/src/app/services/init.clj b/backend/src/app/services/init.clj
index 37edf7d4e8..4df4707edc 100644
--- a/backend/src/app/services/init.clj
+++ b/backend/src/app/services/init.clj
@@ -15,9 +15,9 @@
(defn- load-query-services
[]
(require 'app.services.queries.media)
- (require 'app.services.queries.colors)
(require 'app.services.queries.projects)
(require 'app.services.queries.files)
+ (require 'app.services.queries.comments)
(require 'app.services.queries.profile)
(require 'app.services.queries.recent-files)
(require 'app.services.queries.viewer))
@@ -26,11 +26,12 @@
[]
(require 'app.services.mutations.demo)
(require 'app.services.mutations.media)
- (require 'app.services.mutations.colors)
(require 'app.services.mutations.projects)
(require 'app.services.mutations.files)
+ (require 'app.services.mutations.comments)
(require 'app.services.mutations.profile)
- (require 'app.services.mutations.viewer))
+ (require 'app.services.mutations.viewer)
+ (require 'app.services.mutations.verify-token))
(defstate query-services
:start (load-query-services))
diff --git a/backend/src/app/services/mutations/colors.clj b/backend/src/app/services/mutations/colors.clj
deleted file mode 100644
index 1fbb2dd4fa..0000000000
--- a/backend/src/app/services/mutations/colors.clj
+++ /dev/null
@@ -1,150 +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/.
-;;
-;; This Source Code Form is "Incompatible With Secondary Licenses", as
-;; defined by the Mozilla Public License, v. 2.0.
-;;
-;; Copyright (c) 2020 UXBOX Labs SL
-
-(ns app.services.mutations.colors
- (:require
- [clojure.spec.alpha :as s]
- [app.common.exceptions :as ex]
- [app.common.spec :as us]
- [app.common.uuid :as uuid]
- [app.config :as cfg]
- [app.db :as db]
- [app.services.mutations :as sm]
- [app.services.queries.teams :as teams]
- [app.tasks :as tasks]
- [app.util.time :as dt]))
-
-;; --- Helpers & Specs
-
-(s/def ::id ::us/uuid)
-(s/def ::name ::us/string)
-(s/def ::profile-id ::us/uuid)
-(s/def ::team-id ::us/uuid)
-(s/def ::library-id ::us/uuid)
-(s/def ::content ::us/string)
-
-;; --- Mutation: Create Color
-
-(declare select-file-for-update)
-(declare create-color)
-
-(s/def ::create-color
- (s/keys :req-un [::profile-id ::name ::content ::file-id]
- :opt-un [::id]))
-
-(sm/defmutation ::create-color
- [{:keys [profile-id file-id] :as params}]
- (db/with-atomic [conn db/pool]
- (let [file (select-file-for-update conn file-id)]
- (teams/check-edition-permissions! conn profile-id (:team-id file))
- (create-color conn params))))
-
-(def ^:private sql:create-color
- "insert into color (id, name, file_id, content)
- values ($1, $2, $3, $4) returning *")
-
-(defn create-color
- [conn {:keys [id name file-id content]}]
- (let [id (or id (uuid/next))]
- (db/insert! conn :color {:id id
- :name name
- :file-id file-id
- :content content})))
-
-(def ^:private sql:select-file-for-update
- "select file.*,
- project.team_id as team_id
- from file
- inner join project on (project.id = file.project_id)
- where file.id = ?
- for update of file")
-
-(defn- select-file-for-update
- [conn id]
- (let [row (db/exec-one! conn [sql:select-file-for-update id])]
- (when-not row
- (ex/raise :type :not-found))
- row))
-
-
-;; --- Mutation: Rename Color
-
-(declare select-color-for-update)
-
-(s/def ::rename-color
- (s/keys :req-un [::id ::profile-id ::name]))
-
-(sm/defmutation ::rename-color
- [{:keys [id profile-id name] :as params}]
- (db/with-atomic [conn db/pool]
- (let [clr (select-color-for-update conn id)]
- (teams/check-edition-permissions! conn profile-id (:team-id clr))
- (db/update! conn :color
- {:name name}
- {:id id}))))
-
-(def ^:private sql:select-color-for-update
- "select c.*,
- p.team_id as team_id
- from color as c
- inner join file as f on f.id = c.file_id
- inner join project as p on p.id = f.project_id
- where c.id = ?
- for update of c")
-
-(defn- select-color-for-update
- [conn id]
- (let [row (db/exec-one! conn [sql:select-color-for-update id])]
- (when-not row
- (ex/raise :type :not-found))
- row))
-
-
-;; --- Mutation: Update Color
-
-(s/def ::update-color
- (s/keys :req-un [::profile-id ::id ::content]))
-
-(sm/defmutation ::update-color
- [{:keys [profile-id id content] :as params}]
- (db/with-atomic [conn db/pool]
- (let [clr (select-color-for-update conn id)
- ;; IMPORTANT: if the previous name was equal to the hex content,
- ;; we must rename it in addition to changing the value.
- new-name (if (= (:name clr) (:content clr))
- content
- (:name clr))]
- (teams/check-edition-permissions! conn profile-id (:team-id clr))
- (db/update! conn :color
- {:name new-name
- :content content}
- {:id id}))))
-
-;; --- Delete Color
-
-(declare delete-color)
-
-(s/def ::delete-color
- (s/keys :req-un [::id ::profile-id]))
-
-(sm/defmutation ::delete-color
- [{:keys [profile-id id] :as params}]
- (db/with-atomic [conn db/pool]
- (let [clr (select-color-for-update conn id)]
- (teams/check-edition-permissions! conn profile-id (:team-id clr))
-
- ;; Schedule object deletion
- (tasks/submit! conn {:name "delete-object"
- :delay cfg/default-deletion-delay
- :props {:id id :type :color}})
-
- (db/update! conn :color
- {:deleted-at (dt/now)}
- {:id id})
- nil)))
diff --git a/backend/src/app/services/mutations/comments.clj b/backend/src/app/services/mutations/comments.clj
new file mode 100644
index 0000000000..af798bb073
--- /dev/null
+++ b/backend/src/app/services/mutations/comments.clj
@@ -0,0 +1,260 @@
+;; 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/.
+;;
+;; This Source Code Form is "Incompatible With Secondary Licenses", as
+;; defined by the Mozilla Public License, v. 2.0.
+;;
+;; Copyright (c) 2020 UXBOX Labs SL
+
+(ns app.services.mutations.comments
+ (:require
+ [clojure.spec.alpha :as s]
+ [app.common.exceptions :as ex]
+ [app.common.spec :as us]
+ [app.common.uuid :as uuid]
+ [app.config :as cfg]
+ [app.db :as db]
+ [app.services.mutations :as sm]
+ [app.services.queries.projects :as proj]
+ [app.services.queries.files :as files]
+ [app.services.queries.comments :as comments]
+ [app.tasks :as tasks]
+ [app.util.blob :as blob]
+ [app.util.storage :as ust]
+ [app.util.transit :as t]
+ [app.util.time :as dt]))
+
+;; --- Mutation: Create Comment Thread
+
+(declare upsert-comment-thread-status!)
+(declare create-comment-thread)
+
+(s/def ::file-id ::us/uuid)
+(s/def ::profile-id ::us/uuid)
+(s/def ::position ::us/point)
+(s/def ::content ::us/string)
+(s/def ::page-id ::us/uuid)
+
+(s/def ::create-comment-thread
+ (s/keys :req-un [::profile-id ::file-id ::position ::content ::page-id]))
+
+(sm/defmutation ::create-comment-thread
+ [{:keys [profile-id file-id] :as params}]
+ (db/with-atomic [conn db/pool]
+ (files/check-read-permissions! conn profile-id file-id)
+ (create-comment-thread conn params)))
+
+(defn- retrieve-next-seqn
+ [conn file-id]
+ (let [sql "select (f.comment_thread_seqn + 1) as next_seqn from file as f where f.id = ?"
+ res (db/exec-one! conn [sql file-id])]
+ (:next-seqn res)))
+
+(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)
+
+ thread (db/insert! conn :comment-thread
+ {:file-id file-id
+ :owner-id profile-id
+ :participants (db/tjson #{profile-id})
+ :page-id page-id
+ :created-at now
+ :modified-at now
+ :seqn seqn
+ :position (db/pgpoint position)})
+ ;; Create a comment entry
+ comment (db/insert! conn :comment
+ {:thread-id (:id thread)
+ :owner-id profile-id
+ :created-at now
+ :modified-at now
+ :content content})]
+
+ ;; Make the current thread as read.
+ (upsert-comment-thread-status! conn profile-id (:id thread))
+
+ ;; Optimistic update of current seq number on file.
+ (db/update! conn :file
+ {:comment-thread-seqn seqn}
+ {:id file-id})
+
+ (-> (assoc thread
+ :content content
+ :comment comment)
+ (comments/decode-row))))
+
+(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))))
+
+
+;; --- Mutation: Update Comment Thread Status
+
+(s/def ::id ::us/uuid)
+
+(s/def ::update-comment-thread-status
+ (s/keys :req-un [::profile-id ::id]))
+
+(sm/defmutation ::update-comment-thread-status
+ [{:keys [profile-id id] :as params}]
+ (db/with-atomic [conn db/pool]
+ (let [cthr (db/get-by-id conn :comment-thread id {:for-update true})]
+ (when-not cthr
+ (ex/raise :type :not-found))
+
+ (files/check-read-permissions! conn profile-id (:file-id cthr))
+ (upsert-comment-thread-status! conn profile-id (:id cthr)))))
+
+(def sql:upsert-comment-thread-status
+ "insert into comment_thread_status (thread_id, profile_id)
+ values (?, ?)
+ on conflict (thread_id, profile_id)
+ do update set modified_at = clock_timestamp()
+ returning modified_at;")
+
+(defn- upsert-comment-thread-status!
+ [conn profile-id thread-id]
+ (db/exec-one! conn [sql:upsert-comment-thread-status thread-id profile-id]))
+
+
+;; --- Mutation: Update Comment Thread
+
+(s/def ::is-resolved ::us/boolean)
+(s/def ::update-comment-thread
+ (s/keys :req-un [::profile-id ::id ::is-resolved]))
+
+(sm/defmutation ::update-comment-thread
+ [{:keys [profile-id id is-resolved] :as params}]
+ (db/with-atomic [conn db/pool]
+ (let [thread (db/get-by-id conn :comment-thread id {:for-update true})]
+ (when-not thread
+ (ex/raise :type :not-found)
+
+ (files/check-read-permissions! conn profile-id (:file-id thread))
+
+ (db/update! conn :comment-thread
+ {:is-resolved is-resolved}
+ {:id id})
+ nil))))
+
+
+;; --- Mutation: Add Comment
+
+(s/def ::add-comment
+ (s/keys :req-un [::profile-id ::thread-id ::content]))
+
+(sm/defmutation ::add-comment
+ [{:keys [profile-id thread-id content] :as params}]
+ (db/with-atomic [conn db/pool]
+ (let [thread (-> (db/get-by-id conn :comment-thread thread-id {:for-update true})
+ (comments/decode-row))]
+
+ ;; Standard Checks
+ (when-not thread
+ (ex/raise :type :not-found))
+
+ (files/check-read-permissions! conn profile-id (:file-id thread))
+
+ ;; NOTE: is important that all timestamptz related fields are
+ ;; created or updated on the database level for avoid clock
+ ;; inconsistencies (some user sees something read that is not
+ ;; read, etc...)
+ (let [ppants (:participants thread #{})
+ comment (db/insert! conn :comment
+ {:thread-id thread-id
+ :owner-id profile-id
+ :content content})]
+
+ ;; NOTE: this is done in SQL instead of using db/update!
+ ;; helper bacause currently the helper does not allow pass raw
+ ;; function call parameters to the underlying prepared
+ ;; statement; in a future when we fix/improve it, this can be
+ ;; changed to use the helper.
+
+ ;; Update thread modified-at attribute and assoc the current
+ ;; profile to the participant set.
+ (let [ppants (conj ppants profile-id)
+ sql "update comment_thread
+ set modified_at = clock_timestamp(),
+ participants = ?
+ where id = ?"]
+ (db/exec-one! conn [sql (db/tjson ppants) thread-id]))
+
+ ;; Update the current profile status in relation to the
+ ;; current thread.
+ (upsert-comment-thread-status! conn profile-id thread-id)
+
+ ;; Return the created comment object.
+ comment))))
+
+
+;; --- Mutation: Update Comment
+
+(s/def ::update-comment
+ (s/keys :req-un [::profile-id ::id ::content]))
+
+(sm/defmutation ::update-comment
+ [{:keys [profile-id id content] :as params}]
+ (db/with-atomic [conn db/pool]
+ (let [comment (db/get-by-id conn :comment id {:for-update true})
+ _ (when-not comment (ex/raise :type :not-found))
+ thread (db/get-by-id conn :comment-thread (:thread-id comment) {:for-update true})
+ _ (when-not thread (ex/raise :type :not-found))]
+
+ (files/check-read-permissions! conn profile-id (:file-id thread))
+ (db/update! conn :comment
+ {:content content
+ :modified-at (dt/now)}
+ {:id (:id comment)})
+ (db/update! conn :comment-thread
+ {:modified-at (dt/now)}
+ {:id (:id thread)})
+ nil)))
+
+
+;; --- Mutation: Delete Comment Thread
+
+(s/def ::delete-comment-thread
+ (s/keys :req-un [::profile-id ::id]))
+
+(sm/defmutation ::delete-comment-thread
+ [{:keys [profile-id id] :as params}]
+ (db/with-atomic [conn db/pool]
+ (let [cthr (db/get-by-id conn :comment-thread id {:for-update true})]
+ (when-not (= (:owner-id cthr) profile-id)
+ (ex/raise :type :validation
+ :code :not-allowed))
+ (db/delete! conn :comment-thread {:id id})
+ nil)))
+
+;; --- Mutation: Delete comment
+
+(s/def ::delete-comment
+ (s/keys :req-un [::profile-id ::id]))
+
+(sm/defmutation ::delete-comment
+ [{:keys [profile-id id] :as params}]
+ (db/with-atomic [conn db/pool]
+ (let [comment (db/get-by-id conn :comment id {:for-update true})]
+ (when-not (= (:owner-id comment) profile-id)
+ (ex/raise :type :validation
+ :code :not-allowed))
+
+ (db/delete! conn :comment {:id id}))))
diff --git a/backend/src/app/services/mutations/files.clj b/backend/src/app/services/mutations/files.clj
index d3db468845..0246d58f4e 100644
--- a/backend/src/app/services/mutations/files.clj
+++ b/backend/src/app/services/mutations/files.clj
@@ -21,7 +21,7 @@
[app.db :as db]
[app.redis :as redis]
[app.services.mutations :as sm]
- [app.services.mutations.projects :as proj]
+ [app.services.queries.projects :as proj]
[app.services.queries.files :as files]
[app.tasks :as tasks]
[app.util.blob :as blob]
@@ -49,6 +49,7 @@
(sm/defmutation ::create-file
[{:keys [profile-id project-id] :as params}]
(db/with-atomic [conn db/pool]
+ (proj/check-edition-permissions! conn profile-id project-id)
(create-file conn params)))
(defn- create-file-profile
@@ -241,10 +242,14 @@
;; File changes that affect to the library, and must be notified
;; to all clients using it.
-(def library-changes
- #{:add-color :mod-color :del-color
- :add-media :mod-media :del-media
- :add-component :mod-component :del-component})
+(defn library-change?
+ [change]
+ (or (#{:add-color :mod-color :del-color
+ :add-media :mod-media :del-media
+ :add-component :mod-component :del-component
+ :add-typography :mod-typography :del-typography} (:type change))
+ (and (= (:type change) :mod-obj)
+ (some? (:component-id change)))))
(declare update-file)
(declare retrieve-lagged-changes)
@@ -285,12 +290,12 @@
:revn (:revn file)
:changes changes}
- library-changes (filter #(library-changes (:type %)) changes)]
+ library-changes (filter library-change? changes)]
@(redis/run! :publish {:channel (str (:id file))
:message (t/encode-str msg)})
- (if (and (:is-shared file) (seq library-changes))
+ (when (and (:is-shared file) (seq library-changes))
(let [{:keys [team-id] :as project}
(db/get-by-id conn :project (:project-id file))
@@ -299,7 +304,7 @@
:file-id (:id file)
:session-id sid
:revn (:revn file)
- :modified-at (:modified-at file)
+ :modified-at (dt/now)
:changes library-changes}]
@(redis/run! :publish {:channel (str team-id)
diff --git a/backend/src/app/services/mutations/media.clj b/backend/src/app/services/mutations/media.clj
index f26bc7eded..1495656d3a 100644
--- a/backend/src/app/services/mutations/media.clj
+++ b/backend/src/app/services/mutations/media.clj
@@ -46,19 +46,7 @@
(declare persist-media-object-on-fs)
(declare persist-media-thumbnail-on-fs)
-(s/def :app$upload/filename ::us/string)
-(s/def :app$upload/size ::us/integer)
-(s/def :app$upload/content-type cm/valid-media-types)
-(s/def :app$upload/tempfile any?)
-
-(s/def ::upload
- (s/keys :req-un [:app$upload/filename
- :app$upload/size
- :app$upload/tempfile
- :app$upload/content-type]))
-
-(s/def ::content ::upload)
-
+(s/def ::content ::media/upload)
(s/def ::is-local ::us/boolean)
(s/def ::add-media-object-from-url
diff --git a/backend/src/app/services/mutations/profile.clj b/backend/src/app/services/mutations/profile.clj
index ee8217979e..a2f587cac3 100644
--- a/backend/src/app/services/mutations/profile.clj
+++ b/backend/src/app/services/mutations/profile.clj
@@ -18,32 +18,30 @@
[app.emails :as emails]
[app.media :as media]
[app.media-storage :as mst]
+ [app.http.session :as session]
[app.services.mutations :as sm]
- [app.services.mutations.media :as media-mutations]
[app.services.mutations.projects :as projects]
[app.services.mutations.teams :as teams]
[app.services.queries.profile :as profile]
[app.services.tokens :as tokens]
+ [app.services.mutations.verify-token :refer [process-token]]
[app.tasks :as tasks]
[app.util.blob :as blob]
[app.util.storage :as ust]
[app.util.time :as dt]
- [buddy.core.codecs :as bc]
- [buddy.core.nonce :as bn]
[buddy.hashers :as hashers]
[clojure.spec.alpha :as s]
- [cuerdas.core :as str]
- [datoteka.core :as fs]))
+ [cuerdas.core :as str]))
;; --- Helpers & Specs
(s/def ::email ::us/email)
-(s/def ::fullname ::us/string)
-(s/def ::lang ::us/string)
+(s/def ::fullname ::us/not-empty-string)
+(s/def ::lang ::us/not-empty-string)
(s/def ::path ::us/string)
(s/def ::profile-id ::us/uuid)
-(s/def ::password ::us/string)
-(s/def ::old-password ::us/string)
+(s/def ::password ::us/not-empty-string)
+(s/def ::old-password ::us/not-empty-string)
(s/def ::theme ::us/string)
;; --- Mutation: Register Profile
@@ -51,22 +49,15 @@
(declare check-profile-existence!)
(declare create-profile)
(declare create-profile-relations)
+(declare email-domain-in-whitelist?)
+(s/def ::token ::us/not-empty-string)
(s/def ::register-profile
- (s/keys :req-un [::email ::password ::fullname]))
-
-(defn email-domain-in-whitelist?
- "Returns true if email's domain is in the given whitelist or if given
- whitelist is an empty string."
- [whitelist email]
- (if (str/blank? whitelist)
- true
- (let [domains (str/split whitelist #",\s*")
- email-domain (second (str/split email #"@"))]
- (contains? (set domains) email-domain))))
+ (s/keys :req-un [::email ::password ::fullname]
+ :opt-un [::token]))
(sm/defmutation ::register-profile
- [params]
+ [{:keys [token] :as params}]
(when-not (:registration-enabled cfg/config)
(ex/raise :type :restriction
:code :registration-disabled))
@@ -79,25 +70,68 @@
(db/with-atomic [conn db/pool]
(check-profile-existence! conn params)
(let [profile (->> (create-profile conn params)
- (create-profile-relations conn))
- token (tokens/generate
- {:iss :verify-email
- :exp (dt/in-future "48h")
- :profile-id (:id profile)
- :email (:email profile)})]
+ (create-profile-relations conn))]
- (emails/send! conn emails/register
- {:to (:email profile)
- :name (:fullname profile)
- :token token})
- profile)))
+ (if token
+ ;; If token comes in params, this is because the user comes
+ ;; from team-invitation process; in this case we revalidate
+ ;; the token and process the token claims again with the new
+ ;; profile data.
+ (let [claims (tokens/verify token {:iss :team-invitation})
+ claims (assoc claims :member-id (:id profile))
+ params (assoc params :profile-id (:id profile))]
+ (process-token conn params claims)
+
+ ;; Automatically mark the created profile as active because
+ ;; we already have the verification of email with the
+ ;; team-invitation token.
+ (db/update! conn :profile
+ {:is-active true}
+ {:id (:id profile)})
+
+ ;; Return profile data and create http session for
+ ;; automatically login the profile.
+ (with-meta (assoc profile
+ :is-active true
+ :claims claims)
+ {:transform-response
+ (fn [request response]
+ (let [uagent (get-in request [:headers "user-agent"])
+ id (session/create (:id profile) uagent)]
+ (assoc response
+ :cookies (session/cookies id))))}))
+
+ ;; If no token is provided, send a verification email
+ (let [token (tokens/generate
+ {:iss :verify-email
+ :exp (dt/in-future "48h")
+ :profile-id (:id profile)
+ :email (:email profile)})]
+
+ (emails/send! conn emails/register
+ {:to (:email profile)
+ :name (:fullname profile)
+ :token token})
+
+ profile)))))
+
+
+(defn email-domain-in-whitelist?
+ "Returns true if email's domain is in the given whitelist or if given
+ whitelist is an empty string."
+ [whitelist email]
+ (if (str/blank? whitelist)
+ true
+ (let [domains (str/split whitelist #",\s*")
+ email-domain (second (str/split email #"@"))]
+ (contains? (set domains) email-domain))))
(def ^:private sql:profile-existence
"select exists (select * from profile
where email = ?
and deleted_at is null) as val")
-(defn- check-profile-existence!
+(defn check-profile-existence!
[conn {:keys [email] :as params}]
(let [email (str/lower email)
result (db/exec-one! conn [sql:profile-existence email])]
@@ -151,8 +185,6 @@
;; --- Mutation: Login
-(declare retrieve-profile-by-email)
-
(s/def ::email ::us/email)
(s/def ::scope ::us/string)
@@ -181,22 +213,12 @@
profile)]
(db/with-atomic [conn db/pool]
- (let [prof (-> (retrieve-profile-by-email conn email)
+ (let [prof (-> (profile/retrieve-profile-data-by-email conn email)
(validate-profile)
(profile/strip-private-attrs))
addt (profile/retrieve-additional-data conn (:id prof))]
(merge prof addt)))))
-(def sql:profile-by-email
- "select * from profile
- where email=?
- and deleted_at is null")
-
-(defn- retrieve-profile-by-email
- [conn email]
- (let [email (str/lower email)]
- (db/exec-one! conn [sql:profile-by-email email])))
-
;; --- Mutation: Register if not exists
@@ -221,7 +243,7 @@
(create-profile-relations conn)))]
(db/with-atomic [conn db/pool]
- (let [profile (retrieve-profile-by-email conn email)
+ (let [profile (profile/retrieve-profile-data-by-email conn email)
profile (if profile
(populate-additional-data conn profile)
(register-profile conn params))]
@@ -272,10 +294,9 @@
;; --- Mutation: Update Photo
-(declare upload-photo)
(declare update-profile-photo)
-(s/def ::file ::media-mutations/upload)
+(s/def ::file ::media/upload)
(s/def ::update-profile-photo
(s/keys :req-un [::profile-id ::file]))
@@ -286,7 +307,7 @@
(let [profile (profile/retrieve-profile conn profile-id)
_ (media/run {:cmd :info :input {:path (:tempfile file)
:mtype (:content-type file)}})
- photo (upload-photo conn params)]
+ photo (teams/upload-photo conn params)]
;; Schedule deletion of old photo
(when (and (string? (:photo profile))
@@ -296,22 +317,6 @@
;; Save new photo
(update-profile-photo conn profile-id photo))))
-(defn- upload-photo
- [conn {:keys [file profile-id]}]
- (let [prefix (-> (bn/random-bytes 8)
- (bc/bytes->b64u)
- (bc/bytes->str))
- thumb (media/run
- {:cmd :profile-thumbnail
- :format :jpeg
- :quality 85
- :width 256
- :height 256
- :input {:path (fs/path (:tempfile file))
- :mtype (:content-type file)}})
- name (str prefix (cm/format->extension (:format thumb)))]
- (ust/save! mst/media-storage name (:data thumb))))
-
(defn- update-profile-photo
[conn profile-id path]
(db/update! conn :profile
@@ -345,63 +350,10 @@
:token token})
nil)))
-(defn- select-profile-for-update
+(defn select-profile-for-update
[conn id]
(db/get-by-id conn :profile id {:for-update true}))
-
-;; --- Mutation: Verify Profile Token
-
-;; Generic mutation for perform token based verification for auth
-;; domain.
-
-(defmulti process-token (fn [conn claims] (:iss claims)))
-
-(s/def ::verify-profile-token
- (s/keys :req-un [::token]))
-
-(sm/defmutation ::verify-profile-token
- [{:keys [token] :as params}]
- (db/with-atomic [conn db/pool]
- (let [claims (tokens/verify token)]
- (process-token conn claims))))
-
-(defmethod process-token :change-email
- [conn {:keys [profile-id email] :as claims}]
- (let [profile (select-profile-for-update conn profile-id)]
- (check-profile-existence! conn {:email email})
- (db/update! conn :profile
- {:email email}
- {:id profile-id})
- claims))
-
-(defmethod process-token :verify-email
- [conn {:keys [profile-id] :as claims}]
- (let [profile (select-profile-for-update conn profile-id)]
- (when (:is-active profile)
- (ex/raise :type :validation
- :code :email-already-validated))
- (when (not= (:email profile)
- (:email claims))
- (ex/raise :type :validation
- :code :invalid-token))
-
- (db/update! conn :profile
- {:is-active true}
- {:id (:id profile)})
- claims))
-
-(defmethod process-token :auth
- [conn {:keys [profile-id] :as claims}]
- (let [profile (profile/retrieve-profile conn profile-id)]
- (assoc claims :profile profile)))
-
-(defmethod process-token :default
- [conn claims]
- (ex/raise :type :validation
- :code :invalid-token))
-
-
;; --- Mutation: Request Profile Recovery
(s/def ::request-profile-recovery
@@ -424,7 +376,7 @@
(db/with-atomic [conn db/pool]
(some->> email
- (retrieve-profile-by-email conn)
+ (profile/retrieve-profile-data-by-email conn)
(create-recovery-token conn)
(send-email-notification conn))
nil)))
@@ -473,7 +425,14 @@
(db/update! conn :profile
{:deleted-at (dt/now)}
{:id profile-id})
- nil))
+
+ (with-meta {}
+ {:transform-response
+ (fn [request response]
+ (some-> (session/extract-auth-token request)
+ (session/delete))
+ (assoc response
+ :cookies (session/cookies "" {:max-age -1})))})))
(def ^:private sql:teams-ownership-check
"with teams as (
diff --git a/backend/src/app/services/mutations/projects.clj b/backend/src/app/services/mutations/projects.clj
index 1b7d42b384..bb36b89776 100644
--- a/backend/src/app/services/mutations/projects.clj
+++ b/backend/src/app/services/mutations/projects.clj
@@ -16,6 +16,7 @@
[app.config :as cfg]
[app.db :as db]
[app.services.mutations :as sm]
+ [app.services.queries.projects :as proj]
[app.tasks :as tasks]
[app.util.blob :as blob]))
@@ -25,42 +26,12 @@
(s/def ::name ::us/string)
(s/def ::profile-id ::us/uuid)
-;; --- Permissions Checks
-
-(def ^:private sql:project-permissions
- "select tpr.is_owner,
- tpr.is_admin,
- tpr.can_edit
- from team_profile_rel as tpr
- inner join project as p on (p.team_id = tpr.team_id)
- where p.id = ?
- and tpr.profile_id = ?
- union all
- select ppr.is_owner,
- ppr.is_admin,
- ppr.can_edit
- from project_profile_rel as ppr
- where ppr.project_id = ?
- and ppr.profile_id = ?")
-
-(defn check-edition-permissions!
- [conn profile-id project-id]
- (let [rows (db/exec! conn [sql:project-permissions
- project-id profile-id
- project-id profile-id])]
- (when (empty? rows)
- (ex/raise :type :not-found))
- (when-not (or (some :can-edit rows)
- (some :is-admin rows)
- (some :is-owner rows))
- (ex/raise :type :validation
- :code :not-authorized))))
-
;; --- Mutation: Create Project
(declare create-project)
(declare create-project-profile)
+(declare create-team-project-profile)
(s/def ::team-id ::us/uuid)
(s/def ::create-project
@@ -70,9 +41,11 @@
(sm/defmutation ::create-project
[params]
(db/with-atomic [conn db/pool]
- (let [proj (create-project conn params)]
- (create-project-profile conn (assoc params :project-id (:id proj)))
- proj)))
+ (let [proj (create-project conn params)
+ params (assoc params :project-id (:id proj))]
+ (create-project-profile conn params)
+ (create-team-project-profile conn params)
+ (assoc proj :is-pinned true))))
(defn create-project
[conn {:keys [id profile-id team-id name default?] :as params}]
@@ -93,6 +66,35 @@
:is-admin true
:can-edit true}))
+(defn create-team-project-profile
+ [conn {:keys [team-id project-id profile-id] :as params}]
+ (db/insert! conn :team-project-profile-rel
+ {:project-id project-id
+ :profile-id profile-id
+ :team-id team-id
+ :is-pinned true}))
+
+
+;; --- Mutation: Toggle Project Pin
+
+(def ^:private
+ sql:update-project-pin
+ "insert into team_project_profile_rel (team_id, project_id, profile_id, is_pinned)
+ values (?, ?, ?, ?)
+ on conflict (team_id, project_id, profile_id)
+ do update set is_pinned=?")
+
+(s/def ::is-pinned ::us/boolean)
+(s/def ::project-id ::us/uuid)
+
+(s/def ::update-project-pin
+ (s/keys :req-un [::profile-id ::id ::team-id ::is-pinned]))
+
+(sm/defmutation ::update-project-pin
+ [{:keys [id profile-id team-id is-pinned] :as params}]
+ (db/with-atomic [conn db/pool]
+ (db/exec-one! conn [sql:update-project-pin team-id id profile-id is-pinned is-pinned])
+ nil))
;; --- Mutation: Rename Project
@@ -106,7 +108,7 @@
[{:keys [id profile-id name] :as params}]
(db/with-atomic [conn db/pool]
(let [project (db/get-by-id conn :project id {:for-update true})]
- (check-edition-permissions! conn profile-id id)
+ (proj/check-edition-permissions! conn profile-id id)
(db/update! conn :project
{:name name}
{:id id}))))
@@ -121,7 +123,7 @@
(sm/defmutation ::delete-project
[{:keys [id profile-id] :as params}]
(db/with-atomic [conn db/pool]
- (check-edition-permissions! conn profile-id id)
+ (proj/check-edition-permissions! conn profile-id id)
;; Schedule object deletion
(tasks/submit! conn {:name "delete-object"
diff --git a/backend/src/app/services/mutations/teams.clj b/backend/src/app/services/mutations/teams.clj
index 614d2496d3..305b702753 100644
--- a/backend/src/app/services/mutations/teams.clj
+++ b/backend/src/app/services/mutations/teams.clj
@@ -9,13 +9,28 @@
(ns app.services.mutations.teams
(:require
- [clojure.spec.alpha :as s]
+ [app.common.data :as d]
[app.common.exceptions :as ex]
+ [app.common.media :as cm]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.db :as db]
+ [app.emails :as emails]
+ [app.media :as media]
+ [app.media-storage :as mst]
[app.services.mutations :as sm]
- [app.util.blob :as blob]))
+ [app.services.mutations.projects :as projects]
+ [app.services.queries.teams :as teams]
+ [app.services.tokens :as tokens]
+ [app.services.queries.profile :as profile]
+ [app.tasks :as tasks]
+ [app.util.storage :as ust]
+ [app.util.time :as dt]
+ [buddy.core.codecs :as bc]
+ [buddy.core.nonce :as bn]
+ [clojure.spec.alpha :as s]
+ [cuerdas.core :as str]
+ [datoteka.core :as fs]))
;; --- Helpers & Specs
@@ -27,6 +42,7 @@
(declare create-team)
(declare create-team-profile)
+(declare create-team-default-project)
(s/def ::create-team
(s/keys :req-un [::profile-id ::name]
@@ -35,8 +51,10 @@
(sm/defmutation ::create-team
[params]
(db/with-atomic [conn db/pool]
- (let [team (create-team conn params)]
- (create-team-profile conn (assoc params :team-id (:id team)))
+ (let [team (create-team conn params)
+ params (assoc params :team-id (:id team))]
+ (create-team-profile conn params)
+ (create-team-default-project conn params)
team)))
(defn create-team
@@ -57,3 +75,244 @@
:is-owner true
:is-admin true
:can-edit true}))
+
+(defn create-team-default-project
+ [conn {:keys [team-id profile-id] :as params}]
+ (let [proj (projects/create-project conn {:team-id team-id
+ :name "Drafts"
+ :default? true})]
+ (projects/create-project-profile conn {:project-id (:id proj)
+ :profile-id profile-id})))
+
+
+;; --- Mutation: Update Team
+
+(s/def ::update-team
+ (s/keys :req-un [::profile-id ::name ::id]))
+
+(sm/defmutation ::update-team
+ [{:keys [id name profile-id] :as params}]
+ (db/with-atomic [conn db/pool]
+ (teams/check-edition-permissions! conn profile-id id)
+ (db/update! conn :team
+ {:name name}
+ {:id id})
+ nil))
+
+
+;; --- Mutation: Leave Team
+
+(s/def ::leave-team
+ (s/keys :req-un [::profile-id ::id]))
+
+(sm/defmutation ::leave-team
+ [{:keys [id profile-id] :as params}]
+ (db/with-atomic [conn db/pool]
+ (let [perms (teams/check-read-permissions! conn profile-id id)
+ members (teams/retrieve-team-members conn id)]
+
+ (when (:is-owner perms)
+ (ex/raise :type :validation
+ :code :owner-cant-leave-team
+ :hint "reasing owner before leave"))
+
+ (when-not (> (count members) 1)
+ (ex/raise :type :validation
+ :code :cant-leave-team
+ :context {:members (count members)}))
+
+ (db/delete! conn :team-profile-rel
+ {:profile-id profile-id
+ :team-id id})
+
+ nil)))
+
+
+;; --- Mutation: Delete Team
+
+(s/def ::delete-team
+ (s/keys :req-un [::profile-id ::id]))
+
+(sm/defmutation ::delete-team
+ [{:keys [id profile-id] :as params}]
+ (db/with-atomic [conn db/pool]
+ (let [perms (teams/check-edition-permissions! conn profile-id id)]
+ (when-not (:is-owner perms)
+ (ex/raise :type :validation
+ :code :only-owner-can-delete-team))
+
+ (db/delete! conn :team {:id id})
+ nil)))
+
+;; --- Mutation: Tean Update Role
+
+(declare retrieve-team-member)
+(declare role->params)
+
+(s/def ::team-id ::us/uuid)
+(s/def ::member-id ::us/uuid)
+(s/def ::role #{:owner :admin :editor :viewer})
+
+(s/def ::update-team-member-role
+ (s/keys :req-un [::profile-id ::team-id ::member-id ::role]))
+
+(sm/defmutation ::update-team-member-role
+ [{:keys [team-id profile-id member-id role] :as params}]
+ (db/with-atomic [conn db/pool]
+ (let [perms (teams/check-read-permissions! conn profile-id team-id)
+
+ ;; We retrieve all team members instead of query the
+ ;; database for a single member. This is just for
+ ;; convenience, if this bocomes a bottleneck or problematic,
+ ;; we will change it to more efficient fetch mechanims.
+ members (teams/retrieve-team-members conn team-id)
+ member (d/seek #(= member-id (:id %)) members)]
+
+ ;; If no member is found, just 404
+ (when-not member
+ (ex/raise :type :not-found
+ :code :member-does-not-exist))
+
+ ;; First check if we have permissions to change roles
+ (when-not (or (:is-owner perms)
+ (:is-admin perms))
+ (ex/raise :type :validation
+ :code :insufficient-permissions))
+
+ ;; Don't allow change role of owner member
+ (when (:is-owner member)
+ (ex/raise :type :validation
+ :code :cant-change-role-to-owner))
+
+ ;; Don't allow promote to owner to admin users.
+ (when (and (= role :owner)
+ (not (:is-owner perms)))
+ (ex/raise :type :validation
+ :code :cant-promote-to-owner))
+
+ (let [params (role->params role)]
+ ;; Only allow single owner on team
+ (when (and (= role :owner)
+ (:is-owner perms))
+ (db/update! conn :team-profile-rel
+ {:is-owner false}
+ {:team-id team-id
+ :profile-id profile-id}))
+
+ (db/update! conn :team-profile-rel params
+ {:team-id team-id
+ :profile-id member-id})
+ nil))))
+
+(defn role->params
+ [role]
+ (case role
+ :admin {:is-owner false :is-admin true :can-edit true}
+ :editor {:is-owner false :is-admin false :can-edit true}
+ :owner {:is-owner true :is-admin true :can-edit true}
+ :viewer {:is-owner false :is-admin false :can-edit false}))
+
+
+;; --- Mutation: Team Update Role
+
+(s/def ::delete-team-member
+ (s/keys :req-un [::profile-id ::team-id ::member-id]))
+
+(sm/defmutation ::delete-team-member
+ [{:keys [team-id profile-id member-id] :as params}]
+ (db/with-atomic [conn db/pool]
+ (let [perms (teams/check-read-permissions! conn profile-id team-id)]
+ (when-not (or (:is-owner perms)
+ (:is-admin perms))
+ (ex/raise :type :validation
+ :code :insufficient-permissions))
+
+ (when (= member-id profile-id)
+ (ex/raise :type :validation
+ :code :cant-remove-yourself))
+
+ (db/delete! conn :team-profile-rel {:profile-id member-id
+ :team-id team-id})
+
+ nil)))
+
+
+;; --- Mutation: Update Team Photo
+
+(declare upload-photo)
+
+(s/def ::file ::media/upload)
+(s/def ::update-team-photo
+ (s/keys :req-un [::profile-id ::team-id ::file]))
+
+(sm/defmutation ::update-team-photo
+ [{:keys [profile-id file team-id] :as params}]
+ (media/validate-media-type (:content-type file))
+ (db/with-atomic [conn db/pool]
+ (teams/check-edition-permissions! conn profile-id team-id)
+ (let [team (teams/retrieve-team conn profile-id team-id)
+ _ (media/run {:cmd :info :input {:path (:tempfile file)
+ :mtype (:content-type file)}})
+ photo (upload-photo conn params)]
+
+ ;; Schedule deletion of old photo
+ (when (and (string? (:photo team))
+ (not (str/blank? (:photo team))))
+ (tasks/submit! conn {:name "remove-media"
+ :props {:path (:photo team)}}))
+ ;; Save new photo
+ (db/update! conn :team
+ {:photo (str photo)}
+ {:id team-id})
+
+ (assoc team :photo (str photo)))))
+
+(defn upload-photo
+ [conn {:keys [file profile-id]}]
+ (let [prefix (-> (bn/random-bytes 8)
+ (bc/bytes->b64u)
+ (bc/bytes->str))
+ thumb (media/run
+ {:cmd :profile-thumbnail
+ :format :jpeg
+ :quality 85
+ :width 256
+ :height 256
+ :input {:path (fs/path (:tempfile file))
+ :mtype (:content-type file)}})
+ name (str prefix (cm/format->extension (:format thumb)))]
+ (ust/save! mst/media-storage name (:data thumb))))
+
+
+;; --- Mutation: Invite Member
+
+(s/def ::email ::us/email)
+(s/def ::invite-team-member
+ (s/keys :req-un [::profile-id ::team-id ::email ::role]))
+
+(sm/defmutation ::invite-team-member
+ [{:keys [profile-id team-id email role] :as params}]
+ (db/with-atomic [conn db/pool]
+ (let [perms (teams/check-edition-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)
+ token (tokens/generate
+ {:iss :team-invitation
+ :exp (dt/in-future "24h")
+ :profile-id (:id profile)
+ :role role
+ :team-id team-id
+ :member-email (:email member email)
+ :member-id (:id member)})]
+
+ (when-not (:is-admin perms)
+ (ex/raise :type :validation
+ :code :insufficient-permissions))
+
+ (emails/send! conn emails/invite-to-team
+ {:to email
+ :invited-by (:fullname profile)
+ :team (:name team)
+ :token token})
+ nil)))
diff --git a/backend/src/app/services/mutations/verify_token.clj b/backend/src/app/services/mutations/verify_token.clj
new file mode 100644
index 0000000000..638a048794
--- /dev/null
+++ b/backend/src/app/services/mutations/verify_token.clj
@@ -0,0 +1,143 @@
+;; 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/.
+;;
+;; This Source Code Form is "Incompatible With Secondary Licenses", as
+;; defined by the Mozilla Public License, v. 2.0.
+;;
+;; Copyright (c) 2020 UXBOX Labs SL
+
+(ns app.services.mutations.verify-token
+ (:require
+ [app.common.exceptions :as ex]
+ [app.common.media :as cm]
+ [app.common.spec :as us]
+ [app.common.uuid :as uuid]
+ [app.config :as cfg]
+ [app.db :as db]
+ [app.emails :as emails]
+ [app.http.session :as session]
+ [app.media :as media]
+ [app.media-storage :as mst]
+ [app.services.mutations :as sm]
+ [app.services.mutations.teams :as teams]
+ [app.services.queries.profile :as profile]
+ [app.services.tokens :as tokens]
+ [app.tasks :as tasks]
+ [app.util.blob :as blob]
+ [app.util.storage :as ust]
+ [app.util.time :as dt]
+ [buddy.hashers :as hashers]
+ [clojure.spec.alpha :as s]
+ [cuerdas.core :as str]))
+
+(defmulti process-token (fn [conn params claims] (:iss claims)))
+
+(s/def ::verify-token
+ (s/keys :req-un [::token]
+ :opt-un [::profile-id]))
+
+(sm/defmutation ::verify-token
+ [{:keys [token] :as params}]
+ (db/with-atomic [conn db/pool]
+ (let [claims (tokens/verify token)]
+ (process-token conn params claims))))
+
+(defmethod process-token :change-email
+ [conn params {:keys [profile-id email] :as claims}]
+ (let [profile (db/get-by-id conn :profile profile-id {:for-update true})]
+ (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))
+
+(defmethod process-token :verify-email
+ [conn params {:keys [profile-id] :as claims}]
+ (let [profile (db/get-by-id conn :profile profile-id {:for-update true})]
+ (when (:is-active profile)
+ (ex/raise :type :validation
+ :code :email-already-validated))
+ (when (not= (:email profile)
+ (:email claims))
+ (ex/raise :type :validation
+ :code :invalid-token))
+
+ (db/update! conn :profile
+ {:is-active true}
+ {:id (:id profile)})
+ claims))
+
+(defmethod process-token :auth
+ [conn params {:keys [profile-id] :as claims}]
+ (let [profile (profile/retrieve-profile conn profile-id)]
+ (assoc claims :profile profile)))
+
+
+;; --- Team Invitation
+
+(s/def ::iss keyword?)
+(s/def ::exp ::us/inst)
+
+(s/def :internal.tokens.team-invitation/profile-id ::us/uuid)
+(s/def :internal.tokens.team-invitation/role ::us/keyword)
+(s/def :internal.tokens.team-invitation/team-id ::us/uuid)
+(s/def :internal.tokens.team-invitation/member-email ::us/email)
+(s/def :internal.tokens.team-invitation/member-id (s/nilable ::us/uuid))
+
+(s/def ::team-invitation-claims
+ (s/keys :req-un [::iss ::exp
+ :internal.tokens.team-invitation/profile-id
+ :internal.tokens.team-invitation/role
+ :internal.tokens.team-invitation/team-id
+ :internal.tokens.team-invitation/member-email]
+ :opt-un [:internal.tokens.team-invitation/member-id]))
+
+(defmethod process-token :team-invitation
+ [conn {:keys [profile-id token]} {:keys [member-id team-id role] :as claims}]
+ (us/assert ::team-invitation-claims claims)
+ (if (uuid? member-id)
+ (let [params (merge {:team-id team-id
+ :profile-id member-id}
+ (teams/role->params role))
+ claims (assoc claims :state :created)]
+ (db/insert! conn :team-profile-rel params)
+ (if (and (uuid? profile-id)
+ (= member-id profile-id))
+ ;; If the current session is already matches the invited
+ ;; member, then just return the token and leave the frontend
+ ;; app redirect to correct team.
+ claims
+
+ ;; If the session does not matches the invited member id,
+ ;; replace the session with a new one matching the invited
+ ;; member. This techinique should be considered secure because
+ ;; the user clicking the link he already has access to the
+ ;; email account.
+ (with-meta claims
+ {:transform-response
+ (fn [request response]
+ (let [uagent (get-in request [:headers "user-agent"])
+ id (session/create member-id uagent)]
+ (assoc response
+ :cookies (session/cookies id))))})))
+
+ ;; In this case, we waint until frontend app redirect user to
+ ;; registeration page, the user is correctly registered and the
+ ;; register mutation call us again with the same token to finally
+ ;; create the corresponding team-profile relation from the first
+ ;; condition of this if.
+ (assoc claims
+ :token token
+ :state :pending)))
+
+
+;; --- Default
+
+(defmethod process-token :default
+ [conn params claims]
+ (ex/raise :type :validation
+ :code :invalid-token))
+
diff --git a/backend/src/app/services/queries/colors.clj b/backend/src/app/services/queries/colors.clj
deleted file mode 100644
index 881cff34b6..0000000000
--- a/backend/src/app/services/queries/colors.clj
+++ /dev/null
@@ -1,104 +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/.
-;;
-;; This Source Code Form is "Incompatible With Secondary Licenses", as
-;; defined by the Mozilla Public License, v. 2.0.
-;;
-;; Copyright (c) 2019 Andrey Antukh
-
-(ns app.services.queries.colors
- (:require
- [clojure.spec.alpha :as s]
- [promesa.core :as p]
- [promesa.exec :as px]
- [app.common.exceptions :as ex]
- [app.common.spec :as us]
- [app.common.uuid :as uuid]
- [app.db :as db]
- [app.services.queries :as sq]
- [app.services.queries.teams :as teams]
- [app.util.blob :as blob]
- [app.util.data :as data]))
-
-;; --- Helpers & Specs
-
-(s/def ::id ::us/uuid)
-(s/def ::profile-id ::us/uuid)
-(s/def ::team-id ::us/uuid)
-(s/def ::file-id ::us/uuid)
-
-
-;; --- Query: Colors (by file)
-
-(declare retrieve-colors)
-(declare retrieve-file)
-
-(s/def ::colors
- (s/keys :req-un [::profile-id ::file-id]))
-
-(sq/defquery ::colors
- [{:keys [profile-id file-id] :as params}]
- (db/with-atomic [conn db/pool]
- (let [file (retrieve-file conn file-id)]
- (teams/check-read-permissions! conn profile-id (:team-id file))
- (retrieve-colors conn file-id))))
-
-(def ^:private sql:colors
- "select *
- from color
- where color.deleted_at is null
- and color.file_id = ?
- order by created_at desc")
-
-(defn- retrieve-colors
- [conn file-id]
- (db/exec! conn [sql:colors file-id]))
-
-(def ^:private sql:retrieve-file
- "select file.*,
- project.team_id as team_id
- from file
- inner join project on (project.id = file.project_id)
- where file.id = ?")
-
-(defn- retrieve-file
- [conn id]
- (let [row (db/exec-one! conn [sql:retrieve-file id])]
- (when-not row
- (ex/raise :type :not-found))
- row))
-
-
-;; --- Query: Color (by ID)
-
-(declare retrieve-color)
-
-(s/def ::id ::us/uuid)
-(s/def ::color
- (s/keys :req-un [::profile-id ::id]))
-
-(sq/defquery ::color
- [{:keys [profile-id id] :as params}]
- (db/with-atomic [conn db/pool]
- (let [color (retrieve-color conn id)]
- (teams/check-read-permissions! conn profile-id (:team-id color))
- color)))
-
-(def ^:private sql:single-color
- "select color.*,
- p.team_id as team_id
- from color as color
- inner join file as f on (color.file_id = f.id)
- inner join project as p on (p.id = f.project_id)
- where color.deleted_at is null
- and color.id = ?
- order by created_at desc")
-
-(defn retrieve-color
- [conn id]
- (let [row (db/exec-one! conn [sql:single-color id])]
- (when-not row
- (ex/raise :type :not-found))
- row))
-
diff --git a/backend/src/app/services/queries/comments.clj b/backend/src/app/services/queries/comments.clj
new file mode 100644
index 0000000000..5b000b2123
--- /dev/null
+++ b/backend/src/app/services/queries/comments.clj
@@ -0,0 +1,109 @@
+;; 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/.
+;;
+;; This Source Code Form is "Incompatible With Secondary Licenses", as
+;; defined by the Mozilla Public License, v. 2.0.
+;;
+;; Copyright (c) 2020 UXBOX Labs SL
+
+(ns app.services.queries.comments
+ (:require
+ [app.common.exceptions :as ex]
+ [app.common.spec :as us]
+ [app.common.uuid :as uuid]
+ [app.config :as cfg]
+ [app.db :as db]
+ [app.services.queries :as sq]
+ [app.services.queries.files :as files]
+ [app.util.time :as dt]
+ [app.util.transit :as t]
+ [clojure.spec.alpha :as s]
+ [datoteka.core :as fs]
+ [promesa.core :as p]))
+
+(defn decode-row
+ [{:keys [participants position] :as row}]
+ (cond-> row
+ (db/pgpoint? position) (assoc :position (db/decode-pgpoint position))
+ (db/pgobject? participants) (assoc :participants (db/decode-transit-pgobject participants))))
+
+;; --- Query: Comment Threads
+
+(declare retrieve-comment-threads)
+
+(s/def ::file-id ::us/uuid)
+(s/def ::comment-threads
+ (s/keys :req-un [::profile-id ::file-id]))
+
+(sq/defquery ::comment-threads
+ [{:keys [profile-id file-id] :as params}]
+ (with-open [conn (db/open)]
+ (files/check-read-permissions! conn profile-id file-id)
+ (retrieve-comment-threads conn params)))
+
+(def sql:comment-threads
+ "select distinct on (ct.id)
+ ct.*,
+ first_value(c.content) over w as content,
+ (select count(1)
+ from comment as c
+ where c.thread_id = ct.id) as count_comments,
+ (select count(1)
+ from comment as c
+ where c.thread_id = ct.id
+ and c.created_at >= coalesce(cts.modified_at, ct.created_at)) as count_unread_comments
+ from comment_thread as ct
+ inner join comment as c on (c.thread_id = ct.id)
+ left join comment_thread_status as cts
+ on (cts.thread_id = ct.id and
+ cts.profile_id = ?)
+ where ct.file_id = ?
+ window w as (partition by c.thread_id order by c.created_at asc)")
+
+(defn- retrieve-comment-threads
+ [conn {:keys [profile-id file-id]}]
+ (->> (db/exec! conn [sql:comment-threads profile-id file-id])
+ (into [] (map decode-row))))
+
+;; --- Query: Single Comment Thread
+
+(s/def ::id ::us/uuid)
+(s/def ::comment-thread
+ (s/keys :req-un [::profile-id ::file-id ::id]))
+
+(sq/defquery ::comment-thread
+ [{:keys [profile-id file-id id] :as params}]
+ (with-open [conn (db/open)]
+ (files/check-read-permissions! conn profile-id file-id)
+ (let [sql (str "with threads as (" sql:comment-threads ")"
+ "select * from threads where id = ?")]
+ (-> (db/exec-one! conn [sql profile-id file-id id])
+ (decode-row)))))
+
+
+;; --- Query: Comments
+
+(declare retrieve-comments)
+
+(s/def ::file-id ::us/uuid)
+(s/def ::thread-id ::us/uuid)
+(s/def ::comments
+ (s/keys :req-un [::profile-id ::thread-id]))
+
+(sq/defquery ::comments
+ [{:keys [profile-id thread-id] :as params}]
+ (with-open [conn (db/open)]
+ (let [thread (db/get-by-id conn :comment-thread thread-id)]
+ (files/check-read-permissions! conn profile-id (:file-id thread))
+ (retrieve-comments conn thread-id))))
+
+(def sql:comments
+ "select c.* from comment as c
+ where c.thread_id = ?
+ order by c.created_at asc")
+
+(defn- retrieve-comments
+ [conn thread-id]
+ (->> (db/exec! conn [sql:comments thread-id])
+ (into [] (map decode-row))))
diff --git a/backend/src/app/services/queries/files.clj b/backend/src/app/services/queries/files.clj
index 61665dff72..0aa6b134d2 100644
--- a/backend/src/app/services/queries/files.clj
+++ b/backend/src/app/services/queries/files.clj
@@ -33,6 +33,61 @@
(s/def ::team-id ::us/uuid)
(s/def ::search-term ::us/string)
+
+;; --- Query: File Permissions
+
+(def ^:private sql:file-permissions
+ "select fpr.is_owner,
+ fpr.is_admin,
+ fpr.can_edit
+ from file_profile_rel as fpr
+ where fpr.file_id = ?
+ and fpr.profile_id = ?
+ union all
+ select tpr.is_owner,
+ tpr.is_admin,
+ tpr.can_edit
+ from team_profile_rel as tpr
+ inner join project as p on (p.team_id = tpr.team_id)
+ inner join file as f on (p.id = f.project_id)
+ where f.id = ?
+ and tpr.profile_id = ?
+ union all
+ select ppr.is_owner,
+ ppr.is_admin,
+ ppr.can_edit
+ from project_profile_rel as ppr
+ inner join file as f on (f.project_id = ppr.project_id)
+ where f.id = ?
+ and ppr.profile_id = ?")
+
+(defn check-edition-permissions!
+ [conn profile-id file-id]
+ (let [rows (db/exec! conn [sql:file-permissions
+ file-id profile-id
+ file-id profile-id
+ file-id profile-id])]
+ (when (empty? rows)
+ (ex/raise :type :not-found))
+
+ (when-not (or (some :can-edit rows)
+ (some :is-admin rows)
+ (some :is-owner rows))
+ (ex/raise :type :validation
+ :code :not-authorized))))
+
+
+(defn check-read-permissions!
+ [conn profile-id file-id]
+ (let [rows (db/exec! conn [sql:file-permissions
+ file-id profile-id
+ file-id profile-id
+ file-id profile-id])]
+ (when-not (seq rows)
+ (ex/raise :type :validation
+ :code :not-authorized))))
+
+
;; --- Query: Files search
;; TODO: this query need to a good refactor
@@ -99,52 +154,8 @@
(sq/defquery ::files
[{:keys [profile-id project-id] :as params}]
(with-open [conn (db/open)]
- (let [project (db/get-by-id conn :project project-id)]
- (projects/check-edition-permissions! conn profile-id project)
- (into [] decode-row-xf (db/exec! conn [sql:files project-id])))))
-
-
-;; --- Query: File Permissions
-
-(def ^:private sql:file-permissions
- "select fpr.is_owner,
- fpr.is_admin,
- fpr.can_edit
- from file_profile_rel as fpr
- where fpr.file_id = ?
- and fpr.profile_id = ?
- union all
- select tpr.is_owner,
- tpr.is_admin,
- tpr.can_edit
- from team_profile_rel as tpr
- inner join project as p on (p.team_id = tpr.team_id)
- inner join file as f on (p.id = f.project_id)
- where f.id = ?
- and tpr.profile_id = ?
- union all
- select ppr.is_owner,
- ppr.is_admin,
- ppr.can_edit
- from project_profile_rel as ppr
- inner join file as f on (f.project_id = ppr.project_id)
- where f.id = ?
- and ppr.profile_id = ?")
-
-(defn check-edition-permissions!
- [conn profile-id file-id]
- (let [rows (db/exec! conn [sql:file-permissions
- file-id profile-id
- file-id profile-id
- file-id profile-id])]
- (when (empty? rows)
- (ex/raise :type :not-found))
-
- (when-not (or (some :can-edit rows)
- (some :is-admin rows)
- (some :is-owner rows))
- (ex/raise :type :validation
- :code :not-authorized))))
+ (projects/check-read-permissions! conn profile-id project-id)
+ (into [] decode-row-xf (db/exec! conn [sql:files project-id]))))
;; --- Query: File (By ID)
diff --git a/backend/src/app/services/queries/profile.clj b/backend/src/app/services/queries/profile.clj
index 5ffd10cef6..29b64583e1 100644
--- a/backend/src/app/services/queries/profile.clj
+++ b/backend/src/app/services/queries/profile.clj
@@ -2,11 +2,15 @@
;; 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) 2016 Andrey Antukh
+;; This Source Code Form is "Incompatible With Secondary Licenses", as
+;; defined by the Mozilla Public License, v. 2.0.
+;;
+;; Copyright (c) 2020 UXBOX Labs SL
(ns app.services.queries.profile
(:require
[clojure.spec.alpha :as s]
+ [cuerdas.core :as str]
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.db :as db]
@@ -53,16 +57,16 @@
(def ^:private sql:default-team-and-project
"select t.id
from team as t
- inner join team_profile_rel as tpr on (tpr.team_id = t.id)
- where tpr.profile_id = ?
- and tpr.is_owner is true
+ inner join team_profile_rel as tp on (tp.team_id = t.id)
+ where tp.profile_id = ?
+ and tp.is_owner is true
and t.is_default is true
union all
select p.id
from project as p
- inner join project_profile_rel as tpr on (tpr.project_id = p.id)
- where tpr.profile_id = ?
- and tpr.is_owner is true
+ inner join project_profile_rel as tp on (tp.project_id = p.id)
+ where tp.profile_id = ?
+ and tp.is_owner is true
and p.is_default is true")
(defn retrieve-additional-data
@@ -87,6 +91,18 @@
profile))
+
+(def sql:profile-by-email
+ "select * from profile
+ where email=?
+ and deleted_at is null")
+
+(defn retrieve-profile-data-by-email
+ [conn email]
+ (let [email (str/lower email)]
+ (db/exec-one! conn [sql:profile-by-email email])))
+
+
;; --- Attrs Helpers
(defn strip-private-attrs
diff --git a/backend/src/app/services/queries/projects.clj b/backend/src/app/services/queries/projects.clj
index 688549d38a..892a122b53 100644
--- a/backend/src/app/services/queries/projects.clj
+++ b/backend/src/app/services/queries/projects.clj
@@ -18,41 +18,46 @@
;; --- Check Project Permissions
-;; This SQL checks if the: (1) project is part of the team where the
-;; profile has edition permissions or (2) the profile has direct
-;; edition access granted to this project.
-
-(def sql:project-permissions
- "select tp.can_edit,
- tp.is_admin,
- tp.is_owner
- from team_profile_rel as tp
- where tp.profile_id = ?
- and tp.team_id = ?
- union
- select pp.can_edit,
- pp.is_admin,
- pp.is_owner
- from project_profile_rel as pp
- where pp.profile_id = ?
- and pp.project_id = ?;")
+(def ^:private sql:project-permissions
+ "select tpr.is_owner,
+ tpr.is_admin,
+ tpr.can_edit
+ from team_profile_rel as tpr
+ inner join project as p on (p.team_id = tpr.team_id)
+ where p.id = ?
+ and tpr.profile_id = ?
+ union all
+ select ppr.is_owner,
+ ppr.is_admin,
+ ppr.can_edit
+ from project_profile_rel as ppr
+ where ppr.project_id = ?
+ and ppr.profile_id = ?")
(defn check-edition-permissions!
- [conn profile-id project]
+ [conn profile-id project-id]
(let [rows (db/exec! conn [sql:project-permissions
- profile-id
- (:team-id project)
- profile-id
- (:id project)])]
+ project-id profile-id
+ project-id profile-id])]
(when (empty? rows)
(ex/raise :type :not-found))
-
(when-not (or (some :can-edit rows)
(some :is-admin rows)
(some :is-owner rows))
(ex/raise :type :validation
:code :not-authorized))))
+(defn check-read-permissions!
+ [conn profile-id project-id]
+ (let [rows (db/exec! conn [sql:project-permissions
+ project-id profile-id
+ project-id profile-id])]
+
+ (when-not (seq rows)
+ (ex/raise :type :validation
+ :code :not-authorized))))
+
+
;; --- Query: Projects
@@ -60,7 +65,6 @@
(s/def ::team-id ::us/uuid)
(s/def ::profile-id ::us/uuid)
-
(s/def ::projects
(s/keys :req-un [::profile-id ::team-id]))
@@ -68,31 +72,37 @@
[{:keys [profile-id team-id]}]
(with-open [conn (db/open)]
(teams/check-read-permissions! conn profile-id team-id)
- (retrieve-projects conn team-id)))
+ (retrieve-projects conn profile-id team-id)))
(def sql:projects
"select p.*,
+ coalesce(tpp.is_pinned, false) as is_pinned,
(select count(*) from file as f
- where f.project_id = p.id
- and deleted_at is null)
+ where f.project_id = p.id
+ and deleted_at is null) as count
from project as p
+ 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
order by p.modified_at desc")
(defn retrieve-projects
- [conn team-id]
- (db/exec! conn [sql:projects team-id]))
+ [conn profile-id team-id]
+ (db/exec! conn [sql:projects profile-id team-id]))
-;; --- Query: Projec by ID
-(s/def ::project-id ::us/uuid)
-(s/def ::project-by-id
- (s/keys :req-un [::profile-id ::project-id]))
+;; --- Query: Project
-(sq/defquery ::project-by-id
- [{:keys [profile-id project-id]}]
+(s/def ::id ::us/uuid)
+(s/def ::project
+ (s/keys :req-un [::profile-id ::id]))
+
+(sq/defquery ::project
+ [{:keys [profile-id id]}]
(with-open [conn (db/open)]
- (let [project (db/get-by-id conn :project project-id)]
- (check-edition-permissions! conn profile-id project)
+ (let [project (db/get-by-id conn :project id)]
+ (check-read-permissions! conn profile-id id)
project)))
diff --git a/backend/src/app/services/queries/recent_files.clj b/backend/src/app/services/queries/recent_files.clj
index 6e16dacc89..159bd3025c 100644
--- a/backend/src/app/services/queries/recent_files.clj
+++ b/backend/src/app/services/queries/recent_files.clj
@@ -18,19 +18,18 @@
[app.services.queries.projects :as projects :refer [retrieve-projects]]
[app.services.queries.files :refer [decode-row-xf]]))
-(def sql:project-recent-files
- "select f.*
- from file as f
- where f.project_id = ?
- and f.deleted_at is null
- order by f.modified_at desc
- limit 5")
-
-(defn recent-by-project
- [conn profile-id project]
- (let [project-id (:id project)]
- (projects/check-edition-permissions! conn profile-id project)
- (into [] decode-row-xf (db/exec! conn [sql:project-recent-files project-id]))))
+(def sql:recent-files
+ "with recent_files as (
+ select f.*, row_number() over w as row_num
+ from file as f
+ join project as p on (p.id = f.project_id)
+ where p.team_id = ?
+ and p.deleted_at is null
+ and f.deleted_at is null
+ window w as (partition by f.project_id order by f.modified_at desc)
+ order by f.modified_at desc
+ )
+ select * from recent_files where row_num <= 6;")
(s/def ::team-id ::us/uuid)
(s/def ::profile-id ::us/uuid)
@@ -42,9 +41,7 @@
[{:keys [profile-id team-id]}]
(with-open [conn (db/open)]
(teams/check-read-permissions! conn profile-id team-id)
- (->> (retrieve-projects conn team-id)
- ;; Retrieve for each proyect the 5 more recent files
- (map (partial recent-by-project conn profile-id))
- ;; Change the structure so it's a map with project-id as keys
- (flatten)
- (group-by :project-id))))
+ (let [files (db/exec! conn [sql:recent-files team-id])]
+ (into [] decode-row-xf files))))
+
+
diff --git a/backend/src/app/services/queries/teams.clj b/backend/src/app/services/queries/teams.clj
index 4d86477e11..cddd8b5ab8 100644
--- a/backend/src/app/services/queries/teams.clj
+++ b/backend/src/app/services/queries/teams.clj
@@ -15,6 +15,7 @@
[app.common.uuid :as uuid]
[app.db :as db]
[app.services.queries :as sq]
+ [app.services.queries.profile :as profile]
[app.util.blob :as blob]))
;; --- Team Edition Permissions
@@ -34,7 +35,8 @@
(:is-admin row)
(:is-owner row))
(ex/raise :type :validation
- :code :not-authorized))))
+ :code :not-authorized))
+ row))
(defn check-read-permissions!
[conn profile-id team-id]
@@ -42,4 +44,89 @@
;; when row is found this means that read permission is granted.
(when-not row
(ex/raise :type :validation
- :code :not-authorized))))
+ :code :not-authorized))
+ row))
+
+
+;; --- Query: Teams
+
+(declare retrieve-teams)
+
+(s/def ::profile-id ::us/uuid)
+(s/def ::teams
+ (s/keys :req-un [::profile-id]))
+
+(sq/defquery ::teams
+ [{:keys [profile-id]}]
+ (with-open [conn (db/open)]
+ (retrieve-teams conn profile-id)))
+
+(def sql:teams
+ "select t.*,
+ tp.is_owner,
+ tp.is_admin,
+ tp.can_edit,
+ (t.id = ?) as is_default
+ from team_profile_rel as tp
+ join team as t on (t.id = tp.team_id)
+ where t.deleted_at is null
+ and tp.profile_id = ?
+ order by t.created_at asc")
+
+(defn retrieve-teams
+ [conn profile-id]
+ (let [defaults (profile/retrieve-additional-data conn profile-id)]
+ (db/exec! conn [sql:teams (:default-team-id defaults) profile-id])))
+
+;; --- Query: Team (by ID)
+
+(declare retrieve-team)
+
+(s/def ::id ::us/uuid)
+(s/def ::team
+ (s/keys :req-un [::profile-id ::id]))
+
+(sq/defquery ::team
+ [{:keys [profile-id id]}]
+ (with-open [conn (db/open)]
+ (retrieve-team conn profile-id id)))
+
+(defn retrieve-team
+ [conn profile-id team-id]
+ (let [defaults (profile/retrieve-additional-data conn profile-id)
+ sql (str "WITH teams AS (" sql:teams ") SELECT * FROM teams WHERE id=?")
+ result (db/exec-one! conn [sql (:default-team-id defaults) profile-id team-id])]
+ (when-not result
+ (ex/raise :type :not-found
+ :code :object-does-not-exists))
+ result))
+
+
+;; --- Query: Team Members
+
+(declare retrieve-team-members)
+
+(s/def ::team-id ::us/uuid)
+(s/def ::team-members
+ (s/keys :req-un [::profile-id ::team-id]))
+
+(sq/defquery ::team-members
+ [{:keys [profile-id team-id]}]
+ (with-open [conn (db/open)]
+ (check-edition-permissions! conn profile-id team-id)
+ (retrieve-team-members conn team-id)))
+
+(def sql:team-members
+ "select tp.*,
+ p.id,
+ p.email,
+ p.fullname as name,
+ p.photo,
+ p.is_active
+ from team_profile_rel as tp
+ join profile as p on (p.id = tp.profile_id)
+ where tp.team_id = ?")
+
+(defn retrieve-team-members
+ [conn team-id]
+ (db/exec! conn [sql:team-members team-id]))
diff --git a/backend/tests/app/tests/helpers.clj b/backend/tests/app/tests/helpers.clj
index a1c22e73ee..c99294344f 100644
--- a/backend/tests/app/tests/helpers.clj
+++ b/backend/tests/app/tests/helpers.clj
@@ -22,7 +22,6 @@
[app.services.mutations.projects :as projects]
[app.services.mutations.teams :as teams]
[app.services.mutations.files :as files]
- [app.services.mutations.colors :as colors]
[app.migrations]
[app.media]
[app.media-storage]
diff --git a/common/app/common/data.cljc b/common/app/common/data.cljc
index fe84e3f832..c938e0b757 100644
--- a/common/app/common/data.cljc
+++ b/common/app/common/data.cljc
@@ -6,7 +6,7 @@
(ns app.common.data
"Data manipulation and query helper functions."
- (:refer-clojure :exclude [concat read-string])
+ (:refer-clojure :exclude [concat read-string hash-map])
(:require [clojure.set :as set]
[linked.set :as lks]
#?(:cljs [cljs.reader :as r]
@@ -109,7 +109,7 @@
([coll value]
(sequence (replace-by-id value) coll)))
-(defn remove-nil-vals
+(defn without-nils
"Given a map, return a map removing key-value
pairs when value is `nil`."
[data]
diff --git a/common/app/common/exceptions.cljc b/common/app/common/exceptions.cljc
index ee1370279f..2abd014260 100644
--- a/common/app/common/exceptions.cljc
+++ b/common/app/common/exceptions.cljc
@@ -25,9 +25,24 @@
[& {:keys [type code message hint cause] :as params}]
(s/assert ::error-params params)
(let [message (or message hint "")
- payload (dissoc params :cause :message)]
+ payload (dissoc params :cause)]
(ex-info message payload cause)))
(defmacro raise
[& args]
`(throw (error ~@args)))
+
+(defn try*
+ [f on-error]
+ (try (f) (catch #?(:clj Exception :cljs :default) e (on-error e))))
+
+;; http://clj-me.cgrand.net/2013/09/11/macros-closures-and-unexpected-object-retention/
+;; Explains the use of ^:once metadata
+
+(defmacro ignoring
+ [& exprs]
+ `(try* (^:once fn* [] ~@exprs) (constantly nil)))
+
+(defmacro try
+ [& exprs]
+ `(try* (^:once fn* [] ~@exprs) identity))
diff --git a/common/app/common/geom/point.cljc b/common/app/common/geom/point.cljc
index b523dae2c5..65b453b561 100644
--- a/common/app/common/geom/point.cljc
+++ b/common/app/common/geom/point.cljc
@@ -199,3 +199,27 @@
(defn center-points [points]
(let [k (point (count points))]
(reduce #(add %1 (divide %2 k)) (point) points)))
+
+(defn normal-left
+ "Returns the normal unit vector on the left side"
+ [{:keys [x y]}]
+ (unit (point (- y) x)))
+
+(defn normal-right
+ "Returns the normal unit vector on the right side"
+ [{:keys [x y]}]
+ (unit (point y (- x))))
+
+(defn point-line-distance
+ "Returns the distance from a point to a line defined by two points"
+ [point line-point1 line-point2]
+ (let [{x0 :x y0 :y} point
+ {x1 :x y1 :y} line-point1
+ {x2 :x y2 :y} line-point2
+ num (mth/abs
+ (+ (* x0 (- y2 y1))
+ (- (* y0 (- x2 x1)))
+ (* x2 y1)
+ (- (* y2 x1))))
+ dist (distance line-point2 line-point1)]
+ (/ num dist)))
diff --git a/common/app/common/geom/shapes.cljc b/common/app/common/geom/shapes.cljc
index 83fbd97911..ed053344f3 100644
--- a/common/app/common/geom/shapes.cljc
+++ b/common/app/common/geom/shapes.cljc
@@ -11,7 +11,6 @@
(:require
[clojure.spec.alpha :as s]
[app.common.spec :as us]
- [app.common.pages-helpers :as cph]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.math :as mth]
@@ -58,10 +57,17 @@
(update :points #(mapv inc-point %))
(update :segments #(mapv inc-point %)))))
+;; Duplicated from pages-helpers to remove cyclic dependencies
+(defn get-children [id objects]
+ (let [shapes (vec (get-in objects [id :shapes]))]
+ (if shapes
+ (d/concat shapes (mapcat #(get-children % objects) shapes))
+ [])))
+
(defn recursive-move
"Move the shape and all its recursive children."
[shape dpoint objects]
- (let [children-ids (cph/get-children (:id shape) objects)
+ (let [children-ids (get-children (:id shape) objects)
children (map #(get objects %) children-ids)]
(map #(move % dpoint) (cons shape children))))
@@ -406,6 +412,10 @@
:y miny
:width (- maxx minx)
:height (- maxy miny)
+ :points [(gpt/point minx miny)
+ (gpt/point maxx miny)
+ (gpt/point maxx maxy)
+ (gpt/point minx maxy)]
:type :rect}))
(defn translate-to-frame
@@ -674,7 +684,7 @@
resize-transform (:resize-transform modifiers (gmt/matrix))
resize-transform-inverse (:resize-transform-inverse modifiers (gmt/matrix))
- rt-modif (:rotation modifiers 0)
+ rt-modif (or (:rotation modifiers) 0)
shape (-> shape
(transform ds-modifier))
@@ -792,8 +802,8 @@
(assoc $ :points (shape->points $))
(assoc $ :selrect (points->selrect (:points $)))
(update $ :selrect fix-invalid-rect-values)
- (update $ :rotation #(mod (+ (or % 0) (get-in $ [:modifiers :rotation] 0)) 360))
- )]
+ (update $ :rotation #(mod (+ (or % 0)
+ (or (get-in $ [:modifiers :rotation]) 0)) 360)))]
new-shape))
(declare update-path-selrect)
diff --git a/common/app/common/math.cljc b/common/app/common/math.cljc
index d49bcf42cf..9125c7c351 100644
--- a/common/app/common/math.cljc
+++ b/common/app/common/math.cljc
@@ -12,6 +12,10 @@
#?(:cljs
(:require [goog.math :as math])))
+(def PI
+ #?(:cljs (.-PI js/Math)
+ :clj Math/PI))
+
(defn nan?
[v]
#?(:cljs (js/isNaN v)
diff --git a/common/app/common/pages.cljc b/common/app/common/pages.cljc
index 6aa325c367..348d98b47b 100644
--- a/common/app/common/pages.cljc
+++ b/common/app/common/pages.cljc
@@ -46,6 +46,7 @@
(<= % max-safe-int)))
(s/def ::component-id uuid?)
(s/def ::component-file uuid?)
+(s/def ::component-root? boolean?)
(s/def ::shape-ref uuid?)
(s/def ::safe-number
@@ -54,6 +55,93 @@
(>= % min-safe-int)
(<= % max-safe-int)))
+;; GRADIENTS
+
+(s/def :internal.gradient.stop/color ::string)
+(s/def :internal.gradient.stop/opacity ::safe-number)
+(s/def :internal.gradient.stop/offset ::safe-number)
+
+(s/def :internal.gradient/type #{:linear :radial})
+(s/def :internal.gradient/start-x ::safe-number)
+(s/def :internal.gradient/start-y ::safe-number)
+(s/def :internal.gradient/end-x ::safe-number)
+(s/def :internal.gradient/end-y ::safe-number)
+(s/def :internal.gradient/width ::safe-number)
+
+(s/def :internal.gradient/stop
+ (s/keys :req-un [:internal.gradient.stop/color
+ :internal.gradient.stop/opacity
+ :internal.gradient.stop/offset]))
+
+(s/def :internal.gradient/stops
+ (s/coll-of :internal.gradient/stop :kind vector?))
+
+(s/def ::gradient
+ (s/keys :req-un [:internal.gradient/type
+ :internal.gradient/start-x
+ :internal.gradient/start-y
+ :internal.gradient/end-x
+ :internal.gradient/end-y
+ :internal.gradient/width
+ :internal.gradient/stops]))
+
+
+;;; COLORS
+
+(s/def :internal.color/name ::string)
+(s/def :internal.color/value (s/nilable ::string))
+(s/def :internal.color/color (s/nilable ::string))
+(s/def :internal.color/opacity (s/nilable ::safe-number))
+(s/def :internal.color/gradient (s/nilable ::gradient))
+
+(s/def ::color
+ (s/keys :opt-un [::id
+ :internal.color/name
+ :internal.color/value
+ :internal.color/color
+ :internal.color/opacity
+ :internal.color/gradient]))
+
+
+
+;;; SHADOW EFFECT
+
+(s/def :internal.shadow/id uuid?)
+(s/def :internal.shadow/style #{:drop-shadow :inner-shadow})
+(s/def :internal.shadow/color ::color)
+(s/def :internal.shadow/offset-x ::safe-number)
+(s/def :internal.shadow/offset-y ::safe-number)
+(s/def :internal.shadow/blur ::safe-number)
+(s/def :internal.shadow/spread ::safe-number)
+(s/def :internal.shadow/hidden boolean?)
+
+(s/def :internal.shadow/shadow
+ (s/keys :req-un [:internal.shadow/id
+ :internal.shadow/style
+ :internal.shadow/color
+ :internal.shadow/offset-x
+ :internal.shadow/offset-y
+ :internal.shadow/blur
+ :internal.shadow/spread
+ :internal.shadow/hidden]))
+
+(s/def ::shadow
+ (s/coll-of :internal.shadow/shadow :kind vector?))
+
+
+;;; BLUR EFFECT
+
+(s/def :internal.blur/id uuid?)
+(s/def :internal.blur/type #{:layer-blur})
+(s/def :internal.blur/value ::safe-number)
+(s/def :internal.blur/hidden boolean?)
+
+(s/def ::blur
+ (s/keys :req-un [:internal.blur/id
+ :internal.blur/type
+ :internal.blur/value
+ :internal.blur/hidden]))
+
;; Page Options
(s/def :internal.page.grid.color/value string?)
(s/def :internal.page.grid.color/opacity ::safe-number)
@@ -109,10 +197,13 @@
(s/def :internal.shape/blocked boolean?)
(s/def :internal.shape/collapsed boolean?)
(s/def :internal.shape/content any?)
+
(s/def :internal.shape/fill-color string?)
+(s/def :internal.shape/fill-opacity ::safe-number)
+(s/def :internal.shape/fill-gradient (s/nilable ::gradient))
(s/def :internal.shape/fill-color-ref-file (s/nilable uuid?))
(s/def :internal.shape/fill-color-ref-id (s/nilable uuid?))
-(s/def :internal.shape/fill-opacity ::safe-number)
+
(s/def :internal.shape/font-family string?)
(s/def :internal.shape/font-size ::safe-integer)
(s/def :internal.shape/font-style string?)
@@ -141,6 +232,8 @@
(s/def :internal.shape/width ::safe-number)
(s/def :internal.shape/height ::safe-number)
(s/def :internal.shape/index integer?)
+(s/def :internal.shape/shadow ::shadow)
+(s/def :internal.shape/blur ::blur)
(s/def :internal.shape/x1 ::safe-number)
(s/def :internal.shape/y1 ::safe-number)
@@ -211,7 +304,36 @@
:internal.shape/height
:internal.shape/interactions
:internal.shape/selrect
- :internal.shape/points]))
+ :internal.shape/points
+ :internal.shape/masked-group?
+ :internal.shape/shadow
+ :internal.shape/blur]))
+
+(def component-sync-attrs {:fill-color :fill-group
+ :fill-color-ref-file :fill-group
+ :fill-color-ref-id :fill-group
+ :fill-opacity :fill-group
+ :content :text-content-group
+ :font-family :text-font-group
+ :font-size :text-font-group
+ :font-style :text-font-group
+ :font-weight :text-font-group
+ :letter-spacing :text-display-group
+ :line-height :text-display-group
+ :text-align :text-display-group
+ :stroke-color :stroke-group
+ :stroke-color-ref-file :stroke-group
+ :stroke-color-ref-id :stroke-group
+ :stroke-opacity :stroke-group
+ :stroke-style :stroke-group
+ :stroke-width :stroke-group
+ :stroke-alignment :stroke-group
+ :width :size-group
+ :height :size-group
+ :proportion :size-group
+ :rx :radius-group
+ :ry :radius-group
+ :masked-group? :mask-group})
(s/def ::minimal-shape
(s/keys :req-un [::type ::name]
@@ -222,6 +344,7 @@
(s/keys :opt-un [::id
::component-id
::component-file
+ ::component-root?
::shape-ref])))
(s/def :internal.page/objects (s/map-of uuid? ::shape))
@@ -232,13 +355,12 @@
:internal.page/options
:internal.page/objects]))
-(s/def :internal.color/name ::string)
-(s/def :internal.color/value ::string)
-(s/def ::color
- (s/keys :req-un [::id
- :internal.color/name
- :internal.color/value]))
+(s/def ::recent-color
+ (s/keys :opt-un [:internal.color/value
+ :internal.color/color
+ :internal.color/opacity
+ :internal.color/gradient]))
(s/def :internal.media-object/name ::string)
(s/def :internal.media-object/path ::string)
@@ -264,7 +386,32 @@
(s/map-of ::uuid ::color))
(s/def :internal.file/recent-colors
- (s/coll-of ::string :kind vector?))
+ (s/coll-of ::recent-color :kind vector?))
+
+(s/def :internal.typography/id ::id)
+(s/def :internal.typography/name ::string)
+(s/def :internal.typography/font-id ::string)
+(s/def :internal.typography/font-family ::string)
+(s/def :internal.typography/font-variant-id ::string)
+(s/def :internal.typography/font-size ::string)
+(s/def :internal.typography/font-weight ::string)
+(s/def :internal.typography/font-style ::string)
+(s/def :internal.typography/line-height ::string)
+(s/def :internal.typography/letter-spacing ::string)
+(s/def :internal.typography/text-transform ::string)
+
+(s/def ::typography
+ (s/keys :req-un [:internal.typography/id
+ :internal.typography/name
+ :internal.typography/font-id
+ :internal.typography/font-family
+ :internal.typography/font-variant-id
+ :internal.typography/font-size
+ :internal.typography/font-weight
+ :internal.typography/font-style
+ :internal.typography/line-height
+ :internal.typography/letter-spacing
+ :internal.typography/text-transform]))
(s/def :internal.file/pages
(s/coll-of ::uuid :kind vector?))
@@ -286,11 +433,16 @@
(s/def :internal.operations.set/attr keyword?)
(s/def :internal.operations.set/val any?)
+(s/def :internal.operations.set/touched
+ (s/nilable (s/every keyword? :kind set?)))
(defmethod operation-spec :set [_]
(s/keys :req-un [:internal.operations.set/attr
:internal.operations.set/val]))
+(defmethod operation-spec :set-touched [_]
+ (s/keys :req-un [:internal.operations.set/touched]))
+
(defmulti change-spec :type)
(s/def :internal.changes.set-option/option any?)
@@ -311,7 +463,7 @@
(s/def ::operations (s/coll-of ::operation))
(defmethod change-spec :mod-obj [_]
- (s/keys :req-un [::id ::page-id ::operations]))
+ (s/keys :req-un [::id (or ::page-id ::component-id) ::operations]))
(defmethod change-spec :del-obj [_]
(s/keys :req-un [::id ::page-id]))
@@ -348,8 +500,10 @@
(defmethod change-spec :del-color [_]
(s/keys :req-un [::id]))
+(s/def :internal.changes.add-recent-color/color ::recent-color)
+
(defmethod change-spec :add-recent-color [_]
- (s/keys :req-un [:recent-color/color]))
+ (s/keys :req-un [:internal.changes.add-recent-color/color]))
(s/def :internal.changes.media/object ::media-object)
@@ -374,6 +528,17 @@
(defmethod change-spec :del-component [_]
(s/keys :req-un [::id]))
+(s/def :internal.changes.typography/typography ::typography)
+
+(defmethod change-spec :add-typography [_]
+ (s/keys :req-un [:internal.changes.typography/typography]))
+
+(defmethod change-spec :mod-typography [_]
+ (s/keys :req-un [:internal.changes.typography/typography]))
+
+(defmethod change-spec :del-typography [_]
+ (s/keys :req-un [:internal.typography/id]))
+
(s/def ::change (s/multi-spec change-spec :type))
(s/def ::changes (s/coll-of ::change))
@@ -559,12 +724,14 @@
:else (cph/insert-at-index shapes index [id]))))))))))))
(defmethod process-change :mod-obj
- [data {:keys [id page-id operations] :as change}]
- (d/update-in-when data [:pages-index page-id :objects]
- (fn [objects]
- (if-let [obj (get objects id)]
- (assoc objects id (reduce process-operation obj operations))
- objects))))
+ [data {:keys [id page-id component-id operations] :as change}]
+ (let [update-fn (fn [objects]
+ (if-let [obj (get objects id)]
+ (assoc objects id (reduce process-operation obj operations))
+ objects))]
+ (if page-id
+ (d/update-in-when data [:pages-index page-id :objects] update-fn)
+ (d/update-in-when data [:components component-id :objects] update-fn))))
(defmethod process-change :del-obj
[data {:keys [page-id id] :as change}]
@@ -616,7 +783,10 @@
(assoc :modifiers
(rotation-modifiers gcenter % (- (:rotation group 0))))
(geom/transform-shape))))
- selrect (-> (into [] gxfm (:shapes group))
+ inner-shapes (if (:masked-group? group)
+ [(first (:shapes group))]
+ (:shapes group))
+ selrect (-> (into [] gxfm inner-shapes)
(geom/selection-rect))]
;; Rotate the group shape change the data and rotate back again
@@ -629,7 +799,6 @@
(d/update-in-when data [:pages-index page-id :objects] reg-objects)))
-
(defmethod process-change :mov-objects
[data {:keys [parent-id shapes index page-id] :as change}]
(letfn [(is-valid-move? [objects shape-id]
@@ -641,34 +810,40 @@
(let [prev-shapes (or prev-shapes [])]
(if index
(cph/insert-at-index prev-shapes index shapes)
- (reduce (fn [acc id]
- (if (some #{id} acc)
- acc
- (conj acc id)))
- prev-shapes
- shapes))))
+ (cph/append-at-the-end prev-shapes shapes))))
+
+ (check-insert-items [prev-shapes parent index shapes]
+ (if-not (:masked-group? parent)
+ (insert-items prev-shapes index shapes)
+ ;; For masked groups, the first shape is the mask
+ ;; and it cannot be moved.
+ (let [mask-id (first prev-shapes)
+ other-ids (rest prev-shapes)
+ not-mask-shapes (strip-id shapes mask-id)
+ new-index (if (nil? index) nil (max (dec index) 0))
+ new-shapes (insert-items other-ids new-index not-mask-shapes)]
+ (d/concat [mask-id] new-shapes))))
(strip-id [coll id]
(filterv #(not= % id) coll))
- (remove-from-old-parent [cpindex objects shape-id]
- (let [prev-parent-id (get cpindex shape-id)]
- ;; Do nothing if the parent id of the shape is the same as
- ;; the new destination target parent id.
- (if (= prev-parent-id parent-id)
- objects
- (loop [sid shape-id
- pid prev-parent-id
- objects objects]
- (let [obj (get objects pid)]
- (if (and (= 1 (count (:shapes obj)))
- (= sid (first (:shapes obj)))
- (= :group (:type obj)))
- (recur pid
- (:parent-id obj)
- (dissoc objects pid))
- (update-in objects [pid :shapes] strip-id sid)))))))
-
+ (remove-from-old-parent [cpindex objects shape-id]
+ (let [prev-parent-id (get cpindex shape-id)]
+ ;; Do nothing if the parent id of the shape is the same as
+ ;; the new destination target parent id.
+ (if (= prev-parent-id parent-id)
+ objects
+ (loop [sid shape-id
+ pid prev-parent-id
+ objects objects]
+ (let [obj (get objects pid)]
+ (if (and (= 1 (count (:shapes obj)))
+ (= sid (first (:shapes obj)))
+ (= :group (:type obj)))
+ (recur pid
+ (:parent-id obj)
+ (dissoc objects pid))
+ (update-in objects [pid :shapes] strip-id sid)))))))
(update-parent-id [objects id]
(update objects id assoc :parent-id parent-id))
@@ -700,7 +875,7 @@
(if valid?
(as-> objects $
- (update-in $ [parent-id :shapes] insert-items index shapes)
+ (update-in $ [parent-id :shapes] check-insert-items parent index shapes)
(reduce update-parent-id $ shapes)
(reduce (partial remove-from-old-parent cpindex) $ shapes)
(reduce (partial update-frame-ids frm-id) $ (get-in $ [parent-id :shapes])))
@@ -748,7 +923,7 @@
(defmethod process-change :mod-color
[data {:keys [color]}]
- (d/update-in-when data [:colors (:id color)] merge color))
+ (d/assoc-in-when data [:colors (:id color)] color))
(defmethod process-change :del-color
[data {:keys [id]}]
@@ -763,6 +938,8 @@
(subvec rc 1)
rc)))))
+;; -- Media
+
(defmethod process-change :add-media
[data {:keys [object]}]
(update data :media assoc (:id object) object))
@@ -775,6 +952,8 @@
[data {:keys [id]}]
(update data :media dissoc id))
+;; -- Components
+
(defmethod process-change :add-component
[data {:keys [id name shapes]}]
(assoc-in data [:components id]
@@ -793,16 +972,51 @@
[data {:keys [id]}]
(d/dissoc-in data [:components id]))
+;; -- Typography
+
+(defmethod process-change :add-typography
+ [data {:keys [typography]}]
+ (update data :typographies assoc (:id typography) typography))
+
+(defmethod process-change :mod-typography
+ [data {:keys [typography]}]
+ (d/update-in-when data [:typographies (:id typography)] merge typography))
+
+(defmethod process-change :del-typography
+ [data {:keys [id]}]
+ (update data :typographies dissoc id))
+
+;; -- Operations
+
(defmethod process-operation :set
[shape op]
- (let [attr (:attr op)
- val (:val op)]
- (if (nil? val)
- (dissoc shape attr)
- (assoc shape attr val))))
+ (let [attr (:attr op)
+ val (:val op)
+ ignore (:ignore-touched op)
+ shape-ref (:shape-ref shape)
+ group (get component-sync-attrs attr)]
+
+ (cond-> shape
+ (and shape-ref group (not ignore) (not= val (get shape attr)))
+ (update :touched #(conj (or % #{}) group))
+
+ (nil? val)
+ (dissoc attr)
+
+ (some? val)
+ (assoc attr val))))
+
+(defmethod process-operation :set-touched
+ [shape op]
+ (let [touched (:touched op)
+ shape-ref (:shape-ref shape)]
+ (if (or (nil? shape-ref) (nil? touched) (empty? touched))
+ (dissoc shape :touched)
+ (assoc shape :touched touched))))
(defmethod process-operation :default
[shape op]
(ex/raise :type :not-implemented
:code :operation-not-implemented
:context {:type (:type op)}))
+
diff --git a/common/app/common/pages_helpers.cljc b/common/app/common/pages_helpers.cljc
index 53be7402a0..28f5a3b40a 100644
--- a/common/app/common/pages_helpers.cljc
+++ b/common/app/common/pages_helpers.cljc
@@ -10,7 +10,8 @@
(ns app.common.pages-helpers
(:require
[app.common.data :as d]
- [app.common.uuid :as uuid]))
+ [app.common.uuid :as uuid]
+ [app.common.geom.shapes :as gsh]))
(defn walk-pages
"Go through all pages of a file and apply a function to each one"
@@ -20,9 +21,10 @@
(update data :pages-index #(d/mapm f %)))
(defn select-objects
- "Get a list of all objects in a page that satisfy a condition"
- [f page]
- (filter f (vals (get page :objects))))
+ "Get a list of all objects in a container (a page or a component) that
+ satisfy a condition"
+ [f container]
+ (filter f (vals (get container :objects))))
(defn update-object-list
"Update multiple objects in a page at once"
@@ -30,15 +32,15 @@
(update page :objects
#(into % (d/index-by :id objects-list))))
-(defn get-root-component
- "Get the root shape linked to the component for this shape, if any"
- [id objects]
- (let [obj (get objects id)]
- (if-let [component-id (:component-id obj)]
- id
- (if-let [parent-id (:parent-id obj)]
- (get-root-component parent-id obj)
- nil))))
+(defn get-root-shape
+ "Get the root shape linked to a component for this shape, if any"
+ [shape objects]
+ (if (:component-root? shape)
+ shape
+ (if-let [parent-id (:parent-id shape)]
+ (get-root-shape (get objects (:parent-id shape))
+ objects)
+ nil)))
(defn get-children
"Retrieve all children ids recursively for a given object"
@@ -57,7 +59,7 @@
(defn get-object-with-children
"Retrieve a list with an object and all of its children"
[id objects]
- (map #(get objects %) (concat [id] (get-children id objects))))
+ (map #(get objects %) (cons id (get-children id objects))))
(defn is-shape-grouped
"Checks if a shape is inside a group"
@@ -115,6 +117,15 @@
ids
(remove p? after))))
+(defn append-at-the-end
+ [prev-ids ids]
+ (reduce (fn [acc id]
+ (if (some #{id} acc)
+ acc
+ (conj acc id)))
+ prev-ids
+ ids))
+
(defn select-toplevel-shapes
([objects] (select-toplevel-shapes objects nil))
([objects {:keys [include-frames?] :or {include-frames? false}}]
@@ -186,7 +197,7 @@
updated-object (update-original-object object new-object)
- updated-objects (if (= object updated-object)
+ updated-objects (if (identical? object updated-object)
updated-children
(concat [updated-object] updated-children))]
@@ -204,3 +215,44 @@
(concat new-children new-child-objects)
(concat updated-children updated-child-objects))))))))
+
+(defn indexed-shapes
+ "Retrieves a list with the indexes for each element in the layer tree.
+ This will be used for shift+selection."
+ [objects]
+ (let [rec-index
+ (fn rec-index [cur-idx id]
+ (let [object (get objects id)
+ red-fn
+ (fn [cur-idx id]
+ (let [[prev-idx _] (first cur-idx)
+ prev-idx (or prev-idx 0)
+ cur-idx (conj cur-idx [(inc prev-idx) id])]
+ (rec-index cur-idx id)))]
+ (reduce red-fn cur-idx (reverse (:shapes object)))))]
+ (into {} (rec-index '() uuid/zero))))
+
+
+(defn expand-region-selection
+ "Given a selection selects all the shapes between the first and last in
+ an indexed manner (shift selection)"
+ [objects selection]
+ (let [indexed-shapes (indexed-shapes objects)
+ filter-indexes (->> indexed-shapes
+ (filter (comp selection second))
+ (map first))
+
+ from (apply min filter-indexes)
+ to (apply max filter-indexes)]
+ (->> indexed-shapes
+ (filter (fn [[idx _]] (and (>= idx from) (<= idx to))))
+ (map second)
+ (into #{}))))
+
+(defn frame-id-by-position [objects position]
+ (let [frames (select-frames objects)]
+ (or
+ (->> frames
+ (d/seek #(gsh/has-point? % position))
+ :id)
+ uuid/zero)))
diff --git a/common/app/common/spec.cljc b/common/app/common/spec.cljc
index 4b58b561a5..b9ab80199b 100644
--- a/common/app/common/spec.cljc
+++ b/common/app/common/spec.cljc
@@ -12,11 +12,16 @@
(:refer-clojure :exclude [assert])
#?(:cljs (:require-macros [app.common.spec :refer [assert]]))
(:require
- #?(:clj [clojure.spec.alpha :as s]
+ #?(:clj [clojure.spec.alpha :as s]
:cljs [cljs.spec.alpha :as s])
+
+ #?(:clj [clojure.spec.test.alpha :as stest]
+ :cljs [cljs.spec.test.alpha :as stest])
+
[expound.alpha :as expound]
[app.common.uuid :as uuid]
[app.common.exceptions :as ex]
+ [app.common.geom.point :as gpt]
[cuerdas.core :as str]))
(s/check-asserts true)
@@ -38,7 +43,7 @@
(if (string? v)
(if (re-matches uuid-rx v)
(uuid/uuid v)
- (if (str/empty? v) nil ::s/invalid))
+ ::s/invalid)
::s/invalid)))
(defn boolean-conformer
@@ -87,9 +92,21 @@
v
::s/invalid))
+(defn keyword-conformer
+ [v]
+ (cond
+ (keyword? v)
+ v
+
+ (string? v)
+ (keyword v)
+
+ :else
+ ::s/invalid))
+
;; --- Default Specs
-(s/def ::keyword keyword?)
+(s/def ::keyword (s/conformer keyword-conformer name))
(s/def ::inst inst?)
(s/def ::string string?)
(s/def ::email (s/conformer email-conformer str))
@@ -101,27 +118,54 @@
(s/def ::not-empty-string (s/and string? #(not (str/empty? %))))
(s/def ::url string?)
(s/def ::fn fn?)
+(s/def ::point gpt/point?)
;; --- Macros
(defn spec-assert
- [spec x]
+ [spec x message]
(if (s/valid? spec x)
x
(ex/raise :type :assertion
:data (s/explain-data spec x)
- #?@(:cljs [:stack (.-stack (ex-info "assertion" {}))]))))
+ :message message
+ #?@(:cljs [:stack (.-stack (ex-info message {}))]))))
+
+(defn spec-assert*
+ [spec x message context]
+ (if (s/valid? spec x)
+ x
+ (ex/raise :type :assertion
+ :data (s/explain-data spec x)
+ :context context
+ :message message
+ #?@(:cljs [:stack (.-stack (ex-info message {}))]))))
+
(defmacro assert
"Development only assertion macro."
[spec x]
(when *assert*
- `(spec-assert ~spec ~x)))
+ (let [nsdata (:ns &env)
+ context (when nsdata
+ {:ns (str (:name nsdata))
+ :name (pr-str spec)
+ :line (:line &env)
+ :file (:file (:meta nsdata))})
+ message (str "Spec Assertion: '" (pr-str spec) "'")]
+ `(spec-assert* ~spec ~x ~message ~context))))
(defmacro verify
"Always active assertion macro (does not obey to :elide-asserts)"
[spec x]
- `(spec-assert ~spec ~x))
+ (let [nsdata (:ns &env)
+ context (when nsdata
+ {:ns (str (:name nsdata))
+ :name (pr-str spec)
+ :line (:line &env)
+ :file (:file (:meta nsdata))})
+ message (str "Spec Assertion: '" (pr-str spec) "'")]
+ `(spec-assert* ~spec ~x ~message ~context)))
;; --- Public Api
@@ -136,3 +180,14 @@
(expound/printer edata))
:data (::s/problems edata)))))
result))
+
+(defmacro instrument!
+ [& {:keys [sym spec]}]
+ (when *assert*
+ (let [message (str "Spec failed on: " sym)]
+ `(let [origf# ~sym
+ mdata# (meta (var ~sym))]
+ (set! ~sym (fn [& params#]
+ (spec-assert* ~spec params# ~message mdata#)
+ (apply origf# params#)))))))
+
diff --git a/docker/devenv/Dockerfile b/docker/devenv/Dockerfile
index 72bf68956d..6493e6f112 100644
--- a/docker/devenv/Dockerfile
+++ b/docker/devenv/Dockerfile
@@ -4,18 +4,16 @@ LABEL maintainer="Andrey Antukh "
ARG DEBIAN_FRONTEND=noninteractive
ARG EXTERNAL_UID=1000
-ENV NODE_VERSION=v12.18.4 \
- CLOJURE_VERSION=1.10.1.681 \
+ENV NODE_VERSION=v12.19.0 \
+ CLOJURE_VERSION=1.10.1.727 \
LANG=en_US.UTF-8 \
- LC_ALL=C.UTF-8
+ LC_ALL=en_US.UTF-8
RUN set -ex; \
mkdir -p /etc/resolvconf/resolv.conf.d; \
- echo "nameserver 8.8.8.8" > /etc/resolvconf/resolv.conf.d/tail;
-
-RUN set -ex; \
- apt-get update && \
- apt-get install -yq \
+ echo "nameserver 8.8.8.8" > /etc/resolvconf/resolv.conf.d/tail; \
+ apt-get -qq update; \
+ apt-get -qqy install --no-install-recommends \
locales \
gnupg2 \
ca-certificates \
@@ -27,6 +25,19 @@ RUN set -ex; \
bash \
git \
rlwrap \
+ ; \
+ echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen; \
+ locale-gen; \
+ rm -rf /var/lib/apt/lists/*;
+
+RUN set -ex; \
+ useradd -m -g users -s /bin/bash -u $EXTERNAL_UID uxbox; \
+ passwd uxbox -d; \
+ echo "uxbox ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
+
+RUN set -ex; \
+ apt-get -qq update; \
+ apt-get -qqy install --no-install-recommends \
python \
build-essential \
imagemagick \
@@ -40,8 +51,8 @@ RUN set -ex; \
rm -rf /var/lib/apt/lists/*;
RUN set -ex; \
- apt-get update && \
- apt-get install -yq \
+ apt-get -qq update; \
+ apt-get -qqy install \
gconf-service \
libasound2 \
libatk1.0-0 \
@@ -80,30 +91,25 @@ RUN set -ex; \
rm -rf /var/lib/apt/lists/*;
RUN set -ex; \
- apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 0xB1998361219BD9C9; \
- echo "deb http://repos.azulsystems.com/debian stable main" >> /etc/apt/sources.list.d/zulu.list; \
+ mkdir -p /usr/share/man/man1; \
+ mkdir -p /usr/share/man/man7; \
+ wget -qO - https://adoptopenjdk.jfrog.io/adoptopenjdk/api/gpg/key/public | apt-key add -; \
+ echo "deb https://adoptopenjdk.jfrog.io/adoptopenjdk/deb/ buster main" >> /etc/apt/sources.list.d/adoptopenjdk.list; \
apt-get -qq update; \
- apt-get -qqy install zulu-14; \
+ apt-get -qqy install adoptopenjdk-15-hotspot; \
rm -rf /var/lib/apt/lists/*; \
wget "https://download.clojure.org/install/linux-install-$CLOJURE_VERSION.sh"; \
chmod +x "linux-install-$CLOJURE_VERSION.sh"; \
"./linux-install-$CLOJURE_VERSION.sh"; \
rm -rf "linux-install-$CLOJURE_VERSION.sh"
-ENV JAVA_HOME=/usr/lib/jvm/zulu-14-amd64
-
RUN set -ex; \
curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -; \
echo "deb http://apt.postgresql.org/pub/repos/apt buster-pgdg main" >> /etc/apt/sources.list.d/postgresql.list; \
apt-get -qq update; \
- apt-get -qqy install postgresql-client-12; \
+ apt-get -qqy install postgresql-client-13; \
rm -rf /var/lib/apt/lists/*;
-RUN set -ex; \
- useradd -m -g users -s /bin/bash -u $EXTERNAL_UID uxbox; \
- passwd uxbox -d; \
- echo "uxbox ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
-
RUN set -ex; \
wget https://github.com/RazrFalcon/svgcleaner/releases/download/v0.9.5/svgcleaner_linux_x86_64_0.9.5.tar.gz; \
tar xvf svgcleaner_linux_x86_64_0.9.5.tar.gz; \
@@ -111,12 +117,12 @@ RUN set -ex; \
rm -rf svgcleaner_linux_x86_64_0.9.5.tar.gz;
COPY files/phantomjs-mock /usr/bin/phantomjs
-COPY files/bashrc /root/.bashrc
-COPY files/vimrc /root/.vimrc
-COPY files/tmux.conf /root/.tmux.conf
-COPY files/start-tmux.sh /home/start-tmux.sh
-COPY files/entrypoint.sh /home/entrypoint.sh
-COPY files/init.sh /home/init.sh
+COPY files/bashrc /root/.bashrc
+COPY files/vimrc /root/.vimrc
+COPY files/tmux.conf /root/.tmux.conf
+COPY files/start-tmux.sh /home/start-tmux.sh
+COPY files/entrypoint.sh /home/entrypoint.sh
+COPY files/init.sh /home/init.sh
USER uxbox
WORKDIR /home/uxbox
@@ -134,5 +140,5 @@ EXPOSE 3449
EXPOSE 6060
EXPOSE 9090
-ENTRYPOINT ["bash", "/home/entrypoint.sh"]
+ENTRYPOINT ["/home/entrypoint.sh"]
CMD ["/home/init.sh"]
diff --git a/docker/devenv/docker-compose.yaml b/docker/devenv/docker-compose.yaml
index d93f69e586..bbbbfa40d0 100644
--- a/docker/devenv/docker-compose.yaml
+++ b/docker/devenv/docker-compose.yaml
@@ -55,13 +55,11 @@ services:
- OPENDKIM_DOMAINS=smtp.uxbox.io
postgres:
- image: postgres:12
+ image: postgres:13
command: postgres -c config_file=/etc/postgresql.conf
container_name: "uxbox-devenv-postgres"
restart: always
stop_signal: SIGINT
- ports:
- - 5432:5432
environment:
- POSTGRES_INITDB_ARGS=--data-checksums
- POSTGRES_DB=uxbox
@@ -77,6 +75,3 @@ services:
hostname: "uxbox-devenv-redis"
container_name: "uxbox-devenv-redis"
restart: always
-
- ports:
- - 6379:6379
diff --git a/docker/devenv/files/entrypoint.sh b/docker/devenv/files/entrypoint.sh
index 53a27a077c..bbf4504367 100755
--- a/docker/devenv/files/entrypoint.sh
+++ b/docker/devenv/files/entrypoint.sh
@@ -1,13 +1,12 @@
#!/usr/bin/env bash
-source /home/uxbox/.bashrc
-
-set -ex
+set -e
sudo cp /root/.bashrc /home/uxbox/.bashrc
sudo cp /root/.vimrc /home/uxbox/.vimrc
sudo cp /root/.tmux.conf /home/uxbox/.tmux.conf
+source /home/uxbox/.bashrc
sudo chown uxbox:users /home/uxbox
exec "$@"
diff --git a/docker/devenv/files/postgresql.conf b/docker/devenv/files/postgresql.conf
index 859062c7b6..da63d26021 100644
--- a/docker/devenv/files/postgresql.conf
+++ b/docker/devenv/files/postgresql.conf
@@ -1,753 +1,22 @@
-
-# -----------------------------
-# PostgreSQL configuration file
-# -----------------------------
-#
-# This file consists of lines of the form:
-#
-# name = value
-#
-# (The "=" is optional.) Whitespace may be used. Comments are introduced with
-# "#" anywhere on a line. The complete list of parameter names and allowed
-# values can be found in the PostgreSQL documentation.
-#
-# The commented-out settings shown in this file represent the default values.
-# Re-commenting a setting is NOT sufficient to revert it to the default value;
-# you need to reload the server.
-#
-# This file is read on server startup and when the server receives a SIGHUP
-# signal. If you edit the file on a running system, you have to SIGHUP the
-# server for the changes to take effect, run "pg_ctl reload", or execute
-# "SELECT pg_reload_conf()". Some parameters, which are marked below,
-# require a server shutdown and restart to take effect.
-#
-# Any parameter can also be given as a command-line option to the server, e.g.,
-# "postgres -c log_connections=on". Some parameters can be changed at run time
-# with the "SET" SQL command.
-#
-# Memory units: kB = kilobytes Time units: ms = milliseconds
-# MB = megabytes s = seconds
-# GB = gigabytes min = minutes
-# TB = terabytes h = hours
-# d = days
-
-
-#------------------------------------------------------------------------------
-# FILE LOCATIONS
-#------------------------------------------------------------------------------
-
-# The default values of these variables are driven from the -D command-line
-# option or PGDATA environment variable, represented here as ConfigDir.
-
-#data_directory = 'ConfigDir' # use data in another directory
- # (change requires restart)
-#hba_file = 'ConfigDir/pg_hba.conf' # host-based authentication file
- # (change requires restart)
-#ident_file = 'ConfigDir/pg_ident.conf' # ident configuration file
- # (change requires restart)
-
-# If external_pid_file is not explicitly set, no extra PID file is written.
-#external_pid_file = '' # write an extra PID file
- # (change requires restart)
-
-
-#------------------------------------------------------------------------------
-# CONNECTIONS AND AUTHENTICATION
-#------------------------------------------------------------------------------
-
-# - Connection Settings -
-
listen_addresses = '*'
- # comma-separated list of addresses;
- # defaults to 'localhost'; use '*' for all
- # (change requires restart)
-#port = 5432 # (change requires restart)
-max_connections = 100 # (change requires restart)
-#superuser_reserved_connections = 3 # (change requires restart)
-#unix_socket_directories = '/var/run/postgresql' # comma-separated list of directories
- # (change requires restart)
-#unix_socket_group = '' # (change requires restart)
-#unix_socket_permissions = 0777 # begin with 0 to use octal notation
- # (change requires restart)
-#bonjour = off # advertise server via Bonjour
- # (change requires restart)
-#bonjour_name = '' # defaults to the computer name
- # (change requires restart)
+max_connections = 100
+shared_buffers = 128MB
+temp_buffers = 8MB
+work_mem = 8MB
-# - TCP settings -
-# see "man 7 tcp" for details
-
-#tcp_keepalives_idle = 0 # TCP_KEEPIDLE, in seconds;
- # 0 selects the system default
-#tcp_keepalives_interval = 0 # TCP_KEEPINTVL, in seconds;
- # 0 selects the system default
-#tcp_keepalives_count = 0 # TCP_KEEPCNT;
- # 0 selects the system default
-#tcp_user_timeout = 0 # TCP_USER_TIMEOUT, in milliseconds;
- # 0 selects the system default
-
-# - Authentication -
-
-#authentication_timeout = 1min # 1s-600s
-#password_encryption = md5 # md5 or scram-sha-256
-#db_user_namespace = off
-
-# GSSAPI using Kerberos
-#krb_server_keyfile = ''
-#krb_caseins_users = off
-
-# - SSL -
-
-#ssl = off
-#ssl_ca_file = ''
-#ssl_cert_file = 'server.crt'
-#ssl_crl_file = ''
-#ssl_key_file = 'server.key'
-#ssl_ciphers = 'HIGH:MEDIUM:+3DES:!aNULL' # allowed SSL ciphers
-#ssl_prefer_server_ciphers = on
-#ssl_ecdh_curve = 'prime256v1'
-#ssl_min_protocol_version = 'TLSv1'
-#ssl_max_protocol_version = ''
-#ssl_dh_params_file = ''
-#ssl_passphrase_command = ''
-#ssl_passphrase_command_supports_reload = off
-
-
-#------------------------------------------------------------------------------
-# RESOURCE USAGE (except WAL)
-#------------------------------------------------------------------------------
-
-# - Memory -
-
-shared_buffers = 256MB # min 128kB
- # (change requires restart)
-#huge_pages = try # on, off, or try
- # (change requires restart)
-temp_buffers = 8MB # min 800kB
-#max_prepared_transactions = 0 # zero disables the feature
- # (change requires restart)
-# Caution: it is not advisable to set max_prepared_transactions nonzero unless
-# you actively intend to use prepared transactions.
-work_mem = 8MB # min 64kB
-#maintenance_work_mem = 64MB # min 1MB
-#autovacuum_work_mem = -1 # min 1MB, or -1 to use maintenance_work_mem
-#max_stack_depth = 2MB # min 100kB
-#shared_memory_type = mmap # the default is the first option
- # supported by the operating system:
- # mmap
- # sysv
- # windows
- # (change requires restart)
-dynamic_shared_memory_type = posix # the default is the first option
- # supported by the operating system:
- # posix
- # sysv
- # windows
- # mmap
- # (change requires restart)
-
-# - Disk -
-
-#temp_file_limit = -1 # limits per-process temp file space
- # in kB, or -1 for no limit
-
-# - Kernel Resources -
-
-#max_files_per_process = 1000 # min 25
- # (change requires restart)
-
-# - Cost-Based Vacuum Delay -
-
-#vacuum_cost_delay = 0 # 0-100 milliseconds (0 disables)
-#vacuum_cost_page_hit = 1 # 0-10000 credits
-#vacuum_cost_page_miss = 10 # 0-10000 credits
-#vacuum_cost_page_dirty = 20 # 0-10000 credits
-#vacuum_cost_limit = 200 # 1-10000 credits
-
-# - Background Writer -
-
-#bgwriter_delay = 200ms # 10-10000ms between rounds
-#bgwriter_lru_maxpages = 100 # max buffers written/round, 0 disables
-#bgwriter_lru_multiplier = 2.0 # 0-10.0 multiplier on buffers scanned/round
-#bgwriter_flush_after = 512kB # measured in pages, 0 disables
-
-# - Asynchronous Behavior -
-
-#effective_io_concurrency = 1 # 1-1000; 0 disables prefetching
-#max_worker_processes = 8 # (change requires restart)
-#max_parallel_maintenance_workers = 2 # taken from max_parallel_workers
-#max_parallel_workers_per_gather = 2 # taken from max_parallel_workers
-#parallel_leader_participation = on
-#max_parallel_workers = 8 # maximum number of max_worker_processes that
- # can be used in parallel operations
-#old_snapshot_threshold = -1 # 1min-60d; -1 disables; 0 is immediate
- # (change requires restart)
-#backend_flush_after = 0 # measured in pages, 0 disables
-
-
-#------------------------------------------------------------------------------
-# WRITE-AHEAD LOG
-#------------------------------------------------------------------------------
-
-# - Settings -
-
-#wal_level = replica # minimal, replica, or logical
- # (change requires restart)
-#fsync = on # flush data to disk for crash safety
- # (turning this off can cause
- # unrecoverable data corruption)
-synchronous_commit = off # synchronization level;
- # off, local, remote_write, remote_apply, or on
-#wal_sync_method = fsync # the default is the first option
- # supported by the operating system:
- # open_datasync
- # fdatasync (default on Linux)
- # fsync
- # fsync_writethrough
- # open_sync
-#full_page_writes = on # recover from partial page writes
-#wal_compression = off # enable compression of full-page writes
-#wal_log_hints = off # also do full page writes of non-critical updates
- # (change requires restart)
-#wal_init_zero = on # zero-fill new WAL files
-#wal_recycle = on # recycle WAL files
-#wal_buffers = -1 # min 32kB, -1 sets based on shared_buffers
- # (change requires restart)
-wal_writer_delay = 900ms # 1-10000 milliseconds
-#wal_writer_flush_after = 1MB # measured in pages, 0 disables
-
-#commit_delay = 0 # range 0-100000, in microseconds
-#commit_siblings = 5 # range 1-1000
-
-# - Checkpoints -
-
-#checkpoint_timeout = 5min # range 30s-1d
+dynamic_shared_memory_type = posix
+synchronous_commit = off
+wal_writer_delay = 900ms
max_wal_size = 1GB
min_wal_size = 80MB
-#checkpoint_completion_target = 0.5 # checkpoint target duration, 0.0 - 1.0
-#checkpoint_flush_after = 256kB # measured in pages, 0 disables
-#checkpoint_warning = 30s # 0 disables
-
-# - Archiving -
-
-#archive_mode = off # enables archiving; off, on, or always
- # (change requires restart)
-#archive_command = '' # command to use to archive a logfile segment
- # placeholders: %p = path of file to archive
- # %f = file name only
- # e.g. 'test ! -f /mnt/server/archivedir/%f && cp %p /mnt/server/archivedir/%f'
-#archive_timeout = 0 # force a logfile segment switch after this
- # number of seconds; 0 disables
-
-# - Archive Recovery -
-
-# These are only used in recovery mode.
-
-#restore_command = '' # command to use to restore an archived logfile segment
- # placeholders: %p = path of file to restore
- # %f = file name only
- # e.g. 'cp /mnt/server/archivedir/%f %p'
- # (change requires restart)
-#archive_cleanup_command = '' # command to execute at every restartpoint
-#recovery_end_command = '' # command to execute at completion of recovery
-
-# - Recovery Target -
-
-# Set these only when performing a targeted recovery.
-
-#recovery_target = '' # 'immediate' to end recovery as soon as a
- # consistent state is reached
- # (change requires restart)
-#recovery_target_name = '' # the named restore point to which recovery will proceed
- # (change requires restart)
-#recovery_target_time = '' # the time stamp up to which recovery will proceed
- # (change requires restart)
-#recovery_target_xid = '' # the transaction ID up to which recovery will proceed
- # (change requires restart)
-#recovery_target_lsn = '' # the WAL LSN up to which recovery will proceed
- # (change requires restart)
-#recovery_target_inclusive = on # Specifies whether to stop:
- # just after the specified recovery target (on)
- # just before the recovery target (off)
- # (change requires restart)
-#recovery_target_timeline = 'latest' # 'current', 'latest', or timeline ID
- # (change requires restart)
-#recovery_target_action = 'pause' # 'pause', 'promote', 'shutdown'
- # (change requires restart)
-
-
-#------------------------------------------------------------------------------
-# REPLICATION
-#------------------------------------------------------------------------------
-
-# - Sending Servers -
-
-# Set these on the master and on any standby that will send replication data.
-
-#max_wal_senders = 10 # max number of walsender processes
- # (change requires restart)
-#wal_keep_segments = 0 # in logfile segments; 0 disables
-#wal_sender_timeout = 60s # in milliseconds; 0 disables
-
-#max_replication_slots = 10 # max number of replication slots
- # (change requires restart)
-#track_commit_timestamp = off # collect timestamp of transaction commit
- # (change requires restart)
-
-# - Master Server -
-
-# These settings are ignored on a standby server.
-
-#synchronous_standby_names = '' # standby servers that provide sync rep
- # method to choose sync standbys, number of sync standbys,
- # and comma-separated list of application_name
- # from standby(s); '*' = all
-#vacuum_defer_cleanup_age = 0 # number of xacts by which cleanup is delayed
-
-# - Standby Servers -
-
-# These settings are ignored on a master server.
-
-#primary_conninfo = '' # connection string to sending server
- # (change requires restart)
-#primary_slot_name = '' # replication slot on sending server
- # (change requires restart)
-#promote_trigger_file = '' # file name whose presence ends recovery
-#hot_standby = on # "off" disallows queries during recovery
- # (change requires restart)
-#max_standby_archive_delay = 30s # max delay before canceling queries
- # when reading WAL from archive;
- # -1 allows indefinite delay
-#max_standby_streaming_delay = 30s # max delay before canceling queries
- # when reading streaming WAL;
- # -1 allows indefinite delay
-#wal_receiver_status_interval = 10s # send replies at least this often
- # 0 disables
-#hot_standby_feedback = off # send info from standby to prevent
- # query conflicts
-#wal_receiver_timeout = 60s # time that receiver waits for
- # communication from master
- # in milliseconds; 0 disables
-#wal_retrieve_retry_interval = 5s # time to wait before retrying to
- # retrieve WAL after a failed attempt
-#recovery_min_apply_delay = 0 # minimum delay for applying changes during recovery
-
-# - Subscribers -
-
-# These settings are ignored on a publisher.
-
-#max_logical_replication_workers = 4 # taken from max_worker_processes
- # (change requires restart)
-#max_sync_workers_per_subscription = 2 # taken from max_logical_replication_workers
-
-
-#------------------------------------------------------------------------------
-# QUERY TUNING
-#------------------------------------------------------------------------------
-
-# - Planner Method Configuration -
-
-#enable_bitmapscan = on
-#enable_hashagg = on
-#enable_hashjoin = on
-#enable_indexscan = on
-#enable_indexonlyscan = on
-#enable_material = on
-#enable_mergejoin = on
-#enable_nestloop = on
-#enable_parallel_append = on
-#enable_seqscan = on
-#enable_sort = on
-#enable_tidscan = on
-#enable_partitionwise_join = off
-#enable_partitionwise_aggregate = off
-#enable_parallel_hash = on
-#enable_partition_pruning = on
-
-# - Planner Cost Constants -
-
-#seq_page_cost = 1.0 # measured on an arbitrary scale
-#random_page_cost = 4.0 # same scale as above
-#cpu_tuple_cost = 0.01 # same scale as above
-#cpu_index_tuple_cost = 0.005 # same scale as above
-#cpu_operator_cost = 0.0025 # same scale as above
-#parallel_tuple_cost = 0.1 # same scale as above
-#parallel_setup_cost = 1000.0 # same scale as above
-
-#jit_above_cost = 100000 # perform JIT compilation if available
- # and query more expensive than this;
- # -1 disables
-#jit_inline_above_cost = 500000 # inline small functions if query is
- # more expensive than this; -1 disables
-#jit_optimize_above_cost = 500000 # use expensive JIT optimizations if
- # query is more expensive than this;
- # -1 disables
-
-#min_parallel_table_scan_size = 8MB
-#min_parallel_index_scan_size = 512kB
-#effective_cache_size = 4GB
-
-# - Genetic Query Optimizer -
-
-#geqo = on
-#geqo_threshold = 12
-#geqo_effort = 5 # range 1-10
-#geqo_pool_size = 0 # selects default based on effort
-#geqo_generations = 0 # selects default based on effort
-#geqo_selection_bias = 2.0 # range 1.5-2.0
-#geqo_seed = 0.0 # range 0.0-1.0
-
-# - Other Planner Options -
-
-#default_statistics_target = 100 # range 1-10000
-#constraint_exclusion = partition # on, off, or partition
-#cursor_tuple_fraction = 0.1 # range 0.0-1.0
-#from_collapse_limit = 8
-#join_collapse_limit = 8 # 1 disables collapsing of explicit
- # JOIN clauses
-#force_parallel_mode = off
-#jit = on # allow JIT compilation
-#plan_cache_mode = auto # auto, force_generic_plan or
- # force_custom_plan
-
-
-#------------------------------------------------------------------------------
-# REPORTING AND LOGGING
-#------------------------------------------------------------------------------
-
-# - Where to Log -
-
-#log_destination = 'stderr' # Valid values are combinations of
- # stderr, csvlog, syslog, and eventlog,
- # depending on platform. csvlog
- # requires logging_collector to be on.
-
-# This is used when logging to stderr:
-#logging_collector = off # Enable capturing of stderr and csvlog
- # into log files. Required to be on for
- # csvlogs.
- # (change requires restart)
-
-# These are only used if logging_collector is on:
-#log_directory = 'log' # directory where log files are written,
- # can be absolute or relative to PGDATA
-#log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log' # log file name pattern,
- # can include strftime() escapes
-#log_file_mode = 0600 # creation mode for log files,
- # begin with 0 to use octal notation
-#log_truncate_on_rotation = off # If on, an existing log file with the
- # same name as the new log file will be
- # truncated rather than appended to.
- # But such truncation only occurs on
- # time-driven rotation, not on restarts
- # or size-driven rotation. Default is
- # off, meaning append to existing files
- # in all cases.
-#log_rotation_age = 1d # Automatic rotation of logfiles will
- # happen after that time. 0 disables.
-#log_rotation_size = 10MB # Automatic rotation of logfiles will
- # happen after that much log output.
- # 0 disables.
-
-# These are relevant when logging to syslog:
-#syslog_facility = 'LOCAL0'
-#syslog_ident = 'postgres'
-#syslog_sequence_numbers = on
-#syslog_split_messages = on
-
-# This is only relevant when logging to eventlog (win32):
-# (change requires restart)
-#event_source = 'PostgreSQL'
-
-# - When to Log -
-
-#log_min_messages = warning # values in order of decreasing detail:
- # debug5
- # debug4
- # debug3
- # debug2
- # debug1
- # info
- # notice
- # warning
- # error
- # log
- # fatal
- # panic
-
-#log_min_error_statement = error # values in order of decreasing detail:
- # debug5
- # debug4
- # debug3
- # debug2
- # debug1
- # info
- # notice
- # warning
- # error
- # log
- # fatal
- # panic (effectively off)
-
-#log_min_duration_statement = 0 # -1 is disabled, 0 logs all statements
- # and their durations, > 0 logs only
- # statements running at least this number
- # of milliseconds
-
-#log_transaction_sample_rate = 0.0 # Fraction of transactions whose statements
- # are logged regardless of their duration. 1.0 logs all
- # statements from all transactions, 0.0 never logs.
-
-# - What to Log -
-
-#debug_print_parse = off
-#debug_print_rewritten = off
-#debug_print_plan = off
-#debug_pretty_print = on
-#log_checkpoints = off
-#log_connections = off
-#log_disconnections = off
-#log_duration = off
-#log_error_verbosity = default # terse, default, or verbose messages
-#log_hostname = off
-#log_line_prefix = '%m [%p] ' # special values:
- # %a = application name
- # %u = user name
- # %d = database name
- # %r = remote host and port
- # %h = remote host
- # %p = process ID
- # %t = timestamp without milliseconds
- # %m = timestamp with milliseconds
- # %n = timestamp with milliseconds (as a Unix epoch)
- # %i = command tag
- # %e = SQL state
- # %c = session ID
- # %l = session line number
- # %s = session start timestamp
- # %v = virtual transaction ID
- # %x = transaction ID (0 if none)
- # %q = stop here in non-session
- # processes
- # %% = '%'
- # e.g. '<%u%%%d> '
-#log_lock_waits = off # log lock waits >= deadlock_timeout
-#log_statement = 'none' # none, ddl, mod, all
-#log_replication_commands = off
-#log_temp_files = -1 # log temporary files equal or larger
- # than the specified size in kilobytes;
- # -1 disables, 0 logs all temp files
-log_timezone = 'Etc/UTC'
-
-#------------------------------------------------------------------------------
-# PROCESS TITLE
-#------------------------------------------------------------------------------
-
-#cluster_name = '' # added to process titles if nonempty
- # (change requires restart)
-#update_process_title = on
-
-
-#------------------------------------------------------------------------------
-# STATISTICS
-#------------------------------------------------------------------------------
-
-# - Query and Index Statistics Collector -
-
-#track_activities = on
-#track_counts = on
-#track_io_timing = off
-#track_functions = none # none, pl, all
-#track_activity_query_size = 1024 # (change requires restart)
-#stats_temp_directory = 'pg_stat_tmp'
-
-
-# - Monitoring -
-
-#log_parser_stats = off
-#log_planner_stats = off
-#log_executor_stats = off
-#log_statement_stats = off
-
-
-#------------------------------------------------------------------------------
-# AUTOVACUUM
-#------------------------------------------------------------------------------
-
-#autovacuum = on # Enable autovacuum subprocess? 'on'
- # requires track_counts to also be on.
-#log_autovacuum_min_duration = -1 # -1 disables, 0 logs all actions and
- # their durations, > 0 logs only
- # actions running at least this number
- # of milliseconds.
-#autovacuum_max_workers = 3 # max number of autovacuum subprocesses
- # (change requires restart)
-#autovacuum_naptime = 1min # time between autovacuum runs
-#autovacuum_vacuum_threshold = 50 # min number of row updates before
- # vacuum
-#autovacuum_analyze_threshold = 50 # min number of row updates before
- # analyze
-#autovacuum_vacuum_scale_factor = 0.2 # fraction of table size before vacuum
-#autovacuum_analyze_scale_factor = 0.1 # fraction of table size before analyze
-#autovacuum_freeze_max_age = 200000000 # maximum XID age before forced vacuum
- # (change requires restart)
-#autovacuum_multixact_freeze_max_age = 400000000 # maximum multixact age
- # before forced vacuum
- # (change requires restart)
-#autovacuum_vacuum_cost_delay = 2ms # default vacuum cost delay for
- # autovacuum, in milliseconds;
- # -1 means use vacuum_cost_delay
-#autovacuum_vacuum_cost_limit = -1 # default vacuum cost limit for
- # autovacuum, -1 means use
- # vacuum_cost_limit
-
-
-#------------------------------------------------------------------------------
-# CLIENT CONNECTION DEFAULTS
-#------------------------------------------------------------------------------
-
-# - Statement Behavior -
-
-#client_min_messages = notice # values in order of decreasing detail:
- # debug5
- # debug4
- # debug3
- # debug2
- # debug1
- # log
- # notice
- # warning
- # error
-#search_path = '"$user", public' # schema names
-#row_security = on
-#default_tablespace = '' # a tablespace name, '' uses the default
-#temp_tablespaces = '' # a list of tablespace names, '' uses
- # only default tablespace
-#default_table_access_method = 'heap'
-#check_function_bodies = on
-#default_transaction_isolation = 'read committed'
-#default_transaction_read_only = off
-#default_transaction_deferrable = off
-#session_replication_role = 'origin'
-#statement_timeout = 0 # in milliseconds, 0 is disabled
-#lock_timeout = 0 # in milliseconds, 0 is disabled
-#idle_in_transaction_session_timeout = 0 # in milliseconds, 0 is disabled
-#vacuum_freeze_min_age = 50000000
-#vacuum_freeze_table_age = 150000000
-#vacuum_multixact_freeze_min_age = 5000000
-#vacuum_multixact_freeze_table_age = 150000000
-#vacuum_cleanup_index_scale_factor = 0.1 # fraction of total number of tuples
- # before index cleanup, 0 always performs
- # index cleanup
-#bytea_output = 'hex' # hex, escape
-#xmlbinary = 'base64'
-#xmloption = 'content'
-#gin_fuzzy_search_limit = 0
-#gin_pending_list_limit = 4MB
-
-# - Locale and Formatting -
+# log_min_duration_statement = 0
+log_timezone = 'Europe/Madrid'
datestyle = 'iso, mdy'
-#intervalstyle = 'postgres'
-timezone = 'Etc/UTC'
-#timezone_abbreviations = 'Default' # Select the set of available time zone
- # abbreviations. Currently, there are
- # Default
- # Australia (historical usage)
- # India
- # You can create your own file in
- # share/timezonesets/.
-#extra_float_digits = 1 # min -15, max 3; any value >0 actually
- # selects precise output mode
-#client_encoding = sql_ascii # actually, defaults to database
- # encoding
-
-# These settings are initialized by initdb, but they can be changed.
-lc_messages = 'en_US.utf8' # locale for system error message
- # strings
-lc_monetary = 'en_US.utf8' # locale for monetary formatting
-lc_numeric = 'en_US.utf8' # locale for number formatting
-lc_time = 'en_US.utf8' # locale for time formatting
-
-# default configuration for text search
+timezone = 'Europe/Madrid'
+lc_messages = 'en_US.utf8'
+lc_monetary = 'en_US.utf8'
+lc_numeric = 'en_US.utf8'
+lc_time = 'en_US.utf8'
default_text_search_config = 'pg_catalog.english'
-# - Shared Library Preloading -
-
-#shared_preload_libraries = '' # (change requires restart)
-#local_preload_libraries = ''
-#session_preload_libraries = ''
-#jit_provider = 'llvmjit' # JIT library to use
-
-# - Other Defaults -
-
-#dynamic_library_path = '$libdir'
-
-
-#------------------------------------------------------------------------------
-# LOCK MANAGEMENT
-#------------------------------------------------------------------------------
-
-#deadlock_timeout = 1s
-#max_locks_per_transaction = 64 # min 10
- # (change requires restart)
-#max_pred_locks_per_transaction = 64 # min 10
- # (change requires restart)
-#max_pred_locks_per_relation = -2 # negative values mean
- # (max_pred_locks_per_transaction
- # / -max_pred_locks_per_relation) - 1
-#max_pred_locks_per_page = 2 # min 0
-
-
-#------------------------------------------------------------------------------
-# VERSION AND PLATFORM COMPATIBILITY
-#------------------------------------------------------------------------------
-
-# - Previous PostgreSQL Versions -
-
-#array_nulls = on
-#backslash_quote = safe_encoding # on, off, or safe_encoding
-#escape_string_warning = on
-#lo_compat_privileges = off
-#operator_precedence_warning = off
-#quote_all_identifiers = off
-#standard_conforming_strings = on
-#synchronize_seqscans = on
-
-# - Other Platforms and Clients -
-
-#transform_null_equals = off
-
-
-#------------------------------------------------------------------------------
-# ERROR HANDLING
-#------------------------------------------------------------------------------
-
-#exit_on_error = off # terminate session on any error?
-#restart_after_crash = on # reinitialize after backend crash?
-#data_sync_retry = off # retry or panic on failure to fsync
- # data?
- # (change requires restart)
-
-
-#------------------------------------------------------------------------------
-# CONFIG FILE INCLUDES
-#------------------------------------------------------------------------------
-
-# These options allow settings to be loaded from files other than the
-# default postgresql.conf. Note that these are directives, not variable
-# assignments, so they can usefully be given more than once.
-
-#include_dir = '...' # include files ending in '.conf' from
- # a directory, e.g., 'conf.d'
-#include_if_exists = '...' # include file only if it exists
-#include = '...' # include file
-
-
-#------------------------------------------------------------------------------
-# CUSTOMIZED OPTIONS
-#------------------------------------------------------------------------------
-
-# Add settings for extensions here
-
-
diff --git a/docker/testenv/Dockerfile-backend b/docker/testenv/Dockerfile-backend
index b84fef87d6..03906855e0 100644
--- a/docker/testenv/Dockerfile-backend
+++ b/docker/testenv/Dockerfile-backend
@@ -1,4 +1,4 @@
-FROM azul/zulu-openjdk-debian:14
+FROM adoptopenjdk/openjdk15:debianslim-jre
LABEL maintainer="Andrey Antukh "
ADD ./bundle/backend/ /opt/bundle/
WORKDIR /opt/bundle
diff --git a/docker/testenv/Dockerfile-exporter b/docker/testenv/Dockerfile-exporter
index 40bce1cdd6..eaeee4a9fe 100644
--- a/docker/testenv/Dockerfile-exporter
+++ b/docker/testenv/Dockerfile-exporter
@@ -1,24 +1,35 @@
-FROM debian:buster
+FROM debian:buster-slim
LABEL maintainer="Andrey Antukh "
ARG DEBIAN_FRONTEND=noninteractive
-ENV LANG=en_US.UTF-8 LC_ALL=C.UTF-8
+ENV LANG=en_US.UTF-8 \
+ LC_ALL=en_US.UTF-8 \
+ NODE_VERSION=v12.18.4
RUN set -ex; \
mkdir -p /etc/resolvconf/resolv.conf.d; \
- echo "nameserver 8.8.8.8" > /etc/resolvconf/resolv.conf.d/tail;
-
-RUN set -ex; \
- apt-get update && \
- apt-get install -yq \
+ echo "nameserver 8.8.8.8" > /etc/resolvconf/resolv.conf.d/tail; \
+ apt-get -qq update; \
+ apt-get -qqy install --no-install-recommends \
locales \
gnupg2 \
ca-certificates \
wget \
+ sudo \
+ vim \
curl \
bash \
+ xz-utils \
rlwrap \
+ ; \
+ echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen; \
+ locale-gen; \
+ rm -rf /var/lib/apt/lists/*;
+
+RUN set -ex; \
+ apt-get -qq update; \
+ apt-get -qqy install \
imagemagick \
netpbm \
potrace \
@@ -69,11 +80,11 @@ RUN set -ex; \
RUN set -ex; \
mkdir -p /tmp/node; \
cd /tmp/node; \
- export PATH="$PATH:/usr/local/node-v12.18.3/bin"; \
- wget https://nodejs.org/dist/v12.18.3/node-v12.18.3-linux-x64.tar.xz; \
- tar xvf node-v12.18.3-linux-x64.tar.xz; \
- mv /tmp/node/node-v12.18.3-linux-x64 /usr/local/node-v12.18.3; \
- /usr/local/node-v12.18.3/bin/npm install -g yarn; \
+ export PATH="$PATH:/usr/local/nodejs/bin"; \
+ wget https://nodejs.org/dist/$NODE_VERSION/node-$NODE_VERSION-linux-x64.tar.xz; \
+ tar xvf node-$NODE_VERSION-linux-x64.tar.xz; \
+ mv /tmp/node/node-$NODE_VERSION-linux-x64 /usr/local/nodejs; \
+ /usr/local/nodejs/bin/npm install -g yarn; \
rm -rf /tmp/node;
WORKDIR /opt/app
@@ -81,7 +92,7 @@ WORKDIR /opt/app
ADD ./bundle/exporter/ /opt/app/
RUN set -ex; \
- export PATH="$PATH:/usr/local/node-v12.18.3/bin"; \
+ export PATH="$PATH:/usr/local/nodejs/bin"; \
yarn install;
-CMD ["/usr/local/node-v12.18.3/bin/node", "app.js"]
+CMD ["/usr/local/nodejs/bin/node", "app.js"]
diff --git a/docs/01-Development-Environment.md b/docs/01-Development-Environment.md
index 4a720a4f96..bb5771ebf3 100644
--- a/docs/01-Development-Environment.md
+++ b/docs/01-Development-Environment.md
@@ -1,6 +1,6 @@
# Developer Guide #
-This is a generic "getting started" guide for the uxbox platform. It
+This is a generic "getting started" guide for the Penpot platform. It
intends to explain how to get the development environment up and
running with many additional tips.
@@ -148,7 +148,7 @@ For more information, please refer to: `03-Backend-Guide.md`.
## Start the testenv ##
The purpose of the testenv (Test Environment) is provide an easy way
-to get uxbox running in local pc without getting into the full
+to get Penpot running in local pc without getting into the full
development environment.
As first step we still need to build devenv image because that image
@@ -185,5 +185,5 @@ This will generate the necessary docker images ready to be executed.
And finally, start the docker-compose:
```bash
-./manage.sh start-devenv
+./manage.sh start-testenv
```
diff --git a/docs/02-Frontend-Developer-Guide.md b/docs/02-Frontend-Developer-Guide.md
index edcfef8d55..50b137c92d 100644
--- a/docs/02-Frontend-Developer-Guide.md
+++ b/docs/02-Frontend-Developer-Guide.md
@@ -26,19 +26,22 @@ app.util.debug.debug_all()
app.util.debug.debug_none()
```
-## Traces and step-by-step debugging
+## Logging, Tracing & Debugging
-There are some useful functions to trace program execution:
+As a traditional way for debugging and tracing you have the followimg approach:
+
+
+Print data to the devtool console using clojurescript helper:
+**prn**. This helper automatically formats the clojure and js data
+structures as plain EDN for easy visual inspection of the data and the
+type of the data.
```clojure
-(println "message" expression)
- ; Outputs data to the devtools console. Clojure variables are converted to string, as in (str) function.
+(prn "message" expression)
```
-```clojure
-(js/console.log "message" (clj->js expression))
- ; Clojure values are converted to equivalent js objects, and displayed as a foldable widget in console.
-```
+An alternative is using the pprint function, usefull for pretty
+printing a medium-big data sturcture for completly understand it.
```clojure
(:require [cljs.pprint :refer [pprint]])
@@ -46,19 +49,82 @@ There are some useful functions to trace program execution:
; Outputs a clojure value as a string, nicely formatted and with data type information.
```
+Use the js native functions for printing data. The clj->js converts
+the clojure data sturcture to js data sturcture and it is
+inspeccionable in the devtools console.
+
+```clojure
+(js/console.log "message" (clj->js expression))
+```
+
+
Also we can insert breakpoints in the code with this function:
```clojure
(js-debugger)
```
-You can also set a breakpoint from the sources tab in devtools. One way of locating a source file is to
-output a trace with (js/console.log) and then clicking in the source link that shows in the console.
+You can also set a breakpoint from the sources tab in devtools. One
+way of locating a source file is to output a trace with
+(js/console.log) and then clicking in the source link that shows in
+the console.
+
+
+### Logging framework
+
+Additionally to the traditional way of putting traces in the code, we
+have a logging framework with steroids. It is usefull for casual
+debugging (as replacement for a `prn` and `js/console.log`) and as a
+permanent traces in the code.
+
+You have the ability to specify the logging level per namespace and
+all logging is ellided in production build.
+
+Lets start with a simple example:
+
+```clojure
+(ns some.ns
+ (:require [app.util.logging :as log]))
+
+;; This function sets the level to the current namespace; messages
+;; with level behind this will not be printed.
+(log/set-level! :info)
+
+
+;; Log some data; The `app.util.logging` has the following
+;; functions/macros:
+
+(log/error :msg "error message")
+(log/warn :msg "warn message")
+(log/info :msg "info message")
+(log/debug :msg "debug message")
+(log/trace :msg "trace message")
+```
+
+Each macro accept arbitrary number of key values pairs:
+
+```clojure
+(log/info :foo "bar" :msg "test" :value 1 :items #{1 2 3})
+```
+
+Some keys ara treated as special cases for helping in debugging:
+
+```clojure
+;; The special case for :js/whatever; if you namespace the key
+;; with `js/`, the variable will be printed as javascript
+;; inspectionable object.
+
+(let [foobar {:a 1 :b 2}]
+ (log/info :msg "Some data" :js/data foobar))
+
+;; The special case for `:err`; If you attach this key, the
+;; exception stack trace is printed as additional log entry.
+```
## Access to clojure from javascript console
-The uxbox namespace of the main application is exported, so that is
+The penpot namespace of the main application is exported, so that is
accessible from javascript console in Chrome developer tools. Object
names and data types are converted to javascript style. For example
you can emit the event to reset zoom level by typing this at the
diff --git a/docs/04-Common-Developer-Guide.md b/docs/04-Common-Developer-Guide.md
index 3838edf823..366d577aef 100644
--- a/docs/04-Common-Developer-Guide.md
+++ b/docs/04-Common-Developer-Guide.md
@@ -6,7 +6,7 @@ and backend, such as: code style hints, architecture dicisions, etc...
## Assertions ##
-UXBOX source code has this types of assertions:
+Penpot source code has this types of assertions:
**assert**: just using the clojure builtin `assert` macro.
@@ -55,7 +55,7 @@ This macro enables you have assetions on production code.
**Why don't use the `clojure.spec.alpha/assert` instead of the `app.common.spec/assert`?**
-The uxbox variant does not peforms additional runtime checks for know
+The Penpot variant does not peforms additional runtime checks for know
if asserts are disabled in "runtime". As a result it generates much
simplier code at development and production builds.
diff --git a/docs/screenshot.png b/docs/screenshot.png
new file mode 100644
index 0000000000..c7448c22aa
Binary files /dev/null and b/docs/screenshot.png differ
diff --git a/exporter/shadow-cljs.edn b/exporter/shadow-cljs.edn
index 421623be4e..c8a8a5f5e1 100644
--- a/exporter/shadow-cljs.edn
+++ b/exporter/shadow-cljs.edn
@@ -1,9 +1,9 @@
{:dependencies
- [[funcool/promesa "5.1.0"]
+ [[funcool/promesa "6.0.0"]
[danlentz/clj-uuid "0.1.9"]
[funcool/cuerdas "2020.03.26-3"]
- [lambdaisland/glogi "1.0.63"]
- [metosin/reitit-core "0.5.2"]
+ [lambdaisland/glogi "1.0.74"]
+ [metosin/reitit-core "0.5.9"]
[com.cognitect/transit-cljs "0.8.264"]
[frankiesardo/linked "1.3.0"]]
diff --git a/frontend/deps.edn b/frontend/deps.edn
index d5cf527299..072351097e 100644
--- a/frontend/deps.edn
+++ b/frontend/deps.edn
@@ -5,22 +5,20 @@
com.cognitect/transit-cljs {:mvn/version "0.8.264"}
environ/environ {:mvn/version "1.2.0"}
- metosin/reitit-core {:mvn/version "0.5.5"}
- expound/expound {:mvn/version "0.8.5"}
+ metosin/reitit-core {:mvn/version "0.5.9"}
+ expound/expound {:mvn/version "0.8.6"}
danlentz/clj-uuid {:mvn/version "0.1.9"}
frankiesardo/linked {:mvn/version "1.3.0"}
- funcool/lentes {:mvn/version "1.4.0-SNAPSHOT"}
-
funcool/beicon {:mvn/version "2020.05.08-2"}
funcool/cuerdas {:mvn/version "2020.03.26-3"}
funcool/okulary {:mvn/version "2020.04.14-0"}
funcool/potok {:mvn/version "2020.08.10-2"}
- funcool/promesa {:mvn/version "5.1.0"}
- funcool/rumext {:mvn/version "2020.08.21-0"}
+ funcool/promesa {:mvn/version "6.0.0"}
+ funcool/rumext {:mvn/version "2020.10.14-1"}
- lambdaisland/uri {:mvn/version "1.3.45"
+ lambdaisland/uri {:mvn/version "1.4.54"
:exclusions [org.clojure/data.json]}
}
@@ -37,14 +35,14 @@
funcool/datoteka {:mvn/version "1.2.0"}
binaryage/devtools {:mvn/version "RELEASE"}
- thheller/shadow-cljs {:mvn/version "2.11.4"}
+ thheller/shadow-cljs {:mvn/version "2.11.5"}
;; i18n parsing
carocad/parcera {:mvn/version "0.11.0"}
org.antlr/antlr4-runtime {:mvn/version "4.7"}}}
:outdated
- {:extra-deps {olical/depot {:mvn/version "1.8.4"}}
+ {:extra-deps {olical/depot {:mvn/version "RELEASE"}}
:main-opts ["-m" "depot.outdated.main"]}
:repl
diff --git a/frontend/package.json b/frontend/package.json
index 6123343b65..1cb1fcb125 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -6,14 +6,14 @@
"license": "SEE LICENSE IN ",
"repository": {
"type": "git",
- "url": "https://github.com/app/app"
+ "url": "https://github.com/penpot/penpot"
},
"browserslist": [
"defaults"
],
"scripts": {},
"devDependencies": {
- "autoprefixer": "^9.8.6",
+ "autoprefixer": "^10.0.1",
"clean-css": "^4.2.3",
"gulp": "4.0.2",
"gulp-gzip": "^1.4.2",
@@ -22,21 +22,21 @@
"gulp-rename": "^2.0.0",
"gulp-svg-sprite": "^1.5.0",
"mkdirp": "^1.0.4",
- "postcss": "^7.0.32",
+ "postcss": "^8.1.2",
"rimraf": "^3.0.0",
"sass": "^1.26.10",
- "shadow-cljs": "^2.11.0"
+ "shadow-cljs": "2.11.5"
},
"dependencies": {
"date-fns": "^2.15.0",
"map-stream": "0.0.7",
"mousetrap": "^1.6.5",
"randomcolor": "^0.6.2",
- "react": "^16.13.1",
- "react-dom": "^16.13.1",
- "rxjs": "^7.0.0-beta.4",
- "slate": "^0.58.4",
- "slate-react": "^0.58.4",
+ "react": "17.0.1",
+ "react-dom": "17.0.1",
+ "rxjs": "7.0.0-beta.4",
+ "slate": "^0.59.0",
+ "slate-react": "^0.59.0",
"source-map-support": "^0.5.16",
"tdigest": "^0.1.1",
"xregexp": "^4.3.0"
diff --git a/frontend/resources/fonts/WorkSans-Black.eot b/frontend/resources/fonts/WorkSans-Black.eot
new file mode 100644
index 0000000000..53bb9c861a
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-Black.eot differ
diff --git a/frontend/resources/fonts/WorkSans-Black.svg b/frontend/resources/fonts/WorkSans-Black.svg
new file mode 100644
index 0000000000..766a534d5e
--- /dev/null
+++ b/frontend/resources/fonts/WorkSans-Black.svg
@@ -0,0 +1,16248 @@
+
+
+
diff --git a/frontend/resources/fonts/WorkSans-Black.ttf b/frontend/resources/fonts/WorkSans-Black.ttf
new file mode 100644
index 0000000000..64f046239a
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-Black.ttf differ
diff --git a/frontend/resources/fonts/WorkSans-Black.woff b/frontend/resources/fonts/WorkSans-Black.woff
new file mode 100644
index 0000000000..bed6bf1b46
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-Black.woff differ
diff --git a/frontend/resources/fonts/WorkSans-Black.woff2 b/frontend/resources/fonts/WorkSans-Black.woff2
new file mode 100644
index 0000000000..3b24dc078b
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-Black.woff2 differ
diff --git a/frontend/resources/fonts/WorkSans-BlackItalic.eot b/frontend/resources/fonts/WorkSans-BlackItalic.eot
new file mode 100644
index 0000000000..de216159ec
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-BlackItalic.eot differ
diff --git a/frontend/resources/fonts/WorkSans-BlackItalic.svg b/frontend/resources/fonts/WorkSans-BlackItalic.svg
new file mode 100644
index 0000000000..fed596c151
--- /dev/null
+++ b/frontend/resources/fonts/WorkSans-BlackItalic.svg
@@ -0,0 +1,16277 @@
+
+
+
diff --git a/frontend/resources/fonts/WorkSans-BlackItalic.ttf b/frontend/resources/fonts/WorkSans-BlackItalic.ttf
new file mode 100644
index 0000000000..6018540cac
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-BlackItalic.ttf differ
diff --git a/frontend/resources/fonts/WorkSans-BlackItalic.woff b/frontend/resources/fonts/WorkSans-BlackItalic.woff
new file mode 100644
index 0000000000..cb758bf784
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-BlackItalic.woff differ
diff --git a/frontend/resources/fonts/WorkSans-BlackItalic.woff2 b/frontend/resources/fonts/WorkSans-BlackItalic.woff2
new file mode 100644
index 0000000000..23ff634571
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-BlackItalic.woff2 differ
diff --git a/frontend/resources/fonts/WorkSans-Bold.eot b/frontend/resources/fonts/WorkSans-Bold.eot
new file mode 100644
index 0000000000..230d453b99
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-Bold.eot differ
diff --git a/frontend/resources/fonts/WorkSans-Bold.svg b/frontend/resources/fonts/WorkSans-Bold.svg
new file mode 100644
index 0000000000..780011b827
--- /dev/null
+++ b/frontend/resources/fonts/WorkSans-Bold.svg
@@ -0,0 +1,25069 @@
+
+
+
diff --git a/frontend/resources/fonts/WorkSans-Bold.ttf b/frontend/resources/fonts/WorkSans-Bold.ttf
new file mode 100644
index 0000000000..1db5ef6b0c
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-Bold.ttf differ
diff --git a/frontend/resources/fonts/WorkSans-Bold.woff b/frontend/resources/fonts/WorkSans-Bold.woff
new file mode 100644
index 0000000000..f829987ae9
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-Bold.woff differ
diff --git a/frontend/resources/fonts/WorkSans-Bold.woff2 b/frontend/resources/fonts/WorkSans-Bold.woff2
new file mode 100644
index 0000000000..c8961467da
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-Bold.woff2 differ
diff --git a/frontend/resources/fonts/WorkSans-BoldItalic.eot b/frontend/resources/fonts/WorkSans-BoldItalic.eot
new file mode 100644
index 0000000000..aab9667dcb
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-BoldItalic.eot differ
diff --git a/frontend/resources/fonts/WorkSans-BoldItalic.svg b/frontend/resources/fonts/WorkSans-BoldItalic.svg
new file mode 100644
index 0000000000..e339f34713
--- /dev/null
+++ b/frontend/resources/fonts/WorkSans-BoldItalic.svg
@@ -0,0 +1,24489 @@
+
+
+
diff --git a/frontend/resources/fonts/WorkSans-BoldItalic.ttf b/frontend/resources/fonts/WorkSans-BoldItalic.ttf
new file mode 100644
index 0000000000..f045d9816f
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-BoldItalic.ttf differ
diff --git a/frontend/resources/fonts/WorkSans-BoldItalic.woff b/frontend/resources/fonts/WorkSans-BoldItalic.woff
new file mode 100644
index 0000000000..0d69d0f65b
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-BoldItalic.woff differ
diff --git a/frontend/resources/fonts/WorkSans-BoldItalic.woff2 b/frontend/resources/fonts/WorkSans-BoldItalic.woff2
new file mode 100644
index 0000000000..bb8dfd0593
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-BoldItalic.woff2 differ
diff --git a/frontend/resources/fonts/WorkSans-ExtraBold.eot b/frontend/resources/fonts/WorkSans-ExtraBold.eot
new file mode 100644
index 0000000000..8b733670f8
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-ExtraBold.eot differ
diff --git a/frontend/resources/fonts/WorkSans-ExtraBold.svg b/frontend/resources/fonts/WorkSans-ExtraBold.svg
new file mode 100644
index 0000000000..55e6f50e4d
--- /dev/null
+++ b/frontend/resources/fonts/WorkSans-ExtraBold.svg
@@ -0,0 +1,25065 @@
+
+
+
diff --git a/frontend/resources/fonts/WorkSans-ExtraBold.ttf b/frontend/resources/fonts/WorkSans-ExtraBold.ttf
new file mode 100644
index 0000000000..d56644d2fd
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-ExtraBold.ttf differ
diff --git a/frontend/resources/fonts/WorkSans-ExtraBold.woff b/frontend/resources/fonts/WorkSans-ExtraBold.woff
new file mode 100644
index 0000000000..4741d26470
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-ExtraBold.woff differ
diff --git a/frontend/resources/fonts/WorkSans-ExtraBold.woff2 b/frontend/resources/fonts/WorkSans-ExtraBold.woff2
new file mode 100644
index 0000000000..9aa1eca0b8
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-ExtraBold.woff2 differ
diff --git a/frontend/resources/fonts/WorkSans-ExtraBoldItalic.eot b/frontend/resources/fonts/WorkSans-ExtraBoldItalic.eot
new file mode 100644
index 0000000000..4b032160f9
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-ExtraBoldItalic.eot differ
diff --git a/frontend/resources/fonts/WorkSans-ExtraBoldItalic.svg b/frontend/resources/fonts/WorkSans-ExtraBoldItalic.svg
new file mode 100644
index 0000000000..4ca2b3d807
--- /dev/null
+++ b/frontend/resources/fonts/WorkSans-ExtraBoldItalic.svg
@@ -0,0 +1,24462 @@
+
+
+
diff --git a/frontend/resources/fonts/WorkSans-ExtraBoldItalic.ttf b/frontend/resources/fonts/WorkSans-ExtraBoldItalic.ttf
new file mode 100644
index 0000000000..5a9e046b80
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-ExtraBoldItalic.ttf differ
diff --git a/frontend/resources/fonts/WorkSans-ExtraBoldItalic.woff b/frontend/resources/fonts/WorkSans-ExtraBoldItalic.woff
new file mode 100644
index 0000000000..accdcb01d0
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-ExtraBoldItalic.woff differ
diff --git a/frontend/resources/fonts/WorkSans-ExtraBoldItalic.woff2 b/frontend/resources/fonts/WorkSans-ExtraBoldItalic.woff2
new file mode 100644
index 0000000000..7ce82c82d0
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-ExtraBoldItalic.woff2 differ
diff --git a/frontend/resources/fonts/WorkSans-ExtraLight.eot b/frontend/resources/fonts/WorkSans-ExtraLight.eot
new file mode 100644
index 0000000000..5830248d7b
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-ExtraLight.eot differ
diff --git a/frontend/resources/fonts/WorkSans-ExtraLight.svg b/frontend/resources/fonts/WorkSans-ExtraLight.svg
new file mode 100644
index 0000000000..cf69107509
--- /dev/null
+++ b/frontend/resources/fonts/WorkSans-ExtraLight.svg
@@ -0,0 +1,25774 @@
+
+
+
diff --git a/frontend/resources/fonts/WorkSans-ExtraLight.ttf b/frontend/resources/fonts/WorkSans-ExtraLight.ttf
new file mode 100644
index 0000000000..88ae413be7
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-ExtraLight.ttf differ
diff --git a/frontend/resources/fonts/WorkSans-ExtraLight.woff b/frontend/resources/fonts/WorkSans-ExtraLight.woff
new file mode 100644
index 0000000000..6cca5784b2
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-ExtraLight.woff differ
diff --git a/frontend/resources/fonts/WorkSans-ExtraLight.woff2 b/frontend/resources/fonts/WorkSans-ExtraLight.woff2
new file mode 100644
index 0000000000..ccd3f6bb5b
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-ExtraLight.woff2 differ
diff --git a/frontend/resources/fonts/WorkSans-ExtraLightItalic.eot b/frontend/resources/fonts/WorkSans-ExtraLightItalic.eot
new file mode 100644
index 0000000000..49762c9733
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-ExtraLightItalic.eot differ
diff --git a/frontend/resources/fonts/WorkSans-ExtraLightItalic.svg b/frontend/resources/fonts/WorkSans-ExtraLightItalic.svg
new file mode 100644
index 0000000000..b57d337cdc
--- /dev/null
+++ b/frontend/resources/fonts/WorkSans-ExtraLightItalic.svg
@@ -0,0 +1,25162 @@
+
+
+
diff --git a/frontend/resources/fonts/WorkSans-ExtraLightItalic.ttf b/frontend/resources/fonts/WorkSans-ExtraLightItalic.ttf
new file mode 100644
index 0000000000..816c64df5b
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-ExtraLightItalic.ttf differ
diff --git a/frontend/resources/fonts/WorkSans-ExtraLightItalic.woff b/frontend/resources/fonts/WorkSans-ExtraLightItalic.woff
new file mode 100644
index 0000000000..767271a33b
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-ExtraLightItalic.woff differ
diff --git a/frontend/resources/fonts/WorkSans-ExtraLightItalic.woff2 b/frontend/resources/fonts/WorkSans-ExtraLightItalic.woff2
new file mode 100644
index 0000000000..09f535ef76
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-ExtraLightItalic.woff2 differ
diff --git a/frontend/resources/fonts/WorkSans-Italic.eot b/frontend/resources/fonts/WorkSans-Italic.eot
new file mode 100644
index 0000000000..ac5da85c3a
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-Italic.eot differ
diff --git a/frontend/resources/fonts/WorkSans-Italic.svg b/frontend/resources/fonts/WorkSans-Italic.svg
new file mode 100644
index 0000000000..6b80809b58
--- /dev/null
+++ b/frontend/resources/fonts/WorkSans-Italic.svg
@@ -0,0 +1,21510 @@
+
+
+
diff --git a/frontend/resources/fonts/WorkSans-Italic.ttf b/frontend/resources/fonts/WorkSans-Italic.ttf
new file mode 100644
index 0000000000..d7b91d4c54
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-Italic.ttf differ
diff --git a/frontend/resources/fonts/WorkSans-Italic.woff b/frontend/resources/fonts/WorkSans-Italic.woff
new file mode 100644
index 0000000000..ff7fa7e8c2
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-Italic.woff differ
diff --git a/frontend/resources/fonts/WorkSans-Italic.woff2 b/frontend/resources/fonts/WorkSans-Italic.woff2
new file mode 100644
index 0000000000..e80bfe69d7
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-Italic.woff2 differ
diff --git a/frontend/resources/fonts/WorkSans-Light.eot b/frontend/resources/fonts/WorkSans-Light.eot
new file mode 100644
index 0000000000..1fb80b6584
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-Light.eot differ
diff --git a/frontend/resources/fonts/WorkSans-Light.svg b/frontend/resources/fonts/WorkSans-Light.svg
new file mode 100644
index 0000000000..2f8bd2d709
--- /dev/null
+++ b/frontend/resources/fonts/WorkSans-Light.svg
@@ -0,0 +1,25793 @@
+
+
+
diff --git a/frontend/resources/fonts/WorkSans-Light.ttf b/frontend/resources/fonts/WorkSans-Light.ttf
new file mode 100644
index 0000000000..3a7768d742
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-Light.ttf differ
diff --git a/frontend/resources/fonts/WorkSans-Light.woff b/frontend/resources/fonts/WorkSans-Light.woff
new file mode 100644
index 0000000000..3fe4e73bde
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-Light.woff differ
diff --git a/frontend/resources/fonts/WorkSans-Light.woff2 b/frontend/resources/fonts/WorkSans-Light.woff2
new file mode 100644
index 0000000000..e5095d5fd9
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-Light.woff2 differ
diff --git a/frontend/resources/fonts/WorkSans-LightItalic.eot b/frontend/resources/fonts/WorkSans-LightItalic.eot
new file mode 100644
index 0000000000..a2fef572a8
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-LightItalic.eot differ
diff --git a/frontend/resources/fonts/WorkSans-LightItalic.svg b/frontend/resources/fonts/WorkSans-LightItalic.svg
new file mode 100644
index 0000000000..d730e13d72
--- /dev/null
+++ b/frontend/resources/fonts/WorkSans-LightItalic.svg
@@ -0,0 +1,25177 @@
+
+
+
diff --git a/frontend/resources/fonts/WorkSans-LightItalic.ttf b/frontend/resources/fonts/WorkSans-LightItalic.ttf
new file mode 100644
index 0000000000..f186b05393
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-LightItalic.ttf differ
diff --git a/frontend/resources/fonts/WorkSans-LightItalic.woff b/frontend/resources/fonts/WorkSans-LightItalic.woff
new file mode 100644
index 0000000000..5f21a50e2e
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-LightItalic.woff differ
diff --git a/frontend/resources/fonts/WorkSans-LightItalic.woff2 b/frontend/resources/fonts/WorkSans-LightItalic.woff2
new file mode 100644
index 0000000000..5745d422b9
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-LightItalic.woff2 differ
diff --git a/frontend/resources/fonts/WorkSans-Medium.eot b/frontend/resources/fonts/WorkSans-Medium.eot
new file mode 100644
index 0000000000..67eda94082
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-Medium.eot differ
diff --git a/frontend/resources/fonts/WorkSans-Medium.svg b/frontend/resources/fonts/WorkSans-Medium.svg
new file mode 100644
index 0000000000..2d9fdb520f
--- /dev/null
+++ b/frontend/resources/fonts/WorkSans-Medium.svg
@@ -0,0 +1,25050 @@
+
+
+
diff --git a/frontend/resources/fonts/WorkSans-Medium.ttf b/frontend/resources/fonts/WorkSans-Medium.ttf
new file mode 100644
index 0000000000..66af7dd4f5
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-Medium.ttf differ
diff --git a/frontend/resources/fonts/WorkSans-Medium.woff b/frontend/resources/fonts/WorkSans-Medium.woff
new file mode 100644
index 0000000000..067288fbe3
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-Medium.woff differ
diff --git a/frontend/resources/fonts/WorkSans-Medium.woff2 b/frontend/resources/fonts/WorkSans-Medium.woff2
new file mode 100644
index 0000000000..8701f974f1
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-Medium.woff2 differ
diff --git a/frontend/resources/fonts/WorkSans-MediumItalic.eot b/frontend/resources/fonts/WorkSans-MediumItalic.eot
new file mode 100644
index 0000000000..43b2002f08
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-MediumItalic.eot differ
diff --git a/frontend/resources/fonts/WorkSans-MediumItalic.svg b/frontend/resources/fonts/WorkSans-MediumItalic.svg
new file mode 100644
index 0000000000..64377980b3
--- /dev/null
+++ b/frontend/resources/fonts/WorkSans-MediumItalic.svg
@@ -0,0 +1,24313 @@
+
+
+
diff --git a/frontend/resources/fonts/WorkSans-MediumItalic.ttf b/frontend/resources/fonts/WorkSans-MediumItalic.ttf
new file mode 100644
index 0000000000..3a66a62a96
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-MediumItalic.ttf differ
diff --git a/frontend/resources/fonts/WorkSans-MediumItalic.woff b/frontend/resources/fonts/WorkSans-MediumItalic.woff
new file mode 100644
index 0000000000..62e9ac0a0d
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-MediumItalic.woff differ
diff --git a/frontend/resources/fonts/WorkSans-MediumItalic.woff2 b/frontend/resources/fonts/WorkSans-MediumItalic.woff2
new file mode 100644
index 0000000000..2a906a3b65
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-MediumItalic.woff2 differ
diff --git a/frontend/resources/fonts/WorkSans-Regular.eot b/frontend/resources/fonts/WorkSans-Regular.eot
new file mode 100644
index 0000000000..e3a99d1262
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-Regular.eot differ
diff --git a/frontend/resources/fonts/WorkSans-Regular.svg b/frontend/resources/fonts/WorkSans-Regular.svg
new file mode 100644
index 0000000000..df6da12323
--- /dev/null
+++ b/frontend/resources/fonts/WorkSans-Regular.svg
@@ -0,0 +1,21954 @@
+
+
+
diff --git a/frontend/resources/fonts/WorkSans-Regular.ttf b/frontend/resources/fonts/WorkSans-Regular.ttf
new file mode 100644
index 0000000000..15047f2a0a
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-Regular.ttf differ
diff --git a/frontend/resources/fonts/WorkSans-Regular.woff b/frontend/resources/fonts/WorkSans-Regular.woff
new file mode 100644
index 0000000000..9b54dc6b05
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-Regular.woff differ
diff --git a/frontend/resources/fonts/WorkSans-Regular.woff2 b/frontend/resources/fonts/WorkSans-Regular.woff2
new file mode 100644
index 0000000000..39db44def8
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-Regular.woff2 differ
diff --git a/frontend/resources/fonts/WorkSans-SemiBold.eot b/frontend/resources/fonts/WorkSans-SemiBold.eot
new file mode 100644
index 0000000000..92ca2637eb
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-SemiBold.eot differ
diff --git a/frontend/resources/fonts/WorkSans-SemiBold.svg b/frontend/resources/fonts/WorkSans-SemiBold.svg
new file mode 100644
index 0000000000..5cad7ade8c
--- /dev/null
+++ b/frontend/resources/fonts/WorkSans-SemiBold.svg
@@ -0,0 +1,25059 @@
+
+
+
diff --git a/frontend/resources/fonts/WorkSans-SemiBold.ttf b/frontend/resources/fonts/WorkSans-SemiBold.ttf
new file mode 100644
index 0000000000..3ef9f833aa
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-SemiBold.ttf differ
diff --git a/frontend/resources/fonts/WorkSans-SemiBold.woff b/frontend/resources/fonts/WorkSans-SemiBold.woff
new file mode 100644
index 0000000000..9ebf80f499
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-SemiBold.woff differ
diff --git a/frontend/resources/fonts/WorkSans-SemiBold.woff2 b/frontend/resources/fonts/WorkSans-SemiBold.woff2
new file mode 100644
index 0000000000..6c3f6defdc
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-SemiBold.woff2 differ
diff --git a/frontend/resources/fonts/WorkSans-SemiBoldItalic.eot b/frontend/resources/fonts/WorkSans-SemiBoldItalic.eot
new file mode 100644
index 0000000000..e59f0fcc6a
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-SemiBoldItalic.eot differ
diff --git a/frontend/resources/fonts/WorkSans-SemiBoldItalic.svg b/frontend/resources/fonts/WorkSans-SemiBoldItalic.svg
new file mode 100644
index 0000000000..de7a791c33
--- /dev/null
+++ b/frontend/resources/fonts/WorkSans-SemiBoldItalic.svg
@@ -0,0 +1,24400 @@
+
+
+
diff --git a/frontend/resources/fonts/WorkSans-SemiBoldItalic.ttf b/frontend/resources/fonts/WorkSans-SemiBoldItalic.ttf
new file mode 100644
index 0000000000..afa58c8d6b
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-SemiBoldItalic.ttf differ
diff --git a/frontend/resources/fonts/WorkSans-SemiBoldItalic.woff b/frontend/resources/fonts/WorkSans-SemiBoldItalic.woff
new file mode 100644
index 0000000000..7aed043cde
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-SemiBoldItalic.woff differ
diff --git a/frontend/resources/fonts/WorkSans-SemiBoldItalic.woff2 b/frontend/resources/fonts/WorkSans-SemiBoldItalic.woff2
new file mode 100644
index 0000000000..137a84bfed
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-SemiBoldItalic.woff2 differ
diff --git a/frontend/resources/fonts/WorkSans-Thin.eot b/frontend/resources/fonts/WorkSans-Thin.eot
new file mode 100644
index 0000000000..547ec1299e
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-Thin.eot differ
diff --git a/frontend/resources/fonts/WorkSans-Thin.svg b/frontend/resources/fonts/WorkSans-Thin.svg
new file mode 100644
index 0000000000..b158ae3b66
--- /dev/null
+++ b/frontend/resources/fonts/WorkSans-Thin.svg
@@ -0,0 +1,23764 @@
+
+
+
diff --git a/frontend/resources/fonts/WorkSans-Thin.ttf b/frontend/resources/fonts/WorkSans-Thin.ttf
new file mode 100644
index 0000000000..6f32ad822f
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-Thin.ttf differ
diff --git a/frontend/resources/fonts/WorkSans-Thin.woff b/frontend/resources/fonts/WorkSans-Thin.woff
new file mode 100644
index 0000000000..14df699d44
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-Thin.woff differ
diff --git a/frontend/resources/fonts/WorkSans-Thin.woff2 b/frontend/resources/fonts/WorkSans-Thin.woff2
new file mode 100644
index 0000000000..ede8218421
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-Thin.woff2 differ
diff --git a/frontend/resources/fonts/WorkSans-ThinItalic.eot b/frontend/resources/fonts/WorkSans-ThinItalic.eot
new file mode 100644
index 0000000000..5c79146567
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-ThinItalic.eot differ
diff --git a/frontend/resources/fonts/WorkSans-ThinItalic.svg b/frontend/resources/fonts/WorkSans-ThinItalic.svg
new file mode 100644
index 0000000000..735b630f30
--- /dev/null
+++ b/frontend/resources/fonts/WorkSans-ThinItalic.svg
@@ -0,0 +1,23256 @@
+
+
+
diff --git a/frontend/resources/fonts/WorkSans-ThinItalic.ttf b/frontend/resources/fonts/WorkSans-ThinItalic.ttf
new file mode 100644
index 0000000000..71531ac9d8
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-ThinItalic.ttf differ
diff --git a/frontend/resources/fonts/WorkSans-ThinItalic.woff b/frontend/resources/fonts/WorkSans-ThinItalic.woff
new file mode 100644
index 0000000000..57677116e0
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-ThinItalic.woff differ
diff --git a/frontend/resources/fonts/WorkSans-ThinItalic.woff2 b/frontend/resources/fonts/WorkSans-ThinItalic.woff2
new file mode 100644
index 0000000000..20f3c77c7d
Binary files /dev/null and b/frontend/resources/fonts/WorkSans-ThinItalic.woff2 differ
diff --git a/frontend/resources/fonts/opensans-bold.eot b/frontend/resources/fonts/opensans-bold.eot
deleted file mode 100644
index 5d20d91633..0000000000
Binary files a/frontend/resources/fonts/opensans-bold.eot and /dev/null differ
diff --git a/frontend/resources/fonts/opensans-bold.svg b/frontend/resources/fonts/opensans-bold.svg
deleted file mode 100644
index 3ed7be4bc5..0000000000
--- a/frontend/resources/fonts/opensans-bold.svg
+++ /dev/null
@@ -1,1830 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/frontend/resources/fonts/opensans-bold.ttf b/frontend/resources/fonts/opensans-bold.ttf
deleted file mode 100644
index 2109c958e3..0000000000
Binary files a/frontend/resources/fonts/opensans-bold.ttf and /dev/null differ
diff --git a/frontend/resources/fonts/opensans-bold.woff b/frontend/resources/fonts/opensans-bold.woff
deleted file mode 100644
index 1205787b0e..0000000000
Binary files a/frontend/resources/fonts/opensans-bold.woff and /dev/null differ
diff --git a/frontend/resources/fonts/opensans-bolditalic.eot b/frontend/resources/fonts/opensans-bolditalic.eot
deleted file mode 100644
index 1f639a15ff..0000000000
Binary files a/frontend/resources/fonts/opensans-bolditalic.eot and /dev/null differ
diff --git a/frontend/resources/fonts/opensans-bolditalic.svg b/frontend/resources/fonts/opensans-bolditalic.svg
deleted file mode 100644
index 6a2607b9da..0000000000
--- a/frontend/resources/fonts/opensans-bolditalic.svg
+++ /dev/null
@@ -1,1830 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/frontend/resources/fonts/opensans-bolditalic.ttf b/frontend/resources/fonts/opensans-bolditalic.ttf
deleted file mode 100644
index 242d6b25c3..0000000000
Binary files a/frontend/resources/fonts/opensans-bolditalic.ttf and /dev/null differ
diff --git a/frontend/resources/fonts/opensans-bolditalic.woff b/frontend/resources/fonts/opensans-bolditalic.woff
deleted file mode 100644
index ed760c0628..0000000000
Binary files a/frontend/resources/fonts/opensans-bolditalic.woff and /dev/null differ
diff --git a/frontend/resources/fonts/opensans-extrabold.eot b/frontend/resources/fonts/opensans-extrabold.eot
deleted file mode 100644
index 1e29ad5954..0000000000
Binary files a/frontend/resources/fonts/opensans-extrabold.eot and /dev/null differ
diff --git a/frontend/resources/fonts/opensans-extrabold.svg b/frontend/resources/fonts/opensans-extrabold.svg
deleted file mode 100644
index 27800505a5..0000000000
--- a/frontend/resources/fonts/opensans-extrabold.svg
+++ /dev/null
@@ -1,1830 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/frontend/resources/fonts/opensans-extrabold.woff b/frontend/resources/fonts/opensans-extrabold.woff
deleted file mode 100644
index a7b99d2552..0000000000
Binary files a/frontend/resources/fonts/opensans-extrabold.woff and /dev/null differ
diff --git a/frontend/resources/fonts/opensans-extrabolditalic.eot b/frontend/resources/fonts/opensans-extrabolditalic.eot
deleted file mode 100644
index 77184af422..0000000000
Binary files a/frontend/resources/fonts/opensans-extrabolditalic.eot and /dev/null differ
diff --git a/frontend/resources/fonts/opensans-extrabolditalic.svg b/frontend/resources/fonts/opensans-extrabolditalic.svg
deleted file mode 100644
index 8f080c1e5e..0000000000
--- a/frontend/resources/fonts/opensans-extrabolditalic.svg
+++ /dev/null
@@ -1,1830 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/frontend/resources/fonts/opensans-extrabolditalic.ttf b/frontend/resources/fonts/opensans-extrabolditalic.ttf
deleted file mode 100644
index 26a07e9392..0000000000
Binary files a/frontend/resources/fonts/opensans-extrabolditalic.ttf and /dev/null differ
diff --git a/frontend/resources/fonts/opensans-extrabolditalic.woff b/frontend/resources/fonts/opensans-extrabolditalic.woff
deleted file mode 100644
index 45395d1bbe..0000000000
Binary files a/frontend/resources/fonts/opensans-extrabolditalic.woff and /dev/null differ
diff --git a/frontend/resources/fonts/opensans-italic.eot b/frontend/resources/fonts/opensans-italic.eot
deleted file mode 100644
index 0c8a0ae06e..0000000000
Binary files a/frontend/resources/fonts/opensans-italic.eot and /dev/null differ
diff --git a/frontend/resources/fonts/opensans-italic.svg b/frontend/resources/fonts/opensans-italic.svg
deleted file mode 100644
index e1075dcc24..0000000000
--- a/frontend/resources/fonts/opensans-italic.svg
+++ /dev/null
@@ -1,1830 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/frontend/resources/fonts/opensans-italic.ttf b/frontend/resources/fonts/opensans-italic.ttf
deleted file mode 100644
index 12d25d9a73..0000000000
Binary files a/frontend/resources/fonts/opensans-italic.ttf and /dev/null differ
diff --git a/frontend/resources/fonts/opensans-italic.woff b/frontend/resources/fonts/opensans-italic.woff
deleted file mode 100644
index ff652e6435..0000000000
Binary files a/frontend/resources/fonts/opensans-italic.woff and /dev/null differ
diff --git a/frontend/resources/fonts/opensans-light.eot b/frontend/resources/fonts/opensans-light.eot
deleted file mode 100644
index 14868406aa..0000000000
Binary files a/frontend/resources/fonts/opensans-light.eot and /dev/null differ
diff --git a/frontend/resources/fonts/opensans-light.svg b/frontend/resources/fonts/opensans-light.svg
deleted file mode 100644
index 11a472ca8a..0000000000
--- a/frontend/resources/fonts/opensans-light.svg
+++ /dev/null
@@ -1,1831 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/frontend/resources/fonts/opensans-light.ttf b/frontend/resources/fonts/opensans-light.ttf
deleted file mode 100644
index 63af664cde..0000000000
Binary files a/frontend/resources/fonts/opensans-light.ttf and /dev/null differ
diff --git a/frontend/resources/fonts/opensans-light.woff b/frontend/resources/fonts/opensans-light.woff
deleted file mode 100644
index e786074813..0000000000
Binary files a/frontend/resources/fonts/opensans-light.woff and /dev/null differ
diff --git a/frontend/resources/fonts/opensans-lightitalic.eot b/frontend/resources/fonts/opensans-lightitalic.eot
deleted file mode 100644
index 8f445929ff..0000000000
Binary files a/frontend/resources/fonts/opensans-lightitalic.eot and /dev/null differ
diff --git a/frontend/resources/fonts/opensans-lightitalic.svg b/frontend/resources/fonts/opensans-lightitalic.svg
deleted file mode 100644
index 431d7e3546..0000000000
--- a/frontend/resources/fonts/opensans-lightitalic.svg
+++ /dev/null
@@ -1,1835 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/frontend/resources/fonts/opensans-lightitalic.ttf b/frontend/resources/fonts/opensans-lightitalic.ttf
deleted file mode 100644
index 01dda2858a..0000000000
Binary files a/frontend/resources/fonts/opensans-lightitalic.ttf and /dev/null differ
diff --git a/frontend/resources/fonts/opensans-lightitalic.woff b/frontend/resources/fonts/opensans-lightitalic.woff
deleted file mode 100644
index 43e8b9e6cc..0000000000
Binary files a/frontend/resources/fonts/opensans-lightitalic.woff and /dev/null differ
diff --git a/frontend/resources/fonts/opensans-regular.eot b/frontend/resources/fonts/opensans-regular.eot
deleted file mode 100644
index 6bbc3cf58c..0000000000
Binary files a/frontend/resources/fonts/opensans-regular.eot and /dev/null differ
diff --git a/frontend/resources/fonts/opensans-regular.svg b/frontend/resources/fonts/opensans-regular.svg
deleted file mode 100644
index 25a3952340..0000000000
--- a/frontend/resources/fonts/opensans-regular.svg
+++ /dev/null
@@ -1,1831 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/frontend/resources/fonts/opensans-regular.ttf b/frontend/resources/fonts/opensans-regular.ttf
deleted file mode 100644
index c537f8382a..0000000000
Binary files a/frontend/resources/fonts/opensans-regular.ttf and /dev/null differ
diff --git a/frontend/resources/fonts/opensans-regular.woff b/frontend/resources/fonts/opensans-regular.woff
deleted file mode 100644
index e231183dce..0000000000
Binary files a/frontend/resources/fonts/opensans-regular.woff and /dev/null differ
diff --git a/frontend/resources/fonts/opensans-semibold.eot b/frontend/resources/fonts/opensans-semibold.eot
deleted file mode 100644
index d8375dd0ab..0000000000
Binary files a/frontend/resources/fonts/opensans-semibold.eot and /dev/null differ
diff --git a/frontend/resources/fonts/opensans-semibold.svg b/frontend/resources/fonts/opensans-semibold.svg
deleted file mode 100644
index eec4db8bd7..0000000000
--- a/frontend/resources/fonts/opensans-semibold.svg
+++ /dev/null
@@ -1,1830 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/frontend/resources/fonts/opensans-semibold.ttf b/frontend/resources/fonts/opensans-semibold.ttf
deleted file mode 100644
index b3290843a7..0000000000
Binary files a/frontend/resources/fonts/opensans-semibold.ttf and /dev/null differ
diff --git a/frontend/resources/fonts/opensans-semibold.woff b/frontend/resources/fonts/opensans-semibold.woff
deleted file mode 100644
index 28d6adee03..0000000000
Binary files a/frontend/resources/fonts/opensans-semibold.woff and /dev/null differ
diff --git a/frontend/resources/fonts/opensans-semibolditalic.eot b/frontend/resources/fonts/opensans-semibolditalic.eot
deleted file mode 100644
index 0ab1db22e6..0000000000
Binary files a/frontend/resources/fonts/opensans-semibolditalic.eot and /dev/null differ
diff --git a/frontend/resources/fonts/opensans-semibolditalic.svg b/frontend/resources/fonts/opensans-semibolditalic.svg
deleted file mode 100644
index 7166ec1b90..0000000000
--- a/frontend/resources/fonts/opensans-semibolditalic.svg
+++ /dev/null
@@ -1,1830 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/frontend/resources/fonts/opensans-semibolditalic.ttf b/frontend/resources/fonts/opensans-semibolditalic.ttf
deleted file mode 100644
index d2d6318f66..0000000000
Binary files a/frontend/resources/fonts/opensans-semibolditalic.ttf and /dev/null differ
diff --git a/frontend/resources/fonts/opensans-semibolditalic.woff b/frontend/resources/fonts/opensans-semibolditalic.woff
deleted file mode 100644
index d4dfca402e..0000000000
Binary files a/frontend/resources/fonts/opensans-semibolditalic.woff and /dev/null differ
diff --git a/frontend/resources/fonts/roboto-black.eot b/frontend/resources/fonts/roboto-black.eot
deleted file mode 100644
index fa326d1d12..0000000000
Binary files a/frontend/resources/fonts/roboto-black.eot and /dev/null differ
diff --git a/frontend/resources/fonts/roboto-black.svg b/frontend/resources/fonts/roboto-black.svg
deleted file mode 100644
index 945dec6cbd..0000000000
--- a/frontend/resources/fonts/roboto-black.svg
+++ /dev/null
@@ -1,642 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/frontend/resources/fonts/roboto-black.ttf b/frontend/resources/fonts/roboto-black.ttf
deleted file mode 100644
index 3c3b2b8ae6..0000000000
Binary files a/frontend/resources/fonts/roboto-black.ttf and /dev/null differ
diff --git a/frontend/resources/fonts/roboto-black.woff b/frontend/resources/fonts/roboto-black.woff
deleted file mode 100644
index 0229086571..0000000000
Binary files a/frontend/resources/fonts/roboto-black.woff and /dev/null differ
diff --git a/frontend/resources/fonts/roboto-blackitalic.eot b/frontend/resources/fonts/roboto-blackitalic.eot
deleted file mode 100644
index a2aebfb7da..0000000000
Binary files a/frontend/resources/fonts/roboto-blackitalic.eot and /dev/null differ
diff --git a/frontend/resources/fonts/roboto-blackitalic.svg b/frontend/resources/fonts/roboto-blackitalic.svg
deleted file mode 100644
index c9cc3cdb7b..0000000000
--- a/frontend/resources/fonts/roboto-blackitalic.svg
+++ /dev/null
@@ -1,642 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/frontend/resources/fonts/roboto-blackitalic.ttf b/frontend/resources/fonts/roboto-blackitalic.ttf
deleted file mode 100644
index 2020dcbc9c..0000000000
Binary files a/frontend/resources/fonts/roboto-blackitalic.ttf and /dev/null differ
diff --git a/frontend/resources/fonts/roboto-blackitalic.woff b/frontend/resources/fonts/roboto-blackitalic.woff
deleted file mode 100644
index 1875c0b950..0000000000
Binary files a/frontend/resources/fonts/roboto-blackitalic.woff and /dev/null differ
diff --git a/frontend/resources/fonts/roboto-bold.eot b/frontend/resources/fonts/roboto-bold.eot
deleted file mode 100644
index b73776ee3b..0000000000
Binary files a/frontend/resources/fonts/roboto-bold.eot and /dev/null differ
diff --git a/frontend/resources/fonts/roboto-bold.svg b/frontend/resources/fonts/roboto-bold.svg
deleted file mode 100644
index 43b5ed2222..0000000000
--- a/frontend/resources/fonts/roboto-bold.svg
+++ /dev/null
@@ -1,593 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/frontend/resources/fonts/roboto-bold.ttf b/frontend/resources/fonts/roboto-bold.ttf
deleted file mode 100644
index 1da72769a8..0000000000
Binary files a/frontend/resources/fonts/roboto-bold.ttf and /dev/null differ
diff --git a/frontend/resources/fonts/roboto-bold.woff b/frontend/resources/fonts/roboto-bold.woff
deleted file mode 100644
index 0c6994871e..0000000000
Binary files a/frontend/resources/fonts/roboto-bold.woff and /dev/null differ
diff --git a/frontend/resources/fonts/roboto-bolditalic.eot b/frontend/resources/fonts/roboto-bolditalic.eot
deleted file mode 100644
index b803ec1687..0000000000
Binary files a/frontend/resources/fonts/roboto-bolditalic.eot and /dev/null differ
diff --git a/frontend/resources/fonts/roboto-bolditalic.svg b/frontend/resources/fonts/roboto-bolditalic.svg
deleted file mode 100644
index f877a3c821..0000000000
--- a/frontend/resources/fonts/roboto-bolditalic.svg
+++ /dev/null
@@ -1,642 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/frontend/resources/fonts/roboto-bolditalic.ttf b/frontend/resources/fonts/roboto-bolditalic.ttf
deleted file mode 100644
index 78bab05c8c..0000000000
Binary files a/frontend/resources/fonts/roboto-bolditalic.ttf and /dev/null differ
diff --git a/frontend/resources/fonts/roboto-bolditalic.woff b/frontend/resources/fonts/roboto-bolditalic.woff
deleted file mode 100644
index 99de61af52..0000000000
Binary files a/frontend/resources/fonts/roboto-bolditalic.woff and /dev/null differ
diff --git a/frontend/resources/fonts/roboto-italic.eot b/frontend/resources/fonts/roboto-italic.eot
deleted file mode 100644
index b708f047ff..0000000000
Binary files a/frontend/resources/fonts/roboto-italic.eot and /dev/null differ
diff --git a/frontend/resources/fonts/roboto-italic.svg b/frontend/resources/fonts/roboto-italic.svg
deleted file mode 100644
index 49ddd4ab76..0000000000
--- a/frontend/resources/fonts/roboto-italic.svg
+++ /dev/null
@@ -1,642 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/frontend/resources/fonts/roboto-italic.ttf b/frontend/resources/fonts/roboto-italic.ttf
deleted file mode 100644
index ae258e8416..0000000000
Binary files a/frontend/resources/fonts/roboto-italic.ttf and /dev/null differ
diff --git a/frontend/resources/fonts/roboto-italic.woff b/frontend/resources/fonts/roboto-italic.woff
deleted file mode 100644
index dd74244382..0000000000
Binary files a/frontend/resources/fonts/roboto-italic.woff and /dev/null differ
diff --git a/frontend/resources/fonts/roboto-light.eot b/frontend/resources/fonts/roboto-light.eot
deleted file mode 100644
index 072cdc480c..0000000000
Binary files a/frontend/resources/fonts/roboto-light.eot and /dev/null differ
diff --git a/frontend/resources/fonts/roboto-light.svg b/frontend/resources/fonts/roboto-light.svg
deleted file mode 100644
index db6a6171ed..0000000000
--- a/frontend/resources/fonts/roboto-light.svg
+++ /dev/null
@@ -1,641 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/frontend/resources/fonts/roboto-light.ttf b/frontend/resources/fonts/roboto-light.ttf
deleted file mode 100644
index 3b2fea0ace..0000000000
Binary files a/frontend/resources/fonts/roboto-light.ttf and /dev/null differ
diff --git a/frontend/resources/fonts/roboto-light.woff b/frontend/resources/fonts/roboto-light.woff
deleted file mode 100644
index cc534a3815..0000000000
Binary files a/frontend/resources/fonts/roboto-light.woff and /dev/null differ
diff --git a/frontend/resources/fonts/roboto-lightitalic.eot b/frontend/resources/fonts/roboto-lightitalic.eot
deleted file mode 100644
index 77396a1ff9..0000000000
Binary files a/frontend/resources/fonts/roboto-lightitalic.eot and /dev/null differ
diff --git a/frontend/resources/fonts/roboto-lightitalic.svg b/frontend/resources/fonts/roboto-lightitalic.svg
deleted file mode 100644
index 4bd14bcca7..0000000000
--- a/frontend/resources/fonts/roboto-lightitalic.svg
+++ /dev/null
@@ -1,641 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/frontend/resources/fonts/roboto-lightitalic.ttf b/frontend/resources/fonts/roboto-lightitalic.ttf
deleted file mode 100644
index b9b38118a3..0000000000
Binary files a/frontend/resources/fonts/roboto-lightitalic.ttf and /dev/null differ
diff --git a/frontend/resources/fonts/roboto-lightitalic.woff b/frontend/resources/fonts/roboto-lightitalic.woff
deleted file mode 100644
index 3071ff4f23..0000000000
Binary files a/frontend/resources/fonts/roboto-lightitalic.woff and /dev/null differ
diff --git a/frontend/resources/fonts/roboto-medium.eot b/frontend/resources/fonts/roboto-medium.eot
deleted file mode 100644
index f9ad99566d..0000000000
Binary files a/frontend/resources/fonts/roboto-medium.eot and /dev/null differ
diff --git a/frontend/resources/fonts/roboto-medium.svg b/frontend/resources/fonts/roboto-medium.svg
deleted file mode 100644
index 4ce289dfa4..0000000000
--- a/frontend/resources/fonts/roboto-medium.svg
+++ /dev/null
@@ -1,593 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/frontend/resources/fonts/roboto-medium.ttf b/frontend/resources/fonts/roboto-medium.ttf
deleted file mode 100644
index 8aa64d8232..0000000000
Binary files a/frontend/resources/fonts/roboto-medium.ttf and /dev/null differ
diff --git a/frontend/resources/fonts/roboto-medium.woff b/frontend/resources/fonts/roboto-medium.woff
deleted file mode 100644
index cd810ef929..0000000000
Binary files a/frontend/resources/fonts/roboto-medium.woff and /dev/null differ
diff --git a/frontend/resources/fonts/roboto-mediumitalic.eot b/frontend/resources/fonts/roboto-mediumitalic.eot
deleted file mode 100644
index a03fe4b248..0000000000
Binary files a/frontend/resources/fonts/roboto-mediumitalic.eot and /dev/null differ
diff --git a/frontend/resources/fonts/roboto-mediumitalic.svg b/frontend/resources/fonts/roboto-mediumitalic.svg
deleted file mode 100644
index 904d7c5280..0000000000
--- a/frontend/resources/fonts/roboto-mediumitalic.svg
+++ /dev/null
@@ -1,642 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/frontend/resources/fonts/roboto-mediumitalic.ttf b/frontend/resources/fonts/roboto-mediumitalic.ttf
deleted file mode 100644
index 6439927f16..0000000000
Binary files a/frontend/resources/fonts/roboto-mediumitalic.ttf and /dev/null differ
diff --git a/frontend/resources/fonts/roboto-mediumitalic.woff b/frontend/resources/fonts/roboto-mediumitalic.woff
deleted file mode 100644
index 69a1458011..0000000000
Binary files a/frontend/resources/fonts/roboto-mediumitalic.woff and /dev/null differ
diff --git a/frontend/resources/fonts/roboto-regular.eot b/frontend/resources/fonts/roboto-regular.eot
deleted file mode 100644
index 9b5e8e4138..0000000000
Binary files a/frontend/resources/fonts/roboto-regular.eot and /dev/null differ
diff --git a/frontend/resources/fonts/roboto-regular.svg b/frontend/resources/fonts/roboto-regular.svg
deleted file mode 100644
index de7d77fea5..0000000000
--- a/frontend/resources/fonts/roboto-regular.svg
+++ /dev/null
@@ -1,621 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/frontend/resources/fonts/roboto-regular.ttf b/frontend/resources/fonts/roboto-regular.ttf
deleted file mode 100644
index 44dd78d5e1..0000000000
Binary files a/frontend/resources/fonts/roboto-regular.ttf and /dev/null differ
diff --git a/frontend/resources/fonts/roboto-regular.woff b/frontend/resources/fonts/roboto-regular.woff
deleted file mode 100644
index bfa05d53f4..0000000000
Binary files a/frontend/resources/fonts/roboto-regular.woff and /dev/null differ
diff --git a/frontend/resources/fonts/roboto-thin.eot b/frontend/resources/fonts/roboto-thin.eot
deleted file mode 100644
index 2284a3b3ef..0000000000
Binary files a/frontend/resources/fonts/roboto-thin.eot and /dev/null differ
diff --git a/frontend/resources/fonts/roboto-thin.svg b/frontend/resources/fonts/roboto-thin.svg
deleted file mode 100644
index 7394e3d0a6..0000000000
--- a/frontend/resources/fonts/roboto-thin.svg
+++ /dev/null
@@ -1,631 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/frontend/resources/fonts/roboto-thin.ttf b/frontend/resources/fonts/roboto-thin.ttf
deleted file mode 100644
index 18919f7a96..0000000000
Binary files a/frontend/resources/fonts/roboto-thin.ttf and /dev/null differ
diff --git a/frontend/resources/fonts/roboto-thin.woff b/frontend/resources/fonts/roboto-thin.woff
deleted file mode 100644
index f10b831e85..0000000000
Binary files a/frontend/resources/fonts/roboto-thin.woff and /dev/null differ
diff --git a/frontend/resources/fonts/roboto-thinitalic.eot b/frontend/resources/fonts/roboto-thinitalic.eot
deleted file mode 100644
index e6291f2657..0000000000
Binary files a/frontend/resources/fonts/roboto-thinitalic.eot and /dev/null differ
diff --git a/frontend/resources/fonts/roboto-thinitalic.svg b/frontend/resources/fonts/roboto-thinitalic.svg
deleted file mode 100644
index 951cccd9dc..0000000000
--- a/frontend/resources/fonts/roboto-thinitalic.svg
+++ /dev/null
@@ -1,631 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/frontend/resources/fonts/roboto-thinitalic.ttf b/frontend/resources/fonts/roboto-thinitalic.ttf
deleted file mode 100644
index a4e7ae08e0..0000000000
Binary files a/frontend/resources/fonts/roboto-thinitalic.ttf and /dev/null differ
diff --git a/frontend/resources/fonts/roboto-thinitalic.woff b/frontend/resources/fonts/roboto-thinitalic.woff
deleted file mode 100644
index 9ef17a8681..0000000000
Binary files a/frontend/resources/fonts/roboto-thinitalic.woff and /dev/null differ
diff --git a/frontend/resources/fonts/robotocondensed-bold.eot b/frontend/resources/fonts/robotocondensed-bold.eot
deleted file mode 100644
index bbc67d825b..0000000000
Binary files a/frontend/resources/fonts/robotocondensed-bold.eot and /dev/null differ
diff --git a/frontend/resources/fonts/robotocondensed-bold.svg b/frontend/resources/fonts/robotocondensed-bold.svg
deleted file mode 100644
index 417a2a9e3e..0000000000
--- a/frontend/resources/fonts/robotocondensed-bold.svg
+++ /dev/null
@@ -1,643 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/frontend/resources/fonts/robotocondensed-bold.ttf b/frontend/resources/fonts/robotocondensed-bold.ttf
deleted file mode 100644
index 87256d3a34..0000000000
Binary files a/frontend/resources/fonts/robotocondensed-bold.ttf and /dev/null differ
diff --git a/frontend/resources/fonts/robotocondensed-bold.woff b/frontend/resources/fonts/robotocondensed-bold.woff
deleted file mode 100644
index 235c963d69..0000000000
Binary files a/frontend/resources/fonts/robotocondensed-bold.woff and /dev/null differ
diff --git a/frontend/resources/fonts/robotocondensed-bolditalic.eot b/frontend/resources/fonts/robotocondensed-bolditalic.eot
deleted file mode 100644
index 5a95d1a144..0000000000
Binary files a/frontend/resources/fonts/robotocondensed-bolditalic.eot and /dev/null differ
diff --git a/frontend/resources/fonts/robotocondensed-bolditalic.svg b/frontend/resources/fonts/robotocondensed-bolditalic.svg
deleted file mode 100644
index f468ca3320..0000000000
--- a/frontend/resources/fonts/robotocondensed-bolditalic.svg
+++ /dev/null
@@ -1,643 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/frontend/resources/fonts/robotocondensed-bolditalic.ttf b/frontend/resources/fonts/robotocondensed-bolditalic.ttf
deleted file mode 100644
index 059feb39c5..0000000000
Binary files a/frontend/resources/fonts/robotocondensed-bolditalic.ttf and /dev/null differ
diff --git a/frontend/resources/fonts/robotocondensed-bolditalic.woff b/frontend/resources/fonts/robotocondensed-bolditalic.woff
deleted file mode 100644
index df69b95291..0000000000
Binary files a/frontend/resources/fonts/robotocondensed-bolditalic.woff and /dev/null differ
diff --git a/frontend/resources/fonts/robotocondensed-italic.eot b/frontend/resources/fonts/robotocondensed-italic.eot
deleted file mode 100644
index 985364ffed..0000000000
Binary files a/frontend/resources/fonts/robotocondensed-italic.eot and /dev/null differ
diff --git a/frontend/resources/fonts/robotocondensed-italic.svg b/frontend/resources/fonts/robotocondensed-italic.svg
deleted file mode 100644
index e3e72ddf09..0000000000
--- a/frontend/resources/fonts/robotocondensed-italic.svg
+++ /dev/null
@@ -1,644 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/frontend/resources/fonts/robotocondensed-italic.ttf b/frontend/resources/fonts/robotocondensed-italic.ttf
deleted file mode 100644
index d804301e70..0000000000
Binary files a/frontend/resources/fonts/robotocondensed-italic.ttf and /dev/null differ
diff --git a/frontend/resources/fonts/robotocondensed-italic.woff b/frontend/resources/fonts/robotocondensed-italic.woff
deleted file mode 100644
index 67804e1e4d..0000000000
Binary files a/frontend/resources/fonts/robotocondensed-italic.woff and /dev/null differ
diff --git a/frontend/resources/fonts/robotocondensed-light.eot b/frontend/resources/fonts/robotocondensed-light.eot
deleted file mode 100644
index d492665160..0000000000
Binary files a/frontend/resources/fonts/robotocondensed-light.eot and /dev/null differ
diff --git a/frontend/resources/fonts/robotocondensed-light.svg b/frontend/resources/fonts/robotocondensed-light.svg
deleted file mode 100644
index 6ce1343456..0000000000
--- a/frontend/resources/fonts/robotocondensed-light.svg
+++ /dev/null
@@ -1,643 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/frontend/resources/fonts/robotocondensed-light.ttf b/frontend/resources/fonts/robotocondensed-light.ttf
deleted file mode 100644
index 321c450f22..0000000000
Binary files a/frontend/resources/fonts/robotocondensed-light.ttf and /dev/null differ
diff --git a/frontend/resources/fonts/robotocondensed-light.woff b/frontend/resources/fonts/robotocondensed-light.woff
deleted file mode 100644
index c414478ca5..0000000000
Binary files a/frontend/resources/fonts/robotocondensed-light.woff and /dev/null differ
diff --git a/frontend/resources/fonts/robotocondensed-lightitalic.eot b/frontend/resources/fonts/robotocondensed-lightitalic.eot
deleted file mode 100644
index c169648374..0000000000
Binary files a/frontend/resources/fonts/robotocondensed-lightitalic.eot and /dev/null differ
diff --git a/frontend/resources/fonts/robotocondensed-lightitalic.svg b/frontend/resources/fonts/robotocondensed-lightitalic.svg
deleted file mode 100644
index 8a8b4cb173..0000000000
--- a/frontend/resources/fonts/robotocondensed-lightitalic.svg
+++ /dev/null
@@ -1,643 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/frontend/resources/fonts/robotocondensed-lightitalic.ttf b/frontend/resources/fonts/robotocondensed-lightitalic.ttf
deleted file mode 100644
index 905604d021..0000000000
Binary files a/frontend/resources/fonts/robotocondensed-lightitalic.ttf and /dev/null differ
diff --git a/frontend/resources/fonts/robotocondensed-lightitalic.woff b/frontend/resources/fonts/robotocondensed-lightitalic.woff
deleted file mode 100644
index 5536e16c5d..0000000000
Binary files a/frontend/resources/fonts/robotocondensed-lightitalic.woff and /dev/null differ
diff --git a/frontend/resources/fonts/robotocondensed-regular.eot b/frontend/resources/fonts/robotocondensed-regular.eot
deleted file mode 100644
index 5710fe6ead..0000000000
Binary files a/frontend/resources/fonts/robotocondensed-regular.eot and /dev/null differ
diff --git a/frontend/resources/fonts/robotocondensed-regular.svg b/frontend/resources/fonts/robotocondensed-regular.svg
deleted file mode 100644
index 0ce498562d..0000000000
--- a/frontend/resources/fonts/robotocondensed-regular.svg
+++ /dev/null
@@ -1,644 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/frontend/resources/fonts/robotocondensed-regular.ttf b/frontend/resources/fonts/robotocondensed-regular.ttf
deleted file mode 100644
index be9f6f8ab9..0000000000
Binary files a/frontend/resources/fonts/robotocondensed-regular.ttf and /dev/null differ
diff --git a/frontend/resources/fonts/robotocondensed-regular.woff b/frontend/resources/fonts/robotocondensed-regular.woff
deleted file mode 100644
index ec28f95c87..0000000000
Binary files a/frontend/resources/fonts/robotocondensed-regular.woff and /dev/null differ
diff --git a/frontend/resources/images/icons/checkbox-checked.svg b/frontend/resources/images/icons/checkbox-checked.svg
new file mode 100644
index 0000000000..21d24f176d
--- /dev/null
+++ b/frontend/resources/images/icons/checkbox-checked.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/resources/images/icons/checkbox-unchecked.svg b/frontend/resources/images/icons/checkbox-unchecked.svg
new file mode 100644
index 0000000000..68f7d0d111
--- /dev/null
+++ b/frontend/resources/images/icons/checkbox-unchecked.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/resources/images/icons/code.svg b/frontend/resources/images/icons/code.svg
new file mode 100644
index 0000000000..d8ae15c9ef
--- /dev/null
+++ b/frontend/resources/images/icons/code.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/resources/images/icons/copy.svg b/frontend/resources/images/icons/copy.svg
index 41863f61ef..8a65c91c70 100644
--- a/frontend/resources/images/icons/copy.svg
+++ b/frontend/resources/images/icons/copy.svg
@@ -1 +1,4 @@
-
\ No newline at end of file
+
diff --git a/frontend/resources/images/icons/interaction.svg b/frontend/resources/images/icons/interaction.svg
index 0fd5dd946b..afc359d843 100644
--- a/frontend/resources/images/icons/interaction.svg
+++ b/frontend/resources/images/icons/interaction.svg
@@ -1 +1,5 @@
-
+
diff --git a/frontend/resources/images/icons/mask.svg b/frontend/resources/images/icons/mask.svg
new file mode 100644
index 0000000000..3c2e315340
--- /dev/null
+++ b/frontend/resources/images/icons/mask.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/resources/images/icons/picker-harmony.svg b/frontend/resources/images/icons/picker-harmony.svg
new file mode 100644
index 0000000000..c108e28127
--- /dev/null
+++ b/frontend/resources/images/icons/picker-harmony.svg
@@ -0,0 +1,2 @@
+
diff --git a/frontend/resources/images/icons/picker-hsv.svg b/frontend/resources/images/icons/picker-hsv.svg
new file mode 100644
index 0000000000..2218c82a17
--- /dev/null
+++ b/frontend/resources/images/icons/picker-hsv.svg
@@ -0,0 +1 @@
+
diff --git a/frontend/resources/images/icons/picker-ramp.svg b/frontend/resources/images/icons/picker-ramp.svg
new file mode 100644
index 0000000000..815b3a9449
--- /dev/null
+++ b/frontend/resources/images/icons/picker-ramp.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/resources/images/icons/picker.svg b/frontend/resources/images/icons/picker.svg
index f486028b4b..99c8edc76d 100644
--- a/frontend/resources/images/icons/picker.svg
+++ b/frontend/resources/images/icons/picker.svg
@@ -1 +1,3 @@
-
\ No newline at end of file
+
diff --git a/frontend/resources/images/icons/recent.svg b/frontend/resources/images/icons/recent.svg
index d9d237feee..590de65173 100644
--- a/frontend/resources/images/icons/recent.svg
+++ b/frontend/resources/images/icons/recent.svg
@@ -1,3 +1,3 @@
-