0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-04-13 15:31:26 -05:00

Merge pull request #2138 from penpot/niwinz-builtin-templates

Builtin Templates Load & RPC command for clone
This commit is contained in:
Pablo Alba 2022-08-03 17:44:53 +02:00 committed by GitHub
commit 87419d63a5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 653 additions and 433 deletions

1
.gitignore vendored
View file

@ -23,6 +23,7 @@
/backend/resources/public/assets
/backend/resources/public/media
/backend/target/
/backend/builtin-templates
/bundle*
/cd.md
/clj-profiler/

View file

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

View file

@ -10,10 +10,10 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@200;300;400;500;700&display=swap" rel="stylesheet">
<style>
{% include "api-doc.css" %}
{% include "app/templates/api-doc.css" %}
</style>
<script>
{% include "api-doc.js" %}
{% include "app/templates/api-doc.js" %}
</script>
</head>
<body>
@ -26,21 +26,21 @@
<h2>RPC COMMAND METHODS:</h2>
<ul class="rpc-items">
{% for item in command-methods %}
{% include "api-doc-entry.tmpl" with item=item %}
{% include "app/templates/api-doc-entry.tmpl" with item=item %}
{% endfor %}
</ul>
<h2>RPC QUERY METHODS:</h2>
<ul class="rpc-items">
{% for item in query-methods %}
{% include "api-doc-entry.tmpl" with item=item %}
{% include "app/templates/api-doc-entry.tmpl" with item=item %}
{% endfor %}
</ul>
<h2>RPC MUTATION METHODS:</h2>
<ul class="rpc-items">
{% for item in mutation-methods %}
{% include "api-doc-entry.tmpl" with item=item %}
{% include "app/templates/api-doc-entry.tmpl" with item=item %}
{% endfor %}
</ul>
</section>

View file

@ -7,7 +7,7 @@
<title>{% block title %}{% endblock %}</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono">
<style>
{% include "templates/styles.css" %}
{% include "app/templates/styles.css" %}
</style>
</head>
<body>

View file

@ -1,4 +1,4 @@
{% extends "templates/base.tmpl" %}
{% extends "app/templates/base.tmpl" %}
{% block title %}
Debug Main Page

View file

@ -1,4 +1,4 @@
{% extends "templates/base.tmpl" %}
{% extends "app/templates/base.tmpl" %}
{% block title %}
penpot - error list

View file

@ -1,4 +1,4 @@
{% extends "templates/base.tmpl" %}
{% extends "app/templates/base.tmpl" %}
{% block title %}
penpot - error report {{id}}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

Binary file not shown.

View file

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

View file

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