mirror of
https://github.com/penpot/penpot.git
synced 2025-03-28 15:41:25 -05:00
🎉 Add comments to dashboard.
This commit is contained in:
parent
420294aef4
commit
742af4e066
28 changed files with 968 additions and 438 deletions
|
@ -113,6 +113,9 @@
|
|||
|
||||
{:name "0032-del-unused-tables"
|
||||
:fn (mg/resource "app/migrations/sql/0032-del-unused-tables.sql")}
|
||||
|
||||
{:name "0033-mod-comment-thread-table"
|
||||
:fn (mg/resource "app/migrations/sql/0033-mod-comment-thread-table.sql")}
|
||||
]})
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE comment_thread
|
||||
ADD COLUMN page_name text NULL;
|
|
@ -29,12 +29,13 @@
|
|||
|
||||
(declare upsert-comment-thread-status!)
|
||||
(declare create-comment-thread)
|
||||
(declare retrieve-page-name)
|
||||
|
||||
(s/def ::page-id ::us/uuid)
|
||||
(s/def ::file-id ::us/uuid)
|
||||
(s/def ::profile-id ::us/uuid)
|
||||
(s/def ::position ::us/point)
|
||||
(s/def ::content ::us/string)
|
||||
(s/def ::page-id ::us/uuid)
|
||||
|
||||
(s/def ::create-comment-thread
|
||||
(s/keys :req-un [::profile-id ::file-id ::position ::content ::page-id]))
|
||||
|
@ -53,13 +54,14 @@
|
|||
|
||||
(defn- create-comment-thread*
|
||||
[conn {:keys [profile-id file-id page-id position content] :as params}]
|
||||
(let [seqn (retrieve-next-seqn conn file-id)
|
||||
now (dt/now)
|
||||
|
||||
(let [seqn (retrieve-next-seqn conn file-id)
|
||||
now (dt/now)
|
||||
pname (retrieve-page-name conn params)
|
||||
thread (db/insert! conn :comment-thread
|
||||
{:file-id file-id
|
||||
:owner-id profile-id
|
||||
:participants (db/tjson #{profile-id})
|
||||
:page-name pname
|
||||
:page-id page-id
|
||||
:created-at now
|
||||
:modified-at now
|
||||
|
@ -81,10 +83,7 @@
|
|||
{:comment-thread-seqn seqn}
|
||||
{:id file-id})
|
||||
|
||||
(-> (assoc thread
|
||||
:content content
|
||||
:comment comment)
|
||||
(comments/decode-row))))
|
||||
(select-keys thread [:id :file-id :page-id])))
|
||||
|
||||
(defn- create-comment-thread
|
||||
[conn params]
|
||||
|
@ -104,6 +103,12 @@
|
|||
|
||||
:else res))))
|
||||
|
||||
(defn- retrieve-page-name
|
||||
[conn {:keys [file-id page-id]}]
|
||||
(let [{:keys [data]} (db/get-by-id conn :file file-id)
|
||||
data (blob/decode data)]
|
||||
(get-in data [:pages-index page-id :name])))
|
||||
|
||||
|
||||
;; --- Mutation: Update Comment Thread Status
|
||||
|
||||
|
@ -164,14 +169,21 @@
|
|||
[{:keys [profile-id thread-id content] :as params}]
|
||||
(db/with-atomic [conn db/pool]
|
||||
(let [thread (-> (db/get-by-id conn :comment-thread thread-id {:for-update true})
|
||||
(comments/decode-row))]
|
||||
(comments/decode-row))
|
||||
pname (retrieve-page-name conn thread)]
|
||||
|
||||
;; Standard Checks
|
||||
(when-not thread
|
||||
(ex/raise :type :not-found))
|
||||
(when-not thread (ex/raise :type :not-found))
|
||||
|
||||
;; Permission Checks
|
||||
(files/check-read-permissions! conn profile-id (:file-id thread))
|
||||
|
||||
;; Update the page-name cachedattribute on comment thread table.
|
||||
(when (not= pname (:page-name thread))
|
||||
(db/update! conn :comment-thread
|
||||
{:page-name pname}
|
||||
{:id thread-id}))
|
||||
|
||||
;; NOTE: is important that all timestamptz related fields are
|
||||
;; created or updated on the database level for avoid clock
|
||||
;; inconsistencies (some user sees something read that is not
|
||||
|
@ -216,15 +228,19 @@
|
|||
(let [comment (db/get-by-id conn :comment id {:for-update true})
|
||||
_ (when-not comment (ex/raise :type :not-found))
|
||||
thread (db/get-by-id conn :comment-thread (:thread-id comment) {:for-update true})
|
||||
_ (when-not thread (ex/raise :type :not-found))]
|
||||
_ (when-not thread (ex/raise :type :not-found))
|
||||
pname (retrieve-page-name conn thread)]
|
||||
|
||||
(files/check-read-permissions! conn profile-id (:file-id thread))
|
||||
|
||||
(db/update! conn :comment
|
||||
{:content content
|
||||
:modified-at (dt/now)}
|
||||
{:id (:id comment)})
|
||||
|
||||
(db/update! conn :comment-thread
|
||||
{:modified-at (dt/now)}
|
||||
{:modified-at (dt/now)
|
||||
:page-name pname}
|
||||
{:id (:id thread)})
|
||||
nil)))
|
||||
|
||||
|
@ -244,6 +260,7 @@
|
|||
(db/delete! conn :comment-thread {:id id})
|
||||
nil)))
|
||||
|
||||
|
||||
;; --- Mutation: Delete comment
|
||||
|
||||
(s/def ::delete-comment
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
[app.db :as db]
|
||||
[app.services.queries :as sq]
|
||||
[app.services.queries.files :as files]
|
||||
[app.services.queries.teams :as teams]
|
||||
[app.util.time :as dt]
|
||||
[app.util.transit :as t]
|
||||
[clojure.spec.alpha :as s]
|
||||
|
@ -32,9 +33,13 @@
|
|||
|
||||
(declare retrieve-comment-threads)
|
||||
|
||||
(s/def ::team-id ::us/uuid)
|
||||
(s/def ::file-id ::us/uuid)
|
||||
|
||||
(s/def ::comment-threads
|
||||
(s/keys :req-un [::profile-id ::file-id]))
|
||||
(s/and (s/keys :req-un [::profile-id]
|
||||
:opt-un [::file-id ::team-id])
|
||||
#(or (:file-id %) (:team-id %))))
|
||||
|
||||
(sq/defquery ::comment-threads
|
||||
[{:keys [profile-id file-id] :as params}]
|
||||
|
@ -45,6 +50,8 @@
|
|||
(def sql:comment-threads
|
||||
"select distinct on (ct.id)
|
||||
ct.*,
|
||||
f.name as file_name,
|
||||
f.project_id as project_id,
|
||||
first_value(c.content) over w as content,
|
||||
(select count(1)
|
||||
from comment as c
|
||||
|
@ -55,6 +62,7 @@
|
|||
and c.created_at >= coalesce(cts.modified_at, ct.created_at)) as count_unread_comments
|
||||
from comment_thread as ct
|
||||
inner join comment as c on (c.thread_id = ct.id)
|
||||
inner join file as f on (f.id = ct.file_id)
|
||||
left join comment_thread_status as cts
|
||||
on (cts.thread_id = ct.id and
|
||||
cts.profile_id = ?)
|
||||
|
@ -62,10 +70,59 @@
|
|||
window w as (partition by c.thread_id order by c.created_at asc)")
|
||||
|
||||
(defn- retrieve-comment-threads
|
||||
[conn {:keys [profile-id file-id]}]
|
||||
[conn {:keys [profile-id file-id team-id]}]
|
||||
(files/check-read-permissions! conn profile-id file-id)
|
||||
(->> (db/exec! conn [sql:comment-threads profile-id file-id])
|
||||
(into [] (map decode-row))))
|
||||
|
||||
|
||||
;; --- Query: Unread Comment Threads
|
||||
|
||||
(declare retrieve-unread-comment-threads)
|
||||
|
||||
(s/def ::team-id ::us/uuid)
|
||||
(s/def ::unread-comment-threads
|
||||
(s/keys :req-un [::profile-id ::team-id]))
|
||||
|
||||
(sq/defquery ::unread-comment-threads
|
||||
[{:keys [profile-id team-id] :as params}]
|
||||
(with-open [conn (db/open)]
|
||||
(teams/check-read-permissions! conn profile-id team-id)
|
||||
(retrieve-unread-comment-threads conn params)))
|
||||
|
||||
(def sql:comment-threads-by-team
|
||||
"select distinct on (ct.id)
|
||||
ct.*,
|
||||
f.name as file_name,
|
||||
f.project_id as project_id,
|
||||
first_value(c.content) over w as content,
|
||||
(select count(1)
|
||||
from comment as c
|
||||
where c.thread_id = ct.id) as count_comments,
|
||||
(select count(1)
|
||||
from comment as c
|
||||
where c.thread_id = ct.id
|
||||
and c.created_at >= coalesce(cts.modified_at, ct.created_at)) as count_unread_comments
|
||||
from comment_thread as ct
|
||||
inner join comment as c on (c.thread_id = ct.id)
|
||||
inner join file as f on (f.id = ct.file_id)
|
||||
inner join project as p on (p.id = f.project_id)
|
||||
left join comment_thread_status as cts
|
||||
on (cts.thread_id = ct.id and
|
||||
cts.profile_id = ?)
|
||||
where p.team_id = ?
|
||||
window w as (partition by c.thread_id order by c.created_at asc)")
|
||||
|
||||
(def sql:unread-comment-threads-by-team
|
||||
(str "with threads as (" sql:comment-threads-by-team ")"
|
||||
"select * from threads where count_unread_comments > 0"))
|
||||
|
||||
(defn retrieve-unread-comment-threads
|
||||
[conn {:keys [profile-id team-id]}]
|
||||
(->> (db/exec! conn [sql:unread-comment-threads-by-team profile-id team-id])
|
||||
(into [] (map decode-row))))
|
||||
|
||||
|
||||
;; --- Query: Single Comment Thread
|
||||
|
||||
(s/def ::id ::us/uuid)
|
||||
|
|
|
@ -193,6 +193,12 @@
|
|||
inner join file_profile_rel as fpr on (fpr.profile_id = pf.id)
|
||||
where fpr.file_id = ?
|
||||
union
|
||||
select pf.id, pf.fullname, pf.photo
|
||||
from profile as pf
|
||||
inner join project_profile_rel as ppr on (ppr.profile_id = pf.id)
|
||||
inner join file as f on (f.project_id = ppr.project_id)
|
||||
where f.id = ?
|
||||
union
|
||||
select pf.id, pf.fullname, pf.photo
|
||||
from profile as pf
|
||||
inner join team_profile_rel as tpr on (tpr.profile_id = pf.id)
|
||||
|
@ -202,7 +208,7 @@
|
|||
|
||||
(defn retrieve-file-users
|
||||
[conn id]
|
||||
(db/exec! conn [sql:file-users id id]))
|
||||
(db/exec! conn [sql:file-users id id id]))
|
||||
|
||||
(s/def ::file-users
|
||||
(s/keys :req-un [::profile-id ::id]))
|
||||
|
@ -215,17 +221,8 @@
|
|||
|
||||
;; --- Query: Shared Library Files
|
||||
|
||||
;; TODO: remove the counts, because they are no longer needed.
|
||||
|
||||
(def ^:private sql:shared-files
|
||||
"select f.*,
|
||||
(select count(*) from color as c
|
||||
where c.file_id = f.id
|
||||
and c.deleted_at is null) as colors_count,
|
||||
(select count(*) from media_object as m
|
||||
where m.file_id = f.id
|
||||
and m.is_local = false
|
||||
and m.deleted_at is null) as graphics_count
|
||||
"select f.*
|
||||
from file as f
|
||||
inner join project as p on (p.id = f.project_id)
|
||||
where f.is_shared = true
|
||||
|
|
|
@ -130,3 +130,38 @@
|
|||
(defn retrieve-team-members
|
||||
[conn team-id]
|
||||
(db/exec! conn [sql:team-members team-id]))
|
||||
|
||||
;; --- Query: Team Users
|
||||
|
||||
;; This is a similar query to team members but can contain more data
|
||||
;; because some user can be explicitly added to project or file (not
|
||||
;; implemented in UI)
|
||||
|
||||
(def sql:team-users
|
||||
"select pf.id, pf.fullname, pf.photo
|
||||
from profile as pf
|
||||
inner join team_profile_rel as tpr on (tpr.profile_id = pf.id)
|
||||
where tpr.team_id = ?
|
||||
union
|
||||
select pf.id, pf.fullname, pf.photo
|
||||
from profile as pf
|
||||
inner join project_profile_rel as ppr on (ppr.profile_id = pf.id)
|
||||
inner join project as p on (ppr.project_id = p.id)
|
||||
where p.team_id = ?
|
||||
union
|
||||
select pf.id, pf.fullname, pf.photo
|
||||
from profile as pf
|
||||
inner join file_profile_rel as fpr on (fpr.profile_id = pf.id)
|
||||
inner join file as f on (fpr.file_id = f.id)
|
||||
inner join project as p on (f.project_id = p.id)
|
||||
where p.team_id = ?")
|
||||
|
||||
(s/def ::team-users
|
||||
(s/keys :req-un [::profile-id ::team-id]))
|
||||
|
||||
(sq/defquery ::team-users
|
||||
[{:keys [profile-id team-id]}]
|
||||
(with-open [conn (db/open)]
|
||||
(check-edition-permissions! conn profile-id team-id)
|
||||
(db/exec! conn [sql:team-users team-id team-id team-id])))
|
||||
|
||||
|
|
|
@ -1,20 +1,21 @@
|
|||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const l = require("lodash");
|
||||
const path = require("path");
|
||||
|
||||
const gulp = require("gulp");
|
||||
const gulpSass = require("gulp-sass");
|
||||
const gulpConcat = require("gulp-concat");
|
||||
const gulpGzip = require("gulp-gzip");
|
||||
const gulpMustache = require("gulp-mustache");
|
||||
const gulpRename = require("gulp-rename");
|
||||
const svgSprite = require("gulp-svg-sprite");
|
||||
const gulpPostcss = require("gulp-postcss");
|
||||
const gulpRename = require("gulp-rename");
|
||||
const gulpSass = require("gulp-sass");
|
||||
const svgSprite = require("gulp-svg-sprite");
|
||||
|
||||
const autoprefixer = require("autoprefixer")
|
||||
const clean = require("postcss-clean");
|
||||
const mkdirp = require("mkdirp");
|
||||
const rimraf = require("rimraf");
|
||||
const sass = require("sass");
|
||||
const autoprefixer = require("autoprefixer")
|
||||
const clean = require("postcss-clean");
|
||||
|
||||
const mapStream = require("map-stream");
|
||||
const paths = {};
|
||||
|
@ -52,7 +53,8 @@ function readManifest() {
|
|||
const content = JSON.parse(fs.readFileSync(path, {encoding: "utf8"}));
|
||||
|
||||
const index = {
|
||||
"config": "/js/config.js?ts=" + Date.now()
|
||||
"config": "/js/config.js?ts=" + Date.now(),
|
||||
"polyfills": "js/polyfills.js?ts=" + Date.now(),
|
||||
};
|
||||
|
||||
for (let item of content) {
|
||||
|
@ -64,6 +66,7 @@ function readManifest() {
|
|||
console.error("Error on reading manifest, using default.");
|
||||
return {
|
||||
"config": "/js/config.js",
|
||||
"polyfills": "js/polyfills.js",
|
||||
"main": "/js/main.js",
|
||||
"shared": "/js/shared.js",
|
||||
"worker": "/js/worker.js"
|
||||
|
@ -123,7 +126,7 @@ gulp.task("scss", function() {
|
|||
.pipe(gulpSass().on('error', gulpSass.logError))
|
||||
.pipe(gulpPostcss([
|
||||
autoprefixer,
|
||||
clean({format: "keep-breaks", level: 1})
|
||||
// clean({format: "keep-breaks", level: 1})
|
||||
]))
|
||||
.pipe(gulp.dest(paths.output + "css/"));
|
||||
});
|
||||
|
@ -142,6 +145,12 @@ gulp.task("template:main", templatePipeline({
|
|||
|
||||
gulp.task("templates", gulp.series("svg:sprite", "template:main"));
|
||||
|
||||
gulp.task("polyfills", function() {
|
||||
return gulp.src(paths.resources + "polyfills/*.js")
|
||||
.pipe(gulpConcat("polyfills.js"))
|
||||
.pipe(gulp.dest(paths.output + "js/"));
|
||||
});
|
||||
|
||||
/***********************************************
|
||||
* Development
|
||||
***********************************************/
|
||||
|
@ -177,7 +186,7 @@ gulp.task("watch:main", function() {
|
|||
gulp.series("templates"));
|
||||
});
|
||||
|
||||
gulp.task("build", gulp.parallel("scss", "templates", "copy:assets"));
|
||||
gulp.task("build", gulp.parallel("polyfills", "scss", "templates", "copy:assets"));
|
||||
gulp.task("watch", gulp.series("dev:dirs", "build", "watch:main"));
|
||||
|
||||
/***********************************************
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
"devDependencies": {
|
||||
"autoprefixer": "^10.0.1",
|
||||
"gulp": "4.0.2",
|
||||
"gulp-concat": "^2.6.1",
|
||||
"gulp-gzip": "^1.4.2",
|
||||
"gulp-mustache": "^5.0.0",
|
||||
"gulp-postcss": "^9.0.0",
|
||||
|
|
4
frontend/resources/images/icons/comment.svg
Normal file
4
frontend/resources/images/icons/comment.svg
Normal file
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
|
||||
<path fill="#fff" d="M8.57 16.013l-3.273 2.611-.22-3.618C3.861 14.283 1.29 12.043.736 8.871c-.554-3.17 3.44-6.166 5.505-7.267 4.782-.64 14.025-.157 12.742 6.89-1.284 7.047-7.477 7.949-10.414 7.519z"/>
|
||||
<path fill="#000" d="M9.815.348C6.897.384 3.916 1.493 1.937 3.687.627 5.114-.148 7.07.024 9.019c.136 2.258 1.458 4.312 3.22 5.672.324.256.662.494 1.007.721.038 1.277-.034 2.564.094 3.834.211.513.952.64 1.347.263 1.049-.865 2.109-1.718 3.159-2.584.982.1 1.978.045 2.954-.088 2.748-.447 5.398-1.936 6.94-4.294 1.047-1.577 1.52-3.566 1.105-5.43-.403-1.917-1.648-3.578-3.223-4.706C14.669.982 12.224.302 9.815.348zm.225 1.69c2.414.009 4.91.848 6.613 2.622 1.038 1.062 1.715 2.522 1.638 4.012-.032 1.69-.938 3.275-2.19 4.376-1.788 1.59-4.234 2.352-6.598 2.222-.408.014-.826-.114-1.222-.064-.773.629-1.545 1.258-2.316 1.888-.02-.89-.016-1.781-.026-2.672-1.766-.89-3.369-2.33-3.986-4.253-.532-1.62-.226-3.495.833-4.85 1.667-2.243 4.532-3.295 7.254-3.28z" color="#000"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1 KiB |
File diff suppressed because it is too large
Load diff
27
frontend/resources/polyfills/scrollIntoViewIfNeeded.js
Normal file
27
frontend/resources/polyfills/scrollIntoViewIfNeeded.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
;(function() {
|
||||
if (!Element.prototype.scrollIntoViewIfNeeded) {
|
||||
Element.prototype.scrollIntoViewIfNeeded = function (centerIfNeeded) {
|
||||
centerIfNeeded = arguments.length === 0 ? true : !!centerIfNeeded;
|
||||
|
||||
var parent = this.parentNode,
|
||||
parentComputedStyle = window.getComputedStyle(parent, null),
|
||||
parentBorderTopWidth = parseInt(parentComputedStyle.getPropertyValue('border-top-width')),
|
||||
parentBorderLeftWidth = parseInt(parentComputedStyle.getPropertyValue('border-left-width')),
|
||||
overTop = this.offsetTop - parent.offsetTop < parent.scrollTop,
|
||||
overBottom = (this.offsetTop - parent.offsetTop + this.clientHeight - parentBorderTopWidth) > (parent.scrollTop + parent.clientHeight),
|
||||
overLeft = this.offsetLeft - parent.offsetLeft < parent.scrollLeft,
|
||||
overRight = (this.offsetLeft - parent.offsetLeft + this.clientWidth - parentBorderLeftWidth) > (parent.scrollLeft + parent.clientWidth),
|
||||
alignWithTop = overTop && !overBottom;
|
||||
|
||||
if ((overTop || overBottom) && centerIfNeeded) {
|
||||
parent.scrollTop = this.offsetTop - parent.offsetTop - parent.clientHeight / 2 - parentBorderTopWidth + this.clientHeight / 2;
|
||||
}
|
||||
if ((overLeft || overRight) && centerIfNeeded) {
|
||||
parent.scrollLeft = this.offsetLeft - parent.offsetLeft - parent.clientWidth / 2 - parentBorderLeftWidth + this.clientWidth / 2;
|
||||
}
|
||||
if ((overTop || overBottom || overLeft || overRight) && !centerIfNeeded) {
|
||||
this.scrollIntoView(alignWithTop);
|
||||
}
|
||||
};
|
||||
}
|
||||
})()
|
|
@ -80,6 +80,6 @@
|
|||
@import 'main/partials/user-settings';
|
||||
@import 'main/partials/workspace';
|
||||
@import 'main/partials/workspace-header';
|
||||
@import 'main/partials/workspace-comments';
|
||||
@import 'main/partials/comments';
|
||||
@import 'main/partials/color-bullet';
|
||||
@import "main/partials/handoff";
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
.dashboard-sidebar {
|
||||
grid-row: 1 / span 2;
|
||||
grid-column: 1 / span 2;
|
||||
overflow: hidden;
|
||||
// overflow: hidden;
|
||||
}
|
||||
|
||||
.dashboard-content {
|
||||
|
|
|
@ -1,31 +1,4 @@
|
|||
.viewer-comments {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1000;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
.viewer-comments, .workspace-comments {
|
||||
|
||||
.comments-layer {
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
grid-column: 1/span 2;
|
||||
grid-row: 1/span 2;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
|
||||
.threads {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.comments-section {
|
||||
.thread-bubble {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
|
@ -101,6 +74,8 @@
|
|||
padding: $small;
|
||||
resize: none;
|
||||
width: 100%;
|
||||
border-radius: 2px;
|
||||
border: 1px solid $color-gray-10;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
|
@ -142,7 +117,7 @@
|
|||
.fullname {
|
||||
font-weight: 700;
|
||||
color: $color-gray-60;
|
||||
font-size: $fs13;
|
||||
font-size: $fs10;
|
||||
|
||||
@include text-ellipsis;
|
||||
width: 150px;
|
||||
|
@ -150,7 +125,7 @@
|
|||
}
|
||||
.timeago {
|
||||
margin-top: -2px;
|
||||
font-size: $fs11;
|
||||
font-size: $fs10;
|
||||
color: $color-gray-30;
|
||||
}
|
||||
}
|
||||
|
@ -163,8 +138,8 @@
|
|||
img {
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -205,9 +180,8 @@
|
|||
}
|
||||
|
||||
.content {
|
||||
margin: $medium 0;
|
||||
// margin-left: 26px;
|
||||
font-size: $fs13;
|
||||
margin: 10px 0;
|
||||
font-size: $fs10;
|
||||
color: $color-black;
|
||||
.text {
|
||||
margin-left: 26px;
|
||||
|
@ -225,51 +199,51 @@
|
|||
|
||||
border: 1px solid #B1B2B5;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.workspace-comments-sidebar {
|
||||
pointer-events: auto;
|
||||
.workspace-comment-threads-sidebar-header {
|
||||
display: flex;
|
||||
background-color: $color-black;
|
||||
height: 34px;
|
||||
align-items: center;
|
||||
padding: 0px 9px;
|
||||
color: $color-gray-10;
|
||||
font-size: $fs12;
|
||||
justify-content: space-between;
|
||||
|
||||
.sidebar-title {
|
||||
.options {
|
||||
display: flex;
|
||||
background-color: $color-black;
|
||||
height: 34px;
|
||||
align-items: center;
|
||||
padding: 0px 9px;
|
||||
color: $color-gray-10;
|
||||
font-size: $fs12;
|
||||
justify-content: space-between;
|
||||
margin-right: 3px;
|
||||
cursor: pointer;
|
||||
|
||||
.options {
|
||||
.label {
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
margin-right: 3px;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.label {
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: $color-gray-10;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
svg {
|
||||
fill: $color-gray-10;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-options-dropdown {
|
||||
.dropdown {
|
||||
top: 80px;
|
||||
right: 7px;
|
||||
}
|
||||
}
|
||||
|
||||
.threads {
|
||||
|
||||
|
||||
.comment-threads-section {
|
||||
pointer-events: auto;
|
||||
|
||||
.thread-groups {
|
||||
hr {
|
||||
border: 0;
|
||||
height: 1px;
|
||||
|
@ -278,7 +252,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.page-section {
|
||||
.thread-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: $fs12;
|
||||
|
@ -292,6 +266,9 @@
|
|||
}
|
||||
|
||||
.label {
|
||||
&.filename {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
|
@ -312,6 +289,7 @@
|
|||
}
|
||||
|
||||
.comment {
|
||||
cursor: pointer;
|
||||
.author {
|
||||
margin-bottom: 10px;
|
||||
.name {
|
||||
|
@ -351,3 +329,118 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.viewer-comments-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1000;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
.workspace-comments-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
grid-column: 1/span 2;
|
||||
grid-row: 1/span 2;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
|
||||
.threads {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-comments-section {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: $color-dashboard;
|
||||
border-radius: 3px;
|
||||
position: relative;
|
||||
|
||||
.button {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: $color-dashboard;
|
||||
border-radius: 3px;
|
||||
|
||||
svg {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
&.unread {
|
||||
background-color: $color-warning;
|
||||
}
|
||||
|
||||
&.open {
|
||||
background-color: $color-black;
|
||||
svg { fill: $color-primary; }
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
width: 233px;
|
||||
bottom: 35px;
|
||||
left: 0px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
height: 40px;
|
||||
align-items: center;
|
||||
padding: 0px 11px;
|
||||
|
||||
h3 {
|
||||
font-weight: 400;
|
||||
color: $color-black;
|
||||
font-size: $fs12;
|
||||
line-height: $fs18;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.close {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
||||
svg {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
}
|
||||
|
||||
.thread-groups-placeholder {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.thread-group {
|
||||
.section-title {
|
||||
color: $color-black;
|
||||
}
|
||||
}
|
||||
|
||||
.comment {
|
||||
.author .name .fullname {
|
||||
color: $color-gray-40;
|
||||
}
|
||||
.content {
|
||||
color: $color-black;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -363,26 +363,32 @@
|
|||
padding: 10px 15px;
|
||||
position: relative;
|
||||
|
||||
span {
|
||||
@include text-ellipsis;
|
||||
color: $color-black;
|
||||
margin: 10px 5px;
|
||||
font-size: $fs14;
|
||||
max-width: 160px;
|
||||
}
|
||||
.profile {
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
|
||||
img {
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
height: 25px;
|
||||
width: 25px;
|
||||
}
|
||||
span {
|
||||
@include text-ellipsis;
|
||||
color: $color-black;
|
||||
margin: 10px 5px;
|
||||
font-size: $fs14;
|
||||
max-width: 160px;
|
||||
}
|
||||
|
||||
svg {
|
||||
height: 10px;
|
||||
margin-left: auto;
|
||||
margin-right: $small;
|
||||
width: 10px;
|
||||
img {
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
height: 25px;
|
||||
width: 25px;
|
||||
}
|
||||
svg {
|
||||
height: 10px;
|
||||
margin-left: auto;
|
||||
margin-right: $small;
|
||||
width: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
|
@ -400,6 +406,8 @@
|
|||
|
||||
svg {
|
||||
fill: $color-gray-20;
|
||||
margin-right: $small;
|
||||
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
{{# manifest}}
|
||||
<script>window.appWorkerURI="{{& worker}}"</script>
|
||||
<script src="{{& config}}"></script>
|
||||
<script src="{{& polyfills}}"></script>
|
||||
<script src="{{& shared}}"></script>
|
||||
<script src="{{& main}}"></script>
|
||||
{{/manifest}}
|
||||
|
|
|
@ -40,11 +40,14 @@
|
|||
(s/def ::count-unread-comments ::us/integer)
|
||||
(s/def ::created-at ::us/inst)
|
||||
(s/def ::file-id ::us/uuid)
|
||||
(s/def ::file-name ::us/string)
|
||||
(s/def ::modified-at ::us/inst)
|
||||
(s/def ::owner-id ::us/uuid)
|
||||
(s/def ::page-id ::us/uuid)
|
||||
(s/def ::page-name ::us/string)
|
||||
(s/def ::participants (s/every ::us/uuid :kind set?))
|
||||
(s/def ::position ::us/point)
|
||||
(s/def ::project-id ::us/uuid)
|
||||
(s/def ::seqn ::us/integer)
|
||||
(s/def ::thread-id ::us/uuid)
|
||||
|
||||
|
@ -52,15 +55,18 @@
|
|||
(s/keys :req-un [::us/id
|
||||
::page-id
|
||||
::file-id
|
||||
::project-id
|
||||
::page-name
|
||||
::file-name
|
||||
::seqn
|
||||
::content
|
||||
::participants
|
||||
::count-unread-comments
|
||||
::count-comments
|
||||
::created-at
|
||||
::modified-at
|
||||
::owner-id
|
||||
::position]))
|
||||
::position]
|
||||
:opt-un [::count-unread-comments
|
||||
::count-comments]))
|
||||
|
||||
(s/def ::comment
|
||||
(s/keys :req-un [::us/id
|
||||
|
@ -92,20 +98,18 @@
|
|||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(->> (rp/mutation :create-comment-thread params)
|
||||
(rx/mapcat #(rp/query :comment-thread {:file-id (:file-id %) :id (:id %)}))
|
||||
(rx/map #(partial created %)))))))
|
||||
|
||||
(defn update-comment-thread-status
|
||||
[{:keys [id] :as thread}]
|
||||
(us/assert ::comment-thread thread)
|
||||
(ptk/reify ::update-comment-thread-status
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(d/update-in-when state [:comment-threads id] assoc :count-unread-comments 0))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(->> (rp/mutation :update-comment-thread-status {:id id})
|
||||
(rx/ignore)))))
|
||||
(let [done #(d/update-in-when % [:comment-threads id] assoc :count-unread-comments 0)]
|
||||
(->> (rp/mutation :update-comment-thread-status {:id id})
|
||||
(rx/map (constantly done)))))))
|
||||
|
||||
|
||||
(defn update-comment-thread
|
||||
|
@ -211,6 +215,18 @@
|
|||
(->> (rp/query :comments {:thread-id thread-id})
|
||||
(rx/map #(partial fetched %)))))))
|
||||
|
||||
(defn retrieve-unread-comment-threads
|
||||
"A event used mainly in dashboard for retrieve all unread threads of a team."
|
||||
[team-id]
|
||||
(us/assert ::us/uuid team-id)
|
||||
(ptk/reify ::retrieve-unread-comment-threads
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [fetched #(assoc %2 :comment-threads (d/index-by :id %1))]
|
||||
(->> (rp/query :unread-comment-threads {:team-id team-id})
|
||||
(rx/map #(partial fetched %)))))))
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Local State
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
@ -221,6 +237,7 @@
|
|||
(ptk/reify ::open-thread
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(prn "open-thread" id)
|
||||
(-> state
|
||||
(update :comments-local assoc :open id)
|
||||
(update :workspace-drawing dissoc :comment)))))
|
||||
|
@ -230,6 +247,7 @@
|
|||
(ptk/reify ::close-thread
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(prn "close-thread")
|
||||
(-> state
|
||||
(update :comments-local dissoc :open :draft)
|
||||
(update :workspace-drawing dissoc :comment)))))
|
||||
|
@ -282,11 +300,31 @@
|
|||
(if (= (:page-id current) (:page-id thread))
|
||||
(cons (update current :items conj thread)
|
||||
(rest result))
|
||||
(cons {:page-id (:page-id thread) :items [thread]}
|
||||
(cons {:page-id (:page-id thread)
|
||||
:page-name (:page-name thread)
|
||||
:items [thread]}
|
||||
result))))]
|
||||
(reverse
|
||||
(reduce group-by-page nil threads))))
|
||||
|
||||
|
||||
(defn group-threads-by-file-and-page
|
||||
[threads]
|
||||
(letfn [(group-by-file-and-page [result thread]
|
||||
(let [current (first result)]
|
||||
(if (and (= (:page-id current) (:page-id thread))
|
||||
(= (:file-id current) (:file-id thread)))
|
||||
(cons (update current :items conj thread)
|
||||
(rest result))
|
||||
(cons {:page-id (:page-id thread)
|
||||
:page-name (:page-name thread)
|
||||
:file-id (:file-id thread)
|
||||
:file-name (:file-name thread)
|
||||
:items [thread]}
|
||||
result))))]
|
||||
(reverse
|
||||
(reduce group-by-file-and-page nil threads))))
|
||||
|
||||
(defn apply-filters
|
||||
[cstate profile threads]
|
||||
(let [{:keys [show mode open]} cstate]
|
||||
|
|
|
@ -64,13 +64,6 @@
|
|||
|
||||
;; --- Fetch Team
|
||||
|
||||
(defn assoc-team-avatar
|
||||
[{:keys [photo name] :as team}]
|
||||
(us/assert ::team team)
|
||||
(cond-> team
|
||||
(or (nil? photo) (empty? photo))
|
||||
(assoc :photo (avatars/generate {:name name}))))
|
||||
|
||||
(defn fetch-team
|
||||
[{:keys [id] :as params}]
|
||||
(letfn [(fetched [team state]
|
||||
|
@ -80,20 +73,36 @@
|
|||
(watch [_ state stream]
|
||||
(let [profile (:profile state)]
|
||||
(->> (rp/query :team params)
|
||||
(rx/map assoc-team-avatar)
|
||||
(rx/map #(avatars/assoc-avatar % :name))
|
||||
(rx/map #(partial fetched %))))))))
|
||||
|
||||
(defn fetch-team-members
|
||||
[{:keys [id] :as params}]
|
||||
(us/assert ::us/uuid id)
|
||||
(letfn [(fetched [members state]
|
||||
(assoc-in state [:team-members id] (d/index-by :id members)))]
|
||||
(->> (map #(avatars/assoc-avatar % :name) members)
|
||||
(d/index-by :id)
|
||||
(assoc-in state [:team-members id])))]
|
||||
(ptk/reify ::fetch-team-members
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(->> (rp/query :team-members {:team-id id})
|
||||
(rx/map #(partial fetched %)))))))
|
||||
|
||||
|
||||
(defn fetch-team-users
|
||||
[{:keys [id] :as params}]
|
||||
(us/assert ::us/uuid id)
|
||||
(letfn [(fetched [users state]
|
||||
(->> (map #(avatars/assoc-avatar % :fullname) users)
|
||||
(d/index-by :id)
|
||||
(assoc-in state [:team-users id])))]
|
||||
(ptk/reify ::fetch-team-users
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(->> (rp/query :team-users {:team-id id})
|
||||
(rx/map #(partial fetched %)))))))
|
||||
|
||||
;; --- Fetch Projects
|
||||
|
||||
(defn fetch-projects
|
||||
|
@ -115,7 +124,8 @@
|
|||
(watch [_ state stream]
|
||||
(let [profile (:profile state)]
|
||||
(->> (rx/merge (ptk/watch (fetch-team params) state stream)
|
||||
(ptk/watch (fetch-projects {:team-id id}) state stream))
|
||||
(ptk/watch (fetch-projects {:team-id id}) state stream)
|
||||
(ptk/watch (fetch-team-users params) state stream))
|
||||
(rx/catch (fn [{:keys [type code] :as error}]
|
||||
(cond
|
||||
(and (= :not-found type)
|
||||
|
|
|
@ -55,8 +55,8 @@
|
|||
(update [_ state]
|
||||
(assoc state :profile
|
||||
(cond-> data
|
||||
(nil? (:photo-uri data))
|
||||
(assoc :photo-uri (avatars/generate {:name fullname}))
|
||||
(empty? (:photo data))
|
||||
(assoc :photo (avatars/generate {:name fullname}))
|
||||
|
||||
(nil? (:lang data))
|
||||
(assoc :lang cfg/default-language)
|
||||
|
|
|
@ -14,10 +14,12 @@
|
|||
[app.common.math :as mth]
|
||||
[app.common.spec :as us]
|
||||
[app.main.constants :as c]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.common :as dwc]
|
||||
[app.main.data.comments :as dcm]
|
||||
[app.main.store :as st]
|
||||
[app.main.streams :as ms]
|
||||
[app.util.router :as rt]
|
||||
[beicon.core :as rx]
|
||||
[cljs.spec.alpha :as s]
|
||||
[potok.core :as ptk]))
|
||||
|
@ -82,12 +84,31 @@
|
|||
(update [_ state]
|
||||
(update state :workspace-local
|
||||
(fn [{:keys [vbox vport zoom] :as local}]
|
||||
(prn "center-to-comment-thread" vbox)
|
||||
(let [pw (/ 50 zoom)
|
||||
ph (/ 200 zoom)
|
||||
nw (mth/round (- (/ (:width vbox) 2) pw))
|
||||
nh (mth/round (- (/ (:height vbox) 2) ph))
|
||||
nx (- (:x position) nw)
|
||||
ny (- (:y position) nh)]
|
||||
(update local :vbox assoc :x nx :y ny)))))))
|
||||
|
||||
|
||||
(update local :vbox assoc :x nx :y ny)))))))
|
||||
|
||||
(defn navigate
|
||||
[{:keys [project-id file-id page-id] :as thread}]
|
||||
(us/assert ::dcm/comment-thread thread)
|
||||
(ptk/reify ::navigate
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [pparams {:project-id (:project-id thread)
|
||||
:file-id (:file-id thread)}
|
||||
qparams {:page-id (:page-id thread)}]
|
||||
(rx/merge
|
||||
(rx/of (rt/nav :workspace pparams qparams)
|
||||
(dw/select-for-drawing :comments))
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::dw/initialize-viewport))
|
||||
(rx/take 1)
|
||||
(rx/mapcat #(rx/of (center-to-comment-thread thread)
|
||||
(dcm/open-thread thread)))))))))
|
||||
|
|
|
@ -208,6 +208,9 @@
|
|||
(def viewer-local
|
||||
(l/derived :viewer-local st/state))
|
||||
|
||||
(def comment-threads
|
||||
(l/derived :comment-threads st/state))
|
||||
|
||||
(def comments-local
|
||||
(l/derived :comments-local st/state))
|
||||
|
||||
|
|
|
@ -148,7 +148,7 @@
|
|||
:left (str (+ pos-x 14) "px")}
|
||||
:on-click dom/stop-propagation}
|
||||
[:div.reply-form
|
||||
[:& resizing-textarea {:placeholder "Write new comment"
|
||||
[:& resizing-textarea {:placeholder (tr "labels.write-new-comment")
|
||||
:value (or content "")
|
||||
:on-esc on-esc
|
||||
:on-change on-change}]
|
||||
|
@ -267,10 +267,10 @@
|
|||
[:& dropdown {:show @options
|
||||
:on-close on-hide-options}
|
||||
[:ul.dropdown.comment-options-dropdown
|
||||
[:li {:on-click on-edit-clicked} "Edit"]
|
||||
[:li {:on-click on-edit-clicked} (tr "labels.edit")]
|
||||
(if thread
|
||||
[:li {:on-click on-delete-thread} "Delete thread"]
|
||||
[:li {:on-click on-delete-comment} "Delete comment"])]]]))
|
||||
[:li {:on-click on-delete-thread} (tr "labels.delete-comment-thread")]
|
||||
[:li {:on-click on-delete-comment} (tr "labels.delete-comment")])]]]))
|
||||
|
||||
(defn comments-ref
|
||||
[{:keys [id] :as thread}]
|
||||
|
@ -289,12 +289,13 @@
|
|||
(sort-by :created-at))
|
||||
comment (first comments)]
|
||||
|
||||
(mf/use-effect
|
||||
(st/emitf (dcm/update-comment-thread-status thread)))
|
||||
(mf/use-layout-effect
|
||||
(mf/deps thread)
|
||||
(st/emitf (dcm/retrieve-comments (:id thread))))
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps thread)
|
||||
(st/emitf (dcm/retrieve-comments (:id thread))))
|
||||
(st/emitf (dcm/update-comment-thread-status thread)))
|
||||
|
||||
(mf/use-layout-effect
|
||||
(mf/deps thread comments-map)
|
||||
|
@ -338,3 +339,62 @@
|
|||
:unread (pos? (:count-unread-comments thread)))
|
||||
:on-click on-click*}
|
||||
[:span (:seqn thread)]]))
|
||||
|
||||
(mf/defc comment-thread
|
||||
[{:keys [item users on-click] :as props}]
|
||||
(let [profile (get users (:owner-id item))
|
||||
|
||||
on-click*
|
||||
(mf/use-callback
|
||||
(mf/deps item)
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(dom/prevent-default event)
|
||||
(when (fn? on-click)
|
||||
(on-click item))))]
|
||||
|
||||
[:div.comment {:on-click on-click*}
|
||||
[:div.author
|
||||
[:div.thread-bubble
|
||||
{:class (dom/classnames
|
||||
:resolved (:is-resolved item)
|
||||
:unread (pos? (:count-unread-comments item)))}
|
||||
(:seqn item)]
|
||||
[:div.avatar
|
||||
[:img {:src (cfg/resolve-media-path (:photo profile))}]]
|
||||
[:div.name
|
||||
[:div.fullname (:fullname profile) ", "]
|
||||
[:div.timeago (dt/timeago (:modified-at item))]]]
|
||||
[:div.content
|
||||
[:span.text (:content item)]]
|
||||
[:div.content.replies
|
||||
(let [unread (:count-unread-comments item ::none)
|
||||
total (:count-comments item 1)]
|
||||
[:*
|
||||
(when (> total 1)
|
||||
(if (= total 2)
|
||||
[:span.total-replies "1 reply"]
|
||||
[:span.total-replies (str (dec total) " replies")]))
|
||||
|
||||
(when (and (> total 1) (> unread 0))
|
||||
(if (= unread 1)
|
||||
[:span.new-replies "1 new reply"]
|
||||
[:span.new-replies (str unread " new replies")]))])]]))
|
||||
|
||||
(mf/defc comment-thread-group
|
||||
[{:keys [group users on-thread-click]}]
|
||||
[:div.thread-group
|
||||
(if (:file-name group)
|
||||
[:div.section-title
|
||||
[:span.label.filename (:file-name group) ", "]
|
||||
[:span.label (:page-name group)]]
|
||||
[:div.section-title
|
||||
[:span.icon i/file-html]
|
||||
[:span.label (:page-name group)]])
|
||||
[:div.threads
|
||||
(for [item (:items group)]
|
||||
[:& comment-thread
|
||||
{:item item
|
||||
:on-click on-thread-click
|
||||
:users users
|
||||
:key (:id item)}])]])
|
||||
|
|
104
frontend/src/app/main/ui/dashboard/comments.cljs
Normal file
104
frontend/src/app/main/ui/dashboard/comments.cljs
Normal file
|
@ -0,0 +1,104 @@
|
|||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.dashboard.comments
|
||||
(:require
|
||||
[okulary.core :as l]
|
||||
[app.common.data :as d]
|
||||
[app.common.spec :as us]
|
||||
[app.config :as cfg]
|
||||
[app.main.data.auth :as da]
|
||||
[app.main.data.dashboard :as dd]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.comments :as dwcm]
|
||||
[app.main.data.comments :as dcm]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.dropdown :refer [dropdown]]
|
||||
[app.main.ui.comments :as cmt]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [t tr]]
|
||||
[app.util.object :as obj]
|
||||
[app.util.router :as rt]
|
||||
[app.util.time :as dt]
|
||||
[app.util.timers :as tm]
|
||||
[beicon.core :as rx]
|
||||
[cljs.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
|
||||
(defn team-members-ref
|
||||
[{:keys [id] :as team}]
|
||||
(l/derived (l/in [:team-users id]) st/state))
|
||||
|
||||
(mf/defc comments-section
|
||||
[{:keys [profile team]}]
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps team)
|
||||
(st/emitf (dcm/retrieve-unread-comment-threads (:id team))))
|
||||
|
||||
(let [show-dropdown? (mf/use-state false)
|
||||
show-dropdown (mf/use-fn #(reset! show-dropdown? true))
|
||||
hide-dropdown (mf/use-fn #(reset! show-dropdown? false))
|
||||
threads-map (mf/deref refs/comment-threads)
|
||||
|
||||
users-ref (mf/use-memo (mf/deps team) #(team-members-ref team))
|
||||
users (mf/deref users-ref)
|
||||
|
||||
tgroups (->> (vals threads-map)
|
||||
(sort-by :modified-at)
|
||||
(reverse)
|
||||
(dcm/apply-filters {} profile)
|
||||
(dcm/group-threads-by-file-and-page))
|
||||
|
||||
|
||||
on-navigate
|
||||
(mf/use-callback
|
||||
(fn [thread]
|
||||
(st/emit! (dwcm/navigate thread))))]
|
||||
|
||||
[:div.dashboard-comments-section
|
||||
[:div.button
|
||||
{:on-click show-dropdown
|
||||
:class (dom/classnames :open @show-dropdown?
|
||||
:unread (boolean (seq tgroups)))}
|
||||
i/chat]
|
||||
|
||||
[:& dropdown {:show @show-dropdown? :on-close hide-dropdown}
|
||||
[:div.dropdown.comments-section.comment-threads-section.
|
||||
[:div.header
|
||||
[:h3 (tr "labels.comments")]
|
||||
[:span.close {:on-click hide-dropdown} i/close]]
|
||||
|
||||
[:hr]
|
||||
|
||||
(if (seq tgroups)
|
||||
[:div.thread-groups
|
||||
[:& cmt/comment-thread-group
|
||||
{:group (first tgroups)
|
||||
:on-thread-click on-navigate
|
||||
:show-file-name true
|
||||
:users users}]
|
||||
(for [tgroup (rest tgroups)]
|
||||
[:*
|
||||
[:hr]
|
||||
|
||||
[:& cmt/comment-thread-group
|
||||
{:group tgroup
|
||||
:on-thread-click on-navigate
|
||||
:show-file-name true
|
||||
:users users
|
||||
:key (:page-id tgroup)}]])]
|
||||
|
||||
[:div.thread-groups-placeholder
|
||||
(tr "labels.no-comments-available")])]]]))
|
|
@ -15,21 +15,24 @@
|
|||
[app.main.data.auth :as da]
|
||||
[app.main.data.dashboard :as dd]
|
||||
[app.main.data.messages :as dm]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.comments :as dcm]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.dropdown :refer [dropdown]]
|
||||
[app.main.ui.dashboard.inline-edition :refer [inline-edition]]
|
||||
[app.main.ui.components.forms :as fm]
|
||||
[app.main.ui.dashboard.comments :refer [comments-section]]
|
||||
[app.main.ui.dashboard.inline-edition :refer [inline-edition]]
|
||||
[app.main.ui.dashboard.team-form]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.main.ui.keyboard :as kbd]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [t tr]]
|
||||
[app.util.object :as obj]
|
||||
[app.util.router :as rt]
|
||||
[app.util.time :as dt]
|
||||
[app.util.avatars :as avatars]
|
||||
[beicon.core :as rx]
|
||||
[cljs.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
|
@ -133,7 +136,8 @@
|
|||
(mf/deps (:id team))
|
||||
(fn []
|
||||
(->> (rp/query! :teams)
|
||||
(rx/map #(mapv dd/assoc-team-avatar %))
|
||||
(rx/map (fn [teams]
|
||||
(mapv #(avatars/assoc-avatar % :name) teams)))
|
||||
(rx/subs #(reset! teams %)))))
|
||||
|
||||
[:ul.dropdown.teams-dropdown
|
||||
|
@ -421,12 +425,9 @@
|
|||
|
||||
|
||||
(mf/defc profile-section
|
||||
[{:keys [profile locale] :as props}]
|
||||
[{:keys [profile locale team] :as props}]
|
||||
(let [show (mf/use-state false)
|
||||
photo (:photo-uri profile "")
|
||||
photo (if (str/empty? photo)
|
||||
"/images/avatar.jpg"
|
||||
photo)
|
||||
photo (cfg/resolve-media-path (:photo profile))
|
||||
|
||||
on-click
|
||||
(mf/use-callback
|
||||
|
@ -436,10 +437,10 @@
|
|||
(st/emit! (rt/nav section))
|
||||
(st/emit! section))))]
|
||||
|
||||
[:div.profile-section {:on-click #(reset! show true)}
|
||||
[:img {:src photo}]
|
||||
[:span (:fullname profile)]
|
||||
i/arrow-down
|
||||
[:div.profile-section
|
||||
[:div.profile {:on-click #(reset! show true)}
|
||||
[:img {:src photo}]
|
||||
[:span (:fullname profile)]
|
||||
|
||||
[:& dropdown {:on-close #(reset! show false)
|
||||
:show @show}
|
||||
|
@ -452,17 +453,25 @@
|
|||
[:span.text (t locale "labels.password")]]
|
||||
[:li {:on-click (partial on-click (da/logout))}
|
||||
[:span.icon i/exit]
|
||||
[:span.text (t locale "labels.logout")]]]]]))
|
||||
[:span.text (t locale "labels.logout")]]]]]
|
||||
|
||||
(when (and team profile)
|
||||
[:& comments-section {:profile profile
|
||||
:team team}])]))
|
||||
|
||||
(mf/defc sidebar
|
||||
{::mf/wrap-props false
|
||||
::mf/wrap [mf/memo]}
|
||||
[props]
|
||||
(let [locale (mf/deref i18n/locale)
|
||||
team (obj/get props "team")
|
||||
profile (obj/get props "profile")
|
||||
props (-> (obj/clone props)
|
||||
(obj/set! "locale" locale))]
|
||||
[:div.dashboard-sidebar
|
||||
[:div.sidebar-inside
|
||||
[:> sidebar-content props]
|
||||
[:& profile-section {:profile profile :locale locale}]]]))
|
||||
[:& profile-section
|
||||
{:profile profile
|
||||
:team team
|
||||
:locale locale}]]]))
|
||||
|
|
|
@ -106,8 +106,8 @@
|
|||
(dcm/close-thread)))))
|
||||
]
|
||||
|
||||
[:div.viewer-comments {:on-click on-click}
|
||||
[:div.comments-layer
|
||||
[:div.comments-section {:on-click on-click}
|
||||
[:div.viewer-comments-container
|
||||
[:div.threads
|
||||
(for [item threads]
|
||||
[:& cmt/thread-bubble {:thread item
|
||||
|
|
|
@ -60,7 +60,6 @@
|
|||
#_(dcm/close-thread))))
|
||||
]
|
||||
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps file-id)
|
||||
(fn []
|
||||
|
@ -68,8 +67,8 @@
|
|||
(fn []
|
||||
(st/emit! ::dwcm/finalize))))
|
||||
|
||||
[:div.workspace-comments
|
||||
[:div.comments-layer
|
||||
[:div.comments-section
|
||||
[:div.workspace-comments-container
|
||||
{:style {:width (str (:width vport) "px")
|
||||
:height (str (:height vport) "px")}}
|
||||
[:div.threads {:style {:transform (str/format "translate(%spx, %spx)" pos-x pos-y)}}
|
||||
|
@ -96,66 +95,6 @@
|
|||
;; Sidebar
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(mf/defc sidebar-group-item
|
||||
[{:keys [item] :as props}]
|
||||
(let [profile (get @refs/workspace-users (:owner-id item))
|
||||
page-id (mf/use-ctx ctx/current-page-id)
|
||||
file-id (mf/use-ctx ctx/current-file-id)
|
||||
|
||||
on-click
|
||||
(mf/use-callback
|
||||
(mf/deps item page-id)
|
||||
(fn []
|
||||
(when (not= page-id (:page-id item))
|
||||
(st/emit! (dw/go-to-page (:page-id item))))
|
||||
(tm/schedule
|
||||
(st/emitf (dwcm/center-to-comment-thread item)
|
||||
(dcm/open-thread item)))))]
|
||||
|
||||
[:div.comment {:on-click on-click}
|
||||
[:div.author
|
||||
[:div.thread-bubble
|
||||
{:class (dom/classnames
|
||||
:resolved (:is-resolved item)
|
||||
:unread (pos? (:count-unread-comments item)))}
|
||||
(:seqn item)]
|
||||
[:div.avatar
|
||||
[:img {:src (cfg/resolve-media-path (:photo profile))}]]
|
||||
[:div.name
|
||||
[:div.fullname (:fullname profile) ", "]
|
||||
[:div.timeago (dt/timeago (:modified-at item))]]]
|
||||
[:div.content
|
||||
[:span.text (:content item)]]
|
||||
[:div.content.replies
|
||||
(let [unread (:count-unread-comments item ::none)
|
||||
total (:count-comments item 1)]
|
||||
[:*
|
||||
(when (> total 1)
|
||||
(if (= total 2)
|
||||
[:span.total-replies "1 reply"]
|
||||
[:span.total-replies (str (dec total) " replies")]))
|
||||
|
||||
(when (and (> total 1) (> unread 0))
|
||||
(if (= unread 1)
|
||||
[:span.new-replies "1 new reply"]
|
||||
[:span.new-replies (str unread " new replies")]))])]]))
|
||||
|
||||
(defn page-name-ref
|
||||
[id]
|
||||
(l/derived (l/in [:workspace-data :pages-index id :name]) st/state))
|
||||
|
||||
(mf/defc sidebar-item
|
||||
[{:keys [group]}]
|
||||
(let [page-name-ref (mf/use-memo (mf/deps (:page-id group)) #(page-name-ref (:page-id group)))
|
||||
page-name (mf/deref page-name-ref)]
|
||||
[:div.page-section
|
||||
[:div.section-title
|
||||
[:span.icon i/file-html]
|
||||
[:span.label page-name]]
|
||||
[:div.comments-container
|
||||
(for [item (:items group)]
|
||||
[:& sidebar-group-item {:item item :key (:id item)}])]]))
|
||||
|
||||
(mf/defc sidebar-options
|
||||
[{:keys [local] :as props}]
|
||||
(let [{cmode :mode cshow :show} (mf/deref refs/comments-local)
|
||||
|
@ -171,7 +110,7 @@
|
|||
(fn [mode]
|
||||
(st/emit! (dcm/update-filters {:show mode}))))]
|
||||
|
||||
[:ul.dropdown.with-check.sidebar-options-dropdown
|
||||
[:ul.dropdown.with-check
|
||||
[:li {:class (dom/classnames :selected (or (= :all cmode) (nil? cmode)))
|
||||
:on-click #(update-mode :all)}
|
||||
[:span.icon i/tick]
|
||||
|
@ -193,6 +132,7 @@
|
|||
[]
|
||||
(let [threads-map (mf/deref threads-ref)
|
||||
profile (mf/deref refs/profile)
|
||||
users (mf/deref refs/workspace-users)
|
||||
local (mf/deref refs/comments-local)
|
||||
options? (mf/use-state false)
|
||||
|
||||
|
@ -200,28 +140,45 @@
|
|||
(sort-by :modified-at)
|
||||
(reverse)
|
||||
(dcm/apply-filters local profile)
|
||||
(dcm/group-threads-by-page))]
|
||||
(dcm/group-threads-by-page))
|
||||
|
||||
[:div.workspace-comments.workspace-comments-sidebar
|
||||
[:div.sidebar-title
|
||||
page-id (mf/use-ctx ctx/current-page-id)
|
||||
|
||||
on-thread-click
|
||||
(mf/use-callback
|
||||
(fn [thread]
|
||||
(when (not= page-id (:page-id thread))
|
||||
(st/emit! (dw/go-to-page (:page-id thread))))
|
||||
(tm/schedule
|
||||
(st/emitf (dwcm/center-to-comment-thread thread)
|
||||
(dcm/open-thread thread)))))]
|
||||
|
||||
[:div.comments-section.comment-threads-section
|
||||
[:div.workspace-comment-threads-sidebar-header
|
||||
[:div.label "Comments"]
|
||||
[:div.options {:on-click #(reset! options? true)}
|
||||
[:div.label (case (:filter local)
|
||||
[:div.label (case (:mode local)
|
||||
(nil :all) "All"
|
||||
:yours "Only yours")]
|
||||
[:div.icon i/arrow-down]]]
|
||||
[:div.icon i/arrow-down]]
|
||||
|
||||
[:& dropdown {:show @options?
|
||||
:on-close #(reset! options? false)}
|
||||
[:& sidebar-options {:local local}]]
|
||||
[:& dropdown {:show @options?
|
||||
:on-close #(reset! options? false)}
|
||||
[:& sidebar-options {:local local}]]]
|
||||
|
||||
(when (seq tgroups)
|
||||
[:div.threads
|
||||
[:& sidebar-item {:group (first tgroups)}]
|
||||
[:div.thread-groups
|
||||
[:& cmt/comment-thread-group
|
||||
{:group (first tgroups)
|
||||
:on-thread-click on-thread-click
|
||||
:users users}]
|
||||
(for [tgroup (rest tgroups)]
|
||||
[:*
|
||||
[:hr]
|
||||
[:& sidebar-item {:group tgroup
|
||||
:key (:page-id tgroup)}]])])]))
|
||||
[:& cmt/comment-thread-group
|
||||
{:group tgroup
|
||||
:on-thread-click on-thread-click
|
||||
:users users
|
||||
:key (:page-id tgroup)}]])])]))
|
||||
|
||||
|
||||
|
|
|
@ -36,8 +36,13 @@
|
|||
|
||||
(.toDataURL canvas)))
|
||||
|
||||
(defn assoc-profile-avatar
|
||||
[{:keys [photo fullname] :as profile}]
|
||||
(cond-> profile
|
||||
(defn assoc-avatar
|
||||
[{:keys [photo] :as object} key]
|
||||
(cond-> object
|
||||
(or (nil? photo) (empty? photo))
|
||||
(assoc :photo (generate {:name fullname}))))
|
||||
(assoc :photo (generate {:name (get object key)}))))
|
||||
|
||||
(defn assoc-profile-avatar
|
||||
[object]
|
||||
(assoc-avatar object :fullname))
|
||||
|
||||
|
|
|
@ -933,6 +933,13 @@ concat-stream@^1.6.0, concat-stream@^1.6.2:
|
|||
readable-stream "^2.2.2"
|
||||
typedarray "^0.0.6"
|
||||
|
||||
concat-with-sourcemaps@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/concat-with-sourcemaps/-/concat-with-sourcemaps-1.1.0.tgz#d4ea93f05ae25790951b99e7b3b09e3908a4082e"
|
||||
integrity sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg==
|
||||
dependencies:
|
||||
source-map "^0.6.1"
|
||||
|
||||
config-chain@^1.1.12:
|
||||
version "1.1.12"
|
||||
resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.12.tgz#0fde8d091200eb5e808caf25fe618c02f48e4efa"
|
||||
|
@ -2033,6 +2040,15 @@ gulp-cli@^2.2.0:
|
|||
v8flags "^3.2.0"
|
||||
yargs "^7.1.0"
|
||||
|
||||
gulp-concat@^2.6.1:
|
||||
version "2.6.1"
|
||||
resolved "https://registry.yarnpkg.com/gulp-concat/-/gulp-concat-2.6.1.tgz#633d16c95d88504628ad02665663cee5a4793353"
|
||||
integrity sha1-Yz0WyV2IUEYorQJmVmPO5aR5M1M=
|
||||
dependencies:
|
||||
concat-with-sourcemaps "^1.0.0"
|
||||
through2 "^2.0.0"
|
||||
vinyl "^2.0.0"
|
||||
|
||||
gulp-gzip@^1.4.2:
|
||||
version "1.4.2"
|
||||
resolved "https://registry.yarnpkg.com/gulp-gzip/-/gulp-gzip-1.4.2.tgz#0422a94014248655b5b1a9eea1c2abee1d4f4337"
|
||||
|
|
Loading…
Add table
Reference in a new issue