mirror of
https://github.com/penpot/penpot.git
synced 2025-05-07 17:55:54 +02:00
🎉 Duplicate projects and files
This commit is contained in:
parent
9945243a23
commit
044f1f63c0
6 changed files with 107 additions and 16 deletions
|
@ -28,6 +28,7 @@
|
||||||
(s/def ::project-id ::us/uuid)
|
(s/def ::project-id ::us/uuid)
|
||||||
(s/def ::file-id ::us/uuid)
|
(s/def ::file-id ::us/uuid)
|
||||||
(s/def ::team-id ::us/uuid)
|
(s/def ::team-id ::us/uuid)
|
||||||
|
(s/def ::new-name ::us/string)
|
||||||
|
|
||||||
(defn- remap-id
|
(defn- remap-id
|
||||||
[item index key]
|
[item index key]
|
||||||
|
@ -72,7 +73,7 @@
|
||||||
(blob/encode))))))
|
(blob/encode))))))
|
||||||
|
|
||||||
(defn- duplicate-file
|
(defn- duplicate-file
|
||||||
[conn {:keys [profile-id file index project-id]} {:keys [reset-shared-flag] :as opts}]
|
[conn {:keys [profile-id file index project-id new-name]} {:keys [reset-shared-flag] :as opts}]
|
||||||
(let [flibs (db/query conn :file-library-rel {:file-id (:id file)})
|
(let [flibs (db/query conn :file-library-rel {:file-id (:id file)})
|
||||||
fmeds (db/query conn :file-media-object {:file-id (:id file)})
|
fmeds (db/query conn :file-media-object {:file-id (:id file)})
|
||||||
|
|
||||||
|
@ -94,12 +95,15 @@
|
||||||
(some? project-id)
|
(some? project-id)
|
||||||
(assoc :project-id project-id)
|
(assoc :project-id project-id)
|
||||||
|
|
||||||
|
(some? new-name)
|
||||||
|
(assoc :name new-name)
|
||||||
|
|
||||||
(true? reset-shared-flag)
|
(true? reset-shared-flag)
|
||||||
(assoc :is-shared false))
|
(assoc :is-shared false))
|
||||||
|
|
||||||
file (-> file
|
file (-> file
|
||||||
(update :id #(get index %))
|
(update :id #(get index %))
|
||||||
(process-file index))]
|
(process-file index))]
|
||||||
|
|
||||||
(db/insert! conn :file file)
|
(db/insert! conn :file file)
|
||||||
(db/insert! conn :file-profile-rel
|
(db/insert! conn :file-profile-rel
|
||||||
|
@ -123,10 +127,11 @@
|
||||||
(declare duplicate-file)
|
(declare duplicate-file)
|
||||||
|
|
||||||
(s/def ::duplicate-file
|
(s/def ::duplicate-file
|
||||||
(s/keys :req-un [::profile-id ::file-id]))
|
(s/keys :req-un [::profile-id ::file-id]
|
||||||
|
:opt-un [::new-name]))
|
||||||
|
|
||||||
(sv/defmethod ::duplicate-file
|
(sv/defmethod ::duplicate-file
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
|
[{:keys [pool] :as cfg} {:keys [profile-id file-id new-name] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(let [file (db/get-by-id conn :file file-id)
|
(let [file (db/get-by-id conn :file file-id)
|
||||||
index {file-id (uuid/next)}
|
index {file-id (uuid/next)}
|
||||||
|
@ -141,23 +146,29 @@
|
||||||
(declare duplicate-project)
|
(declare duplicate-project)
|
||||||
|
|
||||||
(s/def ::duplicate-project
|
(s/def ::duplicate-project
|
||||||
(s/keys :req-un [::profile-id ::project-id]))
|
(s/keys :req-un [::profile-id ::project-id]
|
||||||
|
:opt-un [::new-name]))
|
||||||
|
|
||||||
(sv/defmethod ::duplicate-project
|
(sv/defmethod ::duplicate-project
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id project-id] :as params}]
|
[{:keys [pool] :as cfg} {:keys [profile-id project-id new-name] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(let [project (db/get-by-id conn :project project-id)]
|
(let [project (db/get-by-id conn :project project-id)]
|
||||||
(teams/check-edition-permissions! conn profile-id (:team-id project))
|
(teams/check-edition-permissions! conn profile-id (:team-id project))
|
||||||
(duplicate-project conn (assoc params :project project)))))
|
(duplicate-project conn (assoc params :project project)))))
|
||||||
|
|
||||||
(defn duplicate-project
|
(defn duplicate-project
|
||||||
[conn {:keys [profile-id project] :as params}]
|
[conn {:keys [profile-id project new-name] :as params}]
|
||||||
(let [files (db/query conn :file
|
(let [files (db/query conn :file
|
||||||
{:project-id (:id project)}
|
{:project-id (:id project)}
|
||||||
{:columns [:id]})
|
{:columns [:id]})
|
||||||
|
|
||||||
index (reduce #(assoc %1 (:id %2) (uuid/next)) {} files)
|
index (reduce #(assoc %1 (:id %2) (uuid/next)) {} files)
|
||||||
project (assoc project :id (uuid/next))
|
project (cond-> project
|
||||||
|
new-name
|
||||||
|
(assoc :name new-name)
|
||||||
|
|
||||||
|
:always
|
||||||
|
(assoc :id (uuid/next)))
|
||||||
params (assoc params
|
params (assoc params
|
||||||
:project-id (:id project)
|
:project-id (:id project)
|
||||||
:index index)]
|
:index index)]
|
||||||
|
@ -170,7 +181,9 @@
|
||||||
:can-edit true})
|
:can-edit true})
|
||||||
(doseq [{:keys [id]} files]
|
(doseq [{:keys [id]} files]
|
||||||
(let [file (db/get-by-id conn :file id)
|
(let [file (db/get-by-id conn :file id)
|
||||||
params (assoc params :file file)]
|
params (-> params
|
||||||
|
(assoc :file file)
|
||||||
|
(dissoc :new-name))]
|
||||||
(duplicate-file conn params {:reset-shared-flag false
|
(duplicate-file conn params {:reset-shared-flag false
|
||||||
:remap-libraries true})))
|
:remap-libraries true})))
|
||||||
project))
|
project))
|
||||||
|
|
|
@ -49,7 +49,8 @@
|
||||||
|
|
||||||
(let [data {::th/type :duplicate-file
|
(let [data {::th/type :duplicate-file
|
||||||
:profile-id (:id profile)
|
:profile-id (:id profile)
|
||||||
:file-id (:id file1)}
|
:file-id (:id file1)
|
||||||
|
:new-name "file 1 (copy)"}
|
||||||
out (th/mutation! data)]
|
out (th/mutation! data)]
|
||||||
|
|
||||||
;; (th/print-result! out)
|
;; (th/print-result! out)
|
||||||
|
@ -58,9 +59,9 @@
|
||||||
(t/is (nil? (:error out)))
|
(t/is (nil? (:error out)))
|
||||||
(let [result (:result out)]
|
(let [result (:result out)]
|
||||||
|
|
||||||
;; Check that the returned result is a file but has different
|
;; Check that the returned result is a file but has different id
|
||||||
;; and different name.
|
;; and different name.
|
||||||
(t/is (= (:name file1) (:name result)))
|
(t/is (= "file 1 (copy)" (:name result)))
|
||||||
(t/is (not= (:id file1) (:id result)))
|
(t/is (not= (:id file1) (:id result)))
|
||||||
|
|
||||||
;; Check that the new file has a correct file library relation
|
;; Check that the new file has a correct file library relation
|
||||||
|
@ -120,15 +121,16 @@
|
||||||
|
|
||||||
(let [data {::th/type :duplicate-project
|
(let [data {::th/type :duplicate-project
|
||||||
:profile-id (:id profile)
|
:profile-id (:id profile)
|
||||||
:project-id (:id project)}
|
:project-id (:id project)
|
||||||
|
:new-name "project 1 (copy)"}
|
||||||
out (th/mutation! data)]
|
out (th/mutation! data)]
|
||||||
|
|
||||||
;; Check tha tresult is correct
|
;; Check tha tresult is correct
|
||||||
(t/is (nil? (:error out)))
|
(t/is (nil? (:error out)))
|
||||||
|
|
||||||
(let [result (:result out)]
|
(let [result (:result out)]
|
||||||
;; Check that they are the same project but different ids
|
;; Check that they are the same project but different id and name
|
||||||
(t/is (= (:name project) (:name result)))
|
(t/is (= "project 1 (copy)" (:name result)))
|
||||||
(t/is (not= (:id project) (:id result)))
|
(t/is (not= (:id project) (:id result)))
|
||||||
|
|
||||||
;; Check the total number of projects (previously is 2, now is 3)
|
;; Check the total number of projects (previously is 2, now is 3)
|
||||||
|
|
|
@ -400,6 +400,13 @@
|
||||||
},
|
},
|
||||||
"used-in" : [ "src/app/main/ui/settings/profile.cljs" ]
|
"used-in" : [ "src/app/main/ui/settings/profile.cljs" ]
|
||||||
},
|
},
|
||||||
|
"dashboard.copy-suffix" : {
|
||||||
|
"translations" : {
|
||||||
|
"en" : "(copy)",
|
||||||
|
"es" : "(copia)"
|
||||||
|
},
|
||||||
|
"used-in" : [ "src/app/main/data/dashboard.cljs" ]
|
||||||
|
},
|
||||||
"dashboard.create-new-team" : {
|
"dashboard.create-new-team" : {
|
||||||
"translations" : {
|
"translations" : {
|
||||||
"ca" : "+ Crear un nou equip",
|
"ca" : "+ Crear un nou equip",
|
||||||
|
@ -441,6 +448,13 @@
|
||||||
},
|
},
|
||||||
"used-in" : [ "src/app/main/ui/dashboard/files.cljs" ]
|
"used-in" : [ "src/app/main/ui/dashboard/files.cljs" ]
|
||||||
},
|
},
|
||||||
|
"dashboard.duplicate" : {
|
||||||
|
"translations" : {
|
||||||
|
"en" : "Duplicate",
|
||||||
|
"es" : "Duplicar"
|
||||||
|
},
|
||||||
|
"used-in" : [ "src/app/main/ui/dashboard/file_menu.cljs" ]
|
||||||
|
},
|
||||||
"dashboard.empty-files" : {
|
"dashboard.empty-files" : {
|
||||||
"translations" : {
|
"translations" : {
|
||||||
"ca" : "Encara no hi ha cap arxiu aquí",
|
"ca" : "Encara no hi ha cap arxiu aquí",
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
[app.main.repo :as rp]
|
[app.main.repo :as rp]
|
||||||
[app.main.data.users :as du]
|
[app.main.data.users :as du]
|
||||||
|
[app.util.i18n :as i18n :refer [tr]]
|
||||||
[app.util.router :as rt]
|
[app.util.router :as rt]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
[app.util.timers :as ts]
|
[app.util.timers :as ts]
|
||||||
|
@ -347,6 +348,28 @@
|
||||||
(rx/map #(partial created %))
|
(rx/map #(partial created %))
|
||||||
(rx/catch on-error)))))))
|
(rx/catch on-error)))))))
|
||||||
|
|
||||||
|
(defn duplicate-project
|
||||||
|
[{:keys [id name] :as params}]
|
||||||
|
(us/assert ::us/uuid id)
|
||||||
|
(letfn [(duplicated [project state]
|
||||||
|
(-> state
|
||||||
|
(assoc-in [:projects (:team-id project) (:id project)] project)
|
||||||
|
(assoc-in [:dashboard-local :project-for-edit] (:id project))))]
|
||||||
|
(ptk/reify ::duplicate-project
|
||||||
|
ptk/WatchEvent
|
||||||
|
(watch [_ state stream]
|
||||||
|
(let [{:keys [on-success on-error]
|
||||||
|
:or {on-success identity
|
||||||
|
on-error identity}} (meta params)
|
||||||
|
|
||||||
|
new-name (str name " " (tr "dashboard.copy-suffix"))]
|
||||||
|
|
||||||
|
(->> (rp/mutation! :duplicate-project {:project-id id
|
||||||
|
:new-name new-name})
|
||||||
|
(rx/tap on-success)
|
||||||
|
(rx/map #(partial duplicated %))
|
||||||
|
(rx/catch on-error)))))))
|
||||||
|
|
||||||
(def clear-project-for-edit
|
(def clear-project-for-edit
|
||||||
(ptk/reify ::clear-project-for-edit
|
(ptk/reify ::clear-project-for-edit
|
||||||
ptk/UpdateEvent
|
ptk/UpdateEvent
|
||||||
|
@ -494,3 +517,24 @@
|
||||||
(-> state
|
(-> state
|
||||||
(assoc-in [:files project-id id] file)
|
(assoc-in [:files project-id id] file)
|
||||||
(update-in [:recent-files project-id] (fnil conj #{}) id)))))
|
(update-in [:recent-files project-id] (fnil conj #{}) id)))))
|
||||||
|
|
||||||
|
;; --- Duplicate File
|
||||||
|
|
||||||
|
(defn duplicate-file
|
||||||
|
[{:keys [id name] :as params}]
|
||||||
|
(us/assert ::us/uuid id)
|
||||||
|
(ptk/reify ::duplicate-file
|
||||||
|
ptk/WatchEvent
|
||||||
|
(watch [_ state stream]
|
||||||
|
(let [{:keys [on-success on-error]
|
||||||
|
:or {on-success identity
|
||||||
|
on-error identity}} (meta params)
|
||||||
|
|
||||||
|
new-name (str name " " (tr "dashboard.copy-suffix"))]
|
||||||
|
|
||||||
|
(->> (rp/mutation! :duplicate-file {:file-id id
|
||||||
|
:new-name new-name})
|
||||||
|
(rx/tap on-success)
|
||||||
|
(rx/map file-created)
|
||||||
|
(rx/catch on-error))))))
|
||||||
|
|
||||||
|
|
|
@ -36,6 +36,11 @@
|
||||||
qparams {:page-id (first (get-in file [:data :pages]))}]
|
qparams {:page-id (first (get-in file [:data :pages]))}]
|
||||||
(st/emit! (rt/nav-new-window :workspace pparams qparams)))))
|
(st/emit! (rt/nav-new-window :workspace pparams qparams)))))
|
||||||
|
|
||||||
|
on-duplicate
|
||||||
|
(mf/use-callback
|
||||||
|
(mf/deps file)
|
||||||
|
(st/emitf (dd/duplicate-file file)))
|
||||||
|
|
||||||
delete-fn
|
delete-fn
|
||||||
(mf/use-callback
|
(mf/use-callback
|
||||||
(mf/deps file)
|
(mf/deps file)
|
||||||
|
@ -100,6 +105,7 @@
|
||||||
:left left
|
:left left
|
||||||
:options [[(tr "dashboard.open-in-new-tab") on-new-tab]
|
:options [[(tr "dashboard.open-in-new-tab") on-new-tab]
|
||||||
[(tr "labels.rename") on-edit]
|
[(tr "labels.rename") on-edit]
|
||||||
|
[(tr "dashboard.duplicate") on-duplicate]
|
||||||
[(tr "labels.delete") on-delete]
|
[(tr "labels.delete") on-delete]
|
||||||
(if (:is-shared file)
|
(if (:is-shared file)
|
||||||
[(tr "dashboard.remove-shared") on-del-shared]
|
[(tr "dashboard.remove-shared") on-del-shared]
|
||||||
|
|
|
@ -26,6 +26,17 @@
|
||||||
(let [top (or top 0)
|
(let [top (or top 0)
|
||||||
left (or left 0)
|
left (or left 0)
|
||||||
|
|
||||||
|
on-duplicate
|
||||||
|
(mf/use-callback
|
||||||
|
(mf/deps project)
|
||||||
|
#(let [on-success
|
||||||
|
(fn [new-project]
|
||||||
|
(st/emit! (rt/nav :dashboard-files
|
||||||
|
{:team-id (:team-id new-project)
|
||||||
|
:project-id (:id new-project)})))]
|
||||||
|
(st/emit! (dd/duplicate-project
|
||||||
|
(with-meta project {:on-success on-success})))))
|
||||||
|
|
||||||
delete-fn
|
delete-fn
|
||||||
(mf/use-callback
|
(mf/use-callback
|
||||||
(mf/deps project)
|
(mf/deps project)
|
||||||
|
@ -49,5 +60,6 @@
|
||||||
:top top
|
:top top
|
||||||
:left left
|
:left left
|
||||||
:options [[(tr "labels.rename") on-edit]
|
:options [[(tr "labels.rename") on-edit]
|
||||||
|
[(tr "dashboard.duplicate") on-duplicate]
|
||||||
[(tr "labels.delete") on-delete]]}]))
|
[(tr "labels.delete") on-delete]]}]))
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue