;; 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.sidebar (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] [app.common.data.macros :as dm] [app.common.spec :as us] [app.config :as cf] [app.main.data.dashboard :as dd] [app.main.data.events :as ev] [app.main.data.messages :as msg] [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.components.dropdown-menu :refer [dropdown-menu dropdown-menu-item*]] [app.main.ui.components.link :refer [link]] [app.main.ui.dashboard.comments :refer [comments-icon 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.dom :as dom] [app.util.dom.dnd :as dnd] [app.util.i18n :as i18n :refer [tr]] [app.util.keyboard :as kbd] [app.util.object :as obj] [app.util.router :as rt] [app.util.timers :as ts] [beicon.v2.core :as rx] [cljs.spec.alpha :as s] [cuerdas.core :as str] [goog.functions :as f] [potok.v2.core :as ptk] [rumext.v2 :as mf])) (mf/defc sidebar-project [{:keys [item selected?] :as props}] (let [dstate (mf/deref refs/dashboard-local) selected-files (:selected-files dstate) selected-project (:selected-project dstate) edit-id (:project-for-edit dstate) local* (mf/use-state {:menu-open false :menu-pos nil :edition? (= (:id item) edit-id) :dragging? false}) local @local* on-click (mf/use-callback (mf/deps item) (fn [] (st/emit! (dd/go-to-files (:id item))))) on-key-down (mf/use-callback (mf/deps item) (fn [event] (when (kbd/enter? event) (st/emit! (dd/go-to-files (:id item)) (ts/schedule-on-idle (fn [] (let [project-title (dom/get-element (str (:id item)))] (when project-title (dom/set-attribute! project-title "tabindex" "0") (dom/focus! project-title) (dom/set-attribute! project-title "tabindex" "-1"))))))))) 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] (when-not (str/blank? name) (st/emit! (-> (dd/rename-project (assoc item :name name)) (with-meta {::ev/origin "dashboard:sidebar"})))) (swap! local* assoc :edition? false))) on-drag-enter (mf/use-callback (mf/deps selected-project) (fn [e] (when (dnd/has-type? e "penpot/files") (dom/prevent-default e) (when-not (dnd/from-child? e) (when (not= selected-project (:id item)) (swap! local* assoc :dragging? true)))))) on-drag-over (mf/use-callback (fn [e] (when (dnd/has-type? e "penpot/files") (dom/prevent-default e)))) on-drag-leave (mf/use-callback (fn [e] (when-not (dnd/from-child? e) (swap! local* assoc :dragging? false)))) on-drop-success (mf/use-callback (mf/deps (:id item)) #(st/emit! (msg/success (tr "dashboard.success-move-file")) (dd/go-to-files (:id item)))) on-drop (mf/use-callback (mf/deps item selected-files) (fn [_] (swap! local* assoc :dragging? false) (when (not= selected-project (:id item)) (let [data {:ids selected-files :project-id (:id item)} mdata {:on-success on-drop-success}] (st/emit! (dd/move-files (with-meta data mdata)))))))] [:* [:li {:tab-index "0" :class (stl/css-case :project-element true :current selected? :dragging (:dragging? local)) :on-click on-click :on-key-down on-key-down :on-double-click on-edit-open :on-context-menu on-menu-click :on-drag-enter on-drag-enter :on-drag-over on-drag-over :on-drag-leave on-drag-leave :on-drop on-drop} (if (:edition? local) [:& inline-edition {:content (:name item) :on-end on-edit}] [:span {:class (stl/css :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] :as props}] (let [search-term (or search-term "") focused? (mf/use-state false) emit! (mf/use-memo #(f/debounce st/emit! 500)) on-search-blur (mf/use-callback (fn [_] (reset! focused? false))) on-search-change (mf/use-callback (mf/deps team-id) (fn [event] (let [value (dom/get-target-val event)] (emit! (dd/go-to-search value))))) on-clear-click (mf/use-callback (mf/deps team-id) (fn [e] (let [search-input (dom/get-element "search-input")] (dom/clean-value! search-input) (dom/focus! search-input) (emit! (dd/go-to-search)) (dom/prevent-default e) (dom/stop-propagation e)))) on-key-press (mf/use-callback (fn [e] (when (kbd/enter? e) (ts/schedule-on-idle (fn [] (let [search-title (dom/get-element (str "dashboard-search-title"))] (when search-title (dom/set-attribute! search-title "tabindex" "0") (dom/focus! search-title) (dom/set-attribute! search-title "tabindex" "-1"))))) (dom/prevent-default e) (dom/stop-propagation e)))) handle-clear-search (mf/use-callback (mf/deps on-clear-click) (fn [event] (when (kbd/enter? event) (on-clear-click event))))] [:form {:class (stl/css :sidebar-search)} [:input {:class (stl/css :input-text) :key "images-search-box" :id "search-input" :type "text" :aria-label (tr "dashboard.search-placeholder") :placeholder (tr "dashboard.search-placeholder") :default-value search-term :auto-complete "off" ;; :on-focus on-search-focus :on-blur on-search-blur :on-change on-search-change :on-key-press on-key-press :ref #(when % (set! (.-value %) search-term))}] (if (or @focused? (seq search-term)) [:div {:class (stl/css :clear-search) :tab-index "0" :on-click on-clear-click :on-key-down handle-clear-search} i/close] [:div {:class (stl/css :search) :on-click on-clear-click} i/search])])) (mf/defc teams-selector-dropdown-items {::mf/wrap-props false} [{:keys [team profile teams] :as props}] (let [on-create-clicked (mf/use-callback #(st/emit! (modal/show :team-form {}))) team-selected (mf/use-callback (fn [team-id] (st/emit! (dd/go-to-projects team-id)))) handle-select-default (fn [event] (when (kbd/enter? event) (team-selected (:default-team-id profile) event))) handle-select-team (fn [id event] (when (kbd/enter? event) (team-selected id event)))] [:* [:> dropdown-menu-item* {:on-click (partial team-selected (:default-team-id profile)) :on-key-down handle-select-default :id "teams-selector-default-team" :class (stl/css :team-name)} [:span {:class (stl/css :team-icon)} i/logo-icon] [:span {:class (stl/css :team-text)} (tr "dashboard.your-penpot")] (when (= (:default-team-id profile) (:id team)) [:span {:class (stl/css :icon)} i/tick])] (for [team-item (remove :is-default (vals teams))] [:> dropdown-menu-item* {:on-click (partial team-selected (:id team-item)) :on-key-down (partial handle-select-team (:id team-item)) :id (str "teams-selector-" (:id team-item)) :class (stl/css :team-name) :key (str "teams-selector-" (:id team-item))} [:span {:class (stl/css :team-icon)} [:img {:src (cf/resolve-team-photo-url team-item) :alt (:name team-item)}]] [:span {:class (stl/css :team-text) :title (:name team-item)} (:name team-item)] (when (= (:id team-item) (:id team)) [:span {:class (stl/css :icon)} i/tick])]) [:hr {:role "separator"}] [:> dropdown-menu-item* {:on-click on-create-clicked :on-key-down (fn [event] (when (kbd/enter? event) (on-create-clicked event))) :id "teams-selector-create-team" :class (stl/css :team-name :action)} [:span {:class (stl/css :team-icon :new-team)} i/close] [:span {:class (stl/css :team-text)} (tr "dashboard.create-new-team")]]])) (s/def ::member-id ::us/uuid) (s/def ::leave-modal-form (s/keys :req-un [::member-id])) (mf/defc team-options-dropdown [{:keys [team profile] :as props}] (let [go-members #(st/emit! (dd/go-to-team-members)) go-invitations #(st/emit! (dd/go-to-team-invitations)) go-webhooks #(st/emit! (dd/go-to-team-webhooks)) go-settings #(st/emit! (dd/go-to-team-settings)) members-map (mf/deref refs/dashboard-team-members) members (vals members-map) can-rename? (or (get-in team [:permissions :is-owner]) (get-in team [:permissions :is-admin])) on-success (fn [] (st/emit! (dd/go-to-projects (:default-team-id profile)) (modal/hide) (du/fetch-teams))) on-error (fn [{:keys [code] :as error}] (condp = code :no-enough-members-for-leave (rx/of (msg/error (tr "errors.team-leave.insufficient-members"))) :member-does-not-exist (rx/of (msg/error (tr "errors.team-leave.member-does-not-exists"))) :owner-cant-leave-team (rx/of (msg/error (tr "errors.team-leave.owner-cant-leave"))) (rx/throw error))) leave-fn (fn [member-id] (let [params (cond-> {} (uuid? member-id) (assoc :reassign-to member-id))] (st/emit! (dd/leave-team (with-meta params {:on-success on-success :on-error on-error}))))) delete-fn (fn [] (st/emit! (dd/delete-team (with-meta team {:on-success on-success :on-error on-error})))) on-rename-clicked (fn [] (st/emit! (modal/show :team-form {:team team}))) on-leave-clicked #(st/emit! (modal/show {:type :confirm :title (tr "modals.leave-confirm.title") :message (tr "modals.leave-confirm.message") :accept-label (tr "modals.leave-confirm.accept") :on-accept leave-fn})) on-leave-as-owner-clicked (fn [] (st/emit! (dd/fetch-team-members (:id team)) (modal/show {:type :leave-and-reassign :profile profile :team team :accept leave-fn}))) leave-and-close #(st/emit! (modal/show {:type :confirm :title (tr "modals.leave-confirm.title") :message (tr "modals.leave-and-close-confirm.message" (:name team)) :scd-message (tr "modals.leave-and-close-confirm.hint") :accept-label (tr "modals.leave-confirm.accept") :on-accept delete-fn})) on-delete-clicked #(st/emit! (modal/show {:type :confirm :title (tr "modals.delete-team-confirm.title") :message (tr "modals.delete-team-confirm.message") :accept-label (tr "modals.delete-team-confirm.accept") :on-accept delete-fn}))] [:* [:> dropdown-menu-item* {:on-click go-members :on-key-down (fn [event] (when (kbd/enter? event) (go-members))) :id "teams-options-members" :data-test "team-members"} (tr "labels.members")] [:> dropdown-menu-item* {:on-click go-invitations :on-key-down (fn [event] (when (kbd/enter? event) (go-invitations))) :id "teams-options-invitations" :data-test "team-invitations"} (tr "labels.invitations")] (when (contains? cf/flags :webhooks) [:> dropdown-menu-item* {:on-click go-webhooks :on-key-down (fn [event] (when (kbd/enter? event) (go-webhooks))) :id "teams-options-webhooks"} (tr "labels.webhooks")]) [:> dropdown-menu-item* {:on-click go-settings :on-key-down (fn [event] (when (kbd/enter? event) (go-settings))) :id "teams-options-settings" :data-test "team-settings"} (tr "labels.settings")] [:hr] (when can-rename? [:> dropdown-menu-item* {:on-click on-rename-clicked :on-key-down (fn [event] (when (kbd/enter? event) (on-rename-clicked))) :id "teams-options-rename" :data-test "rename-team"} (tr "labels.rename")]) (cond (= (count members) 1) [:> dropdown-menu-item* {:on-click leave-and-close :on-key-down (fn [event] (when (kbd/enter? event) (leave-and-close))) :id "teams-options-leave-team"} (tr "dashboard.leave-team")] (get-in team [:permissions :is-owner]) [:> dropdown-menu-item* {:on-click on-leave-as-owner-clicked :on-key-down (fn [event] (when (kbd/enter? event) (on-leave-as-owner-clicked))) :id "teams-options-leave-team" :data-test "leave-team"} (tr "dashboard.leave-team")] (> (count members) 1) [:> dropdown-menu-item* {:on-click on-leave-clicked :on-key-down (fn [event] (when (kbd/enter? event) (on-leave-clicked))) :id "teams-options-leave-team"} (tr "dashboard.leave-team")]) (when (get-in team [:permissions :is-owner]) [:> dropdown-menu-item* {:on-click on-delete-clicked :on-key-down (fn [event] (when (kbd/enter? event) (on-delete-clicked))) :id "teams-options-delete-team" :class (stl/css :warning) :data-test "delete-team"} (tr "dashboard.delete-team")])])) (mf/defc sidebar-team-switch [{:keys [team profile] :as props}] (let [teams (mf/deref refs/teams) teams-without-default (into {} (filter (fn [[_ v]] (= false (:is-default v))) teams)) team-ids (map #(str "teams-selector-" %) (keys teams-without-default)) ids (concat ["teams-selector-default-team"] team-ids ["teams-selector-create-team"]) show-team-opts-ddwn? (mf/use-state false) show-teams-ddwn? (mf/use-state false) can-rename? (or (get-in team [:permissions :is-owner]) (get-in team [:permissions :is-admin])) options-ids ["teams-options-members" "teams-options-invitations" (when (contains? cf/flags :webhooks) "teams-options-webhooks") "teams-options-settings" (when can-rename? "teams-options-rename") "teams-options-leave-team" (when (get-in team [:permissions :is-owner]) "teams-options-delete-team")] handle-show-team-click (fn [event] (dom/stop-propagation event) (swap! show-teams-ddwn? not) (reset! show-team-opts-ddwn? false)) handle-show-team-keydown (fn [event] (when (or (kbd/space? event) (kbd/enter? event)) (dom/prevent-default event) (reset! show-teams-ddwn? true) (reset! show-team-opts-ddwn? false) (ts/schedule-on-idle (fn [] (let [first-element (dom/get-element (first ids))] (when first-element (dom/focus! first-element))))))) handle-show-opts-click (fn [event] (dom/stop-propagation event) (swap! show-team-opts-ddwn? not) (reset! show-teams-ddwn? false)) handle-show-opts-keydown (fn [event] (when (or (kbd/space? event) (kbd/enter? event)) (dom/prevent-default event) (reset! show-team-opts-ddwn? true) (reset! show-teams-ddwn? false) (ts/schedule-on-idle (fn [] (let [first-element (dom/get-element (first options-ids))] (when first-element (dom/focus! first-element))))))) handle-close-team (fn [] (reset! show-teams-ddwn? false))] [:div {:class (stl/css :sidebar-team-switch)} [:div {:class (stl/css :switch-content)} [:button {:class (stl/css :current-team) :tab-index "0" :on-click handle-show-team-click :on-key-down handle-show-team-keydown} (if (:is-default team) [:div {:class (stl/css :team-name)} [:span {:class (stl/css :team-icon)} i/logo-icon] [:span {:class (stl/css :team-text)} (tr "dashboard.default-team-name")]] [:div {:class (stl/css :team-name)} [:span {:class (stl/css :team-icon)} [:img {:src (cf/resolve-team-photo-url team) :alt (:name team)}]] [:span {:class (stl/css :team-text) :title (:name team)} (:name team)]]) [:span {:class (stl/css :switch-icon)} i/arrow-down]] (when-not (:is-default team) [:button {:class (stl/css :switch-options) :on-click handle-show-opts-click :tab-index "0" :on-key-down handle-show-opts-keydown} i/actions])] ;; Teams Dropdown [:& dropdown-menu {:show @show-teams-ddwn? :on-close handle-close-team :ids ids :list-class (stl/css :dropdown :teams-dropdown)} [:& teams-selector-dropdown-items {:ids ids :team team :profile profile :teams teams}]] [:& dropdown-menu {:show @show-team-opts-ddwn? :on-close #(reset! show-team-opts-ddwn? false) :ids options-ids :list-class (stl/css :dropdown :options-dropdown)} [:& team-options-dropdown {:team team :profile profile}]]])) (mf/defc sidebar-content [{:keys [projects profile section team project search-term] :as props}] (let [default-project-id (->> (vals projects) (d/seek :is-default) (:id)) projects? (= section :dashboard-projects) fonts? (= section :dashboard-fonts) libs? (= section :dashboard-libraries) drafts? (and (= section :dashboard-files) (= (:id project) default-project-id)) go-projects (mf/use-callback (mf/deps team) #(st/emit! (rt/nav :dashboard-projects {:team-id (:id team)}))) go-projects-with-key (mf/use-callback (mf/deps team) #(st/emit! (rt/nav :dashboard-projects {:team-id (:id team)}) (ts/schedule-on-idle (fn [] (let [projects-title (dom/get-element "dashboard-projects-title")] (when projects-title (dom/set-attribute! projects-title "tabindex" "0") (dom/focus! projects-title) (dom/set-attribute! projects-title "tabindex" "-1"))))))) go-fonts (mf/use-callback (mf/deps team) #(st/emit! (rt/nav :dashboard-fonts {:team-id (:id team)}))) go-fonts-with-key (mf/use-callback (mf/deps team) #(st/emit! (rt/nav :dashboard-fonts {:team-id (:id team)}) (ts/schedule-on-idle (fn [] (let [font-title (dom/get-element "dashboard-fonts-title")] (when font-title (dom/set-attribute! font-title "tabindex" "0") (dom/focus! font-title) (dom/set-attribute! font-title "tabindex" "-1"))))))) go-drafts (mf/use-callback (mf/deps team default-project-id) (fn [] (st/emit! (rt/nav :dashboard-files {:team-id (:id team) :project-id default-project-id})))) go-drafts-with-key (mf/use-callback (mf/deps team default-project-id) #(st/emit! (rt/nav :dashboard-files {:team-id (:id team) :project-id default-project-id}) (ts/schedule-on-idle (fn [] (let [drafts-title (dom/get-element "dashboard-drafts-title")] (when drafts-title (dom/set-attribute! drafts-title "tabindex" "0") (dom/focus! drafts-title) (dom/set-attribute! drafts-title "tabindex" "-1"))))))) go-libs (mf/use-callback (mf/deps team) #(st/emit! (rt/nav :dashboard-libraries {:team-id (:id team)}))) go-libs-with-key (mf/use-callback (mf/deps team) #(st/emit! (rt/nav :dashboard-libraries {:team-id (:id team)}) (ts/schedule-on-idle (fn [] (let [libs-title (dom/get-element "dashboard-libraries-title")] (when libs-title (dom/set-attribute! libs-title "tabindex" "0") (dom/focus! libs-title) (dom/set-attribute! libs-title "tabindex" "-1"))))))) pinned-projects (->> (vals projects) (remove :is-default) (filter :is-pinned))] [:div {:class (stl/css :sidebar-content)} [:& sidebar-team-switch {:team team :profile profile}] [:hr] [:& sidebar-search {:search-term search-term :team-id (:id team)}] [:div {:class (stl/css :sidebar-content-section)} [:ul {:class (stl/css :sidebar-nav :no-overflow)} [:li {:class (stl/css :recent-projects) :class-name (when projects? (stl/css :current))} [:& link {:action go-projects :keyboard-action go-projects-with-key} [:span {:class (stl/css :element-title)} (tr "labels.projects")]]] [:li {:class-name (when drafts? (stl/css :current))} [:& link {:action go-drafts :keyboard-action go-drafts-with-key} [:span {:class (stl/css :element-title)} (tr "labels.drafts")]]] [:li {:class-name (when libs? (stl/css :current))} [:& link {:action go-libs :keyboard-action go-libs-with-key} [:span {:class (stl/css :element-title)} (tr "labels.shared-libraries")]]]]] [:hr] [:div {:class (stl/css :sidebar-content-section)} [:ul {:class (stl/css :sidebar-nav :no-overflow)} [:li {:class-name (when fonts? (stl/css :current))} [:& link {:action go-fonts :keyboard-action go-fonts-with-key :data-test "fonts"} [:span {:class (stl/css :element-title)} (tr "labels.fonts")]]]]] [:hr] [:div {:class (stl/css :sidebar-content-section) :data-test "pinned-projects"} (if (seq pinned-projects) [:ul {:class (stl/css :sidebar-nav)} (for [item pinned-projects] [:& sidebar-project {:item item :key (dm/str (:id item)) :id (:id item) :team-id (:id team) :selected? (= (:id item) (:id project))}])] [:div {:class (stl/css :sidebar-empty-placeholder)} [:span {:class (stl/css :icon)} i/pin-refactor] [:span {:class (stl/css :text)} (tr "dashboard.no-projects-placeholder")]])]])) (mf/defc profile-section [{:keys [profile team] :as props}] (let [show (mf/use-state false) photo (cf/resolve-profile-photo-url profile) on-click (mf/use-callback (fn [section event] (dom/stop-propagation event) (reset! show false) (if (keyword? section) (st/emit! (rt/nav section)) (st/emit! section)))) show-release-notes (mf/use-callback (fn [event] (let [version (:main cf/version)] (st/emit! (ptk/event ::ev/event {::ev/name "show-release-notes" :version version})) (if (and (kbd/alt? event) (kbd/mod? event)) (st/emit! (modal/show {:type :onboarding})) (st/emit! (modal/show {:type :release-notes :version version})))))) show-comments* (mf/use-state false) show-comments? @show-comments* handle-hide-comments (mf/use-callback (fn [] (reset! show-comments* false))) handle-show-comments (mf/use-callback (fn [] (reset! show-comments* true))) handle-click (mf/use-callback (fn [event] (dom/stop-propagation event) (swap! show not))) handle-key-down (mf/use-callback (fn [event] (when (kbd/enter? event) (reset! show true)))) handle-close (fn [event] (dom/stop-propagation event) (reset! show false)) handle-key-down-profile (mf/use-callback (mf/deps on-click) (fn [event] (when (kbd/enter? event) (on-click :settings-profile event)))) handle-click-url (mf/use-callback (fn [event] (let [url (-> (dom/get-current-target event) (dom/get-data "url"))] (dom/open-new-window url)))) handle-keydown-url (mf/use-callback (fn [event] (let [url (-> (dom/get-current-target event) (dom/get-data "url"))] (when (kbd/enter? event) (dom/open-new-window url))))) handle-show-release-notes (mf/use-callback (mf/deps show-release-notes) (fn [event] (when (kbd/enter? event) (show-release-notes)))) handle-feedback-click (mf/use-callback (mf/deps on-click) #(on-click :settings-feedback %)) handle-feedback-keydown (mf/use-callback (mf/deps on-click) (fn [event] (when (kbd/enter? event) (on-click :settings-feedback event)))) handle-logout-click (mf/use-callback (mf/deps on-click) #(on-click (du/logout) %)) handle-logout-keydown (mf/use-callback (mf/deps on-click) (fn [event] (when (kbd/enter? event) (on-click (du/logout) event))))] [:* (when (and team profile) [:& comments-section {:profile profile :team team :show? show-comments? :on-show-comments handle-show-comments :on-hide-comments handle-hide-comments}]) [:div {:class (stl/css :profile-section)} [:div {:class (stl/css :profile) :tab-index "0" :on-click handle-click :on-key-down handle-key-down :data-test "profile-btn"} [:img {:src photo :alt (:fullname profile)}] [:span (:fullname profile)]] [:& dropdown-menu {:on-close handle-close :show @show} [:ul {:class (stl/css :dropdown)} [:li {:tab-index (if @show "0" "-1") :on-click (partial on-click :settings-profile) :on-key-down handle-key-down-profile :data-test "profile-profile-opt"} [:span {:class (stl/css :text)} (tr "labels.your-account")]] [:li {:class (stl/css :separator) :tab-index (if @show "0" "-1") :data-url "https://help.penpot.app" :on-click handle-click-url :on-key-down handle-keydown-url :data-test "help-center-profile-opt"} [:span {:class (stl/css :text)} (tr "labels.help-center")]] [:li {:tab-index (if @show "0" "-1") :data-url "https://community.penpot.app" :on-click handle-click-url :on-key-down handle-keydown-url} [:span {:class (stl/css :text)} (tr "labels.community")]] [:li {:tab-index (if @show "0" "-1") :data-url "https://www.youtube.com/c/Penpot" :on-click handle-click-url :on-key-down handle-keydown-url} [:span {:class (stl/css :text)} (tr "labels.tutorials")]] [:li {:tab-index (if @show "0" "-1") :on-click show-release-notes :on-key-down handle-show-release-notes} [:span {:class (stl/css :text)} (tr "labels.release-notes")]] [:li {:class (stl/css :separator) :tab-index (if @show "0" "-1") :data-url "https://penpot.app/libraries-templates" :on-click handle-click-url :on-key-down handle-keydown-url :data-test "libraries-templates-profile-opt"} [:span {:class (stl/css :text)} (tr "labels.libraries-and-templates")]] [:li {:tab-index (if @show "0" "-1") :data-url "https://github.com/penpot/penpot" :on-click handle-click-url :on-key-down handle-keydown-url} [:span {:class (stl/css :text)} (tr "labels.github-repo")]] [:li {:tab-index (if @show "0" "-1") :data-url "https://penpot.app/terms" :on-click handle-click-url :on-key-down handle-keydown-url} [:span {:class (stl/css :text)} (tr "auth.terms-of-service")]] (when (contains? cf/flags :user-feedback) [:li {:class (stl/css :separator) :tab-index (if @show "0" "-1") :on-click handle-feedback-click :on-key-down handle-feedback-keydown :data-test "feedback-profile-opt"} [:span {:class (stl/css :text)} (tr "labels.give-feedback")]]) [:li {:class (stl/css :separator) :tab-index (if @show "0" "-1") :on-click handle-logout-click :on-key-down handle-logout-keydown :data-test "logout-profile-opt"} [:span {:class (stl/css :icon)} i/exit] [:span {:class (stl/css :text)} (tr "labels.logout")]]]] (when (and team profile) [:& comments-icon {:profile profile :show? show-comments? :on-show-comments handle-show-comments}])]])) (mf/defc sidebar {::mf/wrap-props false ::mf/wrap [mf/memo]} [props] (let [team (obj/get props "team") profile (obj/get props "profile")] [:nav {:class (stl/css :dashboard-sidebar)} [:> sidebar-content props] [:& profile-section {:profile profile :team team}]]))