diff --git a/.gitignore b/.gitignore
index 22ae73c022..b7f152cd6b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,6 +23,7 @@
/backend/resources/public/assets
/backend/resources/public/media
/backend/target/
+/backend/builtin-templates
/bundle*
/cd.md
/clj-profiler/
diff --git a/backend/resources/emails-mjml/change-email/en.mjml b/backend/resources/app/emails-mjml/change-email/en.mjml
similarity index 100%
rename from backend/resources/emails-mjml/change-email/en.mjml
rename to backend/resources/app/emails-mjml/change-email/en.mjml
diff --git a/backend/resources/emails-mjml/invite-to-team/en.mjml b/backend/resources/app/emails-mjml/invite-to-team/en.mjml
similarity index 100%
rename from backend/resources/emails-mjml/invite-to-team/en.mjml
rename to backend/resources/app/emails-mjml/invite-to-team/en.mjml
diff --git a/backend/resources/emails-mjml/password-recovery/en.mjml b/backend/resources/app/emails-mjml/password-recovery/en.mjml
similarity index 100%
rename from backend/resources/emails-mjml/password-recovery/en.mjml
rename to backend/resources/app/emails-mjml/password-recovery/en.mjml
diff --git a/backend/resources/emails-mjml/register/en.mjml b/backend/resources/app/emails-mjml/register/en.mjml
similarity index 100%
rename from backend/resources/emails-mjml/register/en.mjml
rename to backend/resources/app/emails-mjml/register/en.mjml
diff --git a/backend/resources/emails/change-email/en.html b/backend/resources/app/emails/change-email/en.html
similarity index 100%
rename from backend/resources/emails/change-email/en.html
rename to backend/resources/app/emails/change-email/en.html
diff --git a/backend/resources/emails/change-email/en.subj b/backend/resources/app/emails/change-email/en.subj
similarity index 100%
rename from backend/resources/emails/change-email/en.subj
rename to backend/resources/app/emails/change-email/en.subj
diff --git a/backend/resources/emails/change-email/en.txt b/backend/resources/app/emails/change-email/en.txt
similarity index 100%
rename from backend/resources/emails/change-email/en.txt
rename to backend/resources/app/emails/change-email/en.txt
diff --git a/backend/resources/emails/feedback/en.html b/backend/resources/app/emails/feedback/en.html
similarity index 100%
rename from backend/resources/emails/feedback/en.html
rename to backend/resources/app/emails/feedback/en.html
diff --git a/backend/resources/emails/feedback/en.subj b/backend/resources/app/emails/feedback/en.subj
similarity index 100%
rename from backend/resources/emails/feedback/en.subj
rename to backend/resources/app/emails/feedback/en.subj
diff --git a/backend/resources/emails/feedback/en.txt b/backend/resources/app/emails/feedback/en.txt
similarity index 100%
rename from backend/resources/emails/feedback/en.txt
rename to backend/resources/app/emails/feedback/en.txt
diff --git a/backend/resources/emails/invite-to-team/en.html b/backend/resources/app/emails/invite-to-team/en.html
similarity index 100%
rename from backend/resources/emails/invite-to-team/en.html
rename to backend/resources/app/emails/invite-to-team/en.html
diff --git a/backend/resources/emails/invite-to-team/en.subj b/backend/resources/app/emails/invite-to-team/en.subj
similarity index 100%
rename from backend/resources/emails/invite-to-team/en.subj
rename to backend/resources/app/emails/invite-to-team/en.subj
diff --git a/backend/resources/emails/invite-to-team/en.txt b/backend/resources/app/emails/invite-to-team/en.txt
similarity index 100%
rename from backend/resources/emails/invite-to-team/en.txt
rename to backend/resources/app/emails/invite-to-team/en.txt
diff --git a/backend/resources/emails/password-recovery/en.html b/backend/resources/app/emails/password-recovery/en.html
similarity index 100%
rename from backend/resources/emails/password-recovery/en.html
rename to backend/resources/app/emails/password-recovery/en.html
diff --git a/backend/resources/emails/password-recovery/en.subj b/backend/resources/app/emails/password-recovery/en.subj
similarity index 100%
rename from backend/resources/emails/password-recovery/en.subj
rename to backend/resources/app/emails/password-recovery/en.subj
diff --git a/backend/resources/emails/password-recovery/en.txt b/backend/resources/app/emails/password-recovery/en.txt
similarity index 100%
rename from backend/resources/emails/password-recovery/en.txt
rename to backend/resources/app/emails/password-recovery/en.txt
diff --git a/backend/resources/emails/register/en.html b/backend/resources/app/emails/register/en.html
similarity index 100%
rename from backend/resources/emails/register/en.html
rename to backend/resources/app/emails/register/en.html
diff --git a/backend/resources/emails/register/en.subj b/backend/resources/app/emails/register/en.subj
similarity index 100%
rename from backend/resources/emails/register/en.subj
rename to backend/resources/app/emails/register/en.subj
diff --git a/backend/resources/emails/register/en.txt b/backend/resources/app/emails/register/en.txt
similarity index 100%
rename from backend/resources/emails/register/en.txt
rename to backend/resources/app/emails/register/en.txt
diff --git a/backend/resources/app/onboarding.edn b/backend/resources/app/onboarding.edn
new file mode 100644
index 0000000000..e2075d35b2
--- /dev/null
+++ b/backend/resources/app/onboarding.edn
@@ -0,0 +1,28 @@
+[{:id "penpot-design-system"
+ :name "Penpot Design System"
+ :thumbnail-uri "https://penpot.app/images/libraries/cover-ds-penpot.jpg"
+ :file-uri "https://github.com/penpot/penpot-files/raw/main/Penpot-Design-system.penpot"}
+ {:id "wireframing-kit"
+ :name "Wireframing Kit"
+ :thumbnail-uri "https://penpot.app/images/libraries/cover-wireframes.jpg"
+ :file-uri "https://github.com/penpot/penpot-files/raw/main/wireframing-kit.penpot"}
+ {:id "ant-design"
+ :name "Ant Design UI Kit (lite)"
+ :thumbnail-uri "https://penpot.app/images/libraries/cover-ant-design.jpg"
+ :file-uri "https://github.com/penpot/penpot-files/raw/main/Ant-Design-UI-Kit-Lite.penpot"}
+ {:id "cocomaterial"
+ :name "Cocomaterial"
+ :thumbnail-uri "https://penpot.app/images/libraries/cover-cocomaterial.jpg"
+ :file-uri "https://github.com/penpot/penpot-files/raw/main/Cocomaterial.penpot"}
+ {:id "circum-icons"
+ :name "Circum Icons pack"
+ :thumbnail-uri "https://penpot.app/images/libraries/cover-circum.jpg"
+ :file-uri "https://github.com/penpot/penpot-files/raw/main/CircumIcons.penpot"}
+ {:id "whiteboarding-kit"
+ :name "Whiteboarding Kit"
+ :thumbnail-uri "https://penpot.app/images/libraries/cover-whiteboards.jpg"
+ :file-uri "https://github.com/penpot/penpot-files/raw/main/Whiteboarding-mapping-kit.penpot"}
+ {:id "material-design-baseline"
+ :name "Material Design (baseline)"
+ :thumbnail-uri "https://penpot.app/images/libraries/cover-material.jpg"
+ :file-uri "https://github.com/penpot/penpot-files/raw/main/Material-Design-Kit.penpot"}]
diff --git a/backend/resources/api-doc-entry.tmpl b/backend/resources/app/templates/api-doc-entry.tmpl
similarity index 100%
rename from backend/resources/api-doc-entry.tmpl
rename to backend/resources/app/templates/api-doc-entry.tmpl
diff --git a/backend/resources/api-doc.css b/backend/resources/app/templates/api-doc.css
similarity index 100%
rename from backend/resources/api-doc.css
rename to backend/resources/app/templates/api-doc.css
diff --git a/backend/resources/api-doc.js b/backend/resources/app/templates/api-doc.js
similarity index 100%
rename from backend/resources/api-doc.js
rename to backend/resources/app/templates/api-doc.js
diff --git a/backend/resources/api-doc.tmpl b/backend/resources/app/templates/api-doc.tmpl
similarity index 78%
rename from backend/resources/api-doc.tmpl
rename to backend/resources/app/templates/api-doc.tmpl
index c7c447b4d3..4ad521a8b3 100644
--- a/backend/resources/api-doc.tmpl
+++ b/backend/resources/app/templates/api-doc.tmpl
@@ -10,10 +10,10 @@
@@ -26,21 +26,21 @@
RPC COMMAND METHODS:
{% for item in command-methods %}
- {% include "api-doc-entry.tmpl" with item=item %}
+ {% include "app/templates/api-doc-entry.tmpl" with item=item %}
{% endfor %}
RPC QUERY METHODS:
{% for item in query-methods %}
- {% include "api-doc-entry.tmpl" with item=item %}
+ {% include "app/templates/api-doc-entry.tmpl" with item=item %}
{% endfor %}
RPC MUTATION METHODS:
{% for item in mutation-methods %}
- {% include "api-doc-entry.tmpl" with item=item %}
+ {% include "app/templates/api-doc-entry.tmpl" with item=item %}
{% endfor %}
diff --git a/backend/resources/templates/base.tmpl b/backend/resources/app/templates/base.tmpl
similarity index 89%
rename from backend/resources/templates/base.tmpl
rename to backend/resources/app/templates/base.tmpl
index 7f8709dd9f..893b6bc066 100644
--- a/backend/resources/templates/base.tmpl
+++ b/backend/resources/app/templates/base.tmpl
@@ -7,7 +7,7 @@
{% block title %}{% endblock %}
diff --git a/backend/resources/templates/debug.tmpl b/backend/resources/app/templates/debug.tmpl
similarity index 99%
rename from backend/resources/templates/debug.tmpl
rename to backend/resources/app/templates/debug.tmpl
index 7479dd9827..e23e7b5470 100644
--- a/backend/resources/templates/debug.tmpl
+++ b/backend/resources/app/templates/debug.tmpl
@@ -1,4 +1,4 @@
-{% extends "templates/base.tmpl" %}
+{% extends "app/templates/base.tmpl" %}
{% block title %}
Debug Main Page
diff --git a/backend/resources/templates/error-list.tmpl b/backend/resources/app/templates/error-list.tmpl
similarity index 88%
rename from backend/resources/templates/error-list.tmpl
rename to backend/resources/app/templates/error-list.tmpl
index 66835867a7..d86d983d35 100644
--- a/backend/resources/templates/error-list.tmpl
+++ b/backend/resources/app/templates/error-list.tmpl
@@ -1,4 +1,4 @@
-{% extends "templates/base.tmpl" %}
+{% extends "app/templates/base.tmpl" %}
{% block title %}
penpot - error list
diff --git a/backend/resources/templates/error-report.tmpl b/backend/resources/app/templates/error-report.tmpl
similarity index 98%
rename from backend/resources/templates/error-report.tmpl
rename to backend/resources/app/templates/error-report.tmpl
index a3fcc158d0..60905c689a 100644
--- a/backend/resources/templates/error-report.tmpl
+++ b/backend/resources/app/templates/error-report.tmpl
@@ -1,4 +1,4 @@
-{% extends "templates/base.tmpl" %}
+{% extends "app/templates/base.tmpl" %}
{% block title %}
penpot - error report {{id}}
diff --git a/backend/resources/templates/styles.css b/backend/resources/app/templates/styles.css
similarity index 100%
rename from backend/resources/templates/styles.css
rename to backend/resources/app/templates/styles.css
diff --git a/backend/scripts/build b/backend/scripts/build
index 1537b7f91e..98174a5c16 100755
--- a/backend/scripts/build
+++ b/backend/scripts/build
@@ -17,5 +17,6 @@ cp scripts/manage.template.sh target/dist/manage.sh;
chmod +x target/dist/run.sh;
chmod +x target/dist/manage.sh;
-
-
+# Prefetch
+bb ./scripts/prefetch-templates.clj resources/app/onboarding.edn builtin-templates/
+cp -r builtin-templates target/dist/
diff --git a/backend/scripts/prefetch-templates.clj b/backend/scripts/prefetch-templates.clj
new file mode 100755
index 0000000000..68936b1589
--- /dev/null
+++ b/backend/scripts/prefetch-templates.clj
@@ -0,0 +1,40 @@
+#!/usr/bin/env bb
+
+(require '[babashka.curl :as curl]
+ '[babashka.fs :as fs])
+
+(defn download-if-needed!
+ [dest data]
+ (doseq [{:keys [id file-uri] :as item} data]
+ (let [file (fs/file dest id)
+ rsp (curl/get file-uri {:as :stream})]
+
+ (when (not= 200 (:status rsp))
+ (println (format "unable to download %s (uri: %s)" id file-uri))
+ (System/exit -1))
+
+ (when-not (fs/exists? (str file))
+ (println (format "=> downloading %s" id))
+ (with-open [output (io/output-stream file)]
+ (io/copy (:body rsp) output))))))
+
+(defn read-defs-file
+ [path]
+ (with-open [content (io/reader path)]
+ (edn/read-string (slurp content))))
+
+(let [[path dest] *command-line-args*]
+ (when (or (nil? path)
+ (nil? dest))
+ (println "invalid arguments")
+ (System/exit -1))
+
+ (when-not (fs/exists? path)
+ (println (format "file %s does not exists" path))
+ (System/exit -1))
+
+ (when-not (fs/exists? dest)
+ (fs/create-dirs dest))
+
+ (let [data (read-defs-file path)]
+ (download-if-needed! dest data)))
diff --git a/backend/src/app/http/client.clj b/backend/src/app/http/client.clj
index bf52b61e25..8718606b1d 100644
--- a/backend/src/app/http/client.clj
+++ b/backend/src/app/http/client.clj
@@ -12,6 +12,8 @@
[integrant.core :as ig]
[java-http-clj.core :as http]))
+(s/def ::client fn?)
+
(defmethod ig/pre-init-spec :app.http/client [_]
(s/keys :req-un [::wrk/executor]))
@@ -28,3 +30,12 @@
(http/send req {:client client :as response-type})
(http/send-async req {:client client :as response-type}))))
{::client client})))
+
+
+(defn req!
+ "A convencience toplevel function for gradual migration to a new API
+ convention."
+ ([client request]
+ (client request))
+ ([client request options]
+ (client request options)))
diff --git a/backend/src/app/http/debug.clj b/backend/src/app/http/debug.clj
index 11a29f892b..21dcb50f02 100644
--- a/backend/src/app/http/debug.clj
+++ b/backend/src/app/http/debug.clj
@@ -66,7 +66,7 @@
:code :only-admins-allowed))
(yrs/response :status 200
:headers {"content-type" "text/html"}
- :body (-> (io/resource "templates/debug.tmpl")
+ :body (-> (io/resource "app/templates/debug.tmpl")
(tmpl/render {}))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -225,7 +225,7 @@
:trace (or (:trace report)
(some-> report :error :trace))
:params (:params report)}]
- (-> (io/resource "templates/error-report.tmpl")
+ (-> (io/resource "app/templates/error-report.tmpl")
(tmpl/render params))))]
(when-not (authorized? pool request)
@@ -253,7 +253,7 @@
(let [items (db/exec! pool [sql:error-reports])
items (map #(update % :created-at dt/format-instant :rfc1123) items)]
(yrs/response :status 200
- :body (-> (io/resource "templates/error-list.tmpl")
+ :body (-> (io/resource "app/templates/error-list.tmpl")
(tmpl/render {:items items}))
:headers {"content-type" "text/html; charset=utf-8"
"x-robots-tag" "noindex"})))
diff --git a/backend/src/app/main.clj b/backend/src/app/main.clj
index cc20cc7e5f..ed26617640 100644
--- a/backend/src/app/main.clj
+++ b/backend/src/app/main.clj
@@ -235,7 +235,8 @@
:audit (ig/ref :app.loggers.audit/collector)
:ldap (ig/ref :app.auth.ldap/provider)
:http-client (ig/ref :app.http/client)
- :executors (ig/ref :app.worker/executors)}
+ :executors (ig/ref :app.worker/executors)
+ :templates (ig/ref :app.setup/builtin-templates)}
:app.rpc.doc/routes
{:methods (ig/ref :app.rpc/methods)}
@@ -352,6 +353,9 @@
{:port (cf/get :srepl-port)
:host (cf/get :srepl-host)}
+ :app.setup/builtin-templates
+ {:http-client (ig/ref :app.http/client)}
+
:app.setup/props
{:pool (ig/ref :app.db/pool)
:key (cf/get :secret-key)}
diff --git a/backend/src/app/rpc.clj b/backend/src/app/rpc.clj
index 477b0a6339..7c59f964f7 100644
--- a/backend/src/app/rpc.clj
+++ b/backend/src/app/rpc.clj
@@ -242,6 +242,7 @@
(let [cfg (assoc cfg ::type "command" ::metrics-id :rpc-command-timing)]
(->> (sv/scan-ns 'app.rpc.commands.binfile
'app.rpc.commands.comments
+ 'app.rpc.commands.management
'app.rpc.commands.auth
'app.rpc.commands.ldap
'app.rpc.commands.demo)
diff --git a/backend/src/app/rpc/commands/binfile.clj b/backend/src/app/rpc/commands/binfile.clj
index 36c51b0e63..c7bdf54dfa 100644
--- a/backend/src/app/rpc/commands/binfile.clj
+++ b/backend/src/app/rpc/commands/binfile.clj
@@ -536,7 +536,7 @@
:or {overwrite? false migrate? false timestamp (dt/now)}
:as options}]
- (us/assert! ::read-import-options options)
+ (us/verify! ::read-import-options options)
(letfn [(lookup-index [id]
(if ignore-index-errors?
@@ -752,7 +752,11 @@
(case section
:v1/rels (read-rels-section! input)
:v1/files (read-files-section! input files)
- :v1/sobjects (read-sobjects-section! input))))))))))
+ :v1/sobjects (read-sobjects-section! input)))
+
+ ;; Knowing that the ids of the created files are in
+ ;; index, just lookup them and return it as a set
+ (into #{} (keep #(get @*index* %)) files))))))))
(defn export!
[cfg]
diff --git a/backend/src/app/rpc/commands/management.clj b/backend/src/app/rpc/commands/management.clj
new file mode 100644
index 0000000000..4e43d79610
--- /dev/null
+++ b/backend/src/app/rpc/commands/management.clj
@@ -0,0 +1,403 @@
+;; This Source Code Form is subject to the terms of the Mozilla Public
+;; License, v. 2.0. If a copy of the MPL was not distributed with this
+;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
+;;
+;; Copyright (c) UXBOX Labs SL
+
+(ns app.rpc.commands.management
+ "A collection of RPC methods for manage the files, projects and team organization."
+ (:require
+ [app.common.data :as d]
+ [app.common.exceptions :as ex]
+ [app.common.pages.migrations :as pmg]
+ [app.common.spec :as us]
+ [app.common.uuid :as uuid]
+ [app.db :as db]
+ [app.rpc.commands.binfile :as binfile]
+ [app.rpc.doc :as-alias doc]
+ [app.rpc.mutations.projects :refer [create-project-role create-project]]
+ [app.rpc.queries.projects :as proj]
+ [app.rpc.queries.teams :as teams]
+ [app.util.blob :as blob]
+ [app.util.services :as sv]
+ [app.util.time :as dt]
+ [clojure.spec.alpha :as s]
+ [clojure.walk :as walk]))
+
+;; --- COMMAND: Duplicate File
+
+(declare duplicate-file)
+
+(s/def ::id ::us/uuid)
+(s/def ::profile-id ::us/uuid)
+(s/def ::project-id ::us/uuid)
+(s/def ::file-id ::us/uuid)
+(s/def ::team-id ::us/uuid)
+(s/def ::name ::us/string)
+
+(s/def ::duplicate-file
+ (s/keys :req-un [::profile-id ::file-id]
+ :opt-un [::name]))
+
+(sv/defmethod ::duplicate-file
+ "Duplicate a single file in the same team."
+ {::doc/added "1.16"}
+ [{:keys [pool] :as cfg} params]
+ (db/with-atomic [conn pool]
+ (duplicate-file conn params)))
+
+(defn- remap-id
+ [item index key]
+ (cond-> item
+ (contains? item key)
+ (assoc key (get index (get item key) (get item key)))))
+
+(defn- process-file
+ [file index]
+ (letfn [(process-form [form]
+ (cond-> form
+ ;; Relink library items
+ (and (map? form)
+ (uuid? (:component-file form)))
+ (update :component-file #(get index % %))
+
+ (and (map? form)
+ (uuid? (:fill-color-ref-file form)))
+ (update :fill-color-ref-file #(get index % %))
+
+ (and (map? form)
+ (uuid? (:stroke-color-ref-file form)))
+ (update :stroke-color-ref-file #(get index % %))
+
+ (and (map? form)
+ (uuid? (:typography-ref-file form)))
+ (update :typography-ref-file #(get index % %))
+
+ ;; Relink Image Shapes
+ (and (map? form)
+ (map? (:metadata form))
+ (= :image (:type form)))
+ (update-in [:metadata :id] #(get index % %))))
+
+ ;; A function responsible to analyze all file data and
+ ;; replace the old :component-file reference with the new
+ ;; ones, using the provided file-index
+ (relink-shapes [data]
+ (walk/postwalk process-form data))
+
+ ;; A function responsible of process the :media attr of file
+ ;; data and remap the old ids with the new ones.
+ (relink-media [media]
+ (reduce-kv (fn [res k v]
+ (let [id (get index k)]
+ (if (uuid? id)
+ (-> res
+ (assoc id (assoc v :id id))
+ (dissoc k))
+ res)))
+ media
+ media))]
+
+ (update file :data
+ (fn [data]
+ (-> data
+ (blob/decode)
+ (assoc :id (:id file))
+ (pmg/migrate-data)
+ (update :pages-index relink-shapes)
+ (update :components relink-shapes)
+ (update :media relink-media)
+ (d/without-nils)
+ (blob/encode))))))
+
+(def sql:retrieve-used-libraries
+ "select flr.*
+ from file_library_rel as flr
+ inner join file as l on (flr.library_file_id = l.id)
+ where flr.file_id = ?
+ and l.deleted_at is null")
+
+(def sql:retrieve-used-media-objects
+ "select fmo.*
+ from file_media_object as fmo
+ inner join storage_object as so on (fmo.media_id = so.id)
+ where fmo.file_id = ?
+ and so.deleted_at is null")
+
+(defn duplicate-file*
+ [conn {:keys [profile-id file index project-id name flibs fmeds]} {:keys [reset-shared-flag] :as opts}]
+ (let [flibs (or flibs (db/exec! conn [sql:retrieve-used-libraries (:id file)]))
+ fmeds (or fmeds (db/exec! conn [sql:retrieve-used-media-objects (:id file)]))
+
+ ;; memo uniform creation/modification date
+ now (dt/now)
+ ignore (dt/plus now (dt/duration {:seconds 5}))
+
+ ;; add to the index all file media objects.
+ index (reduce #(assoc %1 (:id %2) (uuid/next)) index fmeds)
+
+ flibs-xf (comp
+ (map #(remap-id % index :file-id))
+ (map #(remap-id % index :library-file-id))
+ (map #(assoc % :synced-at now))
+ (map #(assoc % :created-at now)))
+
+ ;; remap all file-library-rel row
+ flibs (sequence flibs-xf flibs)
+
+ fmeds-xf (comp
+ (map #(assoc % :id (get index (:id %))))
+ (map #(assoc % :created-at now))
+ (map #(remap-id % index :file-id)))
+
+ ;; remap all file-media-object rows
+ fmeds (sequence fmeds-xf fmeds)
+
+ file (cond-> file
+ (some? project-id)
+ (assoc :project-id project-id)
+
+ (some? name)
+ (assoc :name name)
+
+ (true? reset-shared-flag)
+ (assoc :is-shared false))
+
+ file (-> file
+ (assoc :created-at now)
+ (assoc :modified-at now)
+ (assoc :ignore-sync-until ignore)
+ (update :id #(get index %))
+ (process-file index))]
+
+ (db/insert! conn :file file)
+ (db/insert! conn :file-profile-rel
+ {:file-id (:id file)
+ :profile-id profile-id
+ :is-owner true
+ :is-admin true
+ :can-edit true})
+
+ (doseq [params flibs]
+ (db/insert! conn :file-library-rel params))
+
+ (doseq [params fmeds]
+ (db/insert! conn :file-media-object params))
+
+ file))
+
+(defn duplicate-file
+ [conn {:keys [profile-id file-id] :as params}]
+ (let [file (db/get-by-id conn :file file-id)
+ index {file-id (uuid/next)}
+ params (assoc params :index index :file file)]
+ (proj/check-edition-permissions! conn profile-id (:project-id file))
+ (db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED"])
+ (-> (duplicate-file* conn params {:reset-shared-flag true})
+ (update :data blob/decode))))
+
+;; --- COMMAND: Duplicate Project
+
+(declare duplicate-project)
+
+(s/def ::duplicate-project
+ (s/keys :req-un [::profile-id ::project-id]
+ :opt-un [::name]))
+
+(sv/defmethod ::duplicate-project
+ "Duplicate an entire project with all the files"
+ {::doc/added "1.16"}
+ [{:keys [pool] :as cfg} params]
+ (db/with-atomic [conn pool]
+ (duplicate-project conn params)))
+
+(defn duplicate-project
+ [conn {:keys [profile-id project-id name] :as params}]
+
+ ;; Defer all constraints
+ (db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED"])
+
+ (let [project (db/get-by-id conn :project project-id)
+
+ files (db/query conn :file
+ {:project-id (:id project)
+ :deleted-at nil}
+ {:columns [:id]})
+
+ project (cond-> project
+ (string? name)
+ (assoc :name name)
+
+ :always
+ (assoc :id (uuid/next)))]
+
+ ;; Check if the source team-id allow creating new project for current user
+ (teams/check-edition-permissions! conn profile-id (:team-id project))
+
+ ;; create the duplicated project and assign the current profile as
+ ;; a project owner
+ (create-project conn project)
+ (create-project-role conn {:project-id (:id project)
+ :profile-id profile-id
+ :role :owner})
+
+ ;; duplicate all files
+ (let [index (reduce #(assoc %1 (:id %2) (uuid/next)) {} files)
+ params (-> params
+ (dissoc :name)
+ (assoc :project-id (:id project))
+ (assoc :index index))]
+ (doseq [{:keys [id]} files]
+ (let [file (db/get-by-id conn :file id)
+ params (assoc params :file file)
+ opts {:reset-shared-flag false}]
+ (duplicate-file* conn params opts))))
+
+ ;; return the created project
+ project))
+
+;; --- COMMAND: Move file
+
+(def sql:retrieve-files
+ "select id, project_id from file where id = ANY(?)")
+
+(def sql:move-files
+ "update file set project_id = ? where id = ANY(?)")
+
+(def sql:delete-broken-relations
+ "with broken as (
+ (select * from file_library_rel as flr
+ inner join file as f on (flr.file_id = f.id)
+ inner join project as p on (f.project_id = p.id)
+ inner join file as lf on (flr.library_file_id = lf.id)
+ inner join project as lp on (lf.project_id = lp.id)
+ where p.id = ANY(?)
+ and lp.team_id != p.team_id)
+ )
+ delete from file_library_rel as rel
+ using broken as br
+ where rel.file_id = br.file_id
+ and rel.library_file_id = br.library_file_id")
+
+(defn move-files
+ [conn {:keys [profile-id ids project-id] :as params}]
+
+ (let [fids (db/create-array conn "uuid" ids)
+ files (db/exec! conn [sql:retrieve-files fids])
+ source (into #{} (map :project-id) files)
+ pids (->> (conj source project-id)
+ (db/create-array conn "uuid"))]
+
+ ;; Check if we have permissions on the destination project
+ (proj/check-edition-permissions! conn profile-id project-id)
+
+ ;; Check if we have permissions on all source projects
+ (doseq [project-id source]
+ (proj/check-edition-permissions! conn profile-id project-id))
+
+ (when (contains? source project-id)
+ (ex/raise :type :validation
+ :code :cant-move-to-same-project
+ :hint "Unable to move a file to the same project"))
+
+ ;; move all files to the project
+ (db/exec-one! conn [sql:move-files project-id fids])
+
+ ;; delete possible broken relations on moved files
+ (db/exec-one! conn [sql:delete-broken-relations pids])
+
+ nil))
+
+(s/def ::ids (s/every ::us/uuid :kind set?))
+(s/def ::move-files
+ (s/keys :req-un [::profile-id ::ids ::project-id]))
+
+(sv/defmethod ::move-files
+ "Move a set of files from one project to other."
+ {::doc/added "1.16"}
+ [{:keys [pool] :as cfg} params]
+ (db/with-atomic [conn pool]
+ (move-files conn params)))
+
+
+;; --- COMMAND: Move project
+
+(defn move-project
+ [conn {:keys [profile-id team-id project-id] :as params}]
+ (let [project (db/get-by-id conn :project project-id {:columns [:id :team-id]})
+ pids (->> (db/query conn :project {:team-id (:team-id project)} {:columns [:id]})
+ (map :id)
+ (db/create-array conn "uuid"))]
+
+ (teams/check-edition-permissions! conn profile-id (:team-id project))
+ (teams/check-edition-permissions! conn profile-id team-id)
+
+ (when (= team-id (:team-id project))
+ (ex/raise :type :validation
+ :code :cant-move-to-same-team
+ :hint "Unable to move a project to same team"))
+
+ ;; move project to the destination team
+ (db/update! conn :project
+ {:team-id team-id}
+ {:id project-id})
+
+ ;; delete possible broken relations on moved files
+ (db/exec-one! conn [sql:delete-broken-relations pids])
+
+ nil))
+
+
+(s/def ::move-project
+ (s/keys :req-un [::profile-id ::team-id ::project-id]))
+
+(sv/defmethod ::move-project
+ "Move projects between teams."
+ {::doc/added "1.16"}
+ [{:keys [pool] :as cfg} params]
+ (db/with-atomic [conn pool]
+ (move-project conn params)))
+
+;; --- COMMAND: Clone Template
+
+(declare clone-template)
+
+(s/def ::template-id ::us/not-empty-string)
+(s/def ::clone-template
+ (s/keys :req-un [::profile-id ::project-id ::template-id]))
+
+(sv/defmethod ::clone-template
+ "Clone into the specified project the template by its id."
+ {::doc/added "1.16"}
+ [{:keys [pool] :as cfg} params]
+ (db/with-atomic [conn pool]
+ (-> (assoc cfg :conn conn)
+ (clone-template params))))
+
+(defn- clone-template
+ [{:keys [conn templates] :as cfg} {:keys [profile-id template-id project-id]}]
+ (let [template (d/seek #(= (:id %) template-id) templates)
+ project (db/get-by-id conn :project project-id {:columns [:id :team-id]})]
+
+ (teams/check-edition-permissions! conn profile-id (:team-id project))
+
+ (when-not template
+ (ex/raise :type :not-found
+ :code :template-not-found
+ :hint "template not found"))
+
+ (-> cfg
+ (assoc ::binfile/input (:path template))
+ (assoc ::binfile/project-id (:id project))
+ (assoc ::binfile/ignore-index-errors? true)
+ (assoc ::binfile/migrate? true)
+ (binfile/import!))))
+
+
+;; --- COMMAND: Retrieve list of builtin templates
+
+(s/def ::retrieve-list-of-builtin-templates any?)
+
+(sv/defmethod ::retrieve-list-of-builtin-templates
+ [cfg _params]
+ (mapv #(select-keys % [:id :name]) (:templates cfg)))
diff --git a/backend/src/app/rpc/doc.clj b/backend/src/app/rpc/doc.clj
index 6afec5643c..499af1a053 100644
--- a/backend/src/app/rpc/doc.clj
+++ b/backend/src/app/rpc/doc.clj
@@ -61,7 +61,7 @@
(if (contains? cf/flags :backend-api-doc)
(let [context (prepare-context methods)]
(fn [_ respond _]
- (respond (yrs/response 200 (-> (io/resource "api-doc.tmpl")
+ (respond (yrs/response 200 (-> (io/resource "app/templates/api-doc.tmpl")
(tmpl/render context))))))
(fn [_ respond _]
(respond (yrs/response 404)))))
diff --git a/backend/src/app/rpc/mutations/management.clj b/backend/src/app/rpc/mutations/management.clj
index 322fbdacd7..c2038868ae 100644
--- a/backend/src/app/rpc/mutations/management.clj
+++ b/backend/src/app/rpc/mutations/management.clj
@@ -7,329 +7,52 @@
(ns app.rpc.mutations.management
"Move & Duplicate RPC methods for files and projects."
(:require
- [app.common.data :as d]
- [app.common.exceptions :as ex]
- [app.common.pages.migrations :as pmg]
- [app.common.spec :as us]
- [app.common.uuid :as uuid]
[app.db :as db]
- [app.rpc.mutations.projects :refer [create-project-role create-project]]
- [app.rpc.queries.projects :as proj]
- [app.rpc.queries.teams :as teams]
- [app.util.blob :as blob]
+ [app.rpc.commands.management :as cmd.mgm]
+ [app.rpc.doc :as-alias doc]
[app.util.services :as sv]
- [app.util.time :as dt]
- [clojure.spec.alpha :as s]
- [clojure.walk :as walk]))
-
-(s/def ::id ::us/uuid)
-(s/def ::profile-id ::us/uuid)
-(s/def ::project-id ::us/uuid)
-(s/def ::file-id ::us/uuid)
-(s/def ::team-id ::us/uuid)
-(s/def ::name ::us/string)
-
-(defn- remap-id
- [item index key]
- (cond-> item
- (contains? item key)
- (assoc key (get index (get item key) (get item key)))))
-
-(defn- process-file
- [file index]
- (letfn [(process-form [form]
- (cond-> form
- ;; Relink library items
- (and (map? form)
- (uuid? (:component-file form)))
- (update :component-file #(get index % %))
-
- (and (map? form)
- (uuid? (:fill-color-ref-file form)))
- (update :fill-color-ref-file #(get index % %))
-
- (and (map? form)
- (uuid? (:stroke-color-ref-file form)))
- (update :stroke-color-ref-file #(get index % %))
-
- (and (map? form)
- (uuid? (:typography-ref-file form)))
- (update :typography-ref-file #(get index % %))
-
- ;; Relink Image Shapes
- (and (map? form)
- (map? (:metadata form))
- (= :image (:type form)))
- (update-in [:metadata :id] #(get index % %))))
-
- ;; A function responsible to analyze all file data and
- ;; replace the old :component-file reference with the new
- ;; ones, using the provided file-index
- (relink-shapes [data]
- (walk/postwalk process-form data))
-
- ;; A function responsible of process the :media attr of file
- ;; data and remap the old ids with the new ones.
- (relink-media [media]
- (reduce-kv (fn [res k v]
- (let [id (get index k)]
- (if (uuid? id)
- (-> res
- (assoc id (assoc v :id id))
- (dissoc k))
- res)))
- media
- media))]
-
- (update file :data
- (fn [data]
- (-> data
- (blob/decode)
- (assoc :id (:id file))
- (pmg/migrate-data)
- (update :pages-index relink-shapes)
- (update :components relink-shapes)
- (update :media relink-media)
- (d/without-nils)
- (blob/encode))))))
-
-(def sql:retrieve-used-libraries
- "select flr.*
- from file_library_rel as flr
- inner join file as l on (flr.library_file_id = l.id)
- where flr.file_id = ?
- and l.deleted_at is null")
-
-(def sql:retrieve-used-media-objects
- "select fmo.*
- from file_media_object as fmo
- inner join storage_object as so on (fmo.media_id = so.id)
- where fmo.file_id = ?
- and so.deleted_at is null")
-
-(defn duplicate-file
- [conn {:keys [profile-id file index project-id name flibs fmeds]} {:keys [reset-shared-flag] :as opts}]
- (let [flibs (or flibs (db/exec! conn [sql:retrieve-used-libraries (:id file)]))
- fmeds (or fmeds (db/exec! conn [sql:retrieve-used-media-objects (:id file)]))
-
- ;; memo uniform creation/modification date
- now (dt/now)
- ignore (dt/plus now (dt/duration {:seconds 5}))
-
- ;; add to the index all file media objects.
- index (reduce #(assoc %1 (:id %2) (uuid/next)) index fmeds)
-
- flibs-xf (comp
- (map #(remap-id % index :file-id))
- (map #(remap-id % index :library-file-id))
- (map #(assoc % :synced-at now))
- (map #(assoc % :created-at now)))
-
- ;; remap all file-library-rel row
- flibs (sequence flibs-xf flibs)
-
- fmeds-xf (comp
- (map #(assoc % :id (get index (:id %))))
- (map #(assoc % :created-at now))
- (map #(remap-id % index :file-id)))
-
- ;; remap all file-media-object rows
- fmeds (sequence fmeds-xf fmeds)
-
- file (cond-> file
- (some? project-id)
- (assoc :project-id project-id)
-
- (some? name)
- (assoc :name name)
-
- (true? reset-shared-flag)
- (assoc :is-shared false))
-
- file (-> file
- (assoc :created-at now)
- (assoc :modified-at now)
- (assoc :ignore-sync-until ignore)
- (update :id #(get index %))
- (process-file index))]
-
- (db/insert! conn :file file)
- (db/insert! conn :file-profile-rel
- {:file-id (:id file)
- :profile-id profile-id
- :is-owner true
- :is-admin true
- :can-edit true})
-
- (doseq [params flibs]
- (db/insert! conn :file-library-rel params))
-
- (doseq [params fmeds]
- (db/insert! conn :file-media-object params))
-
- file))
-
+ [clojure.spec.alpha :as s]))
;; --- MUTATION: Duplicate File
-(declare duplicate-file)
-
-(s/def ::duplicate-file
- (s/keys :req-un [::profile-id ::file-id]
- :opt-un [::name]))
+(s/def ::duplicate-file ::cmd.mgm/duplicate-file)
(sv/defmethod ::duplicate-file
- [{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
+ {::doc/added "1.2"
+ ::doc/deprecated "1.16"}
+ [{:keys [pool] :as cfg} params]
(db/with-atomic [conn pool]
- (let [file (db/get-by-id conn :file file-id)
- index {file-id (uuid/next)}
- params (assoc params :index index :file file)]
- (proj/check-edition-permissions! conn profile-id (:project-id file))
- (db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED"])
- (-> (duplicate-file conn params {:reset-shared-flag true})
- (update :data blob/decode)))))
-
+ (cmd.mgm/duplicate-file conn params)))
;; --- MUTATION: Duplicate Project
-(declare duplicate-project)
-
-(s/def ::duplicate-project
- (s/keys :req-un [::profile-id ::project-id]
- :opt-un [::name]))
+(s/def ::duplicate-project ::cmd.mgm/duplicate-project)
(sv/defmethod ::duplicate-project
- [{:keys [pool] :as cfg} {:keys [profile-id project-id] :as params}]
+ {::doc/added "1.2"
+ ::doc/deprecated "1.16"}
+ [{:keys [pool] :as cfg} params]
(db/with-atomic [conn pool]
- (let [project (db/get-by-id conn :project project-id)]
- (teams/check-edition-permissions! conn profile-id (:team-id project))
- (db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED"])
- (duplicate-project conn (assoc params :project project)))))
-
-(defn duplicate-project
- [conn {:keys [profile-id project name] :as params}]
- (let [files (db/query conn :file
- {:project-id (:id project)
- :deleted-at nil}
- {:columns [:id]})
-
- project (cond-> project
- (string? name)
- (assoc :name name)
-
- :always
- (assoc :id (uuid/next)))]
-
- ;; create the duplicated project and assign the current profile as
- ;; a project owner
- (create-project conn project)
- (create-project-role conn {:project-id (:id project)
- :profile-id profile-id
- :role :owner})
-
- ;; duplicate all files
- (let [index (reduce #(assoc %1 (:id %2) (uuid/next)) {} files)
- params (-> params
- (dissoc :name)
- (assoc :project-id (:id project))
- (assoc :index index))]
- (doseq [{:keys [id]} files]
- (let [file (db/get-by-id conn :file id)
- params (assoc params :file file)
- opts {:reset-shared-flag false}]
- (duplicate-file conn params opts))))
-
- ;; return the created project
- project))
-
+ (cmd.mgm/duplicate-project conn params)))
;; --- MUTATION: Move file
-(def sql:retrieve-files
- "select id, project_id from file where id = ANY(?)")
-
-(def sql:move-files
- "update file set project_id = ? where id = ANY(?)")
-
-(def sql:delete-broken-relations
- "with broken as (
- (select * from file_library_rel as flr
- inner join file as f on (flr.file_id = f.id)
- inner join project as p on (f.project_id = p.id)
- inner join file as lf on (flr.library_file_id = lf.id)
- inner join project as lp on (lf.project_id = lp.id)
- where p.id = ANY(?)
- and lp.team_id != p.team_id)
- )
- delete from file_library_rel as rel
- using broken as br
- where rel.file_id = br.file_id
- and rel.library_file_id = br.library_file_id")
-
-(s/def ::ids (s/every ::us/uuid :kind set?))
-(s/def ::move-files
- (s/keys :req-un [::profile-id ::ids ::project-id]))
+(s/def ::move-files ::cmd.mgm/move-files)
(sv/defmethod ::move-files
- [{:keys [pool] :as cfg} {:keys [profile-id ids project-id] :as params}]
+ {::doc/added "1.2"
+ ::doc/deprecated "1.16"}
+ [{:keys [pool] :as cfg} params]
(db/with-atomic [conn pool]
- (let [fids (db/create-array conn "uuid" ids)
- files (db/exec! conn [sql:retrieve-files fids])
- source (into #{} (map :project-id) files)
- pids (->> (conj source project-id)
- (db/create-array conn "uuid"))]
-
- ;; Check if we have permissions on the destination project
- (proj/check-edition-permissions! conn profile-id project-id)
-
- ;; Check if we have permissions on all source projects
- (doseq [project-id source]
- (proj/check-edition-permissions! conn profile-id project-id))
-
- (when (contains? source project-id)
- (ex/raise :type :validation
- :code :cant-move-to-same-project
- :hint "Unable to move a file to the same project"))
-
- ;; move all files to the project
- (db/exec-one! conn [sql:move-files project-id fids])
-
- ;; delete possible broken relations on moved files
- (db/exec-one! conn [sql:delete-broken-relations pids])
-
- nil)))
-
+ (cmd.mgm/move-files conn params)))
;; --- MUTATION: Move project
-(declare move-project)
-
-(s/def ::move-project
- (s/keys :req-un [::profile-id ::team-id ::project-id]))
+(s/def ::move-project ::cmd.mgm/move-project)
(sv/defmethod ::move-project
- [{:keys [pool] :as cfg} {:keys [profile-id team-id project-id] :as params}]
+ {::doc/added "1.2"
+ ::doc/deprecated "1.16"}
+ [{:keys [pool] :as cfg} params]
(db/with-atomic [conn pool]
- (let [project (db/get-by-id conn :project project-id {:columns [:id :team-id]})
-
- pids (->> (db/query conn :project {:team-id (:team-id project)} {:columns [:id]})
- (map :id)
- (db/create-array conn "uuid"))]
-
- (teams/check-edition-permissions! conn profile-id (:team-id project))
- (teams/check-edition-permissions! conn profile-id team-id)
-
- (when (= team-id (:team-id project))
- (ex/raise :type :validation
- :code :cant-move-to-same-team
- :hint "Unable to move a project to same team"))
-
- ;; move project to the destination team
- (db/update! conn :project
- {:team-id team-id}
- {:id project-id})
-
- ;; delete possible broken relations on moved files
- (db/exec-one! conn [sql:delete-broken-relations pids])
-
- nil)))
+ (cmd.mgm/move-project conn params)))
diff --git a/backend/src/app/setup.clj b/backend/src/app/setup.clj
index b1b5e93138..7bc0960ba5 100644
--- a/backend/src/app/setup.clj
+++ b/backend/src/app/setup.clj
@@ -10,6 +10,7 @@
[app.common.logging :as l]
[app.common.uuid :as uuid]
[app.db :as db]
+ [app.setup.builtin-templates]
[buddy.core.codecs :as bc]
[buddy.core.nonce :as bn]
[clojure.spec.alpha :as s]
diff --git a/backend/src/app/setup/builtin_templates.clj b/backend/src/app/setup/builtin_templates.clj
new file mode 100644
index 0000000000..d052550582
--- /dev/null
+++ b/backend/src/app/setup/builtin_templates.clj
@@ -0,0 +1,74 @@
+;; This Source Code Form is subject to the terms of the Mozilla Public
+;; License, v. 2.0. If a copy of the MPL was not distributed with this
+;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
+;;
+;; Copyright (c) UXBOX Labs SL
+
+(ns app.setup.builtin-templates
+ "A service/module that is reponsible for download, load & internally
+ expose a set of builtin penpot file templates."
+ (:require
+ [app.common.logging :as l]
+ [app.common.spec :as us]
+ [app.http.client :as http]
+ [clojure.edn :as edn]
+ [clojure.java.io :as io]
+ [clojure.spec.alpha :as s]
+ [datoteka.core :as fs]
+ [integrant.core :as ig]))
+
+(declare download-all!)
+
+(s/def ::id ::us/not-empty-string)
+(s/def ::name ::us/not-empty-string)
+(s/def ::thumbnail-uri ::us/not-empty-string)
+(s/def ::file-uri ::us/not-empty-string)
+(s/def ::path fs/path?)
+
+(s/def ::template
+ (s/keys :req-un [::id ::name ::thumbnail-uri ::file-uri]
+ :opt-un [::path]))
+
+(s/def ::http-client ::http/client)
+
+(defmethod ig/pre-init-spec :app.setup/builtin-templates [_]
+ (s/keys :req-un [::http-client]))
+
+(defmethod ig/init-key :app.setup/builtin-templates
+ [_ cfg]
+ (let [presets (-> "app/onboarding.edn" io/resource slurp edn/read-string)]
+ (l/info :hint "loading template files" :total (count presets))
+ (let [result (download-all! cfg presets)]
+ (us/conform (s/coll-of ::template) result))))
+
+(defn- download-preset!
+ [cfg {:keys [path file-uri] :as preset}]
+ (let [response (http/req! (:http-client cfg)
+ {:method :get
+ :uri file-uri}
+ {:response-type :input-stream
+ :sync? true})]
+ (us/verify! (= 200 (:status response)) "unexpected response found on fetching preset")
+ (with-open [output (io/output-stream path)]
+ (with-open [input (io/input-stream (:body response))]
+ (io/copy input output)))))
+
+(defn- download-all!
+ "Download presets to the default directory, if preset is already
+ downloaded, no action will be performed."
+ [cfg presets]
+ (let [dest (fs/join fs/*cwd* "builtin-templates")]
+ (when-not (fs/exists? dest)
+ (fs/create-dir dest))
+
+ (doall
+ (map (fn [item]
+ (let [path (fs/join dest (:id item))
+ item (assoc item :path path)]
+ (if (fs/exists? path)
+ (l/trace :hint "template file already present" :id (:id item))
+ (do
+ (l/trace :hint "downloading template file" :id (:id item) :dest (str path))
+ (download-preset! cfg item)))
+ item))
+ presets))))
diff --git a/backend/src/app/setup/initial_data.clj b/backend/src/app/setup/initial_data.clj
deleted file mode 100644
index 6532a54e57..0000000000
--- a/backend/src/app/setup/initial_data.clj
+++ /dev/null
@@ -1,106 +0,0 @@
-;; This Source Code Form is subject to the terms of the Mozilla Public
-;; License, v. 2.0. If a copy of the MPL was not distributed with this
-;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
-;;
-;; Copyright (c) UXBOX Labs SL
-
-(ns app.setup.initial-data
- (:refer-clojure :exclude [load])
- (:require
- [app.common.uuid :as uuid]
- [app.config :as cf]
- [app.db :as db]
- [app.rpc.mutations.management :refer [duplicate-file]]
- [app.rpc.mutations.projects :refer [create-project create-project-role]]
- [app.rpc.queries.profile :as profile]))
-
-;; --- DUMP GENERATION
-
-(def sql:file
- "select * from file where project_id = ?")
-
-(def sql:file-library-rel
- "with file_ids as (select id from file where project_id = ?)
- select *
- from file_library_rel
- where file_id in (select id from file_ids)")
-
-(def sql:file-media-object
- "with file_ids as (select id from file where project_id = ?)
- select *
- from file_media_object
- where file_id in (select id from file_ids)")
-
-(defn dump
- ([system project-id] (dump system project-id nil))
- ([system project-id {:keys [skey project-name]
- :or {project-name "Penpot Onboarding"}}]
- (db/with-atomic [conn (:app.db/pool system)]
- (let [skey (or skey (cf/get :initial-project-skey))
- files (db/exec! conn [sql:file project-id])
- flibs (db/exec! conn [sql:file-library-rel project-id])
- fmeds (db/exec! conn [sql:file-media-object project-id])
- data {:project-name project-name
- :files files
- :flibs flibs
- :fmeds fmeds}]
-
- (db/delete! conn :server-prop {:id skey})
- (db/insert! conn :server-prop
- {:id skey
- :preload false
- :content (db/tjson data)})
- skey))))
-
-
-;; --- DUMP LOADING
-
-(defn- retrieve-data
- [conn skey]
- (when-let [row (db/exec-one! conn ["select content from server_prop where id = ?" skey])]
- (when-let [content (:content row)]
- (when (db/pgobject? content)
- (db/decode-transit-pgobject content)))))
-
-(defn load-initial-project!
- ([conn profile] (load-initial-project! conn profile nil))
- ([conn profile opts]
- (let [skey (or (:skey opts) (cf/get :initial-project-skey))
- data (retrieve-data conn skey)]
- (when data
- (let [index (reduce #(assoc %1 (:id %2) (uuid/next)) {} (:files data))
- project {:id (uuid/next)
- :profile-id (:id profile)
- :team-id (:default-team-id profile)
- :name (:project-name data)}]
-
- (db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED"])
-
- (create-project conn project)
- (create-project-role conn {:project-id (:id project)
- :profile-id (:id profile)
- :role :owner})
-
- (doseq [file (:files data)]
- (let [flibs (filterv #(= (:id file) (:file-id %)) (:flibs data))
- fmeds (filterv #(= (:id file) (:file-id %)) (:fmeds data))
-
- params {:profile-id (:id profile)
- :project-id (:id project)
- :file file
- :index index
- :flibs flibs
- :fmeds fmeds}
-
- opts {:reset-shared-flag false}]
- (duplicate-file conn params opts))))))))
-
-(defn load
- [system {:keys [email] :as opts}]
- (db/with-atomic [conn (:app.db/pool system)]
- (when-let [profile (some->> email
- (profile/retrieve-profile-data-by-email conn)
- (profile/populate-additional-data conn))]
- (load-initial-project! conn profile opts)
- true)))
-
diff --git a/backend/src/app/util/emails.clj b/backend/src/app/util/emails.clj
index dca9b732ea..e0f42e49f4 100644
--- a/backend/src/app/util/emails.clj
+++ b/backend/src/app/util/emails.clj
@@ -178,7 +178,7 @@
;; Template Email Building
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-(def ^:private email-path "emails/%(id)s/%(lang)s.%(type)s")
+(def ^:private email-path "app/emails/%(id)s/%(lang)s.%(type)s")
(defn- render-email-template-part
[type id context]
diff --git a/backend/test/app/services_management_test.clj b/backend/test/app/services_management_test.clj
index 2089537ea6..f738e52e75 100644
--- a/backend/test/app/services_management_test.clj
+++ b/backend/test/app/services_management_test.clj
@@ -19,6 +19,8 @@
(t/use-fixtures :once th/state-init)
(t/use-fixtures :each th/database-reset)
+;; TODO: migrate to commands
+
(t/deftest duplicate-file
(let [storage (-> (:app.storage/storage th/*system*)
(configure-storage-backend))
@@ -602,3 +604,31 @@
(t/is (= (:library-file-id item1) (:id file2))))
)))
+
+(t/deftest clone-template
+ (let [prof (th/create-profile* 1 {:is-active true})
+ data {::th/type :clone-template
+ :profile-id (:id prof)
+ :project-id (:default-project-id prof)
+ :template-id "test"}
+
+ out (th/command! data)]
+ ;; (th/print-result! out)
+
+ (t/is (nil? (:error out)))
+ (let [result (:result out)]
+ (t/is (set? result))
+ (t/is (uuid? (first result)))
+ (t/is (= 1 (count result))))))
+
+(t/deftest retrieve-list-of-buitin-templates
+ (let [prof (th/create-profile* 1 {:is-active true})
+ data {::th/type :retrieve-list-of-builtin-templates
+ :profile-id (:id prof)}
+ out (th/command! data)]
+ ;; (th/print-result! out)
+ (t/is (nil? (:error out)))
+ (let [result (:result out)]
+ (t/is (vector? result))
+ (t/is (= 1 (count result)))
+ (t/is (= "test" (:id (first result)))))))
diff --git a/backend/test/app/test_files/template.penpot b/backend/test/app/test_files/template.penpot
new file mode 100644
index 0000000000..1375c6d525
Binary files /dev/null and b/backend/test/app/test_files/template.penpot differ
diff --git a/backend/test/app/test_helpers.clj b/backend/test/app/test_helpers.clj
index 6768b2a3e2..ccdd5d6d37 100644
--- a/backend/test/app/test_helpers.clj
+++ b/backend/test/app/test_helpers.clj
@@ -52,11 +52,18 @@
(defn state-init
[next]
- (let [config (-> main/system-config
+ (let [templates [{:id "test"
+ :name "test"
+ :file-uri "test"
+ :thumbnail-uri "test"
+ :path (-> "app/test_files/template.penpot" io/resource fs/path)}]
+
+ config (-> main/system-config
(assoc-in [:app.msgbus/msgbus :redis-uri] (:redis-uri config))
(assoc-in [:app.db/pool :uri] (:database-uri config))
(assoc-in [:app.db/pool :username] (:database-username config))
(assoc-in [:app.db/pool :password] (:database-password config))
+ (assoc-in [:app.rpc/methods :templates] templates)
(dissoc :app.srepl/server
:app.http/server
:app.http/router
@@ -66,6 +73,7 @@
:app.auth.oidc/gitlab-provider
:app.auth.oidc/github-provider
:app.auth.oidc/generic-provider
+ :app.setup/builtin-templates
:app.auth.oidc/routes
;; :app.auth.ldap/provider
:app.worker/executors-monitor
diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs
index 33dce462a3..071cb4b3fe 100644
--- a/frontend/src/app/main/data/dashboard.cljs
+++ b/frontend/src/app/main/data/dashboard.cljs
@@ -568,8 +568,7 @@
new-name (str name " " (tr "dashboard.copy-suffix"))]
- (->> (rp/mutation! :duplicate-project {:project-id id
- :name new-name})
+ (->> (rp/command! :duplicate-project {:project-id id :name new-name})
(rx/tap on-success)
(rx/map project-duplicated)
(rx/catch on-error))))))
@@ -589,8 +588,7 @@
:or {on-success identity
on-error rx/throw}} (meta params)]
- (->> (rp/mutation! :move-project {:project-id id
- :team-id team-id})
+ (->> (rp/command! :move-project {:project-id id :team-id team-id})
(rx/tap on-success)
(rx/catch on-error))))))
@@ -771,8 +769,7 @@
new-name (str name " " (tr "dashboard.copy-suffix"))]
- (->> (rp/mutation! :duplicate-file {:file-id id
- :name new-name})
+ (->> (rp/command! :duplicate-file {:file-id id :name new-name})
(rx/tap on-success)
(rx/map file-created)
(rx/catch on-error))))))
@@ -794,7 +791,7 @@
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)]
- (->> (rp/mutation! :move-files {:ids ids :project-id project-id})
+ (->> (rp/command! :move-files {:ids ids :project-id project-id})
(rx/tap on-success)
(rx/catch on-error))))))