diff --git a/backend/scripts/build b/backend/scripts/build index 64f71b7580..e8433bac38 100755 --- a/backend/scripts/build +++ b/backend/scripts/build @@ -18,6 +18,8 @@ cp scripts/manage.py target/dist/manage.py chmod +x target/dist/run.sh; chmod +x target/dist/manage.py -# Prefetch +# Prefetch templates +rm -rf builtin-templates; +mkdir builtin-templates; bb ./scripts/prefetch-templates.clj resources/app/onboarding.edn builtin-templates/ cp -r builtin-templates target/dist/ diff --git a/backend/src/app/main.clj b/backend/src/app/main.clj index 249adc0197..0f1eb8854e 100644 --- a/backend/src/app/main.clj +++ b/backend/src/app/main.clj @@ -29,6 +29,7 @@ [app.redis :as-alias rds] [app.rpc :as-alias rpc] [app.rpc.doc :as-alias rpc.doc] + [app.setup :as-alias setup] [app.srepl :as-alias srepl] [app.storage :as-alias sto] [app.storage.fs :as-alias sto.fs] @@ -220,7 +221,7 @@ {::db/pool (ig/ref ::db/pool)} ::http.awsns/routes - {::props (ig/ref :app.setup/props) + {::props (ig/ref ::setup/props) ::db/pool (ig/ref ::db/pool) ::http.client/client (ig/ref ::http.client/client) ::wrk/executor (ig/ref ::wrk/executor)} @@ -263,7 +264,7 @@ ::oidc/routes {::http.client/client (ig/ref ::http.client/client) ::db/pool (ig/ref ::db/pool) - ::props (ig/ref :app.setup/props) + ::props (ig/ref ::setup/props) ::oidc/providers {:google (ig/ref ::oidc.providers/google) :github (ig/ref ::oidc.providers/github) :gitlab (ig/ref ::oidc.providers/gitlab) @@ -275,7 +276,7 @@ ::db/pool (ig/ref ::db/pool) ::rpc/routes (ig/ref ::rpc/routes) ::rpc.doc/routes (ig/ref ::rpc.doc/routes) - ::props (ig/ref :app.setup/props) + ::props (ig/ref ::setup/props) ::mtx/routes (ig/ref ::mtx/routes) ::oidc/routes (ig/ref ::oidc/routes) ::http.debug/routes (ig/ref ::http.debug/routes) @@ -322,11 +323,10 @@ ::rpc/climit (ig/ref ::rpc/climit) ::rpc/rlimit (ig/ref ::rpc/rlimit) - - ::props (ig/ref :app.setup/props) + ::setup/templates (ig/ref ::setup/templates) + ::props (ig/ref ::setup/props) :pool (ig/ref ::db/pool) - :templates (ig/ref :app.setup/builtin-templates) } :app.rpc.doc/routes @@ -337,7 +337,7 @@ ::db/pool (ig/ref ::db/pool) ::wrk/executor (ig/ref ::wrk/executor) ::session/manager (ig/ref ::session/manager) - ::props (ig/ref :app.setup/props)} + ::props (ig/ref ::setup/props)} ::wrk/registry {::mtx/metrics (ig/ref ::mtx/metrics) @@ -390,7 +390,7 @@ :app.tasks.telemetry/handler {::db/pool (ig/ref ::db/pool) ::http.client/client (ig/ref ::http.client/client) - ::props (ig/ref :app.setup/props)} + ::props (ig/ref ::setup/props)} [::srepl/urepl ::srepl/server] {::srepl/port (cf/get :urepl-port 6062) @@ -400,10 +400,9 @@ {::srepl/port (cf/get :prepl-port 6063) ::srepl/host (cf/get :prepl-host "localhost")} - :app.setup/builtin-templates - {::http.client/client (ig/ref ::http.client/client)} + ::setup/templates {} - :app.setup/props + ::setup/props {::db/pool (ig/ref ::db/pool) ::key (cf/get :secret-key) @@ -412,7 +411,7 @@ ::migrations (ig/ref :app.migrations/migrations)} ::audit.tasks/archive - {::props (ig/ref :app.setup/props) + {::props (ig/ref ::setup/props) ::db/pool (ig/ref ::db/pool) ::http.client/client (ig/ref ::http.client/client)} diff --git a/backend/src/app/media.clj b/backend/src/app/media.clj index 9964c2824d..9fb6515a10 100644 --- a/backend/src/app/media.clj +++ b/backend/src/app/media.clj @@ -9,6 +9,7 @@ (:require [app.common.data :as d] [app.common.exceptions :as ex] + [app.common.logging :as l] [app.common.media :as cm] [app.common.schema :as sm] [app.common.schema.generators :as sg] @@ -226,6 +227,7 @@ (defmethod process-error org.im4java.core.InfoException [error] + (l/error :hint "unexpected error on processing image" :cause error) (ex/raise :type :validation :code :invalid-image :hint "invalid image" diff --git a/backend/src/app/rpc/commands/binfile.clj b/backend/src/app/rpc/commands/binfile.clj index e485fa44ba..b039b60a7c 100644 --- a/backend/src/app/rpc/commands/binfile.clj +++ b/backend/src/app/rpc/commands/binfile.clj @@ -592,7 +592,7 @@ (let [options (-> options (assoc ::section section) (assoc ::input input) - (assoc :conn conn))] + (assoc ::db/conn conn))] (binding [*options* options] (read-section options)))) [:v1/metadata :v1/files :v1/rels :v1/sobjects]) @@ -620,7 +620,7 @@ (update :components pmap-wrap)))) (defmethod read-section :v1/files - [{:keys [conn ::input ::migrate? ::project-id ::timestamp ::overwrite?]}] + [{:keys [::db/conn ::input ::migrate? ::project-id ::timestamp ::overwrite?]}] (doseq [expected-file-id (-> *state* deref :files)] (let [file (read-obj! input) media' (read-obj! input) @@ -678,7 +678,7 @@ (db/delete! conn :file-thumbnail {:file-id file-id'}))))))) (defmethod read-section :v1/rels - [{:keys [conn ::input ::timestamp]}] + [{:keys [::db/conn ::input ::timestamp]}] (let [rels (read-obj! input)] ;; Insert all file relations (doseq [rel rels] @@ -693,7 +693,7 @@ (db/insert! conn :file-library-rel rel))))) (defmethod read-section :v1/sobjects - [{:keys [::sto/storage conn ::input ::overwrite?]}] + [{:keys [::sto/storage ::db/conn ::input ::overwrite?]}] (let [storage (media/configure-assets-storage storage) ids (read-obj! input)] diff --git a/backend/src/app/rpc/commands/management.clj b/backend/src/app/rpc/commands/management.clj index 0c5813a29e..23dbd13581 100644 --- a/backend/src/app/rpc/commands/management.clj +++ b/backend/src/app/rpc/commands/management.clj @@ -10,6 +10,7 @@ [app.common.data :as d] [app.common.exceptions :as ex] [app.common.pages.migrations :as pmg] + [app.common.schema :as sm] [app.common.spec :as us] [app.common.uuid :as uuid] [app.db :as db] @@ -20,6 +21,8 @@ [app.rpc.commands.projects :as proj] [app.rpc.commands.teams :as teams :refer [create-project-role create-project]] [app.rpc.doc :as-alias doc] + [app.setup :as-alias setup] + [app.setup.templates :as tmpl] [app.util.blob :as blob] [app.util.pointer-map :as pmap] [app.util.services :as sv] @@ -233,7 +236,7 @@ (let [project (-> (db/get-by-id conn :project project-id) (assoc :is-pinned false)) - + files (db/query conn :file {:project-id (:id project) :deleted-at nil} @@ -361,7 +364,6 @@ nil)) - (s/def ::move-project (s/keys :req [::rpc/profile-id] :req-un [::team-id ::project-id])) @@ -376,46 +378,54 @@ ;; --- COMMAND: Clone Template -(declare clone-template) - -(s/def ::template-id ::us/not-empty-string) -(s/def ::clone-template - (s/keys :req [::rpc/profile-id] - :req-un [::project-id ::template-id])) - -(sv/defmethod ::clone-template - "Clone into the specified project the template by its id." - {::doc/added "1.16" - ::webhooks/event? true} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}] - (db/with-atomic [conn pool] - (-> (assoc cfg :conn conn) - (clone-template (assoc params :profile-id profile-id))))) - -(defn- clone-template - [{:keys [conn templates] :as cfg} {:keys [profile-id template-id project-id]}] - (let [template (d/seek #(= (:id %) template-id) templates) +(defn- clone-template! + [{:keys [::db/conn] :as cfg} {:keys [profile-id template-id project-id]}] + (let [template (tmpl/get-template-stream cfg template-id) project (db/get-by-id conn :project project-id {:columns [:id :team-id]})] - (teams/check-edition-permissions! conn profile-id (:team-id project)) - (when-not template (ex/raise :type :not-found :code :template-not-found :hint "template not found")) + (teams/check-edition-permissions! conn profile-id (:team-id project)) + (-> cfg - (assoc ::binfile/input (:path template)) + ;; FIXME: maybe reuse the conn instead of creating more + ;; connections in the import process? + (dissoc ::db/conn) + (assoc ::binfile/input template) (assoc ::binfile/project-id (:id project)) (assoc ::binfile/ignore-index-errors? true) (assoc ::binfile/migrate? true) (binfile/import!)))) +(def schema:clone-template + [:map {:title "clone-template"} + [:project-id ::sm/uuid] + [:template-id ::sm/word-string]]) -;; --- COMMAND: Retrieve list of builtin templates +(sv/defmethod ::clone-template + "Clone into the specified project the template by its id." + {::doc/added "1.16" + ::webhooks/event? true + ::sm/params schema:clone-template} + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}] + (db/with-atomic [conn pool] + (-> (assoc cfg ::db/conn conn) + (clone-template! (assoc params :profile-id profile-id))))) + +;; --- COMMAND: Get list of builtin templates (s/def ::retrieve-list-of-builtin-templates any?) (sv/defmethod ::retrieve-list-of-builtin-templates + {::doc/added "1.10" + ::doc/deprecated "1.19"} [cfg _params] - (mapv #(select-keys % [:id :name :thumbnail-uri]) (:templates cfg))) + (mapv #(select-keys % [:id :name :thumbnail-uri]) (::setup/templates cfg))) + +(sv/defmethod ::get-builtin-templates + {::doc/added "1.19"} + [cfg _params] + (mapv #(select-keys % [:id :name :thumbnail-uri]) (::setup/templates cfg))) diff --git a/backend/src/app/setup.clj b/backend/src/app/setup.clj index 856853fc88..8e889e2b46 100644 --- a/backend/src/app/setup.clj +++ b/backend/src/app/setup.clj @@ -12,8 +12,8 @@ [app.common.uuid :as uuid] [app.db :as db] [app.main :as-alias main] - [app.setup.builtin-templates] [app.setup.keys :as keys] + [app.setup.templates] [buddy.core.codecs :as bc] [buddy.core.nonce :as bn] [clojure.spec.alpha :as s] diff --git a/backend/src/app/setup/builtin_templates.clj b/backend/src/app/setup/builtin_templates.clj deleted file mode 100644 index 23b6875aa4..0000000000 --- a/backend/src/app/setup/builtin_templates.clj +++ /dev/null @@ -1,72 +0,0 @@ -;; 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) KALEIDOS INC - -(ns app.setup.builtin-templates - "A service/module that is responsible for download, load & internally - expose a set of builtin penpot file templates." - (:require - [app.common.logging :as l] - [app.common.spec :as us] - [app.http.client :as http] - [clojure.edn :as edn] - [clojure.java.io :as io] - [clojure.spec.alpha :as s] - [datoteka.fs :as fs] - [integrant.core :as ig])) - -(declare download-all!) - -(s/def ::id ::us/not-empty-string) -(s/def ::name ::us/not-empty-string) -(s/def ::thumbnail-uri ::us/not-empty-string) -(s/def ::file-uri ::us/not-empty-string) -(s/def ::path fs/path?) - -(s/def ::template - (s/keys :req-un [::id ::name ::thumbnail-uri ::file-uri] - :opt-un [::path])) - -(defmethod ig/pre-init-spec :app.setup/builtin-templates [_] - (s/keys :req [::http/client])) - -(defmethod ig/init-key :app.setup/builtin-templates - [_ cfg] - (let [presets (-> "app/onboarding.edn" io/resource slurp edn/read-string)] - (l/info :hint "loading template files" :total (count presets)) - (let [result (download-all! cfg presets)] - (us/conform (s/coll-of ::template) result)))) - -(defn- download-preset! - [cfg {:keys [path file-uri] :as preset}] - (let [response (http/req! cfg - {:method :get - :uri file-uri} - {:response-type :input-stream - :sync? true})] - (us/verify! (= 200 (:status response)) "unexpected response found on fetching preset") - (with-open [output (io/output-stream path)] - (with-open [input (io/input-stream (:body response))] - (io/copy input output))))) - -(defn- download-all! - "Download presets to the default directory, if preset is already - downloaded, no action will be performed." - [cfg presets] - (let [dest (fs/join fs/*cwd* "builtin-templates")] - (when-not (fs/exists? dest) - (fs/create-dir dest)) - - (doall - (map (fn [item] - (let [path (fs/join dest (:id item)) - item (assoc item :path path)] - (if (fs/exists? path) - (l/trace :hint "template file already present" :id (:id item)) - (do - (l/trace :hint "downloading template file" :id (:id item) :dest (str path)) - (download-preset! cfg item))) - item)) - presets)))) diff --git a/backend/src/app/setup/templates.clj b/backend/src/app/setup/templates.clj new file mode 100644 index 0000000000..98afd340c9 --- /dev/null +++ b/backend/src/app/setup/templates.clj @@ -0,0 +1,65 @@ +;; 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) KALEIDOS INC + +(ns app.setup.templates + "A service/module that is responsible for download, load & internally + expose a set of builtin penpot file templates." + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.logging :as l] + [app.common.schema :as sm] + [app.http.client :as http] + [app.setup :as-alias setup] + [clojure.edn :as edn] + [clojure.java.io :as io] + [datoteka.fs :as fs] + [integrant.core :as ig])) + +(def ^:private schema:template + [:map {:title "Template"} + [:id ::sm/word-string] + [:name ::sm/word-string] + [:thumbnail-uri ::sm/word-string] + [:file-uri ::sm/word-string]]) + +(def ^:private schema:templates + [:vector schema:template]) + +(defmethod ig/init-key ::setup/templates + [_ _] + (let [templates (-> "app/onboarding.edn" io/resource slurp edn/read-string) + dest (fs/join fs/*cwd* "builtin-templates")] + + (dm/verify! + "expected a valid templates file" + (sm/valid? schema:templates templates)) + + (doseq [{:keys [id path] :as template} templates] + (let [path (or path (fs/join dest id))] + (if (fs/exists? path) + (l/debug :hint "template file" :id id :state "present" :path (dm/str path)) + (l/debug :hint "template file" :id id :state "absent")))) + + templates)) + +(defn get-template-stream + [cfg template-id] + (when-let [template (d/seek #(= (:id %) template-id) + (::setup/templates cfg))] + (let [dest (fs/join fs/*cwd* "builtin-templates") + path (or (:path template) (fs/join dest template-id))] + (if (fs/exists? path) + (io/input-stream path) + (let [resp (http/req! cfg + {:method :get :uri (:file-uri template)} + {:response-type :input-stream :sync? true})] + + (dm/verify! + "unexpected response found on fetching template" + (= 200 (:status resp))) + + (io/input-stream (:body resp))))))) diff --git a/backend/test/backend_tests/helpers.clj b/backend/test/backend_tests/helpers.clj index d2edaeec98..dbd7f464dd 100644 --- a/backend/test/backend_tests/helpers.clj +++ b/backend/test/backend_tests/helpers.clj @@ -128,7 +128,7 @@ (assoc-in [::db/pool ::db/uri] (:database-uri config)) (assoc-in [::db/pool ::db/username] (:database-username config)) (assoc-in [::db/pool ::db/password] (:database-password config)) - (assoc-in [:app.rpc/methods :templates] templates) + (assoc-in [:app.rpc/methods :app.setup/templates] templates) (dissoc :app.srepl/server :app.http/server :app.http/router @@ -136,7 +136,7 @@ :app.auth.oidc/gitlab-provider :app.auth.oidc/github-provider :app.auth.oidc/generic-provider - :app.setup/builtin-templates + :app.setup/templates :app.auth.oidc/routes :app.worker/monitor :app.http.oauth/handler diff --git a/frontend/resources/images/thumbnails/template-ant-design.jpg b/frontend/resources/images/thumbnails/template-ant-design.jpg new file mode 100644 index 0000000000..504977ded9 Binary files /dev/null and b/frontend/resources/images/thumbnails/template-ant-design.jpg differ diff --git a/frontend/resources/images/thumbnails/template-circum-icons.jpg b/frontend/resources/images/thumbnails/template-circum-icons.jpg new file mode 100644 index 0000000000..ecf2ed271e Binary files /dev/null and b/frontend/resources/images/thumbnails/template-circum-icons.jpg differ diff --git a/frontend/resources/images/thumbnails/template-cocomaterial.jpg b/frontend/resources/images/thumbnails/template-cocomaterial.jpg new file mode 100644 index 0000000000..762d28f781 Binary files /dev/null and b/frontend/resources/images/thumbnails/template-cocomaterial.jpg differ diff --git a/frontend/resources/images/thumbnails/template-coreui.jpg b/frontend/resources/images/thumbnails/template-coreui.jpg new file mode 100644 index 0000000000..794f5aa98b Binary files /dev/null and b/frontend/resources/images/thumbnails/template-coreui.jpg differ diff --git a/frontend/resources/images/thumbnails/template-material-design-3.jpg b/frontend/resources/images/thumbnails/template-material-design-3.jpg new file mode 100644 index 0000000000..8c938579f6 Binary files /dev/null and b/frontend/resources/images/thumbnails/template-material-design-3.jpg differ diff --git a/frontend/resources/images/thumbnails/template-penpot-design-system.jpg b/frontend/resources/images/thumbnails/template-penpot-design-system.jpg new file mode 100644 index 0000000000..0b2db57f48 Binary files /dev/null and b/frontend/resources/images/thumbnails/template-penpot-design-system.jpg differ diff --git a/frontend/resources/images/thumbnails/template-tutorial-for-beginners.jpg b/frontend/resources/images/thumbnails/template-tutorial-for-beginners.jpg new file mode 100644 index 0000000000..44e64bfa96 Binary files /dev/null and b/frontend/resources/images/thumbnails/template-tutorial-for-beginners.jpg differ diff --git a/frontend/resources/images/thumbnails/template-whiteboarding-kit.jpg b/frontend/resources/images/thumbnails/template-whiteboarding-kit.jpg new file mode 100644 index 0000000000..ea2e063032 Binary files /dev/null and b/frontend/resources/images/thumbnails/template-whiteboarding-kit.jpg differ diff --git a/frontend/resources/images/thumbnails/template-wireframing-kit.jpg b/frontend/resources/images/thumbnails/template-wireframing-kit.jpg new file mode 100644 index 0000000000..088567b1fb Binary files /dev/null and b/frontend/resources/images/thumbnails/template-wireframing-kit.jpg differ diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs index 0995fefeba..d31c6929d9 100644 --- a/frontend/src/app/main/data/dashboard.cljs +++ b/frontend/src/app/main/data/dashboard.cljs @@ -34,7 +34,6 @@ (declare fetch-projects) (declare fetch-team-members) -(declare fetch-builtin-templates) (defn initialize [{:keys [id] :as params}] @@ -62,8 +61,7 @@ (ptk/watch (fetch-projects) state stream) (ptk/watch (fetch-team-members) state stream) (ptk/watch (du/fetch-teams) state stream) - (ptk/watch (du/fetch-users {:team-id id}) state stream) - (ptk/watch (fetch-builtin-templates) state stream))))) + (ptk/watch (du/fetch-users {:team-id id}) state stream))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Data Fetching (context aware: current team) @@ -275,7 +273,7 @@ (ptk/reify ::fetch-builtin-templates ptk/WatchEvent (watch [_ _ _] - (->> (rp/cmd! :retrieve-list-of-builtin-templates) + (->> (rp/cmd! :get-builtin-templates) (rx/map builtin-templates-fetched))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/frontend/src/app/main/ui/components/numeric_input.cljs b/frontend/src/app/main/ui/components/numeric_input.cljs index 855ded34a6..a7d3ed106f 100644 --- a/frontend/src/app/main/ui/components/numeric_input.cljs +++ b/frontend/src/app/main/ui/components/numeric_input.cljs @@ -208,7 +208,7 @@ (on-focus event)) (when select-on-focus? - (dom/select-text! event) + (dom/select-text! target) ;; In webkit browsers the mouseup event will be called after the on-focus causing and unselect (.addEventListener target "mouseup" dom/prevent-default #js {:once true}))))) diff --git a/frontend/src/app/main/ui/dashboard.cljs b/frontend/src/app/main/ui/dashboard.cljs index 5df57f6603..209705c45d 100644 --- a/frontend/src/app/main/ui/dashboard.cljs +++ b/frontend/src/app/main/ui/dashboard.cljs @@ -7,14 +7,10 @@ (ns app.main.ui.dashboard (:require [app.common.data :as d] - [app.common.math :as mth] [app.common.spec :as us] [app.config :as cf] [app.main.data.dashboard :as dd] [app.main.data.dashboard.shortcuts :as sc] - [app.main.data.events :as ev] - [app.main.data.modal :as modal] - [app.main.data.users :as du] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.context :as ctx] @@ -27,19 +23,13 @@ [app.main.ui.dashboard.search :refer [search-page]] [app.main.ui.dashboard.sidebar :refer [sidebar]] [app.main.ui.dashboard.team :refer [team-settings-page team-members-page team-invitations-page team-webhooks-page]] + [app.main.ui.dashboard.templates :refer [templates-section]] [app.main.ui.hooks :as hooks] - [app.main.ui.icons :as i] [app.util.dom :as dom] - [app.util.i18n :refer [tr]] [app.util.keyboard :as kbd] [app.util.object :as obj] - [app.util.router :as rt] - [cuerdas.core :as str] [goog.events :as events] - [okulary.core :as l] - [potok.core :as ptk] - [rumext.v2 :as mf]) - (:import goog.events.EventType)) + [rumext.v2 :as mf])) (defn ^boolean uuid-str? [s] @@ -60,174 +50,12 @@ (uuid-str? project-id) (assoc :project-id (uuid project-id))))) -(def builtin-templates - (l/derived :builtin-templates st/state)) - -(mf/defc templates-section - [{:keys [default-project-id profile project team content-width] :as props}] - (let [templates (->> (mf/deref builtin-templates) - (filter #(not= (:id %) "tutorial-for-beginners"))) - - route (mf/deref refs/route) - route-name (get-in route [:data :name]) - section (if (= route-name :dashboard-files) - (if (= (:id project) default-project-id) - "dashboard-drafts" - "dashboard-project") - (name route-name)) - props (some-> profile (get :props {})) - collapsed (:builtin-templates-collapsed-status props false) - card-offset (mf/use-state 0) - - card-width 275 - num-cards (count templates) - container-size (* (+ 2 num-cards) card-width) - ;; We need space for num-cards plus the libraries&templates link - more-cards (> (+ @card-offset (* (+ 1 num-cards) card-width)) content-width) - visible-card-count (mth/floor (/ content-width 275)) - left-moves (/ @card-offset -275) - first-visible-card left-moves - last-visible-card (+ (- visible-card-count 1) left-moves) - content-ref (mf/use-ref) - - toggle-collapse - (fn [] - (st/emit! - (du/update-profile-props {:builtin-templates-collapsed-status (not collapsed)}))) - - move-left - (fn [] - (when-not (zero? @card-offset) - (dom/animate! (mf/ref-val content-ref) - [#js {:left (str @card-offset "px")} - #js {:left (str (+ @card-offset card-width) "px")}] - #js {:duration 200 - :easing "linear"}) - (reset! card-offset (+ @card-offset card-width)))) - - move-right - (fn [] - (when more-cards (swap! card-offset inc) - (dom/animate! (mf/ref-val content-ref) - [#js {:left (str @card-offset "px")} - #js {:left (str (- @card-offset card-width) "px")}] - #js {:duration 200 - :easing "linear"}) - (reset! card-offset (- @card-offset card-width)))) - - on-finish-import - (fn [template] - (st/emit! - (ptk/event ::ev/event {::ev/name "import-template-finish" - ::ev/origin "dashboard" - :template (:name template) - :section section}) - (when (not (some? project)) (rt/nav :dashboard-files - {:team-id (:id team) - :project-id default-project-id})))) - - import-template - (fn [template] - (let [templates-project-id (if project (:id project) default-project-id)] - (st/emit! - (ptk/event ::ev/event {::ev/name "import-template-launch" - ::ev/origin "dashboard" - :template (:name template) - :section section}) - - (modal/show - {:type :import - :project-id templates-project-id - :files [] - :template template - :on-finish-import (partial on-finish-import template)})))) - - handle-template-link - (fn [] - (st/emit! (ptk/event ::ev/event {::ev/name "explore-libraries-click" - ::ev/origin "dashboard" - :section section})))] - - [:div.dashboard-templates-section {:class (when collapsed "collapsed")} - [:div.title - [:button {:tab-index "0" - :on-click toggle-collapse - :on-key-down (fn [event] - (when (kbd/enter? event) - (dom/stop-propagation event) - (dom/prevent-default event) - (toggle-collapse)))} - [:span (tr "dashboard.libraries-and-templates")] - [:span.icon (if collapsed i/arrow-up i/arrow-down)]]] - [:div.content {:ref content-ref - :style {:left @card-offset :width (str container-size "px")}} - - (for [num-item (range (count templates)) :let [item (nth templates num-item)]] - (let [is-visible? (and (>= num-item first-visible-card) (<= num-item last-visible-card))] - [:a.card-container {:tab-index (if (or (not is-visible?) collapsed) - "-1" - "0") - :id (str/concat "card-container-" num-item) - :key (:id item) - :on-click #(import-template item) - :on-key-down (fn [event] - (when (kbd/enter? event) - (dom/stop-propagation event) - (import-template item)))} - [:div.template-card - [:div.img-container - [:img {:src (:thumbnail-uri item) - :alt (:name item)}]] - [:div.card-name [:span (:name item)] [:span.icon i/download]]]])) - - (let [is-visible? (and (>= num-cards first-visible-card) (<= num-cards last-visible-card))] - [:div.card-container - [:div.template-card - [:div.img-container - [:a {:id (str/concat "card-container-" num-cards) - :tab-index (if (or (not is-visible?) collapsed) - "-1" - "0") - :href "https://penpot.app/libraries-templates.html" - :target "_blank" - :on-click handle-template-link - :on-key-down (fn [event] - (when (kbd/enter? event) - (dom/stop-propagation event) - (handle-template-link)))} - [:div.template-link - [:div.template-link-title (tr "dashboard.libraries-and-templates")] - [:div.template-link-text (tr "dashboard.libraries-and-templates.explore")]]]]]])] - (when (< @card-offset 0) - [:button.button.left {:tab-index (if collapsed - "-1" - "0") - :on-click move-left - :on-key-down (fn [event] - (when (kbd/enter? event) - (dom/stop-propagation event) - (move-left) - (let [first-element (dom/get-element (str/concat "card-container-" first-visible-card))] - (when first-element - (dom/focus! first-element)))))} i/go-prev]) - (when more-cards - [:button.button.right {:tab-index (if collapsed - "-1" - "0") - :on-click move-right - :aria-label (tr "labels.next") - :on-key-down (fn [event] - (when (kbd/enter? event) - (dom/stop-propagation event) - (move-right) - (let [last-element (dom/get-element (str/concat "card-container-" last-visible-card))] - (when last-element - (dom/focus! last-element)))))} i/go-next])])) - (mf/defc dashboard-content [{:keys [team projects project section search-term profile] :as props}] (let [container (mf/use-ref) content-width (mf/use-state 0) + project-id (:id project) + team-id (:id team) default-project-id (mf/with-memo [projects] @@ -235,6 +63,7 @@ (d/seek :is-default) (:id))) + on-resize (mf/use-fn (fn [_] @@ -256,16 +85,17 @@ (case section :dashboard-projects [:* - [:& projects-section {:team team - :projects projects - :profile profile - :default-project-id default-project-id}] + [:& projects-section + {:team team + :projects projects + :profile profile + :default-project-id default-project-id}] (when (contains? cf/flags :dashboard-templates-section) [:& templates-section {:profile profile - :project project + :project-id project-id + :team-id team-id :default-project-id default-project-id - :team team :content-width @content-width}])] :dashboard-fonts @@ -280,9 +110,9 @@ [:& files-section {:team team :project project}] (when (contains? cf/flags :dashboard-templates-section) [:& templates-section {:profile profile - :project project + :team-id team-id + :project-id project-id :default-project-id default-project-id - :team team :content-width @content-width}])]) :dashboard-search @@ -328,7 +158,7 @@ (mf/use-effect (fn [] - (let [events [(events/listen goog/global EventType.KEYDOWN + (let [events [(events/listen goog/global "keydown" (fn [event] (when (kbd/enter? event) (dom/stop-propagation event) diff --git a/frontend/src/app/main/ui/dashboard/templates.cljs b/frontend/src/app/main/ui/dashboard/templates.cljs new file mode 100644 index 0000000000..9357faa8d1 --- /dev/null +++ b/frontend/src/app/main/ui/dashboard/templates.cljs @@ -0,0 +1,274 @@ +;; 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) KALEIDOS INC + +(ns app.main.ui.dashboard.templates + (:require + [app.common.data.macros :as dm] + [app.common.math :as mth] + [app.config :as cf] + [app.main.data.dashboard :as dd] + [app.main.data.events :as ev] + [app.main.data.modal :as modal] + [app.main.data.users :as du] + [app.main.refs :as refs] + [app.main.store :as st] + [app.main.ui.icons :as i] + [app.util.dom :as dom] + [app.util.i18n :refer [tr]] + [app.util.keyboard :as kbd] + [app.util.router :as rt] + [okulary.core :as l] + [potok.core :as ptk] + [rumext.v2 :as mf])) + +(def builtin-templates + (l/derived :builtin-templates st/state)) + +(defn- import-template! + [template team-id project-id default-project-id section] + (letfn [(on-finish [] + (st/emit! + (ptk/event ::ev/event {::ev/name "import-template-finish" + ::ev/origin "dashboard" + :template (:name template) + :section section}) + + (when-not (some? project-id) + (rt/nav :dashboard-files + {:team-id team-id + :project-id default-project-id}))))] + + (st/emit! + (ptk/event ::ev/event {::ev/name "import-template-launch" + ::ev/origin "dashboard" + :template (:name template) + :section section}) + + (modal/show + {:type :import + :project-id (or project-id default-project-id) + :files [] + :template template + :on-finish-import on-finish})))) + +(mf/defc title + {::mf/wrap-props false} + [{:keys [collapsed]}] + (let [on-click + (mf/use-fn + (mf/deps collapsed) + (fn [_event] + (let [props {:builtin-templates-collapsed-status (not collapsed)}] + (st/emit! (du/update-profile-props props))))) + + on-key-down + (mf/use-fn + (mf/deps on-click) + (fn [event] + (when (kbd/enter? event) + (dom/stop-propagation event) + (dom/prevent-default event) + (on-click event))))] + + [:div.title + [:button {:tab-index "0" + :on-click on-click + :on-key-down on-key-down} + [:span (tr "dashboard.libraries-and-templates")] + [:span.icon (if ^boolean collapsed i/arrow-up i/arrow-down)]]])) + +(mf/defc card-item + {::mf/wrap-props false} + [{:keys [item index is-visible collapsed on-import]}] + (let [id (dm/str "card-container-" index) + thb (assoc cf/public-uri :path (dm/str "/images/thumbnails/template-" (:id item) ".jpg")) + + on-click + (mf/use-fn + (mf/deps on-import) + (fn [event] + (on-import item event))) + + on-key-down + (mf/use-fn + (mf/deps on-import) + (fn [event] + (when (kbd/enter? event) + (dom/stop-propagation event) + (on-import item event))))] + + [:a.card-container + {:tab-index (if (or (not is-visible) collapsed) "-1" "0") + :id id + :data-index index + :on-click on-click + :on-key-down on-key-down} + [:div.template-card + [:div.img-container + [:img {:src (dm/str thb) + :alt (:name item)}]] + [:div.card-name [:span (:name item)] + [:span.icon i/download]]]])) + +(mf/defc card-item-link + {::mf/wrap-props false} + [{:keys [total is-visible collapsed section]}] + (let [id (dm/str "card-container-" total) + + on-click + (mf/use-fn + (mf/deps section) + (fn [] + (st/emit! (ptk/event ::ev/event {::ev/name "explore-libraries-click" + ::ev/origin "dashboard" + :section section})))) + + on-key-down + (mf/use-fn + (mf/deps on-click) + (fn [event] + (when (kbd/enter? event) + (dom/stop-propagation event) + (on-click event))))] + + [:div.card-container + [:div.template-card + [:div.img-container + [:a {:id id + :tab-index (if (or (not is-visible) collapsed) "-1" "0") + :href "https://penpot.app/libraries-templates.html" + :target "_blank" + :on-click on-click + :on-key-down on-key-down} + [:div.template-link + [:div.template-link-title (tr "dashboard.libraries-and-templates")] + [:div.template-link-text (tr "dashboard.libraries-and-templates.explore")]]]]]])) + +(mf/defc templates-section + {::mf/wrap-props false} + [{:keys [default-project-id profile project-id team-id content-width]}] + (let [templates (->> (mf/deref builtin-templates) + (filter #(not= (:id %) "tutorial-for-beginners"))) + + route (mf/deref refs/route) + route-name (get-in route [:data :name]) + section (if (= route-name :dashboard-files) + (if (= project-id default-project-id) + "dashboard-drafts" + "dashboard-project") + (name route-name)) + + props (:props profile) + collapsed (:builtin-templates-collapsed-status props false) + card-offset* (mf/use-state 0) + card-offset (deref card-offset*) + + card-width 275 + total (count templates) + container-size (* (+ 2 total) card-width) + + ;; We need space for total plus the libraries&templates link + more-cards (> (+ card-offset (* (+ 1 total) card-width)) content-width) + card-count (mth/floor (/ content-width 275)) + left-moves (/ card-offset -275) + first-card left-moves + last-card (+ (- card-count 1) left-moves) + content-ref (mf/use-ref) + + on-move-left + (mf/use-fn + (mf/deps card-offset card-width) + (fn [_event] + (when-not (zero? card-offset) + (dom/animate! (mf/ref-val content-ref) + [#js {:left (dm/str card-offset "px")} + #js {:left (dm/str (+ card-offset card-width) "px")}] + #js {:duration 200 :easing "linear"}) + (reset! card-offset* (+ card-offset card-width))))) + + on-move-left-key-down + (mf/use-fn + (mf/deps on-move-left first-card) + (fn [event] + (when (kbd/enter? event) + (dom/stop-propagation event) + (on-move-left event) + (when-let [node (dom/get-element (dm/str "card-container-" first-card))] + (dom/focus! node))))) + + on-move-right + (mf/use-fn + (mf/deps more-cards card-offset card-width) + (fn [_event] + (when more-cards + (swap! card-offset* inc) + (dom/animate! (mf/ref-val content-ref) + [#js {:left (dm/str card-offset "px")} + #js {:left (dm/str (- card-offset card-width) "px")}] + #js {:duration 200 :easing "linear"}) + (reset! card-offset* (- card-offset card-width))))) + + on-move-right-key-down + (mf/use-fn + (mf/deps on-move-right last-card) + (fn [event] + (when (kbd/enter? event) + (dom/stop-propagation event) + (on-move-right event) + (when-let [node (dom/get-element (dm/str "card-container-" last-card))] + (dom/focus! node))))) + + on-import-template + (mf/use-fn + (mf/deps default-project-id project-id section templates team-id) + (fn [template _event] + (import-template! template team-id project-id default-project-id section))) + + ] + + (mf/with-effect [collapsed] + (when-not collapsed + (st/emit! (dd/fetch-builtin-templates)))) + + [:div.dashboard-templates-section + {:class (when ^boolean collapsed "collapsed")} + [:& title {:collapsed collapsed}] + + [:div.content {:ref content-ref + :style {:left card-offset + :width (dm/str container-size "px")}} + + (for [index (range (count templates))] + [:& card-item + {:on-import on-import-template + :item (nth templates index) + :index index + :key index + :is-visible (and (>= index first-card) + (<= index last-card)) + :collapsed collapsed}]) + + [:& card-item-link + {:is-visible (and (>= total first-card) (<= total last-card)) + :collapsed collapsed + :section section + :total total}]] + + (when (< card-offset 0) + [:button.button.left + {:tab-index (if ^boolean collapsed "-1" "0") + :on-click on-move-left + :on-key-down on-move-left-key-down} + i/go-prev]) + + (when more-cards + [:button.button.right + {:tab-index (if collapsed "-1" "0") + :on-click on-move-right + :aria-label (tr "labels.next") + :on-key-down on-move-right-key-down} + i/go-next])])) + diff --git a/frontend/src/app/main/ui/viewer/interactions.cljs b/frontend/src/app/main/ui/viewer/interactions.cljs index 1b7a37aebd..6e2e6320e4 100644 --- a/frontend/src/app/main/ui/viewer/interactions.cljs +++ b/frontend/src/app/main/ui/viewer/interactions.cljs @@ -245,14 +245,14 @@ :dissolve (do (dom/animate! orig-viewport - [#js {:opacity "100"} + [#js {:opacity "100%"} #js {:opacity "0"}] #js {:duration (:duration animation) :easing (name (:easing animation))} #(st/emit! (dv/complete-animation))) (dom/animate! current-viewport [#js {:opacity "0"} - #js {:opacity "100"}] + #js {:opacity "100%"}] #js {:duration (:duration animation) :easing (name (:easing animation))})) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs index eff892981d..a672a1f1bb 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs @@ -199,9 +199,6 @@ ext-delay-ref (mf/use-ref nil) ext-duration-ref (mf/use-ref nil) - select-text - (fn [ref] (fn [_] (dom/select-text! (mf/ref-val ref)))) - change-event-type (fn [event] (let [value (-> event dom/get-target dom/get-value d/read-string)] @@ -336,7 +333,6 @@ [:span.element-set-subtitle.wide (tr "workspace.options.interaction-delay")] [:div.input-element {:title (tr "workspace.options.interaction-ms")} [:> numeric-input {:ref ext-delay-ref - :on-focus (select-text ext-delay-ref) :on-change change-delay :value (:delay interaction) :title (tr "workspace.options.interaction-ms")}] @@ -523,7 +519,6 @@ [:span.element-set-subtitle.wide (tr "workspace.options.interaction-duration")] [:div.input-element {:title (tr "workspace.options.interaction-ms")} [:> numeric-input {:ref ext-duration-ref - :on-focus (select-text ext-duration-ref) :on-change change-duration :value (-> interaction :animation :duration) :title (tr "workspace.options.interaction-ms")}] diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs index 6bfe71ad56..0e670e05d1 100644 --- a/frontend/src/app/util/dom.cljs +++ b/frontend/src/app/util/dom.cljs @@ -234,7 +234,7 @@ (defn select-text! [^js node] - (when (some? node) + (when (and (some? node) (some? (unchecked-get node "select"))) (.select ^js node))) (defn ^boolean equals?