0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-02-13 02:28:18 -05:00

🎉 Move files to other projects and teams

This commit is contained in:
Andrés Moya 2021-02-25 15:39:38 +01:00
parent 6a345c4b8a
commit c6765a48c5
10 changed files with 290 additions and 70 deletions

View file

@ -82,6 +82,50 @@
(db/exec! conn [sql:projects profile-id team-id])) (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 ;; --- Query: Project
(s/def ::id ::us/uuid) (s/def ::id ::us/uuid)
@ -94,3 +138,4 @@
(let [project (db/get-by-id conn :project id)] (let [project (db/get-by-id conn :project id)]
(check-read-permissions! conn profile-id id) (check-read-permissions! conn profile-id id)
project))) project)))

View file

@ -24,7 +24,7 @@
team (th/create-team* 1 {:profile-id (:id profile)}) team (th/create-team* 1 {:profile-id (:id profile)})
project-id (uuid/next)] project-id (uuid/next)]
;; crate project ;; create project
(let [data {::th/type :create-project (let [data {::th/type :create-project
:id project-id :id project-id
:profile-id (:id profile) :profile-id (:id profile)
@ -37,7 +37,7 @@
(let [result (:result out)] (let [result (:result out)]
(t/is (= (:name data) (:name result))))) (t/is (= (:name data) (:name result)))))
;; query a list of projects ;; query the list of projects of a team
(let [data {::th/type :projects (let [data {::th/type :projects
:team-id (:id team) :team-id (:id team)
:profile-id (:id profile)} :profile-id (:id profile)}

View file

@ -507,6 +507,20 @@
}, },
"used-in" : [ "src/app/main/ui/dashboard/grid.cljs" ] "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" : { "dashboard.new-file" : {
"translations" : { "translations" : {
"ca" : "+ Nou Arxiu", "ca" : "+ Nou Arxiu",

View file

@ -35,6 +35,12 @@
overflow: auto; overflow: auto;
position: absolute; position: absolute;
top: $size-3; top: $size-3;
& .separator {
border-top: 1px solid $color-gray-10;
padding: 0px;
margin: 2px;
}
} }
.context-menu-action { .context-menu-action {
@ -49,6 +55,34 @@
color: $color-black; color: $color-black;
background-color: $color-primary-lighter; 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 { .context-menu.is-selectable {

View file

@ -195,13 +195,6 @@
width: 15px; width: 15px;
height: 30px; height: 30px;
svg {
fill: $color-gray-20;
height: 18px;
margin-right: $x-small;
width: 18px;
}
span { span {
color: $color-black; color: $color-black;
} }
@ -218,13 +211,15 @@
align-items: flex-end; align-items: flex-end;
flex-direction: column; flex-direction: column;
svg { > svg {
fill: $color-gray-60; fill: $color-gray-60;
margin-right: 0; margin-right: 0;
height: 18px;
width: 18px;
} }
&:hover { &:hover {
svg { > svg {
fill: $color-primary-dark; fill: $color-primary-dark;
} }
@ -237,7 +232,7 @@
} }
.project-th-actions.force-display { .project-th-actions.force-display {
display: flex; opacity: 1;
} }
} }

View file

@ -523,6 +523,7 @@
(defn duplicate-file (defn duplicate-file
[{:keys [id name] :as params}] [{:keys [id name] :as params}]
(us/assert ::us/uuid id) (us/assert ::us/uuid id)
(us/assert ::name name)
(ptk/reify ::duplicate-file (ptk/reify ::duplicate-file
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
@ -538,3 +539,21 @@
(rx/map file-created) (rx/map file-created)
(rx/catch on-error)))))) (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))))))

View file

@ -12,9 +12,11 @@
[rumext.alpha :as mf] [rumext.alpha :as mf]
[goog.object :as gobj] [goog.object :as gobj]
[app.main.ui.components.dropdown :refer [dropdown']] [app.main.ui.components.dropdown :refer [dropdown']]
[app.main.ui.icons :as i]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.util.data :refer [classnames]] [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/defc context-menu
{::mf/wrap-props false} {::mf/wrap-props false}
@ -24,6 +26,7 @@
(assert (vector? (gobj/get props "options")) "missing `options` prop") (assert (vector? (gobj/get props "options")) "missing `options` prop")
(let [open? (gobj/get props "show") (let [open? (gobj/get props "show")
on-close (gobj/get props "on-close")
options (gobj/get props "options") options (gobj/get props "options")
is-selectable (gobj/get props "selectable") is-selectable (gobj/get props "selectable")
selected (gobj/get props "selected") selected (gobj/get props "selected")
@ -31,11 +34,20 @@
left (gobj/get props "left" 0) left (gobj/get props "left" 0)
fixed? (gobj/get props "fixed?" false) fixed? (gobj/get props "fixed?" false)
offset (mf/use-state 0) local (mf/use-state {:offset 0
:levels [{:parent-option nil
:options options}]})
on-local-close
(mf/use-callback
(fn []
(swap! local assoc :levels [{:parent-option nil
:options options}])
(on-close)))
check-menu-offscreen check-menu-offscreen
(mf/use-callback (mf/use-callback
(mf/deps top @offset) (mf/deps top (:offset @local))
(fn [node] (fn [node]
(when (and node (not fixed?)) (when (and node (not fixed?))
(let [{node-height :height} (dom/get-bounding-rect node) (let [{node-height :height} (dom/get-bounding-rect node)
@ -44,19 +56,58 @@
(- node-height) (- node-height)
0)] 0)]
(if (not= target-offset @offset) (if (not= target-offset (:offset @local))
(reset! offset target-offset))))))] (swap! local assoc :offset target-offset))))))
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})]
(when open? (when open?
[:> dropdown' props [:> dropdown' props
[:div.context-menu {:class (classnames :is-open open? [:div.context-menu {:class (classnames :is-open open?
:fixed fixed? :fixed fixed?
:is-selectable is-selectable) :is-selectable is-selectable)
:style {:top (+ top @offset) :style {:top (+ top (:offset @local))
:left left}} :left left}}
(let [level (-> @local :levels peek)]
[:ul.context-menu-items {:ref check-menu-offscreen} [:ul.context-menu-items {:ref check-menu-offscreen}
(for [[action-name action-handler] options] (when-let [parent-option (:parent-option level)]
[:li.context-menu-item {:class (classnames :is-selected (and selected (= action-name selected))) [:*
:key action-name} [:li.context-menu-item
[:a.context-menu-action {:on-click action-handler} [:a.context-menu-action.submenu-back
action-name]])]]]))) {: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)]
(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]])
]))])]])))

View file

@ -17,12 +17,13 @@
on-click on-click
(fn [event] (fn [event]
(let [target (dom/get-target event)]
(when-not (.-data-no-close ^js target)
(if ref (if ref
(let [target (dom/get-target event) (let [parent (mf/ref-val ref)]
parent (mf/ref-val ref)]
(when-not (or (not parent) (.contains parent target)) (when-not (or (not parent) (.contains parent target))
(on-close))) (on-close)))
(on-close))) (on-close)))))
on-keyup on-keyup
(fn [event] (fn [event]

View file

@ -16,6 +16,7 @@
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.context :as ctx]
[app.main.ui.dashboard.files :refer [files-section]] [app.main.ui.dashboard.files :refer [files-section]]
[app.main.ui.dashboard.libraries :refer [libraries-page]] [app.main.ui.dashboard.libraries :refer [libraries-page]]
[app.main.ui.dashboard.projects :refer [projects-section]] [app.main.ui.dashboard.projects :refer [projects-section]]
@ -105,6 +106,11 @@
(mf/deps team-id) (mf/deps team-id)
(st/emitf (dd/fetch-bundle {:id team-id}))) (st/emitf (dd/fetch-bundle {:id team-id})))
[:& (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 [:section.dashboard-layout
[:& sidebar {:team team [:& sidebar {:team team
:projects projects :projects projects
@ -118,5 +124,5 @@
:project project :project project
:section section :section section
:search-term search-term :search-term search-term
:team team}])])) :team team}])]]]]]))

View file

@ -11,11 +11,14 @@
(:require (:require
[app.main.data.dashboard :as dd] [app.main.data.dashboard :as dd]
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
[app.main.repo :as rp]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.context :as ctx]
[app.main.ui.components.context-menu :refer [context-menu]] [app.main.ui.components.context-menu :refer [context-menu]]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[app.util.router :as rt] [app.util.router :as rt]
[beicon.core :as rx]
[rumext.alpha :as mf])) [rumext.alpha :as mf]))
(mf/defc file-menu (mf/defc file-menu
@ -27,6 +30,12 @@
(let [top (or top 0) (let [top (or top 0)
left (or left 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))
on-new-tab on-new-tab
(mf/use-callback (mf/use-callback
(mf/deps file) (mf/deps file)
@ -58,6 +67,20 @@
:accept-label (tr "modals.delete-file-confirm.accept") :accept-label (tr "modals.delete-file-confirm.accept")
:on-accept delete-fn})))) :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 (rt/nav :dashboard-files
{:team-id team-id
:project-id project-id}))}]
(st/emitf (dd/move-file (with-meta data mdata))))))
add-shared add-shared
(mf/use-callback (mf/use-callback
(mf/deps file) (mf/deps file)
@ -98,6 +121,27 @@
:accept-label (tr "modals.remove-shared-confirm.accept") :accept-label (tr "modals.remove-shared-confirm.accept")
:on-accept del-shared}))))] :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 [:& context-menu {:on-close on-menu-close
:show show? :show show?
:fixed? (or (not= top 0) (not= left 0)) :fixed? (or (not= top 0) (not= left 0))
@ -106,8 +150,19 @@
: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 "dashboard.duplicate") on-duplicate]
[(tr "labels.delete") on-delete] [(tr "dashboard.move-to") nil
(conj (vec (for [project (:projects current-team)]
[(:name project) (on-move (:id current-team)
(:id project))]))
[(tr "dashboard.move-to-other-team") nil
(for [team other-teams]
[(:name team) nil
(for [sub-project (:projects team)]
[(:name sub-project) (on-move (:id team)
(:id sub-project))])])])]
(if (:is-shared file) (if (:is-shared file)
[(tr "dashboard.remove-shared") on-del-shared] [(tr "dashboard.remove-shared") on-del-shared]
[(tr "dashboard.add-shared") on-add-shared])]}])) [(tr "dashboard.add-shared") on-add-shared])
[:separator]
[(tr "labels.delete") on-delete]]}])))