mirror of
https://github.com/penpot/penpot.git
synced 2025-01-04 13:50:12 -05:00
♻️ Visual changes in comments
This commit is contained in:
parent
ff7acea95a
commit
b5e5c4b0dd
17 changed files with 745 additions and 606 deletions
|
@ -104,8 +104,7 @@ export class ViewerPage extends BaseWebSocketPage {
|
|||
|
||||
async showCommentsThread(number, clickOptions = {}) {
|
||||
await this.page
|
||||
.getByTestId("floating-thread-bubble")
|
||||
.filter({ hasText: number.toString() })
|
||||
.getByTestId(`floating-thread-bubble-${number.toString()}`)
|
||||
.click(clickOptions);
|
||||
}
|
||||
|
||||
|
|
|
@ -810,28 +810,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.comment-bubbles {
|
||||
@include bodySmallTypography;
|
||||
@include flexCenter;
|
||||
height: $s-32;
|
||||
width: $s-32;
|
||||
border-radius: $br-circle;
|
||||
background-color: var(--comment-bullet-background-color-rest);
|
||||
border: $s-1 solid var(--comment-bullet-border-color-rest);
|
||||
color: var(--comment-bullet-foreground-color-rest);
|
||||
}
|
||||
|
||||
.resolved-comment-bubble {
|
||||
background-color: var(--comment-bullet-background-color-resolved);
|
||||
border: $s-1 solid var(--comment-bullet-border-color-resolved);
|
||||
color: var(--comment-bullet-foreground-color-resolved);
|
||||
}
|
||||
.unread-comment-bubble {
|
||||
background-color: var(--comment-bullet-background-color-unread);
|
||||
border: $s-1 solid var(--comment-bullet-border-color-unread);
|
||||
color: var(--comment-bullet-foreground-color-unread);
|
||||
}
|
||||
|
||||
// SELECTS AND DROPDOWNS
|
||||
.menu-dropdown {
|
||||
@include menuShadow;
|
||||
|
|
|
@ -502,11 +502,11 @@
|
|||
(d/update-in-when [:comments-local :draft] merge data)))))
|
||||
|
||||
(defn toggle-comment-options
|
||||
[comment]
|
||||
[comment-id]
|
||||
(ptk/reify ::toggle-comment-options
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update-in state [:comments-local :options] #(if (= (:id comment) %) nil (:id comment))))))
|
||||
(update-in state [:comments-local :options] #(if (= comment-id %) nil comment-id)))))
|
||||
|
||||
(defn hide-comment-options
|
||||
[]
|
||||
|
|
|
@ -151,11 +151,13 @@
|
|||
(pcb/with-page page)
|
||||
(pcb/set-comment-thread-position thread))]
|
||||
|
||||
(rx/merge
|
||||
(rx/of (dch/commit-changes changes))
|
||||
(->> (rp/cmd! :update-comment-thread-position thread)
|
||||
(rx/catch #(rx/throw {:type :update-comment-thread-position}))
|
||||
(rx/ignore))))))))
|
||||
(rx/concat
|
||||
(rx/merge
|
||||
(rx/of (dch/commit-changes changes))
|
||||
(->> (rp/cmd! :update-comment-thread-position thread)
|
||||
(rx/catch #(rx/throw {:type :update-comment-thread-position}))
|
||||
(rx/ignore)))
|
||||
(rx/of (dcmt/refresh-comment-thread thread))))))))
|
||||
|
||||
;; Move comment threads that are inside a frame when that frame is moved"
|
||||
(defmethod ptk/resolve ::move-frame-comment-threads
|
||||
|
|
|
@ -267,9 +267,9 @@
|
|||
(def workspace-page-flows
|
||||
(l/derived #(-> % :flows not-empty) workspace-page))
|
||||
|
||||
(defn workspace-page-objects-by-id
|
||||
[page-id]
|
||||
(l/derived #(wsh/lookup-page-objects % page-id) st/state =))
|
||||
(defn workspace-page-object-by-id
|
||||
[page-id shape-id]
|
||||
(l/derived #(wsh/lookup-shape % page-id shape-id) st/state =))
|
||||
|
||||
;; TODO: Looks like using the `=` comparator can be pretty expensive
|
||||
;; on large pages, we are using this for some reason?
|
||||
|
|
|
@ -9,7 +9,9 @@
|
|||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cfg]
|
||||
[app.main.data.comments :as dcm]
|
||||
[app.main.data.modal :as modal]
|
||||
|
@ -17,12 +19,15 @@
|
|||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.dropdown :refer [dropdown]]
|
||||
[app.main.ui.ds.buttons.button :refer [button*]]
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :refer [icon*]]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.keyboard :as kbd]
|
||||
[app.util.time :as dt]
|
||||
[clojure.math :refer [floor]]
|
||||
[cuerdas.core :as str]
|
||||
[okulary.core :as l]
|
||||
[rumext.v2 :as mf]))
|
||||
|
@ -93,8 +98,66 @@
|
|||
:on-change on-change*
|
||||
:max-length max-length}]))
|
||||
|
||||
(mf/defc reply-form
|
||||
[{:keys [thread] :as props}]
|
||||
(def ^:private schema:comment-avatar
|
||||
[:map
|
||||
[:class {:optional true} :string]
|
||||
[:image :string]
|
||||
[:variant {:optional true}
|
||||
[:maybe [:enum "read" "unread" "solved"]]]])
|
||||
|
||||
(mf/defc comment-avatar*
|
||||
{::mf/props :obj
|
||||
::mf/schema schema:comment-avatar}
|
||||
[{:keys [image variant class] :rest props}]
|
||||
(let [variant (or variant "read")
|
||||
class (dm/str class " " (stl/css-case :avatar true
|
||||
:avatar-read (= variant "read")
|
||||
:avatar-unread (= variant "unread")
|
||||
:avatar-solved (= variant "solved")))
|
||||
props (mf/spread-props props {:class class})]
|
||||
[:> :div props
|
||||
[:img {:src image
|
||||
:class (stl/css :avatar-image)}]
|
||||
[:div {:class (stl/css-case :avatar-mask true
|
||||
:avatar-darken (= variant "solved"))}]]))
|
||||
|
||||
(mf/defc comment-info*
|
||||
{::mf/props :obj
|
||||
::mf/private true}
|
||||
[{:keys [item profile]}]
|
||||
[:*
|
||||
[:div {:class (stl/css :author)}
|
||||
[:> comment-avatar* {:image (cfg/resolve-profile-photo-url profile)
|
||||
:class (stl/css :avatar-lg)
|
||||
:variant (cond (:is-resolved item) "solved"
|
||||
(pos? (:count-unread-comments item)) "unread"
|
||||
:else "read")}]
|
||||
[:div {:class (stl/css :author-identity)}
|
||||
[:div {:class (stl/css :author-fullname)} (:fullname profile)]
|
||||
[:div {:class (stl/css :author-timeago)} (dt/timeago (:modified-at item))]]]
|
||||
|
||||
[:div {:class (stl/css :item)}
|
||||
(:content item)]
|
||||
|
||||
[:div {:class (stl/css :replies)}
|
||||
(let [total-comments (:count-comments item 1)
|
||||
total-replies (dec total-comments)
|
||||
unread-replies (:count-unread-comments item 0)]
|
||||
[:*
|
||||
(when (> total-replies 0)
|
||||
(if (= total-replies 1)
|
||||
[:span {:class (stl/css :replies-total)} (str total-replies " " (tr "labels.reply"))]
|
||||
[:span {:class (stl/css :replies-total)} (str total-replies " " (tr "labels.replies"))]))
|
||||
|
||||
(when (and (> total-replies 0) (> unread-replies 0))
|
||||
(if (= unread-replies 1)
|
||||
[:span {:class (stl/css :replies-unread)} (str unread-replies " " (tr "labels.reply.new"))]
|
||||
[:span {:class (stl/css :replies-unread)} (str unread-replies " " (tr "labels.replies.new"))]))])]])
|
||||
|
||||
(mf/defc comment-reply-form*
|
||||
{::mf/props :obj
|
||||
::mf/private true}
|
||||
[{:keys [thread]}]
|
||||
(let [show-buttons? (mf/use-state false)
|
||||
content (mf/use-state "")
|
||||
|
||||
|
@ -124,9 +187,10 @@
|
|||
(fn []
|
||||
(st/emit! (dcm/add-comment thread @content))
|
||||
(on-cancel)))]
|
||||
[:div {:class (stl/css :reply-form)}
|
||||
[:div {:class (stl/css :form)}
|
||||
[:& resizing-textarea {:value @content
|
||||
:placeholder "Reply"
|
||||
:placeholder (tr "labels.reply.thread")
|
||||
:autofocus true
|
||||
:on-blur on-blur
|
||||
:on-focus on-focus
|
||||
:select-on-focus? false
|
||||
|
@ -134,29 +198,62 @@
|
|||
:on-change on-change
|
||||
:max-length 750}]
|
||||
(when (or @show-buttons? (seq @content))
|
||||
[:div {:class (stl/css :buttons-wrapper)}
|
||||
[:input.btn-secondary
|
||||
{:type "button"
|
||||
:class (stl/css :cancel-btn)
|
||||
:value "Cancel"
|
||||
:on-click on-cancel}]
|
||||
[:input
|
||||
{:type "button"
|
||||
:class (stl/css-case :post-btn true
|
||||
:global/disabled disabled?)
|
||||
:value "Post"
|
||||
:on-click on-submit
|
||||
:disabled disabled?}]])]))
|
||||
[:div {:class (stl/css :form-buttons-wrapper)}
|
||||
[:> button* {:variant "ghost"
|
||||
:on-click on-cancel}
|
||||
(tr "ds.confirm-cancel")]
|
||||
[:> button* {:variant "primary"
|
||||
:on-click on-submit
|
||||
:disabled disabled?}
|
||||
(tr "labels.post")]])]))
|
||||
|
||||
(mf/defc draft-thread
|
||||
(mf/defc comment-edit-form*
|
||||
{::mf/props :obj
|
||||
::mf/private true}
|
||||
[{:keys [content on-submit on-cancel]}]
|
||||
(let [content (mf/use-state content)
|
||||
|
||||
on-change
|
||||
(mf/use-fn
|
||||
#(reset! content %))
|
||||
|
||||
on-submit*
|
||||
(mf/use-fn
|
||||
(mf/deps @content)
|
||||
(fn [] (on-submit @content)))
|
||||
|
||||
disabled? (or (str/blank? @content)
|
||||
(str/empty? @content))]
|
||||
|
||||
[:div {:class (stl/css :form)}
|
||||
[:& resizing-textarea {:value @content
|
||||
:autofocus true
|
||||
:select-on-focus true
|
||||
:select-on-focus? false
|
||||
:on-ctrl-enter on-submit*
|
||||
:on-change on-change
|
||||
:max-length 750}]
|
||||
[:div {:class (stl/css :form-buttons-wrapper)}
|
||||
[:> button* {:variant "ghost"
|
||||
:on-click on-cancel}
|
||||
(tr "ds.confirm-cancel")]
|
||||
[:> button* {:variant "primary"
|
||||
:on-click on-submit*
|
||||
:disabled disabled?}
|
||||
(tr "labels.post")]]]))
|
||||
|
||||
(mf/defc comment-floating-thread-draft*
|
||||
{::mf/props :obj}
|
||||
[{:keys [draft zoom on-cancel on-submit position-modifier]}]
|
||||
(let [position (cond-> (:position draft)
|
||||
(some? position-modifier)
|
||||
(gpt/transform position-modifier))
|
||||
content (:content draft)
|
||||
(let [profile (mf/deref refs/profile)
|
||||
|
||||
pos-x (* (:x position) zoom)
|
||||
pos-y (* (:y position) zoom)
|
||||
position (cond-> (:position draft)
|
||||
(some? position-modifier)
|
||||
(gpt/transform position-modifier))
|
||||
content (:content draft)
|
||||
|
||||
pos-x (* (:x position) zoom)
|
||||
pos-y (* (:y position) zoom)
|
||||
|
||||
disabled? (or (str/blank? content)
|
||||
(str/empty? content))
|
||||
|
@ -183,17 +280,18 @@
|
|||
|
||||
[:*
|
||||
[:div
|
||||
{:class (stl/css :floating-thread-bubble)
|
||||
{:class (stl/css :floating-preview-wrapper)
|
||||
:data-testid "floating-thread-bubble"
|
||||
:style {:top (str pos-y "px")
|
||||
:left (str pos-x "px")}
|
||||
:on-click dom/stop-propagation}
|
||||
"?"]
|
||||
[:div {:class (stl/css :thread-content)
|
||||
[:> comment-avatar* {:class (stl/css :avatar-lg)
|
||||
:image (cfg/resolve-profile-photo-url profile)}]]
|
||||
[:div {:class (stl/css :floating-thread-wrapper)
|
||||
:style {:top (str (- pos-y 24) "px")
|
||||
:left (str (+ pos-x 28) "px")}
|
||||
:on-click dom/stop-propagation}
|
||||
[:div {:class (stl/css :reply-form)}
|
||||
[:div {:class (stl/css :form)}
|
||||
[:& resizing-textarea {:placeholder (tr "labels.write-new-comment")
|
||||
:value (or content "")
|
||||
:autofocus true
|
||||
|
@ -202,58 +300,89 @@
|
|||
:on-change on-change
|
||||
:on-ctrl-enter on-submit
|
||||
:max-length 750}]
|
||||
[:div {:class (stl/css :buttons-wrapper)}
|
||||
[:div {:class (stl/css :form-buttons-wrapper)}
|
||||
[:> button* {:variant "ghost"
|
||||
:on-click on-esc}
|
||||
(tr "ds.confirm-cancel")]
|
||||
[:> button* {:variant "primary"
|
||||
:on-click on-submit
|
||||
:disabled disabled?}
|
||||
(tr "labels.post")]]]]]))
|
||||
|
||||
[:input {:on-click on-esc
|
||||
:class (stl/css :cancel-btn)
|
||||
:type "button"
|
||||
:value "Cancel"}]
|
||||
(mf/defc comment-floating-thread-header*
|
||||
{::mf/props :obj
|
||||
::mf/private true}
|
||||
[{:keys [profiles thread origin]}]
|
||||
(let [owner (get profiles (:owner-id thread))
|
||||
profile (mf/deref refs/profile)
|
||||
options (mf/deref comments-local-options)
|
||||
|
||||
[:input {:on-click on-submit
|
||||
:type "button"
|
||||
:value "Post"
|
||||
:class (stl/css-case :post-btn true
|
||||
:global/disabled disabled?)
|
||||
:disabled disabled?}]]]]]))
|
||||
|
||||
(mf/defc edit-form
|
||||
[{:keys [content on-submit on-cancel] :as props}]
|
||||
(let [content (mf/use-state content)
|
||||
|
||||
on-change
|
||||
toggle-resolved
|
||||
(mf/use-fn
|
||||
#(reset! content %))
|
||||
(mf/deps thread)
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(st/emit! (dcm/update-comment-thread (update thread :is-resolved not)))))
|
||||
|
||||
on-submit*
|
||||
on-toggle-options
|
||||
(mf/use-fn
|
||||
(mf/deps @content)
|
||||
(fn [] (on-submit @content)))
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(st/emit! (dcm/toggle-comment-options uuid/zero))))
|
||||
|
||||
disabled? (or (str/blank? @content)
|
||||
(str/empty? @content))]
|
||||
delete-thread
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(st/emit! (dcm/close-thread)
|
||||
(if (= origin :viewer)
|
||||
(dcm/delete-comment-thread-on-viewer thread)
|
||||
(dcm/delete-comment-thread-on-workspace thread)))))
|
||||
|
||||
[:div {:class (stl/css :edit-form)}
|
||||
[:& resizing-textarea {:value @content
|
||||
:autofocus true
|
||||
:select-on-focus true
|
||||
:select-on-focus? false
|
||||
:on-ctrl-enter on-submit*
|
||||
:on-change on-change
|
||||
:max-length 750}]
|
||||
[:div {:class (stl/css :buttons-wrapper)}
|
||||
[:input {:type "button"
|
||||
:value "Cancel"
|
||||
:class (stl/css :cancel-btn)
|
||||
:on-click on-cancel}]
|
||||
[:input {:type "button"
|
||||
:class (stl/css-case :post-btn true
|
||||
:global/disabled disabled?)
|
||||
:value "Post"
|
||||
:on-click on-submit*
|
||||
:disabled disabled?}]]]))
|
||||
on-delete-thread
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(st/emit! (dcm/hide-comment-options))
|
||||
(st/emit! (modal/show
|
||||
{:type :confirm
|
||||
:title (tr "modals.delete-comment-thread.title")
|
||||
:message (tr "modals.delete-comment-thread.message")
|
||||
:accept-label (tr "modals.delete-comment-thread.accept")
|
||||
:on-accept delete-thread}))))
|
||||
|
||||
(mf/defc comment-item
|
||||
[{:keys [comment thread profiles origin] :as props}]
|
||||
on-hide-options
|
||||
(mf/use-fn
|
||||
(mf/deps options)
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(st/emit! (dcm/hide-comment-options))))]
|
||||
|
||||
[:*
|
||||
[:div {:class (stl/css :floating-thread-header-left)}
|
||||
(tr "labels.comment") " " [:span {:class (stl/css :grayed-text)} "#" (:seqn thread)]]
|
||||
[:div {:class (stl/css :floating-thread-header-right)}
|
||||
(when (some? thread)
|
||||
[:div {:class (stl/css :checkbox-wrapper)
|
||||
:title (tr "labels.comment.mark-as-solved")
|
||||
:on-click toggle-resolved}
|
||||
[:span {:class (stl/css-case :checkbox true
|
||||
:global/checked (:is-resolved thread))} i/tick]])
|
||||
(when (= (:id profile) (:id owner))
|
||||
[:> icon-button* {:variant "ghost"
|
||||
:aria-label (tr "labels.options")
|
||||
:on-click on-toggle-options
|
||||
:icon "menu"}])]
|
||||
[:& dropdown {:show (= options uuid/zero)
|
||||
:on-close on-hide-options}
|
||||
[:ul {:class (stl/css :dropdown-menu)}
|
||||
[:li {:class (stl/css :dropdown-menu-option)
|
||||
:on-click on-delete-thread}
|
||||
(tr "labels.delete-comment-thread")]]]]))
|
||||
|
||||
(mf/defc comment-floating-thread-item*
|
||||
{::mf/props :obj
|
||||
::mf/private true}
|
||||
[{:keys [comment thread profiles]}]
|
||||
(let [owner (get profiles (:owner-id comment))
|
||||
profile (mf/deref refs/profile)
|
||||
options (mf/deref comments-local-options)
|
||||
|
@ -264,7 +393,7 @@
|
|||
(mf/deps options)
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(st/emit! (dcm/toggle-comment-options comment))))
|
||||
(st/emit! (dcm/toggle-comment-options (:id comment)))))
|
||||
|
||||
on-hide-options
|
||||
(mf/use-fn
|
||||
|
@ -285,24 +414,6 @@
|
|||
(mf/deps comment)
|
||||
#(st/emit! (dcm/delete-comment comment)))
|
||||
|
||||
delete-thread
|
||||
(mf/use-fn
|
||||
(mf/deps thread)
|
||||
#(st/emit! (dcm/close-thread)
|
||||
(if (= origin :viewer)
|
||||
(dcm/delete-comment-thread-on-viewer thread)
|
||||
(dcm/delete-comment-thread-on-workspace thread))))
|
||||
|
||||
on-delete-thread
|
||||
(mf/use-fn
|
||||
(mf/deps thread)
|
||||
#(st/emit! (modal/show
|
||||
{:type :confirm
|
||||
:title (tr "modals.delete-comment-thread.title")
|
||||
:message (tr "modals.delete-comment-thread.message")
|
||||
:accept-label (tr "modals.delete-comment-thread.accept")
|
||||
:on-accept delete-thread})))
|
||||
|
||||
on-submit
|
||||
(mf/use-fn
|
||||
(mf/deps comment thread)
|
||||
|
@ -311,29 +422,15 @@
|
|||
(st/emit! (dcm/update-comment (assoc comment :content content)))))
|
||||
|
||||
on-cancel
|
||||
(mf/use-fn #(reset! edition? false))
|
||||
(mf/use-fn #(reset! edition? false))]
|
||||
|
||||
toggle-resolved
|
||||
(mf/use-fn
|
||||
(mf/deps thread)
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(st/emit! (dcm/update-comment-thread (update thread :is-resolved not)))))]
|
||||
|
||||
[:div {:class (stl/css :comment-container)}
|
||||
[:div {:class (stl/css :comment)}
|
||||
[:div {:class (stl/css :floating-thread-item-wrapper)}
|
||||
[:div {:class (stl/css :floating-thread-item)}
|
||||
[:div {:class (stl/css :author)}
|
||||
[:div {:class (stl/css :avatar)}
|
||||
[:img {:src (cfg/resolve-profile-photo-url owner)}]]
|
||||
[:div {:class (stl/css :name)}
|
||||
[:div {:class (stl/css :fullname)} (:fullname owner)]
|
||||
[:div {:class (stl/css :timeago)} (dt/timeago (:modified-at comment))]]
|
||||
|
||||
(when (some? thread)
|
||||
[:div {:class (stl/css :options-resolve-wrapper)
|
||||
:on-click toggle-resolved}
|
||||
[:span {:class (stl/css-case :options-resolve true
|
||||
:global/checked (:is-resolved thread))} i/tick]])
|
||||
[:> comment-avatar* {:image (cfg/resolve-profile-photo-url owner)}]
|
||||
[:div {:class (stl/css :author-identity)}
|
||||
[:div {:class (stl/css :author-fullname)} (:fullname owner)]
|
||||
[:div {:class (stl/css :author-timeago)} (dt/timeago (:modified-at comment))]]
|
||||
|
||||
(when (= (:id profile) (:id owner))
|
||||
[:> icon-button* {:variant "ghost"
|
||||
|
@ -341,24 +438,21 @@
|
|||
:on-click on-toggle-options
|
||||
:icon "menu"}])]
|
||||
|
||||
[:div {:class (stl/css :content)}
|
||||
[:div {:class (stl/css :item)}
|
||||
(if @edition?
|
||||
[:& edit-form {:content (:content comment)
|
||||
:on-submit on-submit
|
||||
:on-cancel on-cancel}]
|
||||
[:> comment-edit-form* {:content (:content comment)
|
||||
:on-submit on-submit
|
||||
:on-cancel on-cancel}]
|
||||
[:span {:class (stl/css :text)} (:content comment)])]]
|
||||
|
||||
[:& dropdown {:show (= options (:id comment))
|
||||
:on-close on-hide-options}
|
||||
[:ul {:class (stl/css :comment-options-dropdown)}
|
||||
[:li {:class (stl/css :context-menu-option)
|
||||
[:ul {:class (stl/css :dropdown-menu)}
|
||||
[:li {:class (stl/css :dropdown-menu-option)
|
||||
:on-click on-edit-clicked}
|
||||
(tr "labels.edit")]
|
||||
(if thread
|
||||
[:li {:class (stl/css :context-menu-option)
|
||||
:on-click on-delete-thread}
|
||||
(tr "labels.delete-comment-thread")]
|
||||
[:li {:class (stl/css :context-menu-option)
|
||||
(when-not thread
|
||||
[:li {:class (stl/css :dropdown-menu-option)
|
||||
:on-click on-delete-comment}
|
||||
(tr "labels.delete-comment")])]]]))
|
||||
|
||||
|
@ -370,48 +464,54 @@
|
|||
(let [viewport (or viewport {:offset-x 0 :offset-y 0 :width 0 :height 0})
|
||||
base-x (+ (* (:x position) zoom) (:offset-x viewport))
|
||||
base-y (+ (* (:y position) zoom) (:offset-y viewport))
|
||||
|
||||
x (:x position)
|
||||
y (:y position)
|
||||
|
||||
w (:width viewport)
|
||||
h (:height viewport)
|
||||
|
||||
comment-width 284 ;; TODO: this is the width set via CSS in an outer container…
|
||||
;; We should probably do this in a different way.
|
||||
|
||||
orientation-left? (>= (+ base-x comment-width (:x bubble-margin)) w)
|
||||
orientation-top? (>= base-y (/ h 2))
|
||||
orientation-top? (>= base-y (/ h 2))
|
||||
|
||||
h-dir (if orientation-left? :left :right)
|
||||
v-dir (if orientation-top? :top :bottom)
|
||||
x (:x position)
|
||||
y (:y position)]
|
||||
v-dir (if orientation-top? :top :bottom)]
|
||||
{:x x :y y :h-dir h-dir :v-dir v-dir}))
|
||||
|
||||
(mf/defc thread-comments
|
||||
{::mf/wrap [mf/memo]}
|
||||
(mf/defc comment-floating-thread*
|
||||
{::mf/props :obj
|
||||
::mf/wrap [mf/memo]}
|
||||
[{:keys [thread zoom profiles origin position-modifier viewport]}]
|
||||
(let [ref (mf/use-ref)
|
||||
thread-id (:id thread)
|
||||
thread-pos (:position thread)
|
||||
(let [ref (mf/use-ref)
|
||||
thread-id (:id thread)
|
||||
thread-pos (:position thread)
|
||||
|
||||
base-pos (cond-> thread-pos
|
||||
(some? position-modifier)
|
||||
(gpt/transform position-modifier))
|
||||
base-pos (cond-> thread-pos
|
||||
(some? position-modifier)
|
||||
(gpt/transform position-modifier))
|
||||
|
||||
max-height (when (some? viewport) (int (* (:height viewport) 0.75)))
|
||||
max-height (when (some? viewport) (int (* (:height viewport) 0.75)))
|
||||
;; We should probably look for a better way of doing this.
|
||||
bubble-margin {:x 24 :y 0}
|
||||
pos (offset-position base-pos viewport zoom bubble-margin)
|
||||
bubble-margin {:x 24 :y 24}
|
||||
pos (offset-position base-pos viewport zoom bubble-margin)
|
||||
|
||||
margin-x (* (:x bubble-margin) (if (= (:h-dir pos) :left) -1 1))
|
||||
margin-y (* (:y bubble-margin) (if (= (:v-dir pos) :top) -1 1))
|
||||
pos-x (+ (* (:x pos) zoom) margin-x)
|
||||
pos-y (- (* (:y pos) zoom) margin-y)
|
||||
margin-x (* (:x bubble-margin) (if (= (:h-dir pos) :left) -1 1))
|
||||
margin-y (* (:y bubble-margin) (if (= (:v-dir pos) :top) -1 1))
|
||||
pos-x (+ (* (:x pos) zoom) margin-x)
|
||||
pos-y (- (* (:y pos) zoom) margin-y)
|
||||
|
||||
comments-ref (mf/with-memo [thread-id]
|
||||
(make-comments-ref thread-id))
|
||||
comments-map (mf/deref comments-ref)
|
||||
comments-ref (mf/with-memo [thread-id]
|
||||
(make-comments-ref thread-id))
|
||||
comments-map (mf/deref comments-ref)
|
||||
|
||||
comments (mf/with-memo [comments-map]
|
||||
(->> (vals comments-map)
|
||||
(sort-by :created-at)))
|
||||
comments (mf/with-memo [comments-map]
|
||||
(->> (vals comments-map)
|
||||
(sort-by :created-at)))
|
||||
|
||||
comment (first comments)]
|
||||
first-comment (first comments)]
|
||||
|
||||
(mf/with-effect [thread-id]
|
||||
(st/emit! (dcm/retrieve-comments thread-id)))
|
||||
|
@ -423,158 +523,170 @@
|
|||
(when-let [node (mf/ref-val ref)]
|
||||
(dom/scroll-into-view-if-needed! node)))
|
||||
|
||||
(when (some? comment)
|
||||
[:div {:class (stl/css-case :thread-content true
|
||||
:thread-content-left (= (:h-dir pos) :left)
|
||||
:thread-content-top (= (:v-dir pos) :top))
|
||||
(when (some? first-comment)
|
||||
[:div {:class (stl/css-case :floating-thread-wrapper true
|
||||
:left (= (:h-dir pos) :left)
|
||||
:top (= (:v-dir pos) :top))
|
||||
:id (str "thread-" thread-id)
|
||||
:style {:left (str pos-x "px")
|
||||
:top (str pos-y "px")
|
||||
:max-height max-height}
|
||||
:on-click dom/stop-propagation}
|
||||
|
||||
[:div {:class (stl/css :comments)}
|
||||
[:& comment-item {:comment comment
|
||||
:profiles profiles
|
||||
:thread thread
|
||||
:origin origin}]
|
||||
[:div {:class (stl/css :floating-thread-header)}
|
||||
[:> comment-floating-thread-header* {:profiles profiles
|
||||
:thread thread
|
||||
:origin origin}]]
|
||||
|
||||
[:div {:class (stl/css :floating-thread-main)}
|
||||
[:> comment-floating-thread-item* {:comment first-comment
|
||||
:profiles profiles
|
||||
:thread thread}]
|
||||
(for [item (rest comments)]
|
||||
[:* {:key (dm/str (:id item))}
|
||||
[:& comment-item {:comment item
|
||||
:profiles profiles
|
||||
:origin origin}]])]
|
||||
[:& reply-form {:thread thread}]
|
||||
[:div {:ref ref}]])))
|
||||
[:> comment-floating-thread-item* {:comment item
|
||||
:profiles profiles}]])]
|
||||
|
||||
(defn use-buble
|
||||
[zoom {:keys [position frame-id]}]
|
||||
(let [dragging-ref (mf/use-ref false)
|
||||
[:> comment-reply-form* {:thread thread}]])))
|
||||
|
||||
(mf/defc comment-floating-bubble*
|
||||
{::mf/props :obj
|
||||
::mf/wrap [mf/memo]}
|
||||
[{:keys [thread profiles zoom is-open on-click origin position-modifier]}]
|
||||
(let [owner (get profiles (:owner-id thread))
|
||||
|
||||
base-pos (cond-> (:position thread)
|
||||
(some? position-modifier)
|
||||
(gpt/transform position-modifier))
|
||||
|
||||
drag? (mf/use-ref nil)
|
||||
was-open? (mf/use-ref nil)
|
||||
|
||||
dragging-ref (mf/use-ref false)
|
||||
start-ref (mf/use-ref nil)
|
||||
|
||||
state (mf/use-state {:hover false
|
||||
position (:position thread)
|
||||
frame-id (:frame-id thread)
|
||||
|
||||
state (mf/use-state {:hover? false
|
||||
:grabbing? false
|
||||
:new-position-x nil
|
||||
:new-position-y nil
|
||||
:new-frame-id frame-id})
|
||||
|
||||
pos-x (floor (* (or (:new-position-x @state) (:x base-pos)) zoom))
|
||||
pos-y (floor (* (or (:new-position-y @state) (:y base-pos)) zoom))
|
||||
|
||||
on-pointer-down
|
||||
(mf/use-fn
|
||||
(mf/deps origin was-open? is-open drag?)
|
||||
(fn [event]
|
||||
(dom/capture-pointer event)
|
||||
(mf/set-ref-val! dragging-ref true)
|
||||
(mf/set-ref-val! start-ref (dom/get-client-position event))))
|
||||
(when (not= origin :viewer)
|
||||
(swap! state assoc :grabbing? true)
|
||||
(mf/set-ref-val! was-open? is-open)
|
||||
(when is-open (st/emit! (dcm/close-thread)))
|
||||
(mf/set-ref-val! drag? false)
|
||||
(dom/stop-propagation event)
|
||||
(dom/capture-pointer event)
|
||||
(mf/set-ref-val! dragging-ref true)
|
||||
(mf/set-ref-val! start-ref (dom/get-client-position event)))))
|
||||
|
||||
on-pointer-up
|
||||
(mf/use-fn
|
||||
(mf/deps (select-keys @state [:new-position-x :new-position-y :new-frame-id]))
|
||||
(fn [_ thread]
|
||||
(when (and
|
||||
(some? (:new-position-x @state))
|
||||
(some? (:new-position-y @state)))
|
||||
(st/emit! (dwcm/update-comment-thread-position thread [(:new-position-x @state) (:new-position-y @state)])))))
|
||||
|
||||
on-lost-pointer-capture
|
||||
(mf/use-fn
|
||||
(mf/deps origin thread (select-keys @state [:new-position-x :new-position-y :new-frame-id]))
|
||||
(fn [event]
|
||||
(dom/release-pointer event)
|
||||
(mf/set-ref-val! dragging-ref false)
|
||||
(mf/set-ref-val! start-ref nil)
|
||||
(swap! state assoc :new-position-x nil)
|
||||
(swap! state assoc :new-position-y nil)))
|
||||
(when (not= origin :viewer)
|
||||
(swap! state assoc :grabbing? false)
|
||||
(dom/stop-propagation event)
|
||||
(dom/release-pointer event)
|
||||
(mf/set-ref-val! dragging-ref false)
|
||||
(mf/set-ref-val! start-ref nil)
|
||||
(when (and
|
||||
(some? (:new-position-x @state))
|
||||
(some? (:new-position-y @state)))
|
||||
(st/emit! (dwcm/update-comment-thread-position thread [(:new-position-x @state)
|
||||
(:new-position-y @state)]))
|
||||
(swap! state assoc
|
||||
:new-position-x nil
|
||||
:new-position-y nil)))))
|
||||
|
||||
on-pointer-move
|
||||
(mf/use-fn
|
||||
(mf/deps position zoom)
|
||||
(fn [event]
|
||||
(when-let [_ (mf/ref-val dragging-ref)]
|
||||
(let [start-pt (mf/ref-val start-ref)
|
||||
current-pt (dom/get-client-position event)
|
||||
delta-x (/ (- (:x current-pt) (:x start-pt)) zoom)
|
||||
delta-y (/ (- (:y current-pt) (:y start-pt)) zoom)]
|
||||
(swap! state assoc
|
||||
:new-position-x (+ (:x position) delta-x)
|
||||
:new-position-y (+ (:y position) delta-y))))))]
|
||||
|
||||
{:on-pointer-down on-pointer-down
|
||||
:on-pointer-up on-pointer-up
|
||||
:on-pointer-move on-pointer-move
|
||||
:on-lost-pointer-capture on-lost-pointer-capture
|
||||
:state state}))
|
||||
|
||||
(mf/defc thread-bubble
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [thread zoom open? on-click origin position-modifier]}]
|
||||
(let [pos (cond-> (:position thread)
|
||||
(some? position-modifier)
|
||||
(gpt/transform position-modifier))
|
||||
|
||||
drag? (mf/use-ref nil)
|
||||
was-open? (mf/use-ref nil)
|
||||
|
||||
{:keys [on-pointer-down
|
||||
on-pointer-up
|
||||
on-pointer-move
|
||||
state
|
||||
on-lost-pointer-capture]} (use-buble zoom thread)
|
||||
|
||||
pos-x (* (or (:new-position-x @state) (:x pos)) zoom)
|
||||
pos-y (* (or (:new-position-y @state) (:y pos)) zoom)
|
||||
|
||||
on-pointer-down*
|
||||
(mf/use-fn
|
||||
(mf/deps origin was-open? open? drag? on-pointer-down)
|
||||
(fn [event]
|
||||
(when (not= origin :viewer)
|
||||
(mf/set-ref-val! was-open? open?)
|
||||
(when open? (st/emit! (dcm/close-thread)))
|
||||
(mf/set-ref-val! drag? false)
|
||||
(dom/stop-propagation event)
|
||||
(on-pointer-down event))))
|
||||
|
||||
on-pointer-up*
|
||||
(mf/use-fn
|
||||
(mf/deps origin thread was-open? drag? on-pointer-up)
|
||||
(fn [event]
|
||||
(when (not= origin :viewer)
|
||||
(dom/stop-propagation event)
|
||||
(on-pointer-up event thread)
|
||||
|
||||
(when (or (and (mf/ref-val was-open?) (mf/ref-val drag?))
|
||||
(and (not (mf/ref-val was-open?)) (not (mf/ref-val drag?))))
|
||||
(st/emit! (dcm/open-thread thread))))))
|
||||
|
||||
on-pointer-move*
|
||||
(mf/use-fn
|
||||
(mf/deps origin drag? on-pointer-move)
|
||||
(mf/deps origin drag? position zoom)
|
||||
(fn [event]
|
||||
(when (not= origin :viewer)
|
||||
(mf/set-ref-val! drag? true)
|
||||
(dom/stop-propagation event)
|
||||
(on-pointer-move event))))
|
||||
(when-let [_ (mf/ref-val dragging-ref)]
|
||||
(let [start-pt (mf/ref-val start-ref)
|
||||
current-pt (dom/get-client-position event)
|
||||
delta-x (/ (- (:x current-pt) (:x start-pt)) zoom)
|
||||
delta-y (/ (- (:y current-pt) (:y start-pt)) zoom)]
|
||||
(swap! state assoc
|
||||
:new-position-x (+ (:x position) delta-x)
|
||||
:new-position-y (+ (:y position) delta-y)))))))
|
||||
|
||||
on-pointer-enter
|
||||
(mf/use-fn
|
||||
(mf/deps is-open)
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(when (false? is-open)
|
||||
(swap! state assoc :hover? true))))
|
||||
|
||||
on-pointer-leave
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(swap! state assoc :hover? false)))
|
||||
|
||||
on-click*
|
||||
(mf/use-fn
|
||||
(mf/deps origin thread on-click)
|
||||
(mf/deps origin thread on-click was-open? drag? (select-keys @state [:hover?]))
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(when (or (and (mf/ref-val was-open?) (mf/ref-val drag?))
|
||||
(and (not (mf/ref-val was-open?)) (not (mf/ref-val drag?))))
|
||||
(swap! state assoc :hover? false)
|
||||
(st/emit! (dcm/open-thread thread)))
|
||||
(when (= origin :viewer)
|
||||
(on-click thread))))]
|
||||
|
||||
[:div {:style {:top (str pos-y "px")
|
||||
:left (str pos-x "px")}
|
||||
:on-pointer-down on-pointer-down*
|
||||
:on-pointer-up on-pointer-up*
|
||||
:on-pointer-move on-pointer-move*
|
||||
:on-pointer-down on-pointer-down
|
||||
:on-pointer-up on-pointer-up
|
||||
:on-pointer-move on-pointer-move
|
||||
:on-pointer-enter on-pointer-enter
|
||||
:on-pointer-leave on-pointer-leave
|
||||
:on-click on-click*
|
||||
:on-lost-pointer-capture on-lost-pointer-capture
|
||||
:data-testid "floating-thread-bubble"
|
||||
:class (stl/css-case
|
||||
:floating-thread-bubble true
|
||||
:resolved (:is-resolved thread)
|
||||
:unread (pos? (:count-unread-comments thread)))}
|
||||
[:span (:seqn thread)]]))
|
||||
:class (stl/css-case :floating-preview-wrapper true
|
||||
:floating-preview-bubble (false? (:hover? @state))
|
||||
:grabbing (true? (:grabbing? @state)))}
|
||||
|
||||
(mf/defc comment-thread
|
||||
(if (true? (:hover? @state))
|
||||
|
||||
[:div {:class (stl/css :floating-thread-wrapper :floating-preview-displacement)}
|
||||
[:div {:class (stl/css :floating-thread-item-wrapper)}
|
||||
[:div {:class (stl/css :floating-thread-item)}
|
||||
[:> comment-info* {:item thread
|
||||
:profile owner}]]]]
|
||||
|
||||
[:> comment-avatar* {:image (cfg/resolve-profile-photo-url owner)
|
||||
:class (stl/css :avatar-lg)
|
||||
:data-testid (str "floating-thread-bubble-" (:seqn thread))
|
||||
:variant (cond (:is-resolved thread) "solved"
|
||||
(pos? (:count-unread-comments thread)) "unread"
|
||||
:else "read")}])]))
|
||||
|
||||
(mf/defc comment-sidebar-thread-item*
|
||||
{::mf/props :obj
|
||||
::mf/private true}
|
||||
[{:keys [item profiles on-click]}]
|
||||
(let [owner (get profiles (:owner-id item))
|
||||
|
||||
frame (mf/deref (refs/workspace-page-object-by-id (:page-id item) (:frame-id item)))
|
||||
|
||||
on-click*
|
||||
(mf/use-fn
|
||||
(mf/deps item)
|
||||
|
@ -584,52 +696,64 @@
|
|||
(when (fn? on-click)
|
||||
(on-click item))))]
|
||||
|
||||
[:div {:class (stl/css :comment)
|
||||
[:div {:class (stl/css :cover)
|
||||
:on-click on-click*}
|
||||
[:div {:class (stl/css :author)}
|
||||
[:div {:class (stl/css-case :thread-bubble true
|
||||
:resolved (:is-resolved item)
|
||||
:unread (pos? (:count-unread-comments item)))}
|
||||
(:seqn item)]
|
||||
[:div {:class (stl/css :avatar)}
|
||||
[:img {:src (cfg/resolve-profile-photo-url owner)}]]
|
||||
[:div {:class (stl/css :name)}
|
||||
[:div {:class (stl/css :fullname)} (:fullname owner)]
|
||||
[:div {:class (stl/css :timeago)} (dt/timeago (:modified-at item))]]]
|
||||
[:div {:class (stl/css :content)}
|
||||
(:content item)]
|
||||
[:div {:class (stl/css :replies)}
|
||||
(let [unread (:count-unread-comments item ::none)
|
||||
total (:count-comments item 1)]
|
||||
[:*
|
||||
(when (> total 1)
|
||||
(if (= total 2)
|
||||
[:span {:class (stl/css :total-replies)} "1 reply"]
|
||||
[:span {:class (stl/css :total-replies)} (str (dec total) " replies")]))
|
||||
[:div {:class (stl/css :location)}
|
||||
[:div {:class (stl/css :location-text)}
|
||||
(str "#" (:seqn item))
|
||||
(str " - " (:page-name item))
|
||||
(when (and (some? frame) (not (cfh/root? frame)))
|
||||
(str " - " (:name frame)))]]
|
||||
|
||||
(when (and (> total 1) (> unread 0))
|
||||
(if (= unread 1)
|
||||
[:span {:class (stl/css :new-replies)} "1 new reply"]
|
||||
[:span {:class (stl/css :new-replies)} (str unread " new replies")]))])]]))
|
||||
[:> comment-info* {:item item
|
||||
:profile owner}]]))
|
||||
|
||||
(mf/defc comment-thread-group
|
||||
(mf/defc comment-sidebar-thread-group*
|
||||
{::mf/props :obj}
|
||||
[{:keys [group profiles on-thread-click]}]
|
||||
[:div {:class (stl/css :thread-group)}
|
||||
(if (:file-name group)
|
||||
[:div {:class (stl/css :section-title)
|
||||
:title (str (:file-name group) ", " (:page-name group))}
|
||||
[:span {:class (stl/css :file-name)} (:file-name group) ", "]
|
||||
[:span {:class (stl/css :page-name)} (:page-name group)]]
|
||||
[:div
|
||||
(for [item (:items group)]
|
||||
[:> comment-sidebar-thread-item*
|
||||
{:item item
|
||||
:on-click on-thread-click
|
||||
:profiles profiles
|
||||
:key (:id item)}])])
|
||||
|
||||
[:div {:class (stl/css :section-title)
|
||||
:title (:page-name group)}
|
||||
[:span {:class (stl/css :icon)} i/document]
|
||||
[:span {:class (stl/css :page-name)} (:page-name group)]])
|
||||
(mf/defc comment-dashboard-thread-item*
|
||||
{::mf/props :obj
|
||||
::mf/private true}
|
||||
[{:keys [item profiles on-click]}]
|
||||
(let [owner (get profiles (:owner-id item))
|
||||
|
||||
[:div {:class (stl/css :threads)}
|
||||
(for [item (:items group)]
|
||||
[:& comment-thread
|
||||
{:item item
|
||||
:on-click on-thread-click
|
||||
:profiles profiles
|
||||
:key (:id item)}])]])
|
||||
on-click*
|
||||
(mf/use-fn
|
||||
(mf/deps item)
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(dom/prevent-default event)
|
||||
(when (fn? on-click)
|
||||
(on-click item))))]
|
||||
|
||||
[:div {:class (stl/css :cover)
|
||||
:on-click on-click*}
|
||||
[:div {:class (stl/css :location)}
|
||||
[:> icon* {:id "comments"
|
||||
:class (stl/css :location-icon)}]
|
||||
[:div {:class (stl/css :location-text)}
|
||||
(str "#" (:seqn item))
|
||||
(str " " (:file-name item))
|
||||
(str ", " (:page-name item))]]
|
||||
|
||||
[:> comment-info* {:item item
|
||||
:profile owner}]]))
|
||||
|
||||
(mf/defc comment-dashboard-thread-group*
|
||||
{::mf/props :obj}
|
||||
[{:keys [group profiles on-thread-click]}]
|
||||
[:div
|
||||
(for [item (:items group)]
|
||||
[:> comment-dashboard-thread-item*
|
||||
{:item item
|
||||
:on-click on-thread-click
|
||||
:profiles profiles
|
||||
:key (:id item)}])])
|
||||
|
|
|
@ -6,103 +6,98 @@
|
|||
|
||||
@import "refactor/common-refactor.scss";
|
||||
|
||||
// Comment-thread-group
|
||||
.thread-group {
|
||||
padding: 0 $s-12;
|
||||
cursor: pointer;
|
||||
border-radius: $br-8;
|
||||
padding: $s-8 $s-16;
|
||||
|
||||
&:hover {
|
||||
background: var(--comment-thread-background-color-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
@include bodySmallTypography;
|
||||
height: $s-32;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: $s-8;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
@include textEllipsis;
|
||||
.grayed-text {
|
||||
color: var(--comment-subtitle-color);
|
||||
}
|
||||
|
||||
.page-name {
|
||||
@include textEllipsis;
|
||||
.location {
|
||||
color: var(--comment-subtitle-color);
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 $s-6 0 $s-4;
|
||||
width: $s-24;
|
||||
height: $s-32;
|
||||
margin-left: $s-6;
|
||||
svg {
|
||||
@extend .button-icon-small;
|
||||
stroke: var(--icon-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.threads {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $s-24;
|
||||
}
|
||||
|
||||
// Comment-thread
|
||||
.comment {
|
||||
@include bodySmallTypography;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $s-12;
|
||||
}
|
||||
|
||||
.author {
|
||||
display: flex;
|
||||
gap: $s-8;
|
||||
}
|
||||
|
||||
.thread-bubble {
|
||||
@extend .comment-bubbles;
|
||||
&.resolved {
|
||||
@extend .resolved-comment-bubble;
|
||||
}
|
||||
&.unread {
|
||||
@extend .unread-comment-bubble;
|
||||
}
|
||||
.location-icon {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.location-text {
|
||||
@include textEllipsis;
|
||||
}
|
||||
|
||||
.author {
|
||||
@include bodySmallTypography;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $s-8;
|
||||
}
|
||||
|
||||
.author-identity {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.author-fullname {
|
||||
@include textEllipsis;
|
||||
color: var(--comment-title-color);
|
||||
}
|
||||
|
||||
.author-timeago {
|
||||
@include textEllipsis;
|
||||
color: var(--comment-subtitle-color);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
position: relative;
|
||||
height: $s-24;
|
||||
width: $s-24;
|
||||
border-radius: $br-circle;
|
||||
}
|
||||
|
||||
.avatar-lg {
|
||||
height: $s-32;
|
||||
width: $s-32;
|
||||
}
|
||||
|
||||
.avatar-read {
|
||||
border: $s-2 solid var(--color-background-tertiary);
|
||||
}
|
||||
|
||||
.avatar-unread {
|
||||
border: $s-2 solid var(--color-accent-primary);
|
||||
}
|
||||
|
||||
.avatar-solved {
|
||||
border: $s-2 solid var(--color-background-tertiary);
|
||||
}
|
||||
|
||||
.avatar-image {
|
||||
border-radius: $br-circle;
|
||||
img {
|
||||
border-radius: $br-circle;
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
flex-grow: 1;
|
||||
.fullname {
|
||||
@include textEllipsis;
|
||||
color: var(--comment-title-color);
|
||||
}
|
||||
.timeago {
|
||||
@include textEllipsis;
|
||||
color: var(--comment-subtitle-color);
|
||||
}
|
||||
.avatar-mask {
|
||||
border-radius: $br-circle;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
.avatar-darken {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.cover {
|
||||
@include bodySmallTypography;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $s-8;
|
||||
padding: $s-20;
|
||||
border-bottom: $s-1 solid var(--color-background-quaternary);
|
||||
}
|
||||
|
||||
.item {
|
||||
@include bodySmallTypography;
|
||||
color: var(--color-foreground-primary);
|
||||
word-wrap: break-word;
|
||||
|
@ -112,119 +107,128 @@
|
|||
}
|
||||
|
||||
.replies {
|
||||
@include bodySmallTypography;
|
||||
display: flex;
|
||||
gap: $s-8;
|
||||
}
|
||||
|
||||
.total-replies {
|
||||
.replies-total {
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
.new-replies {
|
||||
.replies-unread {
|
||||
color: var(--color-accent-primary);
|
||||
}
|
||||
// Thread-bubble
|
||||
|
||||
.floating-thread-bubble {
|
||||
@extend .comment-bubbles;
|
||||
.floating-preview-wrapper {
|
||||
z-index: $z-index-1;
|
||||
position: absolute;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
transform: translate(calc(-1 * $s-16), calc(-1 * $s-16));
|
||||
|
||||
&.resolved {
|
||||
@extend .resolved-comment-bubble;
|
||||
}
|
||||
&.unread {
|
||||
@extend .unread-comment-bubble;
|
||||
}
|
||||
}
|
||||
|
||||
// thread-content
|
||||
.thread-content {
|
||||
position: absolute;
|
||||
overflow-y: auto;
|
||||
width: $s-284;
|
||||
padding: $s-12;
|
||||
padding-inline-end: $s-8;
|
||||
.floating-preview-bubble {
|
||||
z-index: initial;
|
||||
}
|
||||
|
||||
.floating-preview-displacement {
|
||||
margin-left: calc(-1 * ($s-12 + $s-2));
|
||||
margin-top: calc(-1 * ($s-8 + $s-2));
|
||||
}
|
||||
|
||||
.grabbing {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.floating-thread-wrapper {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $s-12;
|
||||
width: $s-284;
|
||||
padding: $s-8 $s-12 $s-8 $s-12;
|
||||
pointer-events: auto;
|
||||
user-select: text;
|
||||
border-radius: $br-8;
|
||||
border: $s-2 solid var(--modal-border-color);
|
||||
background-color: var(--comment-modal-background-color);
|
||||
--translate-x: 0%;
|
||||
--translate-y: 0%;
|
||||
transform: translate(var(--translate-x), var(--translate-y));
|
||||
.comments {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $s-24;
|
||||
&.left {
|
||||
--translate-x: -100%;
|
||||
}
|
||||
&.top {
|
||||
--translate-y: -100%;
|
||||
}
|
||||
}
|
||||
|
||||
.thread-content-left {
|
||||
--translate-x: -100%;
|
||||
}
|
||||
.thread-content-top {
|
||||
--translate-y: -100%;
|
||||
}
|
||||
|
||||
// comment-item
|
||||
|
||||
.comment-container {
|
||||
.floating-thread-header {
|
||||
position: relative;
|
||||
.comment {
|
||||
@include bodySmallTypography;
|
||||
.author {
|
||||
display: flex;
|
||||
gap: $s-8;
|
||||
.avatar {
|
||||
height: $s-32;
|
||||
width: $s-32;
|
||||
border-radius: $br-circle;
|
||||
img {
|
||||
border-radius: $br-circle;
|
||||
}
|
||||
}
|
||||
.name {
|
||||
flex-grow: 1;
|
||||
.fullname {
|
||||
@include textEllipsis;
|
||||
color: var(--comment-title-color);
|
||||
}
|
||||
.timeago {
|
||||
@include textEllipsis;
|
||||
color: var(--comment-subtitle-color);
|
||||
}
|
||||
}
|
||||
.options-resolve-wrapper {
|
||||
@include flexCenter;
|
||||
width: $s-16;
|
||||
height: $s-32;
|
||||
.options-resolve {
|
||||
@extend .checkbox-icon;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.comment-options-dropdown {
|
||||
@extend .dropdown-wrapper;
|
||||
position: absolute;
|
||||
width: fit-content;
|
||||
max-width: $s-200;
|
||||
right: 0;
|
||||
left: unset;
|
||||
.context-menu-option {
|
||||
@extend .dropdown-element-base;
|
||||
}
|
||||
}
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: $s-32;
|
||||
}
|
||||
|
||||
// edit-form & reply-form
|
||||
.floating-thread-header-left {
|
||||
@include bodySmallTypography;
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
|
||||
.edit-form,
|
||||
.reply-form {
|
||||
.floating-thread-header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.floating-thread-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $s-16;
|
||||
overflow-y: auto;
|
||||
padding-bottom: $s-16;
|
||||
}
|
||||
|
||||
.floating-thread-item-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.floating-thread-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $s-8;
|
||||
@include bodySmallTypography;
|
||||
}
|
||||
|
||||
.checkbox-wrapper {
|
||||
@include flexCenter;
|
||||
width: $s-16;
|
||||
height: $s-24;
|
||||
margin-right: $s-8;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
@extend .checkbox-icon;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
@extend .dropdown-wrapper;
|
||||
position: absolute;
|
||||
width: fit-content;
|
||||
max-width: $s-200;
|
||||
right: $s-32;
|
||||
top: 0;
|
||||
left: unset;
|
||||
}
|
||||
|
||||
.dropdown-menu-option {
|
||||
@extend .dropdown-element-base;
|
||||
}
|
||||
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $s-8;
|
||||
textarea {
|
||||
@extend .input-element;
|
||||
@include bodySmallTypography;
|
||||
|
@ -232,8 +236,8 @@
|
|||
height: 100%;
|
||||
width: 100%;
|
||||
max-width: $s-260;
|
||||
margin-bottom: $s-8;
|
||||
padding: $s-8;
|
||||
margin-top: $s-4;
|
||||
color: var(--input-foreground-color-active);
|
||||
resize: vertical;
|
||||
&:focus {
|
||||
|
@ -241,21 +245,10 @@
|
|||
outline: none;
|
||||
}
|
||||
}
|
||||
.buttons-wrapper {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: $s-4;
|
||||
.post-btn {
|
||||
@extend .button-primary;
|
||||
height: $s-32;
|
||||
width: $s-92;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.cancel-btn {
|
||||
@extend .button-secondary;
|
||||
height: $s-32;
|
||||
width: $s-92;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-buttons-wrapper {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: $s-8;
|
||||
}
|
||||
|
|
|
@ -14,25 +14,18 @@
|
|||
[app.main.store :as st]
|
||||
[app.main.ui.comments :as cmt]
|
||||
[app.main.ui.components.dropdown :refer [dropdown]]
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.keyboard :as kbd]
|
||||
[potok.v2.core :as ptk]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
|
||||
(def ^:private close-icon
|
||||
(i/icon-xref :close (stl/css :close-icon)))
|
||||
|
||||
(def ^:private comments-icon-svg
|
||||
(i/icon-xref :comments (stl/css :comments-icon)))
|
||||
|
||||
|
||||
(def ^:private comments-icon-small
|
||||
(i/icon-xref :comments (stl/css :comments-icon-small)))
|
||||
|
||||
(mf/defc comments-icon
|
||||
[{:keys [profile show? on-show-comments]}]
|
||||
(mf/defc comments-icon*
|
||||
{::mf/props :obj}
|
||||
[{:keys [profile on-show-comments]}]
|
||||
|
||||
(let [threads-map (mf/deref refs/comment-threads)
|
||||
|
||||
|
@ -41,24 +34,18 @@
|
|||
(sort-by :modified-at)
|
||||
(reverse)
|
||||
(dcm/apply-filters {} profile)
|
||||
(dcm/group-threads-by-file-and-page))
|
||||
|
||||
handle-keydown
|
||||
(mf/use-callback
|
||||
(mf/deps on-show-comments)
|
||||
(fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(on-show-comments event))))]
|
||||
(dcm/group-threads-by-file-and-page))]
|
||||
|
||||
[:div {:class (stl/css :dashboard-comments-section)}
|
||||
[:button {:tab-index "0"
|
||||
:on-click on-show-comments
|
||||
:on-key-down handle-keydown
|
||||
:data-testid "open-comments"
|
||||
:class (stl/css-case :comment-button true
|
||||
:open show?
|
||||
:unread (boolean (seq tgroups)))}
|
||||
comments-icon-small]]))
|
||||
[:> icon-button* {:variant "ghost"
|
||||
:tab-index "0"
|
||||
:class (stl/css :comment-button)
|
||||
:data-testid "open-comments"
|
||||
:aria-label (tr "dashboard.notifications.view")
|
||||
:on-click on-show-comments
|
||||
:icon "comments"}
|
||||
(when (seq tgroups)
|
||||
[:div {:class (stl/css :unread)}])]]))
|
||||
|
||||
(mf/defc comments-section
|
||||
[{:keys [profile team show? on-hide-comments]}]
|
||||
|
@ -72,13 +59,6 @@
|
|||
(dcm/apply-filters {} profile)
|
||||
(dcm/group-threads-by-file-and-page))
|
||||
|
||||
handle-keydown
|
||||
(mf/use-callback
|
||||
(mf/deps on-hide-comments)
|
||||
(fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(on-hide-comments event))))
|
||||
|
||||
on-navigate
|
||||
(mf/use-callback
|
||||
(fn [thread]
|
||||
|
@ -101,22 +81,22 @@
|
|||
[:& dropdown {:show show? :on-close on-hide-comments}
|
||||
[:div {:class (stl/css :dropdown :comments-section :comment-threads-section)}
|
||||
[:div {:class (stl/css :header)}
|
||||
[:h3 {:class (stl/css :header-title)} (tr "labels.comments")]
|
||||
[:button {:class (stl/css :close-btn)
|
||||
:tab-index (if show? "0" "-1")
|
||||
:on-click on-hide-comments
|
||||
:on-key-down handle-keydown}
|
||||
close-icon]]
|
||||
[:h3 {:class (stl/css :header-title)} (tr "dashboard.notifications")]
|
||||
[:> icon-button* {:variant "ghost"
|
||||
:tab-index (if show? "0" "-1")
|
||||
:aria-label (tr "labels.close")
|
||||
:on-click on-hide-comments
|
||||
:icon "close"}]]
|
||||
|
||||
(if (seq tgroups)
|
||||
[:div {:class (stl/css :thread-groups)}
|
||||
[:& cmt/comment-thread-group
|
||||
[:> cmt/comment-dashboard-thread-group*
|
||||
{:group (first tgroups)
|
||||
:on-thread-click on-navigate
|
||||
:show-file-name true
|
||||
:profiles profiles}]
|
||||
(for [tgroup (rest tgroups)]
|
||||
[:& cmt/comment-thread-group
|
||||
[:> cmt/comment-dashboard-thread-group*
|
||||
{:group tgroup
|
||||
:on-thread-click on-navigate
|
||||
:show-file-name true
|
||||
|
|
|
@ -44,21 +44,16 @@
|
|||
}
|
||||
|
||||
.comment-button {
|
||||
@include buttonStyle;
|
||||
@include flexCenter;
|
||||
border-radius: $br-8;
|
||||
height: $s-32;
|
||||
width: $s-32;
|
||||
--comment-icon-small-foreground-color: var(--icon-foreground);
|
||||
|
||||
&.unread,
|
||||
&.open {
|
||||
--comment-icon-small-foreground-color: var(--icon-foreground-selected);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-background-quaternary);
|
||||
--comment-icon-small-foreground-color: var(--icon-foreground-active);
|
||||
position: relative;
|
||||
.unread {
|
||||
position: absolute;
|
||||
width: $s-8;
|
||||
height: $s-8;
|
||||
border: $s-2 solid var(--color-background-tertiary);
|
||||
border-radius: 50%;
|
||||
background: red;
|
||||
top: $s-6;
|
||||
right: $s-6;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -100,13 +95,3 @@
|
|||
flex-grow: 1;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
@include buttonStyle;
|
||||
@include flexCenter;
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
@extend .button-icon;
|
||||
stroke: var(--icon-foreground);
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
[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.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]
|
||||
|
@ -1071,9 +1071,8 @@
|
|||
(tr "labels.logout")]]
|
||||
|
||||
(when (and team profile)
|
||||
[:& comments-icon
|
||||
[:> comments-icon*
|
||||
{:profile profile
|
||||
:show? show-comments?
|
||||
:on-show-comments handle-show-comments}])]]))
|
||||
|
||||
(mf/defc sidebar*
|
||||
|
|
|
@ -206,25 +206,26 @@
|
|||
[:div {:class (stl/css :viewer-comments-container)}
|
||||
[:div {:class (stl/css :threads)}
|
||||
(for [item threads]
|
||||
[:& cmt/thread-bubble
|
||||
[:> cmt/comment-floating-bubble*
|
||||
{:thread item
|
||||
:profiles users
|
||||
:position-modifier modifier1
|
||||
:zoom zoom
|
||||
:on-click on-bubble-click
|
||||
:open? (= (:id item) (:open local))
|
||||
:is-open (= (:id item) (:open local))
|
||||
:key (:seqn item)
|
||||
:origin :viewer}])
|
||||
|
||||
(when-let [thread (get threads-map open-thread-id)]
|
||||
[:& cmt/thread-comments
|
||||
[:> cmt/comment-floating-thread*
|
||||
{:thread thread
|
||||
:profiles users
|
||||
:position-modifier modifier1
|
||||
:viewport {:offset-x 0 :offset-y 0 :width (:width vsize) :height (:height vsize)}
|
||||
:profiles users
|
||||
:zoom zoom}])
|
||||
|
||||
(when-let [draft (:draft local)]
|
||||
[:& cmt/draft-thread
|
||||
[:> cmt/comment-floating-thread-draft*
|
||||
{:draft draft
|
||||
:position-modifier modifier1
|
||||
:on-cancel on-draft-cancel
|
||||
|
|
|
@ -150,12 +150,12 @@
|
|||
|
||||
(if (seq tgroups)
|
||||
[:div {:class (stl/css :thread-groups)}
|
||||
[:& cmt/comment-thread-group
|
||||
[:> cmt/comment-sidebar-thread-group*
|
||||
{:group (first tgroups)
|
||||
:on-thread-click on-thread-click
|
||||
:profiles profiles}]
|
||||
(for [tgroup (rest tgroups)]
|
||||
[:& cmt/comment-thread-group
|
||||
[:> cmt/comment-sidebar-thread-group*
|
||||
{:group tgroup
|
||||
:on-thread-click on-thread-click
|
||||
:profiles profiles
|
||||
|
|
|
@ -125,7 +125,6 @@
|
|||
.thread-groups {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $s-24;
|
||||
}
|
||||
|
||||
.thread-group-placeholder {
|
||||
|
|
|
@ -211,7 +211,7 @@
|
|||
show-outlines? (and (nil? transform)
|
||||
(not edition)
|
||||
(not drawing-obj)
|
||||
(not (#{:comments :path :curve} drawing-tool)))
|
||||
(not (#{:path :curve} drawing-tool)))
|
||||
|
||||
show-pixel-grid? (and (contains? layout :show-pixel-grid)
|
||||
(>= zoom 8))
|
||||
|
|
|
@ -75,22 +75,23 @@
|
|||
[:div {:class (stl/css :threads)
|
||||
:style {:transform (dm/fmt "translate(%px, %px)" pos-x pos-y)}}
|
||||
(for [item threads]
|
||||
[:& cmt/thread-bubble {:thread item
|
||||
:zoom zoom
|
||||
:open? (= (:id item) (:open local))
|
||||
:key (:seqn item)}])
|
||||
[:> cmt/comment-floating-bubble* {:thread item
|
||||
:profiles profiles
|
||||
:zoom zoom
|
||||
:is-open (= (:id item) (:open local))
|
||||
:key (:seqn item)}])
|
||||
|
||||
(when-let [id (:open local)]
|
||||
(when-let [thread (get threads-map id)]
|
||||
(when (seq (dcm/apply-filters local profile [thread]))
|
||||
[:& cmt/thread-comments {:thread (update-position positions thread)
|
||||
:profiles profiles
|
||||
:viewport {:offset-x pos-x :offset-y pos-y :width (:width vport) :height (:height vport)}
|
||||
:zoom zoom}])))
|
||||
[:> cmt/comment-floating-thread* {:thread (update-position positions thread)
|
||||
:profiles profiles
|
||||
:viewport {:offset-x pos-x :offset-y pos-y :width (:width vport) :height (:height vport)}
|
||||
:zoom zoom}])))
|
||||
|
||||
(when-let [draft (:comment drawing)]
|
||||
[:& cmt/draft-thread {:draft draft
|
||||
:on-cancel on-draft-cancel
|
||||
:on-submit on-draft-submit
|
||||
:zoom zoom}])]]]))
|
||||
[:> cmt/comment-floating-thread-draft* {:draft draft
|
||||
:on-cancel on-draft-cancel
|
||||
:on-submit on-draft-submit
|
||||
:zoom zoom}])]]]))
|
||||
|
||||
|
|
|
@ -748,6 +748,14 @@ msgstr "No matches found for “%s“"
|
|||
msgid "dashboard.no-projects-placeholder"
|
||||
msgstr "Pinned projects will appear here"
|
||||
|
||||
#: src/app/main/ui/dashboard/comments.cljs
|
||||
msgid "dashboard.notifications"
|
||||
msgstr "Notifications"
|
||||
|
||||
#: src/app/main/ui/dashboard/comments.cljs
|
||||
msgid "dashboard.notifications.view"
|
||||
msgstr "View notifications"
|
||||
|
||||
#: src/app/main/ui/auth/verify_token.cljs:32
|
||||
msgid "dashboard.notifications.email-changed-successfully"
|
||||
msgstr "Your email address has been updated successfully"
|
||||
|
@ -1629,6 +1637,13 @@ msgstr "Close"
|
|||
msgid "labels.collapse"
|
||||
msgstr "Collapse"
|
||||
|
||||
msgid "labels.comment"
|
||||
msgstr "Comment"
|
||||
|
||||
#: src/app/main/ui/comments.cljs:446
|
||||
msgid "labels.comment.mark-as-solved"
|
||||
msgstr "Mark as solved"
|
||||
|
||||
#: src/app/main/ui/dashboard/comments.cljs:104, src/app/main/ui/viewer/comments.cljs:70, src/app/main/ui/workspace/comments.cljs:127
|
||||
msgid "labels.comments"
|
||||
msgstr "Comments"
|
||||
|
@ -1952,6 +1967,10 @@ msgstr "Password"
|
|||
msgid "labels.pending-invitation"
|
||||
msgstr "Pending"
|
||||
|
||||
#: src/app/main/ui/comments.cljs:147
|
||||
msgid "labels.post"
|
||||
msgstr "Post"
|
||||
|
||||
#: src/app/main/ui/onboarding/questions.cljs:51
|
||||
msgid "labels.previous"
|
||||
msgstr "Previous"
|
||||
|
@ -1998,6 +2017,26 @@ msgstr "Rename"
|
|||
msgid "labels.rename-team"
|
||||
msgstr "Rename team"
|
||||
|
||||
#: src/app/main/ui/comments.cljs:145
|
||||
msgid "labels.reply"
|
||||
msgstr "reply"
|
||||
|
||||
#: src/app/main/ui/comments.cljs:150
|
||||
msgid "labels.reply.new"
|
||||
msgstr "new reply"
|
||||
|
||||
#: src/app/main/ui/comments.cljs:188
|
||||
msgid "labels.reply.thread"
|
||||
msgstr "Reply"
|
||||
|
||||
#: src/app/main/ui/comments.cljs:146
|
||||
msgid "labels.replies"
|
||||
msgstr "replies"
|
||||
|
||||
#: src/app/main/ui/comments.cljs:151
|
||||
msgid "labels.replies.new"
|
||||
msgstr "new replies"
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs:681
|
||||
msgid "labels.resend-invitation"
|
||||
msgstr "Resend invitation"
|
||||
|
|
|
@ -759,6 +759,14 @@ msgstr "No se encuentra “%s“"
|
|||
msgid "dashboard.no-projects-placeholder"
|
||||
msgstr "Los proyectos fijados aparecerán aquí"
|
||||
|
||||
#: src/app/main/ui/dashboard/comments.cljs
|
||||
msgid "dashboard.notifications"
|
||||
msgstr "Notificaciones"
|
||||
|
||||
#: src/app/main/ui/dashboard/comments.cljs
|
||||
msgid "dashboard.notifications.view"
|
||||
msgstr "Ver notificaciones"
|
||||
|
||||
#: src/app/main/ui/auth/verify_token.cljs:32
|
||||
msgid "dashboard.notifications.email-changed-successfully"
|
||||
msgstr "Tu dirección de correo ha sido actualizada"
|
||||
|
@ -1634,6 +1642,13 @@ msgstr "Cerrar"
|
|||
msgid "labels.collapse"
|
||||
msgstr "Colapsar"
|
||||
|
||||
msgid "labels.comment"
|
||||
msgstr "Comentario"
|
||||
|
||||
#: src/app/main/ui/comments.cljs:446
|
||||
msgid "labels.comment.mark-as-solved"
|
||||
msgstr "Marcar como resuelto"
|
||||
|
||||
#: src/app/main/ui/dashboard/comments.cljs:104, src/app/main/ui/viewer/comments.cljs:70, src/app/main/ui/workspace/comments.cljs:127
|
||||
msgid "labels.comments"
|
||||
msgstr "Comentarios"
|
||||
|
@ -1957,6 +1972,10 @@ msgstr "Contraseña"
|
|||
msgid "labels.pending-invitation"
|
||||
msgstr "Pendiente"
|
||||
|
||||
#: src/app/main/ui/comments.cljs:147
|
||||
msgid "labels.post"
|
||||
msgstr "Publicar"
|
||||
|
||||
#: src/app/main/ui/onboarding/questions.cljs:51
|
||||
msgid "labels.previous"
|
||||
msgstr "Anterior"
|
||||
|
@ -2003,6 +2022,26 @@ msgstr "Renombrar"
|
|||
msgid "labels.rename-team"
|
||||
msgstr "Renombra el equipo"
|
||||
|
||||
#: src/app/main/ui/comments.cljs:145
|
||||
msgid "labels.reply"
|
||||
msgstr "respuesta"
|
||||
|
||||
#: src/app/main/ui/comments.cljs:150
|
||||
msgid "labels.reply.new"
|
||||
msgstr "nueva respuesta"
|
||||
|
||||
#: src/app/main/ui/comments.cljs:188
|
||||
msgid "labels.reply.thread"
|
||||
msgstr "Responder"
|
||||
|
||||
#: src/app/main/ui/comments.cljs:146
|
||||
msgid "labels.replies"
|
||||
msgstr "respuestas"
|
||||
|
||||
#: src/app/main/ui/comments.cljs:151
|
||||
msgid "labels.replies.new"
|
||||
msgstr "nuevas respuestas"
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs:681
|
||||
msgid "labels.resend-invitation"
|
||||
msgstr "Reenviar invitacion"
|
||||
|
|
Loading…
Reference in a new issue