mirror of
https://github.com/penpot/penpot.git
synced 2025-02-12 18:18:24 -05:00
🎉 Move files to other projects and teams
This commit is contained in:
parent
6a345c4b8a
commit
c6765a48c5
10 changed files with 290 additions and 70 deletions
|
@ -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)))
|
||||
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -507,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",
|
||||
|
|
|
@ -35,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 {
|
||||
|
@ -49,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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -523,6 +523,7 @@
|
|||
(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]
|
||||
|
@ -538,3 +539,21 @@
|
|||
(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,6 +26,7 @@
|
|||
(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")
|
||||
|
@ -31,11 +34,20 @@
|
|||
left (gobj/get props "left" 0)
|
||||
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
|
||||
(mf/use-callback
|
||||
(mf/deps top @offset)
|
||||
(mf/deps top (:offset @local))
|
||||
(fn [node]
|
||||
(when (and node (not fixed?))
|
||||
(let [{node-height :height} (dom/get-bounding-rect node)
|
||||
|
@ -44,19 +56,58 @@
|
|||
(- node-height)
|
||||
0)]
|
||||
|
||||
(if (not= target-offset @offset)
|
||||
(reset! offset target-offset))))))]
|
||||
(if (not= target-offset (:offset @local))
|
||||
(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?
|
||||
[:> 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)]
|
||||
(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}])]]]]]))
|
||||
|
||||
|
|
|
@ -11,11 +11,14 @@
|
|||
(:require
|
||||
[app.main.data.dashboard :as dd]
|
||||
[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
|
||||
|
@ -24,8 +27,14 @@
|
|||
(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)
|
||||
(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))
|
||||
|
||||
on-new-tab
|
||||
(mf/use-callback
|
||||
|
@ -58,6 +67,20 @@
|
|||
: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 (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)
|
||||
|
@ -85,29 +108,61 @@
|
|||
|
||||
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/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}))))]
|
||||
|
||||
[:& 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]
|
||||
[(tr "labels.delete") on-delete]
|
||||
(if (:is-shared file)
|
||||
[(tr "dashboard.remove-shared") on-del-shared]
|
||||
[(tr "dashboard.add-shared") on-add-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]
|
||||
[(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)
|
||||
[(tr "dashboard.remove-shared") on-del-shared]
|
||||
[(tr "dashboard.add-shared") on-add-shared])
|
||||
[:separator]
|
||||
[(tr "labels.delete") on-delete]]}])))
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue