0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-01-06 14:50:20 -05:00

🎉 Manage file images as assets

This commit is contained in:
Andrés Moya 2020-07-23 17:11:36 +02:00
parent 8f8dc80cad
commit 8c8b5887d6
14 changed files with 482 additions and 18 deletions

2
backend/scripts/psql.sh Executable file
View file

@ -0,0 +1,2 @@
#!/usr/bin/env bash
PGPASSWORD=$UXBOX_DATABASE_PASSWORD psql $UXBOX_DATABASE_URI -U $UXBOX_DATABASE_USERNAME

View file

@ -133,6 +133,7 @@
(declare create-file-image)
(s/def ::file-id ::us/uuid)
(s/def ::image-id ::us/uuid)
(s/def ::content ::imgs/upload)
(s/def ::add-file-image-from-url
@ -189,6 +190,33 @@
(images/resolve-urls :thumb-path :thumb-uri))))
;; --- Mutation: Delete File Image
(declare mark-file-image-deleted)
(s/def ::delete-file-image
(s/keys :req-un [::file-id ::image-id ::profile-id]))
(sm/defmutation ::delete-file-image
[{:keys [file-id image-id profile-id] :as params}]
(db/with-atomic [conn db/pool]
(files/check-edition-permissions! conn profile-id file-id)
;; Schedule object deletion
(tasks/submit! conn {:name "delete-object"
:delay cfg/default-deletion-delay
:props {:id image-id :type :file-image}})
(mark-file-image-deleted conn params)))
(defn mark-file-image-deleted
[conn {:keys [image-id] :as params}]
(db/update! conn :file-image
{:deleted-at (dt/now)}
{:id image-id})
nil)
;; --- Mutation: Import from collection
(declare copy-image)

View file

@ -169,7 +169,8 @@
(def ^:private sql:file-images
"select fi.*
from file_image as fi
where fi.file_id = ?")
where fi.file_id = ?
and fi.deleted_at is null")
(defn retrieve-file-images
[conn {:keys [file-id] :as params}]

View file

@ -1358,6 +1358,87 @@
"es" : "Alinear arriba"
}
},
"workspace.assets.assets" : {
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/assets.cljs:144" ],
"translations" : {
"en" : "Assets",
"fr" : "",
"ru" : "",
"es" : "Recursos"
}
},
"workspace.assets.box-filter-all" : {
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/assets.cljs:154" ],
"translations" : {
"en" : "All assets",
"fr" : "",
"ru" : "",
"es" : "Todos"
}
},
"workspace.assets.box-filter-colors" : {
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/assets.cljs:156" ],
"translations" : {
"en" : "Colors",
"fr" : "",
"ru" : "",
"es" : "Colores"
}
},
"workspace.assets.box-filter-graphics" : {
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/assets.cljs:155" ],
"translations" : {
"en" : "Graphics",
"fr" : "",
"ru" : "",
"es" : "Gráficos"
}
},
"workspace.assets.colors" : {
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/assets.cljs:91" ],
"translations" : {
"en" : "Colors",
"fr" : "",
"ru" : "",
"es" : "Colores"
}
},
"workspace.assets.delete" : {
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/assets.cljs:83" ],
"translations" : {
"en" : "Delete",
"fr" : "",
"ru" : "",
"es" : "Borrar"
}
},
"workspace.assets.file-library" : {
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/assets.cljs:104" ],
"translations" : {
"en" : "File library",
"fr" : "",
"ru" : "",
"es" : "Bilioteca del archivo"
}
},
"workspace.assets.graphics" : {
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/assets.cljs:68" ],
"translations" : {
"en" : "Graphics",
"fr" : "",
"ru" : "",
"es" : "Gráficos"
}
},
"workspace.assets.search" : {
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/assets.cljs:147" ],
"translations" : {
"en" : "Search assets",
"fr" : "",
"ru" : "",
"es" : "Buscar recursos"
}
},
"workspace.header.menu.disable-dynamic-alignment" : {
"used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:117" ],
"translations" : {
@ -2179,13 +2260,13 @@
}
},
"workspace.sidebar.icons" : {
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/icons.cljs:89" ],
"translations" : {
"en" : "Icons",
"fr" : "Icône",
"ru" : "Иконки",
"es" : "Iconos"
}
},
"unused" : true
},
"workspace.sidebar.sitemap" : {
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/sitemap.cljs:150" ],
@ -2196,6 +2277,15 @@
"es" : "Páginas"
}
},
"workspace.toolbar.assets" : {
"used-in" : [ "src/uxbox/main/ui/workspace/left_toolbar.cljs:105" ],
"translations" : {
"en" : "Assets (Ctrl + I)",
"fr" : "",
"ru" : "",
"es" : "Recursos (Ctrl + I)"
}
},
"workspace.toolbar.circle" : {
"used-in" : [ "src/uxbox/main/ui/workspace/left_toolbar.cljs:64" ],
"translations" : {
@ -2206,7 +2296,7 @@
}
},
"workspace.toolbar.color-palette" : {
"used-in" : [ "src/uxbox/main/ui/workspace/left_toolbar.cljs:108" ],
"used-in" : [ "src/uxbox/main/ui/workspace/left_toolbar.cljs:113" ],
"translations" : {
"en" : "Color Palette (---)",
"fr" : "Palette de couleurs (---)",
@ -2242,13 +2332,13 @@
}
},
"workspace.toolbar.libraries" : {
"used-in" : [ "src/uxbox/main/ui/workspace/left_toolbar.cljs:100" ],
"translations" : {
"en" : "Libraries (Ctrl + Shift + L)",
"fr" : "Librairies (Ctrl + Shift + L)",
"ru" : "Библиотеки (Ctrl + Shift + L)",
"es" : "Bibliotecas (Ctrl + Mays + L)"
}
},
"unused" : true
},
"workspace.toolbar.path" : {
"used-in" : [ "src/uxbox/main/ui/workspace/left_toolbar.cljs:88" ],

View file

@ -72,6 +72,7 @@
@import 'main/partials/sidebar-layers';
@import 'main/partials/sidebar-sitemap';
@import 'main/partials/sidebar-tools';
@import 'main/partials/sidebar-assets';
@import 'main/partials/tab-container';
@import 'main/partials/tool-bar';
@import 'main/partials/user-settings';

View file

@ -0,0 +1,143 @@
// 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/.
//
// Copyright (c) 2015-2016 Andrey Antukh <niwi@niwi.nz>
// Copyright (c) 2015-2016 Juan de la Cruz <delacruzgarciajuan@gmail.com>
.assets-bar {
display: flex;
flex-direction: column;
.assets-bar-title {
color: $color-gray-10;
font-size: $fs14;
margin: $small $small 0 $small;
}
.search-input {
background-color: $color-gray-50;
border: 1px solid $color-gray-30;
color: $color-gray-10;
font-size: $fs12;
margin: $small $small 0 $small;
padding: $x-small;
&:focus {
color: lighten($color-gray-10, 8%);
border-color: $color-primary !important;
}
&:hover {
border-color: $color-gray-20;
}
}
.input-select {
background-color: $color-gray-50;
color: $color-gray-10;
border: 1px solid transparent;
border-bottom-color: $color-gray-40;
padding: $x-small $x-small 0 $x-small;
margin: $small $small $medium $small;
&:focus {
color: lighten($color-gray-10, 8%);
}
option {
color: $color-gray-60;
background: $color-white;
font-size: $fs11;
}
}
.collapse-library {
margin-right: $small;
cursor: pointer;
&.open svg {
transform: rotate(90deg);
}
}
.asset-group {
background-color: $color-gray-60;
padding: $small;
font-size: $fs11;
color: $color-gray-20;
.group-title {
display: flex;
& span {
color: $color-gray-30;
}
}
.group-button {
margin-left: auto;
cursor: pointer;
& svg {
width: 0.7rem;
height: 0.7rem;
fill: #F0F0F0;
}
}
.group-grid {
margin-top: $small;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-auto-rows: 7vh;
column-gap: 0.5rem;
row-gap: 0.5rem;
}
.grid-cell {
background-color: $color-white;
border-radius: 4px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
position: relative;
& img {
max-height: 100%;
max-width: 100%;
height: auto;
width: auto;
}
}
.cell-name {
background-color: $color-gray-60;
font-size: $fs9;
display: none;
position: absolute;
left: 0;
bottom: 0;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.grid-cell:hover {
border: 1px solid $color-primary;
& .cell-name {
display: block;
}
}
.context-menu {
position: absolute;
top: 10px;
left: 10px;
}
}
}

View file

@ -25,7 +25,7 @@ $width-settings-bar: 15rem;
}
.settings-bar-inside {
align-items: center;
align-items: flex-start;
display: grid;
grid-template-columns: 100%;

View file

@ -63,6 +63,7 @@
:sitemap-pages
:layers
:libraries
:assets
:document-history
:colorpalette
:element-options
@ -74,9 +75,10 @@
(s/def ::layout-flags (s/coll-of ::layout-flag))
(def default-layout
#{:sitemap
:sitemap-pages
:layers
#{;; :sitemap
;; :sitemap-pages
;; :layers
:assets
:element-options
:rules
:display-grid
@ -305,7 +307,8 @@
left-sidebar? (not (empty? (keep layout [:layers
:sitemap
:document-history
:libraries])))
:libraries
:assets])))
right-sidebar? (not (empty? (keep layout [:element-options])))]
(update-in state [:workspace-local]
assoc :left-sidebar? left-sidebar?
@ -1434,8 +1437,10 @@
;; Persistence
(def fetch-images dwp/fetch-images)
(def add-image-from-url dwp/add-image-from-url)
(def upload-image dwp/upload-image)
(def delete-file-image dwp/delete-file-image)
(def rename-page dwp/rename-page)
(def delete-page dwp/delete-page)
(def create-empty-page dwp/create-empty-page)

View file

@ -430,6 +430,23 @@
(update [_ state]
(update state :workspace-images assoc (:id item) item))))
;; --- Delete image
(defn delete-file-image
[file-id image-id]
(ptk/reify ::delete-file-image
ptk/UpdateEvent
(update [_ state]
(update state :workspace-images dissoc image-id))
ptk/WatchEvent
(watch [_ state stream]
(let [params {:file-id file-id
:image-id image-id}]
(rp/mutation :delete-file-image params)))))
;; --- Helpers
(defn purge-page

View file

@ -25,11 +25,15 @@
(let [open? (gobj/get props "show")
options (gobj/get props "options")
is-selectable (gobj/get props "selectable")
selected (gobj/get props "selected")]
selected (gobj/get props "selected")
top (gobj/get props "top")
left (gobj/get props "left")]
(when open?
[:> dropdown' props
[:div.context-menu {:class (classnames :is-open open?
:is-selectable is-selectable)}
:is-selectable is-selectable)
:style {:top top
:left left}}
[:ul.context-menu-items
(for [[action-name action-handler] options]
[:li.context-menu-item {:class (classnames :is-selected (and selected (= action-name selected)))

View file

@ -79,6 +79,7 @@
(def picker (icon-xref :picker))
(def pin (icon-xref :pin))
(def play (icon-xref :play))
(def plus (icon-xref :plus))
(def radius (icon-xref :radius))
(def recent (icon-xref :recent))
(def redo (icon-xref :redo))

View file

@ -96,10 +96,15 @@
:class (when (contains? layout :layers) "selected")
:on-click #(st/emit! (dw/toggle-layout-flags :sitemap :layers))}
i/layers]
;; [:li.tooltip.tooltip-right
;; {:alt (t locale "workspace.toolbar.libraries")
;; :class (when (contains? layout :libraries) "selected")
;; :on-click #(st/emit! (dw/toggle-layout-flags :libraries))}
;; i/icon-set]
[:li.tooltip.tooltip-right
{:alt (t locale "workspace.toolbar.libraries")
:class (when (contains? layout :libraries) "selected")
:on-click #(st/emit! (dw/toggle-layout-flags :libraries))}
{:alt (t locale "workspace.toolbar.assets")
:class (when (contains? layout :assets) "selected")
:on-click #(st/emit! (dw/toggle-layout-flags :assets))}
i/icon-set]
[:li.tooltip.tooltip-right
{:alt "History"}

View file

@ -15,7 +15,8 @@
[uxbox.main.ui.workspace.sidebar.layers :refer [layers-toolbox]]
[uxbox.main.ui.workspace.sidebar.options :refer [options-toolbox]]
[uxbox.main.ui.workspace.sidebar.sitemap :refer [sitemap-toolbox]]
[uxbox.main.ui.workspace.sidebar.libraries :refer [libraries-toolbox]]))
[uxbox.main.ui.workspace.sidebar.libraries :refer [libraries-toolbox]]
[uxbox.main.ui.workspace.sidebar.assets :refer [assets-toolbox]]))
;; --- Left Sidebar (Component)
@ -34,7 +35,9 @@
(when (contains? layout :layers)
[:& layers-toolbox {:page page}])
(when (contains? layout :libraries)
[:& libraries-toolbox])]])
[:& libraries-toolbox])
(when (contains? layout :assets)
[:& assets-toolbox])]])
;; --- Right Sidebar (Component)

