mirror of
https://github.com/penpot/penpot.git
synced 2025-03-20 19:51:23 -05:00
commit
2ca8ff4db1
30 changed files with 1766 additions and 376 deletions
backend
frontend
|
@ -217,9 +217,10 @@
|
|||
(defn get-by-params
|
||||
([ds table params]
|
||||
(get-by-params ds table params nil))
|
||||
([ds table params opts]
|
||||
([ds table params {:keys [uncheked] :or {uncheked false} :as opts}]
|
||||
(let [res (exec-one! ds (sql/select table params opts))]
|
||||
(when (or (:deleted-at res) (not res))
|
||||
(when (and (not uncheked)
|
||||
(or (:deleted-at res) (not res)))
|
||||
(ex/raise :type :not-found
|
||||
:hint "database object not found"))
|
||||
res)))
|
||||
|
@ -261,9 +262,12 @@
|
|||
(PGpoint. (:x p) (:y p)))
|
||||
|
||||
(defn create-array
|
||||
[conn type aobjects]
|
||||
[conn type objects]
|
||||
(let [^PGConnection conn (unwrap conn org.postgresql.PGConnection)]
|
||||
(.createArrayOf conn ^String type aobjects)))
|
||||
(if (coll? objects)
|
||||
(.createArrayOf conn ^String type (into-array Object objects))
|
||||
(.createArrayOf conn ^String type objects))))
|
||||
|
||||
|
||||
(defn decode-pgpoint
|
||||
[^PGpoint v]
|
||||
|
|
|
@ -135,6 +135,7 @@
|
|||
'app.rpc.mutations.projects
|
||||
'app.rpc.mutations.viewer
|
||||
'app.rpc.mutations.teams
|
||||
'app.rpc.mutations.management
|
||||
'app.rpc.mutations.ldap
|
||||
'app.rpc.mutations.verify-token)
|
||||
(map (partial process-method cfg))
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
[app.common.uuid :as uuid]
|
||||
[app.config :as cfg]
|
||||
[app.db :as db]
|
||||
[app.rpc.permissions :as perms]
|
||||
[app.rpc.queries.files :as files]
|
||||
[app.rpc.queries.projects :as proj]
|
||||
[app.tasks :as tasks]
|
||||
|
@ -47,14 +48,13 @@
|
|||
(proj/check-edition-permissions! conn profile-id project-id)
|
||||
(create-file conn params)))
|
||||
|
||||
(defn- create-file-profile
|
||||
[conn {:keys [profile-id file-id] :as params}]
|
||||
(db/insert! conn :file-profile-rel
|
||||
{:profile-id profile-id
|
||||
:file-id file-id
|
||||
:is-owner true
|
||||
:is-admin true
|
||||
:can-edit true}))
|
||||
|
||||
(defn create-file-role
|
||||
[conn {:keys [file-id profile-id role]}]
|
||||
(let [params {:file-id file-id
|
||||
:profile-id profile-id}]
|
||||
(->> (perms/assign-role-flags params role)
|
||||
(db/insert! conn :file-profile-rel))))
|
||||
|
||||
(defn create-file
|
||||
[conn {:keys [id name project-id is-shared]
|
||||
|
@ -68,8 +68,8 @@
|
|||
:name name
|
||||
:is-shared is-shared
|
||||
:data (blob/encode data)})]
|
||||
(->> (assoc params :file-id id)
|
||||
(create-file-profile conn))
|
||||
(->> (assoc params :file-id id :role :owner)
|
||||
(create-file-role conn))
|
||||
(assoc file :data data)))
|
||||
|
||||
|
||||
|
|
310
backend/src/app/rpc/mutations/management.clj
Normal file
310
backend/src/app/rpc/mutations/management.clj
Normal file
|
@ -0,0 +1,310 @@
|
|||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2021 UXBOX Labs SL
|
||||
|
||||
(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.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 Components
|
||||
(and (map? form)
|
||||
(uuid? (:component-file form)))
|
||||
(update :component-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 analize 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))))))
|
||||
|
||||
(defn duplicate-file
|
||||
[conn {:keys [profile-id file index project-id name]} {:keys [reset-shared-flag] :as opts}]
|
||||
(let [flibs (db/query conn :file-library-rel {:file-id (:id file)})
|
||||
fmeds (db/query conn :file-media-object {:file-id (: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))
|
||||
|
||||
|
||||
;; --- MUTATION: Duplicate File
|
||||
|
||||
(declare duplicate-file)
|
||||
|
||||
(s/def ::duplicate-file
|
||||
(s/keys :req-un [::profile-id ::file-id]
|
||||
:opt-un [::name]))
|
||||
|
||||
(sv/defmethod ::duplicate-file
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id name] :as 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))
|
||||
|
||||
(-> (duplicate-file conn params {:reset-shared-flag true})
|
||||
(update :data blob/decode)))))
|
||||
|
||||
|
||||
;; --- MUTATION: Duplicate Project
|
||||
|
||||
(declare duplicate-project)
|
||||
|
||||
(s/def ::duplicate-project
|
||||
(s/keys :req-un [::profile-id ::project-id]
|
||||
:opt-un [::name]))
|
||||
|
||||
(sv/defmethod ::duplicate-project
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id project-id name] :as 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))
|
||||
(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)}
|
||||
{: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))
|
||||
|
||||
|
||||
;; --- 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]))
|
||||
|
||||
(sv/defmethod ::move-files
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id ids project-id] :as 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 posible broken relations on moved files
|
||||
(db/exec-one! conn [sql:delete-broken-relations pids])
|
||||
|
||||
nil)))
|
||||
|
||||
|
||||
;; --- MUTATION: Move project
|
||||
|
||||
(declare move-project)
|
||||
|
||||
(s/def ::move-project
|
||||
(s/keys :req-un [::profile-id ::team-id ::project-id]))
|
||||
|
||||
(sv/defmethod ::move-project
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id team-id project-id] :as 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 posible broken relations on moved files
|
||||
(db/exec-one! conn [sql:delete-broken-relations pids])
|
||||
|
||||
nil)))
|
|
@ -196,21 +196,25 @@
|
|||
|
||||
(defn create-profile-relations
|
||||
[conn profile]
|
||||
(let [team (teams/create-team conn {:profile-id (:id profile)
|
||||
:name "Default"
|
||||
:default? true})
|
||||
proj (projects/create-project conn {:profile-id (:id profile)
|
||||
:team-id (:id team)
|
||||
:name "Drafts"
|
||||
:default? true})]
|
||||
(teams/create-team-profile conn {:team-id (:id team)
|
||||
:profile-id (:id profile)})
|
||||
(projects/create-project-profile conn {:project-id (:id proj)
|
||||
:profile-id (:id profile)})
|
||||
(let [team (teams/create-team conn {:profile-id (:id profile)
|
||||
:name "Default"
|
||||
:is-default true})
|
||||
project (projects/create-project conn {:profile-id (:id profile)
|
||||
:team-id (:id team)
|
||||
:name "Drafts"
|
||||
:is-default true})
|
||||
params {:team-id (:id team)
|
||||
:profile-id (:id profile)
|
||||
:project-id (:id project)
|
||||
:role :owner}]
|
||||
|
||||
(merge (profile/strip-private-attrs profile)
|
||||
{:default-team-id (:id team)
|
||||
:default-project-id (:id proj)})))
|
||||
(teams/create-team-role conn params)
|
||||
(projects/create-project-role conn params)
|
||||
|
||||
(-> profile
|
||||
(profile/strip-private-attrs)
|
||||
(assoc :default-team-id (:id team))
|
||||
(assoc :default-project-id (:id project)))))
|
||||
|
||||
;; --- Mutation: Login
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
[app.common.uuid :as uuid]
|
||||
[app.config :as cfg]
|
||||
[app.db :as db]
|
||||
[app.rpc.permissions :as perms]
|
||||
[app.rpc.queries.projects :as proj]
|
||||
[app.rpc.queries.teams :as teams]
|
||||
[app.tasks :as tasks]
|
||||
|
@ -30,7 +31,7 @@
|
|||
;; --- Mutation: Create Project
|
||||
|
||||
(declare create-project)
|
||||
(declare create-project-profile)
|
||||
(declare create-project-role)
|
||||
(declare create-team-project-profile)
|
||||
|
||||
(s/def ::team-id ::us/uuid)
|
||||
|
@ -43,30 +44,31 @@
|
|||
(db/with-atomic [conn pool]
|
||||
(teams/check-edition-permissions! conn profile-id team-id)
|
||||
(let [project (create-project conn params)
|
||||
params (assoc params :project-id (:id project))]
|
||||
(create-project-profile conn params)
|
||||
params (assoc params
|
||||
:project-id (:id project)
|
||||
:role :owner)]
|
||||
(create-project-role conn params)
|
||||
(create-team-project-profile conn params)
|
||||
(assoc project :is-pinned true))))
|
||||
|
||||
(defn create-project
|
||||
[conn {:keys [id team-id name default?] :as params}]
|
||||
(let [id (or id (uuid/next))
|
||||
default? (if (boolean? default?) default? false)]
|
||||
[conn {:keys [id team-id name is-default] :as params}]
|
||||
(let [id (or id (uuid/next))
|
||||
is-default (if (boolean? is-default) is-default false)]
|
||||
(db/insert! conn :project
|
||||
{:id id
|
||||
:team-id team-id
|
||||
:name name
|
||||
:is-default default?})))
|
||||
:team-id team-id
|
||||
:is-default is-default})))
|
||||
|
||||
(defn create-project-profile
|
||||
[conn {:keys [project-id profile-id] :as params}]
|
||||
(db/insert! conn :project-profile-rel
|
||||
{:project-id project-id
|
||||
:profile-id profile-id
|
||||
:is-owner true
|
||||
:is-admin true
|
||||
:can-edit true}))
|
||||
(defn create-project-role
|
||||
[conn {:keys [project-id profile-id role]}]
|
||||
(let [params {:project-id project-id
|
||||
:profile-id profile-id}]
|
||||
(->> (perms/assign-role-flags params role)
|
||||
(db/insert! conn :project-profile-rel))))
|
||||
|
||||
;; TODO: pending to be refactored
|
||||
(defn create-team-project-profile
|
||||
[conn {:keys [team-id project-id profile-id] :as params}]
|
||||
(db/insert! conn :team-project-profile-rel
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
[app.emails :as emails]
|
||||
[app.media :as media]
|
||||
[app.rpc.mutations.projects :as projects]
|
||||
[app.rpc.permissions :as perms]
|
||||
[app.rpc.queries.profile :as profile]
|
||||
[app.rpc.queries.teams :as teams]
|
||||
[app.storage :as sto]
|
||||
|
@ -36,7 +37,7 @@
|
|||
;; --- Mutation: Create Team
|
||||
|
||||
(declare create-team)
|
||||
(declare create-team-profile)
|
||||
(declare create-team-role)
|
||||
(declare create-team-default-project)
|
||||
|
||||
(s/def ::create-team
|
||||
|
@ -47,37 +48,39 @@
|
|||
[{:keys [pool] :as cfg} params]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [team (create-team conn params)
|
||||
params (assoc params :team-id (:id team))]
|
||||
(create-team-profile conn params)
|
||||
params (assoc params
|
||||
:team-id (:id team)
|
||||
:role :owner)]
|
||||
(create-team-role conn params)
|
||||
(create-team-default-project conn params)
|
||||
team)))
|
||||
|
||||
(defn create-team
|
||||
[conn {:keys [id name default?] :as params}]
|
||||
(let [id (or id (uuid/next))
|
||||
default? (if (boolean? default?) default? false)]
|
||||
[conn {:keys [id name is-default] :as params}]
|
||||
(let [id (or id (uuid/next))
|
||||
is-default (if (boolean? is-default) is-default false)]
|
||||
(db/insert! conn :team
|
||||
{:id id
|
||||
:name name
|
||||
:is-default default?})))
|
||||
:is-default is-default})))
|
||||
|
||||
(defn create-team-profile
|
||||
[conn {:keys [team-id profile-id] :as params}]
|
||||
(db/insert! conn :team-profile-rel
|
||||
{:team-id team-id
|
||||
:profile-id profile-id
|
||||
:is-owner true
|
||||
:is-admin true
|
||||
:can-edit true}))
|
||||
(defn create-team-role
|
||||
[conn {:keys [team-id profile-id role] :as params}]
|
||||
(let [params {:team-id team-id
|
||||
:profile-id profile-id}]
|
||||
(->> (perms/assign-role-flags params role)
|
||||
(db/insert! conn :team-profile-rel))))
|
||||
|
||||
(defn create-team-default-project
|
||||
[conn {:keys [team-id profile-id] :as params}]
|
||||
(let [proj (projects/create-project conn {:team-id team-id
|
||||
:name "Drafts"
|
||||
:default? true})]
|
||||
(projects/create-project-profile conn {:project-id (:id proj)
|
||||
:profile-id profile-id})))
|
||||
|
||||
(let [project {:id (uuid/next)
|
||||
:team-id team-id
|
||||
:name "Drafts"
|
||||
:is-default true}]
|
||||
(projects/create-project conn project)
|
||||
(projects/create-project-role conn {:project-id (:id project)
|
||||
:profile-id profile-id
|
||||
:role :owner})))
|
||||
|
||||
;; --- Mutation: Update Team
|
||||
|
||||
|
|
|
@ -11,7 +11,34 @@
|
|||
"A permission checking helper factories."
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.spec :as us]))
|
||||
[app.common.spec :as us]
|
||||
[clojure.spec.alpha :as s]))
|
||||
|
||||
(s/def ::role #{:admin :owner :editor :viewer})
|
||||
|
||||
(defn assign-role-flags
|
||||
[params role]
|
||||
(us/verify ::role role)
|
||||
(cond-> params
|
||||
(= role :owner)
|
||||
(assoc :is-owner true
|
||||
:is-admin true
|
||||
:can-edit true)
|
||||
|
||||
(= role :admin)
|
||||
(assoc :is-owner false
|
||||
:is-admin true
|
||||
:can-edit true)
|
||||
|
||||
(= role :editor)
|
||||
(assoc :is-owner false
|
||||
:is-admin false
|
||||
:can-edit true)
|
||||
|
||||
(= role :viewer)
|
||||
(assoc :is-owner false
|
||||
:is-admin false
|
||||
:can-edit false)))
|
||||
|
||||
(defn make-edition-check-fn
|
||||
"A simple factory for edition permission check functions."
|
||||
|
|
|
@ -82,6 +82,50 @@
|
|||
(db/exec! conn [sql:projects profile-id team-id]))
|
||||
|
||||
|
||||
;; --- Query: All projects
|
||||
|
||||
(declare retrieve-all-projects)
|
||||
|
||||
(s/def ::profile-id ::us/uuid)
|
||||
(s/def ::all-projects
|
||||
(s/keys :req-un [::profile-id]))
|
||||
|
||||
(sv/defmethod ::all-projects
|
||||
[{:keys [pool]} {:keys [profile-id]}]
|
||||
(with-open [conn (db/open pool)]
|
||||
(retrieve-all-projects conn profile-id)))
|
||||
|
||||
(def sql:all-projects
|
||||
"select p1.*, t.name as team_name
|
||||
from project as p1
|
||||
inner join team as t
|
||||
on t.id = p1.team_id
|
||||
where t.id in (select team_id
|
||||
from team_profile_rel as tpr
|
||||
where tpr.profile_id = ?
|
||||
and (tpr.can_edit = true or
|
||||
tpr.is_owner = true or
|
||||
tpr.is_admin = true))
|
||||
and p1.deleted_at is null
|
||||
union
|
||||
select p2.*, t.name as team_name
|
||||
from project as p2
|
||||
inner join team as t
|
||||
on t.id = p2.team_id
|
||||
where p2.id in (select project_id
|
||||
from project_profile_rel as ppr
|
||||
where ppr.profile_id = ?
|
||||
and (ppr.can_edit = true or
|
||||
ppr.is_owner = true or
|
||||
ppr.is_admin = true))
|
||||
and p2.deleted_at is null
|
||||
order by team_name, name;")
|
||||
|
||||
(defn retrieve-all-projects
|
||||
[conn profile-id]
|
||||
(db/exec! conn [sql:all-projects profile-id profile-id]))
|
||||
|
||||
|
||||
;; --- Query: Project
|
||||
|
||||
(s/def ::id ::us/uuid)
|
||||
|
@ -94,3 +138,4 @@
|
|||
(let [project (db/get-by-id conn :project id)]
|
||||
(check-read-permissions! conn profile-id id)
|
||||
project)))
|
||||
|
||||
|
|
|
@ -10,16 +10,12 @@
|
|||
(ns app.setup.initial-data
|
||||
(:refer-clojure :exclude [load])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.pages.migrations :as pmg]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cfg]
|
||||
[app.db :as db]
|
||||
[app.rpc.mutations.projects :as projects]
|
||||
[app.rpc.queries.profile :as profile]
|
||||
[app.util.blob :as blob]
|
||||
[app.util.time :as dt]
|
||||
[clojure.walk :as walk]))
|
||||
[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
|
||||
|
||||
|
@ -62,58 +58,6 @@
|
|||
|
||||
;; --- DUMP LOADING
|
||||
|
||||
(defn- process-file
|
||||
[file index]
|
||||
(letfn [(process-form [form]
|
||||
(cond-> form
|
||||
;; Relink Components
|
||||
(and (map? form)
|
||||
(uuid? (:component-file form)))
|
||||
(update :component-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 analize 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))))))
|
||||
|
||||
(defn- remap-id
|
||||
[item index key]
|
||||
(cond-> item
|
||||
(contains? item key)
|
||||
(assoc key (get index (get item key) (get item key)))))
|
||||
|
||||
(defn- retrieve-data
|
||||
[conn skey]
|
||||
(when-let [row (db/exec-one! conn ["select content from server_prop where id = ?" skey])]
|
||||
|
@ -127,60 +71,24 @@
|
|||
(let [skey (or (:skey opts) (cfg/get :initial-project-skey))
|
||||
data (retrieve-data conn skey)]
|
||||
(when data
|
||||
(let [project (projects/create-project conn {:profile-id (:id profile)
|
||||
:team-id (:default-team-id profile)
|
||||
:name (:project-name 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)}]
|
||||
|
||||
now (dt/now)
|
||||
ignore (dt/plus now (dt/duration {:seconds 5}))
|
||||
index (as-> {} index
|
||||
(reduce #(assoc %1 (:id %2) (uuid/next)) index (:files data))
|
||||
(reduce #(assoc %1 (:id %2) (uuid/next)) index (:fmeds data)))
|
||||
(create-project conn project)
|
||||
(create-project-role conn {:project-id (:id project)
|
||||
:profile-id (:id profile)
|
||||
:role :owner})
|
||||
|
||||
flibs (->> (:flibs data)
|
||||
(map #(remap-id % index :file-id))
|
||||
(map #(remap-id % index :library-file-id))
|
||||
(map #(assoc % :synced-at now))
|
||||
(map #(assoc % :created-at now)))
|
||||
|
||||
files (->> (:files data)
|
||||
(map #(assoc % :id (get index (:id %))))
|
||||
(map #(assoc % :project-id (:id project)))
|
||||
(map #(assoc % :created-at now))
|
||||
(map #(assoc % :modified-at now))
|
||||
(map #(assoc % :ignore-sync-until ignore))
|
||||
(map #(process-file % index)))
|
||||
|
||||
fmeds (->> (:fmeds data)
|
||||
(map #(assoc % :id (get index (:id %))))
|
||||
(map #(assoc % :created-at now))
|
||||
(map #(remap-id % index :file-id)))
|
||||
|
||||
fprofs (map #(array-map :file-id (:id %)
|
||||
:profile-id (:id profile)
|
||||
:is-owner true
|
||||
:is-admin true
|
||||
:can-edit true) files)]
|
||||
|
||||
(projects/create-project-profile conn {:project-id (:id project)
|
||||
:profile-id (:id profile)})
|
||||
|
||||
(projects/create-team-project-profile conn {:team-id (:default-team-id profile)
|
||||
:project-id (:id project)
|
||||
:profile-id (:id profile)})
|
||||
|
||||
;; Re-insert into the database
|
||||
(doseq [params files]
|
||||
(db/insert! conn :file params))
|
||||
|
||||
(doseq [params fprofs]
|
||||
(db/insert! conn :file-profile-rel params))
|
||||
|
||||
(doseq [params flibs]
|
||||
(db/insert! conn :file-library-rel params))
|
||||
|
||||
(doseq [params fmeds]
|
||||
(db/insert! conn :file-media-object params)))))))
|
||||
(doseq [file (:files data)]
|
||||
(let [params {:profile-id (:id profile)
|
||||
:project-id (:id project)
|
||||
:file file
|
||||
:index index}
|
||||
opts {:reset-shared-flag false}]
|
||||
(duplicate-file conn params opts))))))))
|
||||
|
||||
(defn load
|
||||
[system {:keys [email] :as opts}]
|
||||
|
|
|
@ -52,8 +52,10 @@
|
|||
:app.http/server
|
||||
:app.http/router
|
||||
:app.notifications/handler
|
||||
:app.http.auth/google
|
||||
:app.http.auth/gitlab
|
||||
:app.http.oauth/google
|
||||
:app.http.oauth/gitlab
|
||||
:app.http.oauth/github
|
||||
:app.http.oauth/all
|
||||
:app.worker/scheduler
|
||||
:app.worker/worker)
|
||||
(d/deep-merge
|
||||
|
@ -152,14 +154,27 @@
|
|||
team (#'teams/create-team conn {:id id
|
||||
:profile-id profile-id
|
||||
:name (str "team" i)})]
|
||||
(#'teams/create-team-profile conn
|
||||
{:team-id id
|
||||
:profile-id profile-id
|
||||
:is-owner true
|
||||
:is-admin true
|
||||
:can-edit true})
|
||||
(#'teams/create-team-role conn
|
||||
{:team-id id
|
||||
:profile-id profile-id
|
||||
:role :owner})
|
||||
team)))
|
||||
|
||||
|
||||
(defn create-file-media-object*
|
||||
([params] (create-file-media-object* *pool* params))
|
||||
([conn {:keys [name width height mtype file-id is-local media-id]
|
||||
:or {name "sample" width 100 height 100 mtype "image/svg+xml" is-local true}}]
|
||||
(db/insert! conn :file-media-object
|
||||
{:id (uuid/next)
|
||||
:file-id file-id
|
||||
:is-local is-local
|
||||
:name name
|
||||
:media-id media-id
|
||||
:width width
|
||||
:height height
|
||||
:mtype mtype})))
|
||||
|
||||
(defn link-file-to-library*
|
||||
([params] (link-file-to-library* *pool* params))
|
||||
([conn {:keys [file-id library-id] :as params}]
|
||||
|
@ -181,37 +196,39 @@
|
|||
:created-at (or created-at (dt/now))
|
||||
:content (db/tjson {})}))
|
||||
|
||||
(defn create-team-role*
|
||||
([params] (create-team-role* *pool* params))
|
||||
([conn {:keys [team-id profile-id role] :or {role :owner}}]
|
||||
(#'teams/create-team-role conn {:team-id team-id
|
||||
:profile-id profile-id
|
||||
:role role})))
|
||||
|
||||
(defn create-team-permission*
|
||||
([params] (create-team-permission* *pool* params))
|
||||
([conn {:keys [team-id profile-id is-owner is-admin can-edit]
|
||||
:or {is-owner true is-admin true can-edit true}}]
|
||||
(db/insert! conn :team-profile-rel {:team-id team-id
|
||||
:profile-id profile-id
|
||||
:is-owner is-owner
|
||||
:is-admin is-admin
|
||||
:can-edit can-edit})))
|
||||
(defn create-project-role*
|
||||
([params] (create-project-role* *pool* params))
|
||||
([conn {:keys [project-id profile-id role] :or {role :owner}}]
|
||||
(#'projects/create-project-role conn {:project-id project-id
|
||||
:profile-id profile-id
|
||||
:role role})))
|
||||
|
||||
(defn create-project-permission*
|
||||
([params] (create-project-permission* *pool* params))
|
||||
([conn {:keys [project-id profile-id is-owner is-admin can-edit]
|
||||
:or {is-owner true is-admin true can-edit true}}]
|
||||
(db/insert! conn :project-profile-rel {:project-id project-id
|
||||
:profile-id profile-id
|
||||
:is-owner is-owner
|
||||
:is-admin is-admin
|
||||
:can-edit can-edit})))
|
||||
|
||||
(defn create-file-permission*
|
||||
([params] (create-file-permission* *pool* params))
|
||||
([conn {:keys [file-id profile-id is-owner is-admin can-edit]
|
||||
:or {is-owner true is-admin true can-edit true}}]
|
||||
(db/insert! conn :project-profile-rel {:file-id file-id
|
||||
:profile-id profile-id
|
||||
:is-owner is-owner
|
||||
:is-admin is-admin
|
||||
:can-edit can-edit})))
|
||||
(defn create-file-role*
|
||||
([params] (create-file-role* *pool* params))
|
||||
([conn {:keys [file-id profile-id role] :or {role :owner}}]
|
||||
(#'files/create-file-role conn {:file-id file-id
|
||||
:profile-id profile-id
|
||||
:role role})))
|
||||
|
||||
(defn update-file*
|
||||
([params] (update-file* *pool* params))
|
||||
([conn {:keys [file-id changes session-id profile-id revn]
|
||||
:or {session-id (uuid/next) revn 0}}]
|
||||
(let [file (db/get-by-id conn :file file-id)
|
||||
msgbus (:app.msgbus/msgbus *system*)]
|
||||
(#'files/update-file {:conn conn :msgbus msgbus}
|
||||
{:file file
|
||||
:revn revn
|
||||
:changes changes
|
||||
:session-id session-id
|
||||
:profile-id profile-id}))))
|
||||
|
||||
;; --- RPC HELPERS
|
||||
|
||||
|
|
479
backend/tests/app/tests/test_services_management.clj
Normal file
479
backend/tests/app/tests/test_services_management.clj
Normal file
|
@ -0,0 +1,479 @@
|
|||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2021 UXBOX Labs SL
|
||||
|
||||
(ns app.tests.test-services-management
|
||||
(:require
|
||||
[app.common.uuid :as uuid]
|
||||
[app.db :as db]
|
||||
[app.http :as http]
|
||||
[app.storage :as sto]
|
||||
[app.tests.helpers :as th]
|
||||
[clojure.test :as t]
|
||||
[buddy.core.bytes :as b]
|
||||
[datoteka.core :as fs]))
|
||||
|
||||
(t/use-fixtures :once th/state-init)
|
||||
(t/use-fixtures :each th/database-reset)
|
||||
|
||||
(t/deftest duplicate-file
|
||||
(let [storage (:app.storage/storage th/*system*)
|
||||
sobject (sto/put-object storage {:content (sto/content "content")
|
||||
:content-type "text/plain"
|
||||
:other "data"})
|
||||
profile (th/create-profile* 1 {:is-active true})
|
||||
project (th/create-project* 1 {:team-id (:default-team-id profile)
|
||||
:profile-id (:id profile)})
|
||||
file1 (th/create-file* 1 {:profile-id (:id profile)
|
||||
:project-id (:id project)})
|
||||
file2 (th/create-file* 2 {:profile-id (:id profile)
|
||||
:project-id (:id project)
|
||||
:is-shared true})
|
||||
|
||||
libl (th/link-file-to-library* {:file-id (:id file1)
|
||||
:library-id (:id file2)})
|
||||
|
||||
mobj (th/create-file-media-object* {:file-id (:id file1)
|
||||
:is-local false
|
||||
:media-id (:id sobject)})]
|
||||
(th/update-file*
|
||||
{:file-id (:id file1)
|
||||
:profile-id (:id profile)
|
||||
:changes [{:type :add-media
|
||||
:object (select-keys mobj [:id :width :height :mtype :name])}]})
|
||||
|
||||
(let [data {::th/type :duplicate-file
|
||||
:profile-id (:id profile)
|
||||
:file-id (:id file1)
|
||||
:name "file 1 (copy)"}
|
||||
out (th/mutation! data)]
|
||||
|
||||
;; (th/print-result! out)
|
||||
|
||||
;; Check tha tresult is correct
|
||||
(t/is (nil? (:error out)))
|
||||
(let [result (:result out)]
|
||||
|
||||
;; Check that the returned result is a file but has different id
|
||||
;; and different name.
|
||||
(t/is (= "file 1 (copy)" (:name result)))
|
||||
(t/is (not= (:id file1) (:id result)))
|
||||
|
||||
;; Check that the new file has a correct file library relation
|
||||
(let [[item :as rows] (db/query th/*pool* :file-library-rel {:file-id (:id result)})]
|
||||
(t/is (= 1 (count rows)))
|
||||
(t/is (= (:id file2) (:library-file-id item))))
|
||||
|
||||
;; Check that the new file has a correct file media objects
|
||||
(let [[item :as rows] (db/query th/*pool* :file-media-object {:file-id (:id result)})]
|
||||
(t/is (= 1 (count rows)))
|
||||
|
||||
;; Checj that bot items have different ids
|
||||
(t/is (not= (:id item) (:id mobj)))
|
||||
|
||||
;; check that both file-media-objects points to the same storage object.
|
||||
(t/is (= (:media-id item) (:media-id mobj)))
|
||||
(t/is (= (:media-id item) (:id sobject)))
|
||||
|
||||
;; Check if media correctly contains the new file-media-object id
|
||||
(t/is (contains? (get-in result [:data :media]) (:id item)))
|
||||
|
||||
;; And does not contains the old one
|
||||
(t/is (not (contains? (get-in result [:data :media]) (:id mobj)))))
|
||||
|
||||
;; Check the total number of files
|
||||
(let [rows (db/query th/*pool* :file {:project-id (:id project)})]
|
||||
(t/is (= 3 (count rows))))
|
||||
|
||||
))))
|
||||
|
||||
(t/deftest duplicate-project
|
||||
(let [storage (:app.storage/storage th/*system*)
|
||||
sobject (sto/put-object storage {:content (sto/content "content")
|
||||
:content-type "text/plain"
|
||||
:other "data"})
|
||||
profile (th/create-profile* 1 {:is-active true})
|
||||
project (th/create-project* 1 {:team-id (:default-team-id profile)
|
||||
:profile-id (:id profile)})
|
||||
file1 (th/create-file* 1 {:profile-id (:id profile)
|
||||
:project-id (:id project)})
|
||||
file2 (th/create-file* 2 {:profile-id (:id profile)
|
||||
:project-id (:id project)
|
||||
:is-shared true})
|
||||
|
||||
libl (th/link-file-to-library* {:file-id (:id file1)
|
||||
:library-id (:id file2)})
|
||||
mobj (th/create-file-media-object* {:file-id (:id file1)
|
||||
:is-local false
|
||||
:media-id (:id sobject)})]
|
||||
|
||||
(th/update-file*
|
||||
{:file-id (:id file1)
|
||||
:profile-id (:id profile)
|
||||
:changes [{:type :add-media
|
||||
:object (select-keys mobj [:id :width :height :mtype :name])}]})
|
||||
|
||||
|
||||
(let [data {::th/type :duplicate-project
|
||||
:profile-id (:id profile)
|
||||
:project-id (:id project)
|
||||
:name "project 1 (copy)"}
|
||||
out (th/mutation! data)]
|
||||
|
||||
;; Check tha tresult is correct
|
||||
(t/is (nil? (:error out)))
|
||||
|
||||
(let [result (:result out)]
|
||||
;; Check that they are the same project but different id and name
|
||||
(t/is (= "project 1 (copy)" (:name result)))
|
||||
(t/is (not= (:id project) (:id result)))
|
||||
|
||||
;; Check the total number of projects (previously is 2, now is 3)
|
||||
(let [rows (db/query th/*pool* :project {:team-id (:default-team-id profile)})]
|
||||
(t/is (= 3 (count rows))))
|
||||
|
||||
;; Check that the new project has the same files
|
||||
(let [p1-files (db/query th/*pool* :file
|
||||
{:project-id (:id project)}
|
||||
{:order-by [:name]})
|
||||
p2-files (db/query th/*pool* :file
|
||||
{:project-id (:id result)}
|
||||
{:order-by [:name]})]
|
||||
(t/is (= (count p1-files)
|
||||
(count p2-files)))
|
||||
|
||||
;; check that the both files are equivalent
|
||||
(doseq [[fa fb] (map vector p1-files p2-files)]
|
||||
(t/is (not= (:id fa) (:id fb)))
|
||||
(t/is (= (:name fa) (:name fb)))
|
||||
|
||||
(when (= (:id fa) (:id file1))
|
||||
(t/is (false? (b/equals? (:data fa)
|
||||
(:data fb)))))
|
||||
|
||||
(when (= (:id fa) (:id file2))
|
||||
(t/is (false? (b/equals? (:data fa)
|
||||
(:data fb))))))
|
||||
|
||||
)))))
|
||||
|
||||
(t/deftest move-file-on-same-team
|
||||
(let [profile (th/create-profile* 1 {:is-active true})
|
||||
team (th/create-team* 1 {:profile-id (:id profile)})
|
||||
|
||||
project1 (th/create-project* 1 {:team-id (:default-team-id profile)
|
||||
:profile-id (:id profile)})
|
||||
|
||||
project2 (th/create-project* 2 {:team-id (:default-team-id profile)
|
||||
:profile-id (:id profile)})
|
||||
|
||||
file1 (th/create-file* 1 {:profile-id (:id profile)
|
||||
:project-id (:id project1)})
|
||||
file2 (th/create-file* 2 {:profile-id (:id profile)
|
||||
:project-id (:id project1)
|
||||
:is-shared true})]
|
||||
|
||||
(th/link-file-to-library* {:file-id (:id file1)
|
||||
:library-id (:id file2)})
|
||||
|
||||
;; Try to move to same project
|
||||
(let [data {::th/type :move-files
|
||||
:profile-id (:id profile)
|
||||
:project-id (:id project1)
|
||||
:ids #{(:id file1)}}
|
||||
|
||||
out (th/mutation! data)
|
||||
error (:error out)]
|
||||
(t/is (th/ex-info? error))
|
||||
(t/is (th/ex-of-type? error :validation))
|
||||
(t/is (th/ex-of-code? error :cant-move-to-same-project)))
|
||||
|
||||
;; initially project1 should have 2 files
|
||||
(let [rows (db/query th/*pool* :file {:project-id (:id project1)})]
|
||||
(t/is (= 2 (count rows))))
|
||||
|
||||
;; initially project2 should be empty
|
||||
(let [rows (db/query th/*pool* :file {:project-id (:id project2)})]
|
||||
(t/is (= 0 (count rows))))
|
||||
|
||||
;; move a file1 to project2 (in the same team)
|
||||
(let [data {::th/type :move-files
|
||||
:profile-id (:id profile)
|
||||
:project-id (:id project2)
|
||||
:ids #{(:id file1)}}
|
||||
|
||||
out (th/mutation! data)]
|
||||
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (nil? (:result out)))
|
||||
|
||||
;; project1 now should contain 1 file
|
||||
(let [rows (db/query th/*pool* :file {:project-id (:id project1)})]
|
||||
(t/is (= 1 (count rows))))
|
||||
|
||||
;; project2 now should contain 1 file
|
||||
(let [rows (db/query th/*pool* :file {:project-id (:id project2)})]
|
||||
(t/is (= 1 (count rows))))
|
||||
|
||||
;; file1 should be still linked to file2
|
||||
(let [[item :as rows] (db/query th/*pool* :file-library-rel {:file-id (:id file1)})]
|
||||
(t/is (= 1 (count rows)))
|
||||
(t/is (= (:file-id item) (:id file1)))
|
||||
(t/is (= (:library-file-id item) (:id file2))))
|
||||
|
||||
;; should be no libraries on file2
|
||||
(let [rows (db/query th/*pool* :file-library-rel {:file-id (:id file2)})]
|
||||
(t/is (= 0 (count rows))))
|
||||
)))
|
||||
|
||||
|
||||
;; TODO: move a library to other team
|
||||
(t/deftest move-file-to-other-team
|
||||
(let [profile (th/create-profile* 1 {:is-active true})
|
||||
team (th/create-team* 1 {:profile-id (:id profile)})
|
||||
|
||||
project1 (th/create-project* 1 {:team-id (:default-team-id profile)
|
||||
:profile-id (:id profile)})
|
||||
|
||||
project2 (th/create-project* 2 {:team-id (:id team)
|
||||
:profile-id (:id profile)})
|
||||
|
||||
file1 (th/create-file* 1 {:profile-id (:id profile)
|
||||
:project-id (:id project1)})
|
||||
file2 (th/create-file* 2 {:profile-id (:id profile)
|
||||
:project-id (:id project1)
|
||||
:is-shared true})
|
||||
file3 (th/create-file* 3 {:profile-id (:id profile)
|
||||
:project-id (:id project1)
|
||||
:is-shared true})]
|
||||
|
||||
(th/link-file-to-library* {:file-id (:id file1)
|
||||
:library-id (:id file2)})
|
||||
|
||||
(th/link-file-to-library* {:file-id (:id file2)
|
||||
:library-id (:id file3)})
|
||||
|
||||
;; --- initial data checks
|
||||
|
||||
;; the project1 should have 3 files
|
||||
(let [rows (db/query th/*pool* :file {:project-id (:id project1)})]
|
||||
(t/is (= 3 (count rows))))
|
||||
|
||||
;; should be no files on project2
|
||||
(let [rows (db/query th/*pool* :file {:project-id (:id project2)})]
|
||||
(t/is (= 0 (count rows))))
|
||||
|
||||
;; the file1 should be linked to file2
|
||||
(let [[item :as rows] (db/query th/*pool* :file-library-rel {:file-id (:id file1)})]
|
||||
(t/is (= 1 (count rows)))
|
||||
(t/is (= (:file-id item) (:id file1)))
|
||||
(t/is (= (:library-file-id item) (:id file2))))
|
||||
|
||||
;; the file2 should be linked to file3
|
||||
(let [[item :as rows] (db/query th/*pool* :file-library-rel {:file-id (:id file2)})]
|
||||
(t/is (= 1 (count rows)))
|
||||
(t/is (= (:file-id item) (:id file2)))
|
||||
(t/is (= (:library-file-id item) (:id file3))))
|
||||
|
||||
;; should be no libraries on file3
|
||||
(let [rows (db/query th/*pool* :file-library-rel {:file-id (:id file3)})]
|
||||
(t/is (= 0 (count rows))))
|
||||
|
||||
;; move to other project in other team
|
||||
(let [data {::th/type :move-files
|
||||
:profile-id (:id profile)
|
||||
:project-id (:id project2)
|
||||
:ids #{(:id file1)}}
|
||||
out (th/mutation! data)]
|
||||
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (nil? (:result out)))
|
||||
|
||||
;; project1 now should have 2 file
|
||||
(let [[item1 item2 :as rows] (db/query th/*pool* :file {:project-id (:id project1)}
|
||||
{:order-by [:created-at]})]
|
||||
;; (clojure.pprint/pprint rows)
|
||||
(t/is (= 2 (count rows)))
|
||||
(t/is (= (:id item1) (:id file2))))
|
||||
|
||||
;; project2 now should have 1 file
|
||||
(let [[item :as rows] (db/query th/*pool* :file {:project-id (:id project2)})]
|
||||
(t/is (= 1 (count rows)))
|
||||
(t/is (= (:id item) (:id file1))))
|
||||
|
||||
;; the moved file1 should not have any link to libraries
|
||||
(let [rows (db/query th/*pool* :file-library-rel {:file-id (:id file1)})]
|
||||
(t/is (zero? (count rows))))
|
||||
|
||||
;; the file2 should still be linked to file3
|
||||
(let [[item :as rows] (db/query th/*pool* :file-library-rel {:file-id (:id file2)})]
|
||||
(t/is (= 1 (count rows)))
|
||||
(t/is (= (:file-id item) (:id file2)))
|
||||
(t/is (= (:library-file-id item) (:id file3))))
|
||||
)))
|
||||
|
||||
|
||||
(t/deftest move-library-to-other-team
|
||||
(let [profile (th/create-profile* 1 {:is-active true})
|
||||
team (th/create-team* 1 {:profile-id (:id profile)})
|
||||
|
||||
project1 (th/create-project* 1 {:team-id (:default-team-id profile)
|
||||
:profile-id (:id profile)})
|
||||
|
||||
project2 (th/create-project* 2 {:team-id (:id team)
|
||||
:profile-id (:id profile)})
|
||||
|
||||
file1 (th/create-file* 1 {:profile-id (:id profile)
|
||||
:project-id (:id project1)})
|
||||
file2 (th/create-file* 2 {:profile-id (:id profile)
|
||||
:project-id (:id project1)
|
||||
:is-shared true})]
|
||||
|
||||
(th/link-file-to-library* {:file-id (:id file1)
|
||||
:library-id (:id file2)})
|
||||
|
||||
;; --- initial data checks
|
||||
|
||||
;; the project1 should have 2 files
|
||||
(let [rows (db/query th/*pool* :file {:project-id (:id project1)})]
|
||||
(t/is (= 2 (count rows))))
|
||||
|
||||
;; should be no files on project2
|
||||
(let [rows (db/query th/*pool* :file {:project-id (:id project2)})]
|
||||
(t/is (= 0 (count rows))))
|
||||
|
||||
;; the file1 should be linked to file2
|
||||
(let [[item :as rows] (db/query th/*pool* :file-library-rel {:file-id (:id file1)})]
|
||||
(t/is (= 1 (count rows)))
|
||||
(t/is (= (:file-id item) (:id file1)))
|
||||
(t/is (= (:library-file-id item) (:id file2))))
|
||||
|
||||
;; should be no libraries on file2
|
||||
(let [rows (db/query th/*pool* :file-library-rel {:file-id (:id file2)})]
|
||||
(t/is (= 0 (count rows))))
|
||||
|
||||
;; move the library to other project
|
||||
(let [data {::th/type :move-files
|
||||
:profile-id (:id profile)
|
||||
:project-id (:id project2)
|
||||
:ids #{(:id file2)}}
|
||||
out (th/mutation! data)]
|
||||
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (nil? (:result out)))
|
||||
|
||||
;; project1 now should have 1 file
|
||||
(let [[item :as rows] (db/query th/*pool* :file {:project-id (:id project1)}
|
||||
{:order-by [:created-at]})]
|
||||
(t/is (= 1 (count rows)))
|
||||
(t/is (= (:id item) (:id file1))))
|
||||
|
||||
;; project2 now should have 1 file
|
||||
(let [[item :as rows] (db/query th/*pool* :file {:project-id (:id project2)})]
|
||||
(t/is (= 1 (count rows)))
|
||||
(t/is (= (:id item) (:id file2))))
|
||||
|
||||
;; the file1 should not have any link to libraries
|
||||
(let [rows (db/query th/*pool* :file-library-rel {:file-id (:id file1)})]
|
||||
(t/is (zero? (count rows))))
|
||||
|
||||
;; the file2 should not have any link to libraries
|
||||
(let [rows (db/query th/*pool* :file-library-rel {:file-id (:id file2)})]
|
||||
(t/is (zero? (count rows))))
|
||||
|
||||
)))
|
||||
|
||||
(t/deftest move-project
|
||||
(let [profile (th/create-profile* 1 {:is-active true})
|
||||
team (th/create-team* 1 {:profile-id (:id profile)})
|
||||
|
||||
project1 (th/create-project* 1 {:team-id (:default-team-id profile)
|
||||
:profile-id (:id profile)})
|
||||
|
||||
project2 (th/create-project* 2 {:team-id (:default-team-id profile)
|
||||
:profile-id (:id profile)})
|
||||
|
||||
file1 (th/create-file* 1 {:profile-id (:id profile)
|
||||
:project-id (:id project1)})
|
||||
|
||||
file2 (th/create-file* 2 {:profile-id (:id profile)
|
||||
:project-id (:id project1)
|
||||
:is-shared true})
|
||||
|
||||
file3 (th/create-file* 3 {:profile-id (:id profile)
|
||||
:project-id (:id project2)
|
||||
:is-shared true})]
|
||||
|
||||
(th/link-file-to-library* {:file-id (:id file1)
|
||||
:library-id (:id file2)})
|
||||
|
||||
(th/link-file-to-library* {:file-id (:id file1)
|
||||
:library-id (:id file3)})
|
||||
|
||||
;; --- initial data checks
|
||||
|
||||
;; the project1 should have 2 files
|
||||
(let [rows (db/query th/*pool* :file {:project-id (:id project1)})]
|
||||
(t/is (= 2 (count rows))))
|
||||
|
||||
;; the project2 should have 1 file
|
||||
(let [rows (db/query th/*pool* :file {:project-id (:id project2)})]
|
||||
(t/is (= 1 (count rows))))
|
||||
|
||||
;; the file1 should be linked to file2 and file3
|
||||
(let [[item1 item2 :as rows] (db/query th/*pool* :file-library-rel {:file-id (:id file1)}
|
||||
{:order-by [:created-at]})]
|
||||
(t/is (= 2 (count rows)))
|
||||
(t/is (= (:file-id item1) (:id file1)))
|
||||
(t/is (= (:library-file-id item1) (:id file2)))
|
||||
(t/is (= (:file-id item2) (:id file1)))
|
||||
(t/is (= (:library-file-id item2) (:id file3))))
|
||||
|
||||
;; the file2 should not be linked to any file
|
||||
(let [[rows] (db/query th/*pool* :file-library-rel {:file-id (:id file2)})]
|
||||
(t/is (= 0 (count rows))))
|
||||
|
||||
;; the file3 should not be linked to any file
|
||||
(let [[rows] (db/query th/*pool* :file-library-rel {:file-id (:id file3)})]
|
||||
(t/is (= 0 (count rows))))
|
||||
|
||||
;; move project1 to other team
|
||||
;; TODO: correct team change of project
|
||||
(let [data {::th/type :move-project
|
||||
:profile-id (:id profile)
|
||||
:project-id (:id project1)
|
||||
:team-id (:id team)}
|
||||
out (th/mutation! data)]
|
||||
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (nil? (:result out)))
|
||||
|
||||
;; project1 now should still have 2 files
|
||||
(let [[item1 item2 :as rows] (db/query th/*pool* :file {:project-id (:id project1)}
|
||||
{:order-by [:created-at]})]
|
||||
;; (clojure.pprint/pprint rows)
|
||||
(t/is (= 2 (count rows)))
|
||||
(t/is (= (:id item1) (:id file1)))
|
||||
(t/is (= (:id item2) (:id file2))))
|
||||
|
||||
;; project2 now should still have 1 file
|
||||
(let [[item :as rows] (db/query th/*pool* :file {:project-id (:id project2)})]
|
||||
(t/is (= 1 (count rows)))
|
||||
(t/is (= (:id item) (:id file3))))
|
||||
|
||||
;; the file1 should be linked to file2 but not file3
|
||||
(let [[item1 :as rows] (db/query th/*pool* :file-library-rel {:file-id (:id file1)}
|
||||
{:order-by [:created-at]})]
|
||||
(t/is (= 1 (count rows)))
|
||||
(t/is (= (:file-id item1) (:id file1)))
|
||||
(t/is (= (:library-file-id item1) (:id file2))))
|
||||
|
||||
)))
|
||||
|
||||
|
||||
|
|
@ -24,7 +24,7 @@
|
|||
team (th/create-team* 1 {:profile-id (:id profile)})
|
||||
project-id (uuid/next)]
|
||||
|
||||
;; crate project
|
||||
;; create project
|
||||
(let [data {::th/type :create-project
|
||||
:id project-id
|
||||
:profile-id (:id profile)
|
||||
|
@ -37,7 +37,7 @@
|
|||
(let [result (:result out)]
|
||||
(t/is (= (:name data) (:name result)))))
|
||||
|
||||
;; query a list of projects
|
||||
;; query the list of projects of a team
|
||||
(let [data {::th/type :projects
|
||||
:team-id (:id team)
|
||||
:profile-id (:id profile)}
|
||||
|
@ -50,7 +50,21 @@
|
|||
(t/is project-id (get-in result [0 :id]))
|
||||
(t/is (= "test project" (get-in result [0 :name])))))
|
||||
|
||||
;; rename project"
|
||||
;; query all projects of a user
|
||||
(let [data {::th/type :all-projects
|
||||
:profile-id (:id profile)}
|
||||
out (th/query! data)]
|
||||
;; (th/print-result! out)
|
||||
|
||||
(t/is (nil? (:error out)))
|
||||
(let [result (:result out)]
|
||||
(t/is (= 2 (count result)))
|
||||
(t/is (not= project-id (get-in result [0 :id])))
|
||||
(t/is (= "Drafts" (get-in result [0 :name])))
|
||||
(t/is project-id (get-in result [1 :id]))
|
||||
(t/is (= "test project" (get-in result [1 :name])))))
|
||||
|
||||
;; rename project
|
||||
(let [data {::th/type :rename-project
|
||||
:id project-id
|
||||
:name "renamed project"
|
||||
|
|
|
@ -400,6 +400,13 @@
|
|||
},
|
||||
"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" : {
|
||||
"translations" : {
|
||||
"ca" : "+ Crear un nou equip",
|
||||
|
@ -441,6 +448,13 @@
|
|||
},
|
||||
"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" : {
|
||||
"translations" : {
|
||||
"ca" : "Encara no hi ha cap arxiu aquí",
|
||||
|
@ -493,6 +507,20 @@
|
|||
},
|
||||
"used-in" : [ "src/app/main/ui/dashboard/grid.cljs" ]
|
||||
},
|
||||
"dashboard.move-to" : {
|
||||
"translations" : {
|
||||
"en" : "Move to",
|
||||
"es" : "Mover a"
|
||||
},
|
||||
"used-in" : [ "src/app/main/ui/dashboard/file_menu.cljs" ]
|
||||
},
|
||||
"dashboard.move-to-other-team" : {
|
||||
"translations" : {
|
||||
"en" : "Move to other team",
|
||||
"es" : "Mover a otro equipo"
|
||||
},
|
||||
"used-in" : [ "src/app/main/ui/dashboard/file_menu.cljs" ]
|
||||
},
|
||||
"dashboard.new-file" : {
|
||||
"translations" : {
|
||||
"ca" : "+ Nou Arxiu",
|
||||
|
@ -579,6 +607,13 @@
|
|||
},
|
||||
"used-in" : [ "src/app/main/ui/dashboard/team.cljs" ]
|
||||
},
|
||||
"dashboard.open-in-new-tab" : {
|
||||
"translations" : {
|
||||
"en" : "Open file in a new tab",
|
||||
"es" : "Abrir en una pestaña nueva"
|
||||
},
|
||||
"used-in" : [ "src/app/main/ui/dashboard/file_menu.cljs" ]
|
||||
},
|
||||
"dashboard.password-change" : {
|
||||
"translations" : {
|
||||
"ca" : "Canvia la contrasenya",
|
||||
|
@ -590,6 +625,13 @@
|
|||
},
|
||||
"used-in" : [ "src/app/main/ui/settings/password.cljs" ]
|
||||
},
|
||||
"dashboard.pin-unpin" : {
|
||||
"translations" : {
|
||||
"en" : "Pin/Unpin",
|
||||
"es" : "Fijar/Desfijar"
|
||||
},
|
||||
"used-in" : [ "src/app/main/ui/dashboard/project_menu.cljs" ]
|
||||
},
|
||||
"dashboard.projects-title" : {
|
||||
"translations" : {
|
||||
"ca" : "Projectes",
|
||||
|
@ -687,6 +729,48 @@
|
|||
},
|
||||
"used-in" : [ "src/app/main/ui/dashboard/grid.cljs" ]
|
||||
},
|
||||
"dashboard.success-delete-project" : {
|
||||
"translations" : {
|
||||
"en" : "Your project has been deleted successfully",
|
||||
"es" : "Tu proyecto ha sido borrado con éxito"
|
||||
},
|
||||
"used-in" : [ "src/app/main/ui/dashboard/project_menu.cljs" ]
|
||||
},
|
||||
"dashboard.success-delete-file" : {
|
||||
"translations" : {
|
||||
"en" : "Your file has been deleted successfully",
|
||||
"es" : "Tu archivo ha sido borrado con éxito"
|
||||
},
|
||||
"used-in" : [ "src/app/main/ui/dashboard/file_menu.cljs" ]
|
||||
},
|
||||
"dashboard.success-duplicate-project" : {
|
||||
"translations" : {
|
||||
"en" : "Your project has been duplicated successfully",
|
||||
"es" : "Tu proyecto ha sido duplicado con éxito"
|
||||
},
|
||||
"used-in" : [ "src/app/main/ui/dashboard/project_menu.cljs" ]
|
||||
},
|
||||
"dashboard.success-duplicate-file" : {
|
||||
"translations" : {
|
||||
"en" : "Your file has been duplicated successfully",
|
||||
"es" : "Tu archivo ha sido duplicado con éxito"
|
||||
},
|
||||
"used-in" : [ "src/app/main/ui/dashboard/file_menu.cljs" ]
|
||||
},
|
||||
"dashboard.success-move-project" : {
|
||||
"translations" : {
|
||||
"en" : "Your project has been moved successfully",
|
||||
"es" : "Tu proyecto ha sido movido con éxito"
|
||||
},
|
||||
"used-in" : [ "src/app/main/ui/dashboard/project_menu.cljs" ]
|
||||
},
|
||||
"dashboard.success-move-file" : {
|
||||
"translations" : {
|
||||
"en" : "Your file has been moved successfully",
|
||||
"es" : "Tu archivo ha sido movido con éxito"
|
||||
},
|
||||
"used-in" : [ "src/app/main/ui/dashboard/file_menu.cljs" ]
|
||||
},
|
||||
"dashboard.switch-team" : {
|
||||
"translations" : {
|
||||
"ca" : "Cambiar d'equip",
|
||||
|
@ -964,7 +1048,7 @@
|
|||
"translations" : {
|
||||
"ca" : "Sembla que el contingut de la imatge no coincideix amb l'extensió del arxiu",
|
||||
"en" : "Seems that the contents of the image does not match the file extension.",
|
||||
"es" : "Parece que el contenido de la imagen no coincide con la etensión del archivo.",
|
||||
"es" : "Parece que el contenido de la imagen no coincide con la extensión del archivo.",
|
||||
"fr" : "Il semble que le contenu de l’image ne correspond pas à l’extension de fichier.",
|
||||
"ru" : "",
|
||||
"zh_cn" : "图片内容好像与文档扩展名不匹配。"
|
||||
|
|
|
@ -21,6 +21,10 @@
|
|||
visibility: visible;
|
||||
}
|
||||
|
||||
.context-menu.fixed {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.context-menu-items {
|
||||
background: $color-white;
|
||||
border-radius: $br-small;
|
||||
|
@ -31,6 +35,12 @@
|
|||
overflow: auto;
|
||||
position: absolute;
|
||||
top: $size-3;
|
||||
|
||||
& .separator {
|
||||
border-top: 1px solid $color-gray-10;
|
||||
padding: 0px;
|
||||
margin: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.context-menu-action {
|
||||
|
@ -45,6 +55,34 @@
|
|||
color: $color-black;
|
||||
background-color: $color-primary-lighter;
|
||||
}
|
||||
|
||||
&.submenu {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
& span {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
& svg {
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&.submenu-back {
|
||||
color: $color-gray-30;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
& svg {
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
transform: rotate(180deg);
|
||||
margin-right: $small;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.context-menu.is-selectable {
|
||||
|
|
|
@ -195,13 +195,6 @@
|
|||
width: 15px;
|
||||
height: 30px;
|
||||
|
||||
svg {
|
||||
fill: $color-gray-20;
|
||||
height: 18px;
|
||||
margin-right: $x-small;
|
||||
width: 18px;
|
||||
}
|
||||
|
||||
span {
|
||||
color: $color-black;
|
||||
}
|
||||
|
@ -218,13 +211,15 @@
|
|||
align-items: flex-end;
|
||||
flex-direction: column;
|
||||
|
||||
svg {
|
||||
> svg {
|
||||
fill: $color-gray-60;
|
||||
margin-right: 0;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
svg {
|
||||
> svg {
|
||||
fill: $color-primary-dark;
|
||||
}
|
||||
|
||||
|
@ -237,7 +232,7 @@
|
|||
}
|
||||
|
||||
.project-th-actions.force-display {
|
||||
display: flex;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -105,4 +105,14 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pin-icon {
|
||||
svg {
|
||||
fill: $color-gray-20;
|
||||
}
|
||||
|
||||
&.active {
|
||||
svg { fill: $color-gray-50; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -162,7 +162,7 @@
|
|||
overflow: unset;
|
||||
}
|
||||
|
||||
li {
|
||||
& > li {
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
|
|
|
@ -49,6 +49,10 @@
|
|||
margin-right: $medium;
|
||||
}
|
||||
|
||||
.edit-wrapper {
|
||||
margin-right: $medium;
|
||||
}
|
||||
|
||||
.info {
|
||||
font-size: 15px;
|
||||
line-height: 1rem;
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
[app.common.uuid :as uuid]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.data.users :as du]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.router :as rt]
|
||||
[app.util.time :as dt]
|
||||
[app.util.timers :as ts]
|
||||
|
@ -347,6 +348,43 @@
|
|||
(rx/map #(partial created %))
|
||||
(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)))]
|
||||
(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
|
||||
:name new-name})
|
||||
(rx/tap on-success)
|
||||
(rx/map #(partial duplicated %))
|
||||
(rx/catch on-error)))))))
|
||||
|
||||
(defn move-project
|
||||
[{:keys [id team-id] :as params}]
|
||||
(us/assert ::us/uuid id)
|
||||
(us/assert ::us/uuid team-id)
|
||||
(ptk/reify ::move-project
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [{:keys [on-success on-error]
|
||||
:or {on-success identity
|
||||
on-error identity}} (meta params)]
|
||||
|
||||
(->> (rp/mutation! :move-project {:project-id id
|
||||
:team-id team-id})
|
||||
(rx/tap on-success)
|
||||
(rx/catch on-error))))))
|
||||
|
||||
(def clear-project-for-edit
|
||||
(ptk/reify ::clear-project-for-edit
|
||||
ptk/UpdateEvent
|
||||
|
@ -494,3 +532,43 @@
|
|||
(-> state
|
||||
(assoc-in [:files project-id id] file)
|
||||
(update-in [:recent-files project-id] (fnil conj #{}) id)))))
|
||||
|
||||
;; --- Duplicate File
|
||||
|
||||
(defn duplicate-file
|
||||
[{:keys [id name] :as params}]
|
||||
(us/assert ::us/uuid id)
|
||||
(us/assert ::name name)
|
||||
(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
|
||||
:name new-name})
|
||||
(rx/tap on-success)
|
||||
(rx/map file-created)
|
||||
(rx/catch on-error))))))
|
||||
|
||||
;; --- Move File
|
||||
|
||||
(defn move-file
|
||||
[{:keys [id project-id] :as params}]
|
||||
(us/assert ::us/uuid id)
|
||||
(us/assert ::us/uuid project-id)
|
||||
(ptk/reify ::move-file
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [{:keys [on-success on-error]
|
||||
:or {on-success identity
|
||||
on-error identity}} (meta params)]
|
||||
|
||||
(->> (rp/mutation! :move-files {:ids #{id}
|
||||
:project-id project-id})
|
||||
(rx/tap on-success)
|
||||
(rx/catch on-error))))))
|
||||
|
||||
|
|
|
@ -12,9 +12,11 @@
|
|||
[rumext.alpha :as mf]
|
||||
[goog.object :as gobj]
|
||||
[app.main.ui.components.dropdown :refer [dropdown']]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.util.data :refer [classnames]]
|
||||
[app.util.dom :as dom]))
|
||||
[app.util.dom :as dom]
|
||||
[app.util.object :as obj]))
|
||||
|
||||
(mf/defc context-menu
|
||||
{::mf/wrap-props false}
|
||||
|
@ -24,37 +26,92 @@
|
|||
(assert (vector? (gobj/get props "options")) "missing `options` prop")
|
||||
|
||||
(let [open? (gobj/get props "show")
|
||||
on-close (gobj/get props "on-close")
|
||||
options (gobj/get props "options")
|
||||
is-selectable (gobj/get props "selectable")
|
||||
selected (gobj/get props "selected")
|
||||
top (gobj/get props "top")
|
||||
left (gobj/get props "left")
|
||||
top (gobj/get props "top" 0)
|
||||
left (gobj/get props "left" 0)
|
||||
fixed? (gobj/get props "fixed?" false)
|
||||
|
||||
offset (mf/use-state 0)
|
||||
local (mf/use-state {:offset 0
|
||||
:levels nil})
|
||||
|
||||
on-local-close
|
||||
(mf/use-callback
|
||||
(fn []
|
||||
(swap! local assoc :levels [{:parent-option nil
|
||||
:options options}])
|
||||
(on-close)))
|
||||
|
||||
check-menu-offscreen
|
||||
(mf/use-callback
|
||||
(mf/deps top @offset)
|
||||
(mf/deps top (:offset @local))
|
||||
(fn [node]
|
||||
(when node
|
||||
(when (and node (not fixed?))
|
||||
(let [{node-height :height} (dom/get-bounding-rect node)
|
||||
{window-height :height} (dom/get-window-size)
|
||||
target-offset (if (> (+ top node-height) window-height)
|
||||
(- node-height)
|
||||
0)]
|
||||
|
||||
(if (not= target-offset @offset)
|
||||
(reset! offset target-offset))))))]
|
||||
(if (not= target-offset (:offset @local))
|
||||
(swap! local assoc :offset target-offset))))))
|
||||
|
||||
(when open?
|
||||
enter-submenu
|
||||
(mf/use-callback
|
||||
(mf/deps options)
|
||||
(fn [option-name sub-options]
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(swap! local update :levels
|
||||
conj {:parent-option option-name
|
||||
:options sub-options}))))
|
||||
|
||||
exit-submenu
|
||||
(mf/use-callback
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(swap! local update :levels pop)))
|
||||
|
||||
props (obj/merge props #js {:on-close on-local-close})]
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps options)
|
||||
#(swap! local assoc :levels [{:parent-option nil
|
||||
:options options}]))
|
||||
|
||||
(when (and open? (some? (:levels @local)))
|
||||
[:> dropdown' props
|
||||
[:div.context-menu {:class (classnames :is-open open?
|
||||
:fixed fixed?
|
||||
:is-selectable is-selectable)
|
||||
:style {:top (+ top @offset)
|
||||
:style {:top (+ top (:offset @local))
|
||||
:left left}}
|
||||
[:ul.context-menu-items {:ref check-menu-offscreen}
|
||||
(for [[action-name action-handler] options]
|
||||
[:li.context-menu-item {:class (classnames :is-selected (and selected (= action-name selected)))
|
||||
:key action-name}
|
||||
[:a.context-menu-action {:on-click action-handler}
|
||||
action-name]])]]])))
|
||||
(let [level (-> @local :levels peek)]
|
||||
[:ul.context-menu-items {:ref check-menu-offscreen}
|
||||
(when-let [parent-option (:parent-option level)]
|
||||
[:*
|
||||
[:li.context-menu-item
|
||||
[:a.context-menu-action.submenu-back
|
||||
{:data-no-close true
|
||||
:on-click exit-submenu}
|
||||
[:span i/arrow-slide]
|
||||
parent-option]]
|
||||
[:li.separator]])
|
||||
(for [[option-name option-handler sub-options] (:options level)]
|
||||
(when option-name
|
||||
(if (= option-name :separator)
|
||||
[:li.separator]
|
||||
[:li.context-menu-item
|
||||
{:class (classnames :is-selected (and selected
|
||||
(= option-name selected)))
|
||||
:key option-name}
|
||||
(if-not sub-options
|
||||
[:a.context-menu-action {:on-click option-handler}
|
||||
option-name]
|
||||
[:a.context-menu-action.submenu
|
||||
{:data-no-close true
|
||||
:on-click (enter-submenu option-name sub-options)}
|
||||
option-name
|
||||
[:span i/arrow-slide]])])))])]])))
|
||||
|
|
|
@ -17,12 +17,13 @@
|
|||
|
||||
on-click
|
||||
(fn [event]
|
||||
(if ref
|
||||
(let [target (dom/get-target event)
|
||||
parent (mf/ref-val ref)]
|
||||
(when-not (or (not parent) (.contains parent target))
|
||||
(on-close)))
|
||||
(on-close)))
|
||||
(let [target (dom/get-target event)]
|
||||
(when-not (.-data-no-close ^js target)
|
||||
(if ref
|
||||
(let [parent (mf/ref-val ref)]
|
||||
(when-not (or (not parent) (.contains parent target))
|
||||
(on-close)))
|
||||
(on-close)))))
|
||||
|
||||
on-keyup
|
||||
(fn [event]
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
[app.main.data.modal :as modal]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.context :as ctx]
|
||||
[app.main.ui.dashboard.files :refer [files-section]]
|
||||
[app.main.ui.dashboard.libraries :refer [libraries-page]]
|
||||
[app.main.ui.dashboard.projects :refer [projects-section]]
|
||||
|
@ -105,18 +106,23 @@
|
|||
(mf/deps team-id)
|
||||
(st/emitf (dd/fetch-bundle {:id team-id})))
|
||||
|
||||
[:section.dashboard-layout
|
||||
[:& sidebar {:team team
|
||||
:projects projects
|
||||
:project project
|
||||
:profile profile
|
||||
:section section
|
||||
:search-term search-term}]
|
||||
(when (and team (seq projects))
|
||||
[:& dashboard-content {:projects projects
|
||||
:profile profile
|
||||
:project project
|
||||
:section section
|
||||
:search-term search-term
|
||||
:team team}])]))
|
||||
[:& (mf/provider ctx/current-file-id) {:value nil}
|
||||
[:& (mf/provider ctx/current-team-id) {:value team-id}
|
||||
[:& (mf/provider ctx/current-project-id) {:value project-id}
|
||||
[:& (mf/provider ctx/current-page-id) {:value nil}
|
||||
|
||||
[:section.dashboard-layout
|
||||
[:& sidebar {:team team
|
||||
:projects projects
|
||||
:project project
|
||||
:profile profile
|
||||
:section section
|
||||
:search-term search-term}]
|
||||
(when (and team (seq projects))
|
||||
[:& dashboard-content {:projects projects
|
||||
:profile profile
|
||||
:project project
|
||||
:section section
|
||||
:search-term search-term
|
||||
:team team}])]]]]]))
|
||||
|
||||
|
|
183
frontend/src/app/main/ui/dashboard/file_menu.cljs
Normal file
183
frontend/src/app/main/ui/dashboard/file_menu.cljs
Normal file
|
@ -0,0 +1,183 @@
|
|||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2021 UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.dashboard.file-menu
|
||||
(:require
|
||||
[app.main.data.dashboard :as dd]
|
||||
[app.main.data.messages :as dm]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.context :as ctx]
|
||||
[app.main.ui.components.context-menu :refer [context-menu]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.router :as rt]
|
||||
[beicon.core :as rx]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(mf/defc file-menu
|
||||
[{:keys [file show? on-edit on-menu-close top left] :as props}]
|
||||
(assert (some? file) "missing `file` prop")
|
||||
(assert (boolean? show?) "missing `show?` prop")
|
||||
(assert (fn? on-edit) "missing `on-edit` prop")
|
||||
(assert (fn? on-menu-close) "missing `on-menu-close` prop")
|
||||
(let [top (or top 0)
|
||||
left (or left 0)
|
||||
|
||||
current-team-id (mf/use-ctx ctx/current-team-id)
|
||||
teams (mf/use-state nil)
|
||||
current-team (get @teams current-team-id)
|
||||
other-teams (remove #(= (:id %) current-team-id)
|
||||
(vals @teams))
|
||||
current-projects (remove #(= (:id %) (:project-id file))
|
||||
(:projects current-team))
|
||||
|
||||
project-name (fn [project]
|
||||
(if (:is-default project)
|
||||
(tr "labels.drafts")
|
||||
(:name project)))
|
||||
|
||||
on-new-tab
|
||||
(mf/use-callback
|
||||
(mf/deps file)
|
||||
(fn [event]
|
||||
(let [pparams {:project-id (:project-id file)
|
||||
:file-id (:id file)}
|
||||
qparams {:page-id (first (get-in file [:data :pages]))}]
|
||||
(st/emit! (rt/nav-new-window :workspace pparams qparams)))))
|
||||
|
||||
on-duplicate
|
||||
(mf/use-callback
|
||||
(mf/deps file)
|
||||
(st/emitf (dm/success (tr "dashboard.success-duplicate-file"))
|
||||
(dd/duplicate-file file)))
|
||||
|
||||
delete-fn
|
||||
(mf/use-callback
|
||||
(mf/deps file)
|
||||
(st/emitf (dm/success (tr "dashboard.success-delete-file"))
|
||||
(dd/delete-file file)))
|
||||
|
||||
on-delete
|
||||
(mf/use-callback
|
||||
(mf/deps file)
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(st/emit! (modal/show
|
||||
{:type :confirm
|
||||
:title (tr "modals.delete-file-confirm.title")
|
||||
:message (tr "modals.delete-file-confirm.message")
|
||||
:accept-label (tr "modals.delete-file-confirm.accept")
|
||||
:on-accept delete-fn}))))
|
||||
|
||||
on-move
|
||||
(mf/use-callback
|
||||
(mf/deps file)
|
||||
(fn [team-id project-id]
|
||||
(let [data {:id (:id file)
|
||||
:project-id project-id}
|
||||
|
||||
mdata {:on-success
|
||||
(st/emitf (dm/success (tr "dashboard.success-move-file"))
|
||||
(rt/nav :dashboard-files
|
||||
{:team-id team-id
|
||||
:project-id project-id}))}]
|
||||
|
||||
(st/emitf (dd/move-file (with-meta data mdata))))))
|
||||
|
||||
add-shared
|
||||
(mf/use-callback
|
||||
(mf/deps file)
|
||||
(st/emitf (dd/set-file-shared (assoc file :is-shared true))))
|
||||
|
||||
del-shared
|
||||
(mf/use-callback
|
||||
(mf/deps file)
|
||||
(st/emitf (dd/set-file-shared (assoc file :is-shared false))))
|
||||
|
||||
on-add-shared
|
||||
(mf/use-callback
|
||||
(mf/deps file)
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(st/emit! (modal/show
|
||||
{:type :confirm
|
||||
:message ""
|
||||
:title (tr "modals.add-shared-confirm.message" (:name file))
|
||||
:hint (tr "modals.add-shared-confirm.hint")
|
||||
:cancel-label :omit
|
||||
:accept-label (tr "modals.add-shared-confirm.accept")
|
||||
:accept-style :primary
|
||||
:on-accept add-shared}))))
|
||||
|
||||
on-del-shared
|
||||
(mf/use-callback
|
||||
(mf/deps file)
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(dom/stop-propagation event)
|
||||
(st/emit! (modal/show
|
||||
{:type :confirm
|
||||
:message ""
|
||||
:title (tr "modals.remove-shared-confirm.message" (:name file))
|
||||
:hint (tr "modals.remove-shared-confirm.hint")
|
||||
:cancel-label :omit
|
||||
:accept-label (tr "modals.remove-shared-confirm.accept")
|
||||
:on-accept del-shared}))))]
|
||||
|
||||
(mf/use-layout-effect
|
||||
(mf/deps show?)
|
||||
(fn []
|
||||
(let [group-by-team (fn [projects]
|
||||
(reduce
|
||||
(fn [teams project]
|
||||
(update teams (:team-id project)
|
||||
#(if (nil? %)
|
||||
{:id (:team-id project)
|
||||
:name (:team-name project)
|
||||
:projects [project]}
|
||||
(update % :projects conj project))))
|
||||
{}
|
||||
projects))]
|
||||
(if show?
|
||||
(->> (rp/query! :all-projects)
|
||||
(rx/map group-by-team)
|
||||
(rx/subs #(reset! teams %)))
|
||||
(reset! teams [])))))
|
||||
|
||||
(when current-team
|
||||
[:& context-menu {:on-close on-menu-close
|
||||
:show show?
|
||||
:fixed? (or (not= top 0) (not= left 0))
|
||||
:top top
|
||||
:left left
|
||||
:options [[(tr "dashboard.open-in-new-tab") on-new-tab]
|
||||
[(tr "labels.rename") on-edit]
|
||||
[(tr "dashboard.duplicate") on-duplicate]
|
||||
(when (or (seq current-projects) (seq other-teams))
|
||||
[(tr "dashboard.move-to") nil
|
||||
(conj (vec (for [project current-projects]
|
||||
[(project-name project)
|
||||
(on-move (:id current-team)
|
||||
(:id project))]))
|
||||
(when (seq other-teams)
|
||||
[(tr "dashboard.move-to-other-team") nil
|
||||
(for [team other-teams]
|
||||
[(:name team) nil
|
||||
(for [sub-project (:projects team)]
|
||||
[(project-name sub-project)
|
||||
(on-move (:id team)
|
||||
(:id sub-project))])])]))])
|
||||
(if (:is-shared file)
|
||||
[(tr "dashboard.remove-shared") on-del-shared]
|
||||
[(tr "dashboard.add-shared") on-add-shared])
|
||||
[:separator]
|
||||
[(tr "labels.delete") on-delete]]}])))
|
||||
|
|
@ -15,6 +15,7 @@
|
|||
[app.main.ui.components.context-menu :refer [context-menu]]
|
||||
[app.main.ui.dashboard.grid :refer [grid]]
|
||||
[app.main.ui.dashboard.inline-edition :refer [inline-edition]]
|
||||
[app.main.ui.dashboard.project-menu :refer [project-menu]]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [t]]
|
||||
|
@ -40,23 +41,10 @@
|
|||
on-edit
|
||||
(mf/use-callback #(swap! local assoc :edition true :menu-open false))
|
||||
|
||||
|
||||
delete-fn
|
||||
toggle-pin
|
||||
(mf/use-callback
|
||||
(mf/deps project)
|
||||
(fn [event]
|
||||
(st/emit! (dd/delete-project project)
|
||||
(rt/nav :dashboard-projects {:team-id (:id team)}))))
|
||||
|
||||
on-delete
|
||||
(mf/use-callback
|
||||
(mf/deps project)
|
||||
(st/emitf (modal/show
|
||||
{:type :confirm
|
||||
:title (t locale "modals.delete-project-confirm.title")
|
||||
:message (t locale "modals.delete-project-confirm.message")
|
||||
:accept-label (t locale "modals.delete-project-confirm.accept")
|
||||
:on-accept delete-fn})))
|
||||
(st/emitf (dd/toggle-project-pin project)))
|
||||
|
||||
on-create-clicked
|
||||
(mf/use-callback
|
||||
|
@ -77,12 +65,18 @@
|
|||
(st/emit! (dd/rename-project (assoc project :name name)))
|
||||
(swap! local assoc :edition false))}]
|
||||
[:div.dashboard-title
|
||||
[:h1 (:name project)]
|
||||
[:div.icon {:on-click on-menu-click} i/actions]
|
||||
[:& context-menu {:on-close on-menu-close
|
||||
:show (:menu-open @local)
|
||||
:options [[(t locale "labels.rename") on-edit]
|
||||
[(t locale "labels.delete") on-delete]]}]]))
|
||||
[:h1 {:on-double-click on-edit}
|
||||
(:name project)]
|
||||
[:div.icon {:on-click on-menu-click}
|
||||
i/actions]
|
||||
[:& project-menu {:project project
|
||||
:show? (:menu-open @local)
|
||||
:on-edit on-edit
|
||||
:on-menu-close on-menu-close}]
|
||||
[:div.icon.pin-icon
|
||||
{:class (when (:is-pinned project) "active")
|
||||
:on-click toggle-pin}
|
||||
i/pin]]))
|
||||
[:a.btn-secondary.btn-small {:on-click on-create-clicked}
|
||||
(t locale "dashboard.new-file")]]))
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
[app.main.fonts :as fonts]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.context-menu :refer [context-menu]]
|
||||
[app.main.ui.dashboard.file-menu :refer [file-menu]]
|
||||
[app.main.ui.dashboard.inline-edition :refer [inline-edition]]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.main.worker :as wrk]
|
||||
|
@ -61,84 +62,36 @@
|
|||
(mf/defc grid-item
|
||||
{:wrap [mf/memo]}
|
||||
[{:keys [id file] :as props}]
|
||||
(let [local (mf/use-state {:menu-open false :edition false})
|
||||
(let [local (mf/use-state {:menu-open false
|
||||
:menu-pos nil
|
||||
:edition false})
|
||||
locale (mf/deref i18n/locale)
|
||||
on-close (mf/use-callback #(swap! local assoc :menu-open false))
|
||||
menu-ref (mf/use-ref)
|
||||
|
||||
delete-fn
|
||||
on-menu-close
|
||||
(mf/use-callback
|
||||
(mf/deps file)
|
||||
(st/emitf (dd/delete-file file)))
|
||||
|
||||
on-delete
|
||||
(mf/use-callback
|
||||
(mf/deps file)
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(st/emit! (modal/show
|
||||
{:type :confirm
|
||||
:title (t locale "modals.delete-file-confirm.title")
|
||||
:message (t locale "modals.delete-file-confirm.message")
|
||||
:accept-label (t locale "modals.delete-file-confirm.accept")
|
||||
:on-accept delete-fn}))))
|
||||
#(swap! local assoc :menu-open false))
|
||||
|
||||
on-navigate
|
||||
(mf/use-callback
|
||||
(mf/deps id)
|
||||
(fn []
|
||||
(let [pparams {:project-id (:project-id file)
|
||||
:file-id (:id file)}
|
||||
qparams {:page-id (first (get-in file [:data :pages]))}]
|
||||
(st/emit! (rt/nav :workspace pparams qparams)))))
|
||||
|
||||
|
||||
add-shared
|
||||
(mf/use-callback
|
||||
(mf/deps file)
|
||||
(st/emitf (dd/set-file-shared (assoc file :is-shared true))))
|
||||
|
||||
del-shared
|
||||
(mf/use-callback
|
||||
(mf/deps file)
|
||||
(st/emitf (dd/set-file-shared (assoc file :is-shared false))))
|
||||
|
||||
on-add-shared
|
||||
(mf/use-callback
|
||||
(mf/deps file)
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(st/emit! (modal/show
|
||||
{:type :confirm
|
||||
:message ""
|
||||
:title (t locale "modals.add-shared-confirm.message" (:name file))
|
||||
:hint (t locale "modals.add-shared-confirm.hint")
|
||||
:cancel-label :omit
|
||||
:accept-label (t locale "modals.add-shared-confirm.accept")
|
||||
:accept-style :primary
|
||||
:on-accept add-shared}))))
|
||||
|
||||
on-del-shared
|
||||
(mf/use-callback
|
||||
(mf/deps file)
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(dom/stop-propagation event)
|
||||
(st/emit! (modal/show
|
||||
{:type :confirm
|
||||
:message ""
|
||||
:title (t locale "modals.remove-shared-confirm.message" (:name file))
|
||||
:hint (t locale "modals.remove-shared-confirm.hint")
|
||||
:cancel-label :omit
|
||||
:accept-label (t locale "modals.remove-shared-confirm.accept")
|
||||
:on-accept del-shared}))))
|
||||
(let [menu-icon (mf/ref-val menu-ref)
|
||||
target (dom/get-target event)]
|
||||
(when-not (dom/child? target menu-icon)
|
||||
(let [pparams {:project-id (:project-id file)
|
||||
:file-id (:id file)}
|
||||
qparams {:page-id (first (get-in file [:data :pages]))}]
|
||||
(st/emit! (rt/nav :workspace pparams qparams)))))))
|
||||
|
||||
on-menu-click
|
||||
(mf/use-callback
|
||||
(mf/deps file)
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(dom/stop-propagation event)
|
||||
(swap! local assoc :menu-open true)))
|
||||
(let [position (dom/get-client-position event)]
|
||||
(swap! local assoc :menu-open true
|
||||
:menu-pos position))))
|
||||
|
||||
edit
|
||||
(mf/use-callback
|
||||
|
@ -154,10 +107,10 @@
|
|||
(dom/stop-propagation event)
|
||||
(swap! local assoc
|
||||
:edition true
|
||||
:menu-open false)))
|
||||
:menu-open false)))]
|
||||
|
||||
]
|
||||
[:div.grid-item.project-th {:on-click on-navigate}
|
||||
[:div.grid-item.project-th {:on-click on-navigate
|
||||
:on-context-menu on-menu-click}
|
||||
[:div.overlay]
|
||||
[:& grid-item-thumbnail {:file file}]
|
||||
(when (:is-shared file)
|
||||
|
@ -171,15 +124,15 @@
|
|||
[:div.project-th-actions {:class (dom/classnames
|
||||
:force-display (:menu-open @local))}
|
||||
[:div.project-th-icon.menu
|
||||
{:on-click on-menu-click}
|
||||
i/actions]
|
||||
[:& context-menu {:on-close on-close
|
||||
:show (:menu-open @local)
|
||||
:options [[(t locale "labels.rename") on-edit]
|
||||
[(t locale "labels.delete") on-delete]
|
||||
(if (:is-shared file)
|
||||
[(t locale "dashboard.remove-shared") on-del-shared]
|
||||
[(t locale "dashboard.add-shared") on-add-shared])]}]]]))
|
||||
{:ref menu-ref
|
||||
:on-click on-menu-click}
|
||||
i/actions
|
||||
[:& file-menu {:file file
|
||||
:show? (:menu-open @local)
|
||||
:left (:x (:menu-pos @local))
|
||||
:top (:y (:menu-pos @local))
|
||||
:on-edit on-edit
|
||||
:on-menu-close on-menu-close}]]]]))
|
||||
|
||||
(mf/defc empty-placeholder
|
||||
[]
|
||||
|
|
110
frontend/src/app/main/ui/dashboard/project_menu.cljs
Normal file
110
frontend/src/app/main/ui/dashboard/project_menu.cljs
Normal file
|
@ -0,0 +1,110 @@
|
|||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2021 UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.dashboard.project-menu
|
||||
(:require
|
||||
[app.main.data.dashboard :as dd]
|
||||
[app.main.data.messages :as dm]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.context :as ctx]
|
||||
[app.main.ui.components.context-menu :refer [context-menu]]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.router :as rt]
|
||||
[beicon.core :as rx]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(mf/defc project-menu
|
||||
[{:keys [project show? on-edit on-menu-close top left] :as props}]
|
||||
(assert (some? project) "missing `project` prop")
|
||||
(assert (boolean? show?) "missing `show?` prop")
|
||||
(assert (fn? on-edit) "missing `on-edit` prop")
|
||||
(assert (fn? on-menu-close) "missing `on-menu-close` prop")
|
||||
(let [top (or top 0)
|
||||
left (or left 0)
|
||||
|
||||
current-team-id (mf/use-ctx ctx/current-team-id)
|
||||
teams (mf/use-state nil)
|
||||
|
||||
on-duplicate
|
||||
(mf/use-callback
|
||||
(mf/deps project)
|
||||
#(let [on-success
|
||||
(fn [new-project]
|
||||
(st/emit! (dm/success (tr "dashboard.success-duplicate-project"))
|
||||
(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})))))
|
||||
|
||||
toggle-pin
|
||||
(mf/use-callback
|
||||
(mf/deps project)
|
||||
(st/emitf (dd/toggle-project-pin project)))
|
||||
|
||||
on-move
|
||||
(mf/use-callback
|
||||
(mf/deps project)
|
||||
(fn [team-id]
|
||||
(let [data {:id (:id project)
|
||||
:team-id team-id}
|
||||
|
||||
mdata {:on-success
|
||||
(st/emitf (rt/nav :dashboard-projects
|
||||
{:team-id team-id}))}]
|
||||
|
||||
(st/emitf (dm/success (tr "dashboard.success-move-project"))
|
||||
(dd/move-project (with-meta data mdata))))))
|
||||
|
||||
delete-fn
|
||||
(mf/use-callback
|
||||
(mf/deps project)
|
||||
(fn [event]
|
||||
(st/emit! (dm/success (tr "dashboard.success-delete-project"))
|
||||
(dd/delete-project project)
|
||||
(rt/nav :dashboard-projects {:team-id (:team-id project)}))))
|
||||
|
||||
on-delete
|
||||
(mf/use-callback
|
||||
(mf/deps project)
|
||||
(st/emitf (modal/show
|
||||
{:type :confirm
|
||||
:title (tr "modals.delete-project-confirm.title")
|
||||
:message (tr "modals.delete-project-confirm.message")
|
||||
:accept-label (tr "modals.delete-project-confirm.accept")
|
||||
:on-accept delete-fn})))]
|
||||
|
||||
(mf/use-layout-effect
|
||||
(mf/deps show?)
|
||||
(fn []
|
||||
(if show?
|
||||
(->> (rp/query! :teams)
|
||||
(rx/map (fn [teams]
|
||||
(remove #(= (:id %) current-team-id) teams)))
|
||||
(rx/subs #(reset! teams %)))
|
||||
(reset! teams []))))
|
||||
|
||||
(when @teams
|
||||
[:& context-menu {:on-close on-menu-close
|
||||
:show show?
|
||||
:fixed? (or (not= top 0) (not= left 0))
|
||||
:top top
|
||||
:left left
|
||||
:options [[(tr "labels.rename") on-edit]
|
||||
[(tr "dashboard.duplicate") on-duplicate]
|
||||
[(tr "dashboard.pin-unpin") toggle-pin]
|
||||
(when (seq @teams)
|
||||
[(tr "dashboard.move-to") nil
|
||||
(for [team @teams]
|
||||
[(:name team) (on-move (:id team))])])
|
||||
[:separator]
|
||||
[(tr "labels.delete") on-delete]]}])))
|
||||
|
|
@ -15,6 +15,8 @@
|
|||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.dashboard.grid :refer [line-grid]]
|
||||
[app.main.ui.dashboard.inline-edition :refer [inline-edition]]
|
||||
[app.main.ui.dashboard.project-menu :refer [project-menu]]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [t tr]]
|
||||
|
@ -60,16 +62,43 @@
|
|||
team-id (:team-id project)
|
||||
file-count (or (:count project) 0)
|
||||
|
||||
dstate (mf/deref refs/dashboard-local)
|
||||
edit-id (:project-for-edit dstate)
|
||||
local (mf/use-state {:menu-open false
|
||||
:menu-pos nil
|
||||
:edition? (= (:id project) edit-id)})
|
||||
|
||||
on-nav
|
||||
(mf/use-callback
|
||||
(mf/deps project)
|
||||
(st/emitf (rt/nav :dashboard-files {:team-id (:team-id project)
|
||||
:project-id (:id project)})))
|
||||
|
||||
toggle-pin
|
||||
(mf/use-callback
|
||||
(mf/deps project)
|
||||
(st/emitf (dd/toggle-project-pin project)))
|
||||
|
||||
on-menu-click
|
||||
(mf/use-callback (fn [event]
|
||||
(let [position (dom/get-client-position event)]
|
||||
(dom/prevent-default event)
|
||||
(swap! local assoc :menu-open true
|
||||
:menu-pos position))))
|
||||
|
||||
on-menu-close
|
||||
(mf/use-callback #(swap! local assoc :menu-open false))
|
||||
|
||||
on-edit-open
|
||||
(mf/use-callback #(swap! local assoc :edition? true))
|
||||
|
||||
on-edit
|
||||
(mf/use-callback
|
||||
(mf/deps project)
|
||||
(fn [name]
|
||||
(st/emit! (dd/rename-project (assoc project :name name)))
|
||||
(swap! local assoc :edition? false)))
|
||||
|
||||
on-file-created
|
||||
(mf/use-callback
|
||||
(mf/deps project)
|
||||
|
@ -88,7 +117,6 @@
|
|||
params {:project-id (:id project)}]
|
||||
(st/emit! (dd/create-file (with-meta params mdata))))))]
|
||||
|
||||
|
||||
[:div.dashboard-project-row {:class (when first? "first")}
|
||||
[:div.project
|
||||
(when-not (:is-default project)
|
||||
|
@ -96,7 +124,18 @@
|
|||
{:class (when (:is-pinned project) "active")
|
||||
:on-click toggle-pin}
|
||||
i/pin])
|
||||
[:h2 {:on-click on-nav} (:name project)]
|
||||
(if (:edition? @local)
|
||||
[:& inline-edition {:content (:name project)
|
||||
:on-end on-edit}]
|
||||
[:h2 {:on-click on-nav
|
||||
:on-context-menu on-menu-click}
|
||||
(:name project)])
|
||||
[:& project-menu {:project project
|
||||
:show? (:menu-open @local)
|
||||
:left (:x (:menu-pos @local))
|
||||
:top (:y (:menu-pos @local))
|
||||
:on-edit on-edit-open
|
||||
:on-menu-close on-menu-close}]
|
||||
[:span.info (str file-count " files")]
|
||||
(when (> file-count 0)
|
||||
(let [time (-> (:modified-at project)
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
[app.main.ui.components.forms :as fm]
|
||||
[app.main.ui.dashboard.comments :refer [comments-section]]
|
||||
[app.main.ui.dashboard.inline-edition :refer [inline-edition]]
|
||||
[app.main.ui.dashboard.project-menu :refer [project-menu]]
|
||||
[app.main.ui.dashboard.team-form]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.util.avatars :as avatars]
|
||||
|
@ -42,10 +43,12 @@
|
|||
|
||||
(mf/defc sidebar-project
|
||||
[{:keys [item selected?] :as props}]
|
||||
(let [dstate (mf/deref refs/dashboard-local)
|
||||
edit-id (:project-for-edit dstate)
|
||||
(let [dstate (mf/deref refs/dashboard-local)
|
||||
edit-id (:project-for-edit dstate)
|
||||
|
||||
edition? (mf/use-state (= (:id item) edit-id))
|
||||
local (mf/use-state {:menu-open false
|
||||
:menu-pos nil
|
||||
:edition? (= (:id item) edit-id)})
|
||||
|
||||
on-click
|
||||
(mf/use-callback
|
||||
|
@ -54,23 +57,41 @@
|
|||
(st/emit! (rt/nav :dashboard-files {:team-id (:team-id item)
|
||||
:project-id (:id item)}))))
|
||||
|
||||
on-dbl-click
|
||||
(mf/use-callback #(reset! edition? true))
|
||||
on-menu-click
|
||||
(mf/use-callback (fn [event]
|
||||
(let [position (dom/get-client-position event)]
|
||||
(dom/prevent-default event)
|
||||
(swap! local assoc :menu-open true
|
||||
:menu-pos position))))
|
||||
|
||||
on-menu-close
|
||||
(mf/use-callback #(swap! local assoc :menu-open false))
|
||||
|
||||
on-edit-open
|
||||
(mf/use-callback #(swap! local assoc :edition? true))
|
||||
|
||||
on-edit
|
||||
(mf/use-callback
|
||||
(mf/deps item)
|
||||
(fn [name]
|
||||
(st/emit! (dd/rename-project (assoc item :name name)))
|
||||
(reset! edition? false)))]
|
||||
(swap! local assoc :edition? false)))]
|
||||
|
||||
[:li {:on-click on-click
|
||||
:on-double-click on-dbl-click
|
||||
:class (when selected? "current")}
|
||||
(if @edition?
|
||||
[:& inline-edition {:content (:name item)
|
||||
:on-end on-edit}]
|
||||
[:span.element-title (:name item)])]))
|
||||
[:*
|
||||
[:li {:on-click on-click
|
||||
:on-double-click on-edit-open
|
||||
:on-context-menu on-menu-click
|
||||
:class (when selected? "current")}
|
||||
(if (:edition? @local)
|
||||
[:& inline-edition {:content (:name item)
|
||||
:on-end on-edit}]
|
||||
[:span.element-title (:name item)])]
|
||||
[:& project-menu {:project item
|
||||
:show? (:menu-open @local)
|
||||
:left (:x (:menu-pos @local))
|
||||
:top (:y (:menu-pos @local))
|
||||
:on-edit on-edit-open
|
||||
:on-menu-close on-menu-close}]]))
|
||||
|
||||
(mf/defc sidebar-search
|
||||
[{:keys [search-term team-id locale] :as props}]
|
||||
|
|
|
@ -249,6 +249,9 @@
|
|||
(let [class-list (.-classList ^js node)]
|
||||
(.contains ^js class-list class-name)))
|
||||
|
||||
(defn child? [node1 node2]
|
||||
(.contains ^js node2 ^js node1))
|
||||
|
||||
(defn get-user-agent []
|
||||
(.-userAgent globals/navigator))
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue