mirror of
https://github.com/penpot/penpot.git
synced 2025-01-09 16:30:37 -05:00
Merge pull request #3953 from penpot/niwinz-staging-duplicate-team
✨ Add internal helper for team duplication
This commit is contained in:
commit
de48ec4cbf
3 changed files with 200 additions and 74 deletions
|
@ -35,6 +35,20 @@
|
|||
[promesa.core :as p]
|
||||
[promesa.exec :as px]))
|
||||
|
||||
(defn- index-row
|
||||
[index obj]
|
||||
(assoc index (:id obj) (uuid/next)))
|
||||
|
||||
(defn- lookup-index
|
||||
[id index]
|
||||
(get index id id))
|
||||
|
||||
(defn- remap-id
|
||||
[item index key]
|
||||
(cond-> item
|
||||
(contains? item key)
|
||||
(update key lookup-index index)))
|
||||
|
||||
;; --- COMMAND: Duplicate File
|
||||
|
||||
(declare duplicate-file)
|
||||
|
@ -51,14 +65,15 @@
|
|||
{::doc/added "1.16"
|
||||
::webhooks/event? true
|
||||
::sm/params schema:duplicate-file}
|
||||
[cfg {:keys [::rpc/profile-id] :as params}]
|
||||
(db/tx-run! cfg duplicate-file (assoc params :profile-id profile-id)))
|
||||
[cfg {:keys [::rpc/profile-id file-id] :as params}]
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||
(db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED"])
|
||||
|
||||
(defn- remap-id
|
||||
[item index key]
|
||||
(cond-> item
|
||||
(contains? item key)
|
||||
(assoc key (get index (get item key) (get item key)))))
|
||||
(let [params (-> params
|
||||
(assoc :index {file-id (uuid/next)})
|
||||
(assoc :profile-id profile-id)
|
||||
(assoc ::reset-shared-flag? true))]
|
||||
(duplicate-file cfg params)))))
|
||||
|
||||
(defn- process-file
|
||||
[cfg index {:keys [id] :as file}]
|
||||
|
@ -109,7 +124,6 @@
|
|||
(process-file [{:keys [id] :as file}]
|
||||
(-> file
|
||||
(update :data assoc :id id)
|
||||
(update :data feat.fdata/process-pointers deref)
|
||||
(pmg/migrate-file)
|
||||
(update :data (fn [data]
|
||||
(-> data
|
||||
|
@ -118,10 +132,12 @@
|
|||
(update :media relink-media)
|
||||
(d/without-nils))))))]
|
||||
|
||||
(let [new-id (get index id)
|
||||
file (binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)]
|
||||
(-> (assoc file :id new-id)
|
||||
(process-file)))
|
||||
(let [file (binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)]
|
||||
(-> file
|
||||
(update :id lookup-index index)
|
||||
(update :project-id lookup-index index)
|
||||
(update :data feat.fdata/process-pointers deref)
|
||||
(process-file)))
|
||||
|
||||
file (if (contains? (:features file) "fdata/objects-map")
|
||||
(feat.fdata/enable-objects-map file)
|
||||
|
@ -135,31 +151,34 @@
|
|||
file)]
|
||||
file)))
|
||||
|
||||
(def sql:get-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")
|
||||
(defn duplicate-file
|
||||
[{:keys [::db/conn] :as cfg} {:keys [profile-id index file-id name ::reset-shared-flag?]}]
|
||||
(let [;; We don't touch the original file on duplication
|
||||
file (files/get-file cfg file-id :migrate? false)
|
||||
|
||||
(def sql:get-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")
|
||||
;; We only check permissions if profile-id is present; it can
|
||||
;; be omited when this function is called from SREPL helpers
|
||||
_ (when (uuid? profile-id)
|
||||
(proj/check-edition-permissions! conn profile-id (:project-id file)))
|
||||
|
||||
(defn duplicate-file*
|
||||
[{:keys [::db/conn] :as cfg} {:keys [profile-id file index project-id name flibs fmeds]} {:keys [reset-shared-flag]}]
|
||||
(let [flibs (or flibs (db/exec! conn [sql:get-used-libraries (:id file)]))
|
||||
fmeds (or fmeds (db/exec! conn [sql:get-used-media-objects (:id file)]))
|
||||
flibs (let [sql (str "SELECT flr.* "
|
||||
" FROM file_library_rel AS flr "
|
||||
" JOIN file AS l ON (flr.library_file_id = l.id) "
|
||||
" WHERE flr.file_id = ? AND l.deleted_at is null")]
|
||||
(db/exec! conn [sql file-id]))
|
||||
|
||||
fmeds (let [sql (str "SELECT fmo.* "
|
||||
" FROM file_media_object AS fmo "
|
||||
" JOIN storage_object AS so ON (fmo.media_id = so.id) "
|
||||
" WHERE fmo.file_id = ? AND so.deleted_at is null")]
|
||||
(db/exec! conn [sql file-id]))
|
||||
|
||||
;; 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)
|
||||
index (reduce index-row index fmeds)
|
||||
|
||||
flibs-xf (comp
|
||||
(map #(remap-id % index :file-id))
|
||||
|
@ -179,13 +198,10 @@
|
|||
fmeds (sequence fmeds-xf fmeds)
|
||||
|
||||
file (cond-> file
|
||||
(some? project-id)
|
||||
(assoc :project-id project-id)
|
||||
|
||||
(some? name)
|
||||
(string? name)
|
||||
(assoc :name name)
|
||||
|
||||
(true? reset-shared-flag)
|
||||
(true? reset-shared-flag?)
|
||||
(assoc :is-shared false))
|
||||
|
||||
file (-> file
|
||||
|
@ -201,13 +217,18 @@
|
|||
(update :data blob/encode))
|
||||
{::db/return-keys? false})
|
||||
|
||||
(db/insert! conn :file-profile-rel
|
||||
{:file-id (:id file)
|
||||
:profile-id profile-id
|
||||
:is-owner true
|
||||
:is-admin true
|
||||
:can-edit true}
|
||||
{::db/return-keys? false})
|
||||
;; The file profile creation is optional, so when no profile is
|
||||
;; present (when this function is called from profile less
|
||||
;; environment: SREPL) we just omit the creation of the relation
|
||||
|
||||
(when (uuid? profile-id)
|
||||
(db/insert! conn :file-profile-rel
|
||||
{:file-id (:id file)
|
||||
:profile-id profile-id
|
||||
:is-owner true
|
||||
:is-admin true
|
||||
:can-edit true}
|
||||
{::db/return-keys? false}))
|
||||
|
||||
(doseq [params flibs]
|
||||
(db/insert! conn :file-library-rel params ::db/return-keys? false))
|
||||
|
@ -217,16 +238,6 @@
|
|||
|
||||
file))
|
||||
|
||||
(defn duplicate-file
|
||||
[{:keys [::db/conn] :as cfg} {:keys [profile-id file-id] :as params}]
|
||||
(let [;; We don't touch the original file on duplication
|
||||
file (files/get-file cfg file-id :migrate? false)
|
||||
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* cfg params {:reset-shared-flag true})))
|
||||
|
||||
;; --- COMMAND: Duplicate Project
|
||||
|
||||
(declare duplicate-project)
|
||||
|
@ -244,28 +255,29 @@
|
|||
::webhooks/event? true
|
||||
::sm/params schema:duplicate-project}
|
||||
[cfg {:keys [::rpc/profile-id] :as params}]
|
||||
(db/tx-run! cfg duplicate-project (assoc params :profile-id profile-id)))
|
||||
(db/tx-run! cfg (fn [cfg]
|
||||
;; Defer all constraints
|
||||
(db/exec-one! cfg ["SET CONSTRAINTS ALL DEFERRED"])
|
||||
(duplicate-project cfg (assoc params :profile-id profile-id)))))
|
||||
|
||||
(defn duplicate-project
|
||||
[{:keys [::db/conn] :as cfg} {: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)
|
||||
(assoc :is-pinned false))
|
||||
|
||||
files (db/query conn :file
|
||||
{:project-id (:id project)
|
||||
{:project-id project-id
|
||||
:deleted-at nil}
|
||||
{:columns [:id]})
|
||||
|
||||
index (reduce index-row {project-id (uuid/next)} files)
|
||||
|
||||
project (cond-> project
|
||||
(string? name)
|
||||
(assoc :name name)
|
||||
|
||||
:always
|
||||
(assoc :id (uuid/next)))]
|
||||
(update :id lookup-index index))]
|
||||
|
||||
;; Check if the source team-id allow creating new project for current user
|
||||
(teams/check-edition-permissions! conn profile-id (:team-id project))
|
||||
|
@ -273,23 +285,119 @@
|
|||
;; create the duplicated project and assign the current profile as
|
||||
;; a project owner
|
||||
(teams/create-project conn project)
|
||||
(teams/create-project-role conn profile-id (:id project) :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 (files/get-file cfg id :migrate? false)
|
||||
params (assoc params :file file)
|
||||
opts {:reset-shared-flag false}]
|
||||
(duplicate-file* cfg params opts))))
|
||||
;; The project profile creation is optional, so when no profile is
|
||||
;; present (when this function is called from profile less
|
||||
;; environment: SREPL) we just omit the creation of the relation
|
||||
(when (uuid? profile-id)
|
||||
(teams/create-project-role conn profile-id (:id project) :owner))
|
||||
|
||||
(doseq [{:keys [id] :as file} files]
|
||||
(let [params (-> params
|
||||
(dissoc :name)
|
||||
(assoc :file-id id)
|
||||
(assoc :index index)
|
||||
(assoc ::reset-shared-flag? false))]
|
||||
(duplicate-file cfg params)))
|
||||
|
||||
;; return the created project
|
||||
project))
|
||||
|
||||
(defn duplicate-team
|
||||
[{:keys [::db/conn] :as cfg} & {:keys [profile-id team-id name] :as params}]
|
||||
|
||||
;; Check if the source team-id allowed to be read by the user if
|
||||
;; profile-id is present; it can be ommited if this function is
|
||||
;; called from SREPL helpers where no profile is available
|
||||
(when (uuid? profile-id)
|
||||
(teams/check-read-permissions! conn profile-id team-id))
|
||||
|
||||
(let [projs (db/query conn :project
|
||||
{:team-id team-id})
|
||||
|
||||
files (let [sql (str "SELECT f.id "
|
||||
" 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")]
|
||||
(db/exec! conn [sql team-id]))
|
||||
|
||||
trels (db/query conn :team-profile-rel
|
||||
{:team-id team-id})
|
||||
|
||||
prels (let [sql (str "SELECT r.* FROM project_profile_rel AS r "
|
||||
" JOIN project AS p ON (r.project_id = p.id) "
|
||||
" WHERE p.team_id = ?")]
|
||||
(db/exec! conn [sql team-id]))
|
||||
|
||||
|
||||
fonts (db/query conn :team-font-variant
|
||||
{:team-id team-id})
|
||||
|
||||
index (as-> {team-id (uuid/next)} index
|
||||
(reduce index-row index projs)
|
||||
(reduce index-row index files)
|
||||
(reduce index-row index fonts))
|
||||
|
||||
team (db/get-by-id conn :team team-id)
|
||||
team (cond-> team
|
||||
(string? name)
|
||||
(assoc :name name)
|
||||
|
||||
:always
|
||||
(update :id lookup-index index))]
|
||||
|
||||
;; FIXME: disallow clone default team
|
||||
|
||||
;; Create the new team in the database
|
||||
(db/insert! conn :team team)
|
||||
|
||||
;; Duplicate team <-> profile relations
|
||||
(doseq [params trels]
|
||||
(let [params (-> params
|
||||
(assoc :id (uuid/next))
|
||||
(update :team-id lookup-index index))]
|
||||
(db/insert! conn :team-profile-rel params
|
||||
{::db/return-keys? false})))
|
||||
|
||||
;; Duplucate team fonts
|
||||
(doseq [font fonts]
|
||||
(let [params (-> font
|
||||
(update :id lookup-index index)
|
||||
(update :team-id lookup-index index))]
|
||||
(db/insert! conn :team-font-variant params
|
||||
{::db/return-keys? false})))
|
||||
|
||||
;; Create all the projects in the database
|
||||
(doseq [project projs]
|
||||
(let [project (-> project
|
||||
(update :id lookup-index index)
|
||||
(update :team-id lookup-index index))]
|
||||
(teams/create-project conn project)
|
||||
|
||||
;; The project profile creation is optional, so when no profile is
|
||||
;; present (when this function is called from profile less
|
||||
;; environment: SREPL) we just omit the creation of the relation
|
||||
(when (uuid? profile-id)
|
||||
(teams/create-project-role conn profile-id (:id project) :owner))))
|
||||
|
||||
;; Duplicate project <-> profile relations
|
||||
(doseq [params prels]
|
||||
(let [params (-> params
|
||||
(assoc :id (uuid/next))
|
||||
(update :project-id lookup-index index))]
|
||||
(db/insert! conn :project-profile-rel params)))
|
||||
|
||||
(doseq [file-id (map :id files)]
|
||||
(let [params (-> params
|
||||
(dissoc :name)
|
||||
(assoc :index index)
|
||||
(assoc :file-id file-id)
|
||||
(assoc ::reset-shared-flag? false))]
|
||||
(duplicate-file cfg params)))
|
||||
|
||||
team))
|
||||
|
||||
;; --- COMMAND: Move file
|
||||
|
||||
(def sql:get-files
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
[app.msgbus :as mbus]
|
||||
[app.rpc.commands.auth :as auth]
|
||||
[app.rpc.commands.files-snapshot :as fsnap]
|
||||
[app.rpc.commands.management :as mgmt]
|
||||
[app.rpc.commands.profile :as profile]
|
||||
[app.srepl.cli :as cli]
|
||||
[app.srepl.helpers :as h]
|
||||
|
@ -332,3 +333,13 @@
|
|||
(filter some?)
|
||||
(into #{})
|
||||
(run! send))))
|
||||
|
||||
|
||||
(defn duplicate-team
|
||||
[system team-id & {:keys [name]}]
|
||||
(let [team-id (if (string? team-id) (parse-uuid team-id) team-id)
|
||||
name (or name (fn [prev-name]
|
||||
(str/ffmt "Cloned: % (%)" prev-name (dt/format-instant (dt/now)))))]
|
||||
(db/tx-run! system (fn [cfg]
|
||||
(db/exec-one! cfg ["SET CONSTRAINTS ALL DEFERRED"])
|
||||
(mgmt/duplicate-team cfg :team-id team-id :name name)))))
|
||||
|
|
|
@ -209,9 +209,16 @@
|
|||
([v] (.format DateTimeFormatter/ISO_INSTANT ^Instant v))
|
||||
([v fmt]
|
||||
(case fmt
|
||||
:iso (.format DateTimeFormatter/ISO_INSTANT ^Instant v)
|
||||
:rfc1123 (.format DateTimeFormatter/RFC_1123_DATE_TIME
|
||||
^ZonedDateTime (instant->zoned-date-time v)))))
|
||||
:iso
|
||||
(.format DateTimeFormatter/ISO_INSTANT ^Instant v)
|
||||
|
||||
:iso-local-time
|
||||
(.format DateTimeFormatter/ISO_LOCAL_TIME
|
||||
^ZonedDateTime (instant->zoned-date-time v))
|
||||
|
||||
:rfc1123
|
||||
(.format DateTimeFormatter/RFC_1123_DATE_TIME
|
||||
^ZonedDateTime (instant->zoned-date-time v)))))
|
||||
|
||||
(defmethod print-method Instant
|
||||
[mv ^java.io.Writer writer]
|
||||
|
|
Loading…
Reference in a new issue