View file

@ -0,0 +1,164 @@
;; 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) 2020 UXBOX Labs SL
(ns uxbox.main.ui.workspace.sidebar.assets
(:require
[okulary.core :as l]
[cuerdas.core :as str]
[rumext.alpha :as mf]
[uxbox.common.data :as d]
[uxbox.common.pages :as cp]
[uxbox.common.geom.shapes :as geom]
[uxbox.common.geom.point :as gpt]
[uxbox.main.ui.icons :as i]
[uxbox.main.data.workspace :as dw]
[uxbox.main.refs :as refs]
[uxbox.main.store :as st]
[uxbox.main.ui.keyboard :as kbd]
[uxbox.main.ui.shapes.icon :as icon]
[uxbox.util.dom :as dom]
[uxbox.util.dom.dnd :as dnd]
[uxbox.util.timers :as timers]
[uxbox.common.uuid :as uuid]
[uxbox.util.i18n :as i18n :refer [tr]]
[uxbox.util.data :refer [classnames]]
[uxbox.main.ui.components.tab-container :refer [tab-container tab-element]]
[uxbox.main.data.library :as dlib]
[uxbox.main.ui.components.context-menu :refer [context-menu]]))
(defn matches-search
[name search-term]
(if (empty? search-term)
true
(let [st (str/trim (str/lower search-term))
nm (str/trim (str/lower name))]
(str/includes? nm st))))
(mf/defc graphics-box
[{:keys [library-id images] :as props}]
(let [state (mf/use-state {:menu-open false
:top nil
:left nil
:image-id nil})
add-graphic #(println "añadir gráfico")
delete-graphic
#(st/emit! (dw/delete-file-image library-id (:image-id @state)))
on-context-menu (fn [image-id]
(fn [event]
(let [pos (dom/get-client-position event)
top (:y pos)
left (- (:x pos) 20)]
(dom/prevent-default event)
(swap! state assoc :menu-open true
:top top
:left left
:image-id image-id))))]
[:div.asset-group
[:div.group-title
(tr "workspace.assets.graphics")
[:span (str "\u00A0(") (count images) ")"] ;; Unicode 00A0 is non-breaking space
[:div.group-button {:on-click add-graphic} i/plus]]
[:div.group-grid
(for [image (sort-by :name images)]
[:div.grid-cell {:key (:id image)
:on-context-menu (on-context-menu (:id image))}
[:img {:src (:thumb-uri image)}]
[:div.cell-name (:name image)]])
[:& context-menu
{:selectable false
:show (:menu-open @state)
:on-close #(swap! state assoc :menu-open false)
:top (:top @state)
:left (:left @state)
:options [[(tr "workspace.assets.delete") delete-graphic]]}]]
]))
(mf/defc colors-box
[{:keys [colors] :as props}]
(let [add-color #(println "añadir color")]
[:div.asset-group
[:div.group-title
(tr "workspace.assets.colors")
[:div.group-button {:on-click add-color} i/plus]]]))
(mf/defc library-toolbox
[{:keys [library-id images initial-open? search-term box-filter] :as props}]
(let [open? (mf/use-state initial-open?)
toggle-open #(swap! open? not)]
[:div.tool-window
[:div.tool-window-bar
[:div.collapse-library
{:class (classnames :open @open?)
:on-click toggle-open}
i/arrow-slide]
[:span (tr "workspace.assets.file-library")]]
(when @open?
[:div.tool-window-content
(when (or (= box-filter :all) (= box-filter :graphics))
[:& graphics-box {:library-id library-id :images images}])
(when (or (= box-filter :all) (= box-filter :colors))
[:& colors-box {:colors {}}])])]))
(mf/defc assets-toolbox
[]
(let [team-id (-> refs/workspace-project mf/deref :team-id)
file-id (-> refs/workspace-file mf/deref :id)
file-images (mf/deref refs/workspace-images)
state (mf/use-state {:search-term ""
:box-filter :all})
filtered-images (filter #(matches-search (:name %) (:search-term @state))
(vals file-images))
on-search-term-change (fn [event]
(let [value (-> (dom/get-target event)
(dom/get-value))]
(swap! state assoc :search-term value)))
on-box-filter-change (fn [event]
(let [value (-> (dom/get-target event)
(dom/get-value)
(d/read-string))]
(swap! state assoc :box-filter value)))]
(mf/use-effect
(mf/deps file-id)
#(when file-id
(st/emit! (dw/fetch-images file-id))))
[:div.assets-bar
[:div.tool-window
[:div.tool-window-content
[:div.assets-bar-title (tr "workspace.assets.assets")]
[:input.search-input
{:placeholder (tr "workspace.assets.search")
:type "text"
:value (:search-term @state)
:on-change on-search-term-change}]
[:select.input-select {:value (:box-filter @state)
:on-change on-box-filter-change}
[:option {:value ":all"} (tr "workspace.assets.box-filter-all")]
[:option {:value ":graphics"} (tr "workspace.assets.box-filter-graphics")]
[:option {:value ":colors"} (tr "workspace.assets.box-filter-colors")]]
]]
[:& library-toolbox {:library-id file-id
:images filtered-images
:initial-open? true
:search-term (:search-term @state)
:box-filter (:box-filter @state)}]]))