Merge pull request #23609 from nextcloud/fix/comments/various

pull/23762/head
John Molakvoæ 2020-10-28 12:32:28 +07:00 committed by GitHub
commit 73e063e63c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 119 additions and 185 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1,2 +1,2 @@
!function(e){var n={};function t(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,t),o.l=!0,o.exports}t.m=e,t.c=n,t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:r})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,n){if(1&n&&(e=t(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(t.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var o in e)t.d(r,o,function(n){return e[n]}.bind(null,o));return r},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},t.p="/js/",t(t.s=853)}({853:function(e,n){function r(e,n,t,r,o,i,u){try{var c=e[i](u),a=c.value}catch(e){return void t(e)}c.done?n(a):Promise.resolve(a).then(r,o)}var o=null,i=new OCA.Files.Sidebar.Tab({id:"comments",name:t("comments","Comments"),icon:"icon-comment",mount:function(e,n,t){return(i=regeneratorRuntime.mark((function r(){return regeneratorRuntime.wrap((function(r){for(;;)switch(r.prev=r.next){case 0:return o&&o.$destroy(),o=new OCA.Comments.View("files",{parent:t}),r.next=4,o.update(n.id);case 4:o.$mount(e);case 5:case"end":return r.stop()}}),r)})),function(){var e=this,n=arguments;return new Promise((function(t,o){var u=i.apply(e,n);function c(e){r(u,t,o,c,a,"next",e)}function a(e){r(u,t,o,c,a,"throw",e)}c(void 0)}))})();var i},update:function(e){o.update(e.id)},destroy:function(){o.$destroy(),o=null},scrollBottomReached:function(){o.onScrollBottomReached()}});window.addEventListener("DOMContentLoaded",(function(){OCA.Files&&OCA.Files.Sidebar&&OCA.Files.Sidebar.registerTab(i)}))}});
!function(e){var n={};function t(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,t),o.l=!0,o.exports}t.m=e,t.c=n,t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:r})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,n){if(1&n&&(e=t(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(t.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var o in e)t.d(r,o,function(n){return e[n]}.bind(null,o));return r},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},t.p="/js/",t(t.s=852)}({852:function(e,n){function r(e,n,t,r,o,i,u){try{var c=e[i](u),a=c.value}catch(e){return void t(e)}c.done?n(a):Promise.resolve(a).then(r,o)}var o=null,i=new OCA.Files.Sidebar.Tab({id:"comments",name:t("comments","Comments"),icon:"icon-comment",mount:function(e,n,t){return(i=regeneratorRuntime.mark((function r(){return regeneratorRuntime.wrap((function(r){for(;;)switch(r.prev=r.next){case 0:return o&&o.$destroy(),o=new OCA.Comments.View("files",{parent:t}),r.next=4,o.update(n.id);case 4:o.$mount(e);case 5:case"end":return r.stop()}}),r)})),function(){var e=this,n=arguments;return new Promise((function(t,o){var u=i.apply(e,n);function c(e){r(u,t,o,c,a,"next",e)}function a(e){r(u,t,o,c,a,"throw",e)}c(void 0)}))})();var i},update:function(e){o.update(e.id)},destroy:function(){o.$destroy(),o=null},scrollBottomReached:function(){o.onScrollBottomReached()}});window.addEventListener("DOMContentLoaded",(function(){OCA.Files&&OCA.Files.Sidebar&&OCA.Files.Sidebar.registerTab(i)}))}});
//# sourceMappingURL=comments-tab.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -66,8 +66,9 @@
</div>
<!-- Message editor -->
<div class="comment__message" v-if="editor || editing">
<RichContenteditable v-model="localMessage"
<div class="comment__editor " v-if="editor || editing">
<RichContenteditable ref="editor"
v-model="localMessage"
:auto-complete="autoComplete"
:contenteditable="!loading"
@submit="onSubmit" />
@ -83,7 +84,11 @@
<!-- Message content -->
<!-- The html is escaped and sanitized before rendering -->
<!-- eslint-disable-next-line vue/no-v-html-->
<div v-else class="comment__message" v-html="renderedContent" />
<div v-else
:class="{'comment__message--expanded': expanded}"
class="comment__message"
@click="onExpand"
v-html="renderedContent" />
</div>
</template>
@ -117,10 +122,6 @@ export default {
inheritAttrs: false,
props: {
source: {
type: Object,
default: () => ({}),
},
actorDisplayName: {
type: String,
required: true,
@ -153,6 +154,7 @@ export default {
data() {
return {
expanded: false,
// Only change data locally and update the original
// parent data when the request is sent and resolved
localMessage: '',
@ -215,11 +217,24 @@ export default {
* Dispatch message between edit and create
*/
onSubmit() {
// Do not submit if message is empty
if (this.localMessage.trim() === '') {
return
}
if (this.editor) {
this.onNewComment(this.localMessage)
this.onNewComment(this.localMessage.trim())
this.$nextTick(() => {
// Focus the editor again
this.$refs.editor.$el.focus()
})
return
}
this.onEditComment(this.localMessage)
this.onEditComment(this.localMessage.trim())
},
onExpand() {
this.expanded = true
},
},
@ -258,6 +273,7 @@ $comment-padding: 10px;
color: var(--color-text-maxcontrast);
}
&__editor,
&__message {
position: relative;
// Avatar size, align with author name
@ -287,12 +303,23 @@ $comment-padding: 10px;
opacity: 1;
}
}
&__message {
white-space: pre-wrap;
word-break: break-word;
max-height: 70px;
overflow: hidden;
&--expanded {
max-height: none;
overflow: visible;
}
}
}
.rich-contenteditable__input {
min-height: 44px;
margin: 0;
padding: $comment-padding;
min-height: 44px;
}
</style>

@ -32,8 +32,7 @@ export default {
default: null,
},
message: {
// GenFileInfo can convert message as numbers if they doesn't contains text
type: [String, Number],
type: String,
default: '',
},
ressourceId: {
@ -103,6 +102,7 @@ export default {
const newComment = await NewComment(this.commentsType, this.ressourceId, message)
this.logger.debug('New comment posted', { commentsType: this.commentsType, ressourceId: this.ressourceId, newComment })
this.$emit('new', newComment)
// Clear old content
this.$emit('update:message', '')
this.localMessage = ''

@ -23,7 +23,6 @@
import { parseXML, prepareFileFromProps } from 'webdav/dist/node/interface/dav'
import { processResponsePayload } from 'webdav/dist/node/response'
import client from './DavClient'
import { genFileInfo } from '../utils/fileUtils'
export const DEFAULT_LIMIT = 20
/**
@ -61,7 +60,7 @@ export default async function({ commentsType, ressourceId }, options = {}) {
.then(parseXML)
.then(xml => processMultistatus(xml, true))
.then(comments => processResponsePayload(response, comments, true))
.then(response => response.data.map(genFileInfo))
.then(response => response.data)
}
// https://github.com/perry-mitchell/webdav-client/blob/9de2da4a2599e06bd86c2778145b7ade39fe0b3c/source/interface/directoryContents.js#L32

@ -20,7 +20,6 @@
*
*/
import { genFileInfo } from '../utils/fileUtils'
import { getCurrentUser } from '@nextcloud/auth'
import { getRootPath } from '../utils/davUtils'
import axios from '@nextcloud/axios'
@ -56,5 +55,5 @@ export default async function(commentsType, ressourceId, message) {
details: true,
})
return genFileInfo(comment)
return comment.data
}

@ -1,122 +0,0 @@
/**
* @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import camelcase from 'camelcase'
import { isNumber } from './numberUtil'
/**
* Get an url encoded path
*
* @param {String} path the full path
* @returns {string} url encoded file path
*/
const encodeFilePath = function(path) {
const pathSections = (path.startsWith('/') ? path : `/${path}`).split('/')
let relativePath = ''
pathSections.forEach((section) => {
if (section !== '') {
relativePath += '/' + encodeURIComponent(section)
}
})
return relativePath
}
/**
* Extract dir and name from file path
*
* @param {String} path the full path
* @returns {String[]} [dirPath, fileName]
*/
const extractFilePaths = function(path) {
const pathSections = path.split('/')
const fileName = pathSections[pathSections.length - 1]
const dirPath = pathSections.slice(0, pathSections.length - 1).join('/')
return [dirPath, fileName]
}
/**
* Sorting comparison function
*
* @param {Object} fileInfo1 file 1 fileinfo
* @param {Object} fileInfo2 file 2 fileinfo
* @param {string} key key to sort with
* @param {boolean} [asc=true] sort ascending?
* @returns {number}
*/
const sortCompare = function(fileInfo1, fileInfo2, key, asc = true) {
if (fileInfo1.isFavorite && !fileInfo2.isFavorite) {
return -1
} else if (!fileInfo1.isFavorite && fileInfo2.isFavorite) {
return 1
}
// if this is a number, let's sort by integer
if (isNumber(fileInfo1[key]) && isNumber(fileInfo2[key])) {
return Number(fileInfo1[key]) - Number(fileInfo2[key])
}
// else we sort by string, so let's sort directories first
if (fileInfo1.type === 'directory' && fileInfo2.type !== 'directory') {
return -1
} else if (fileInfo1.type !== 'directory' && fileInfo2.type === 'directory') {
return 1
}
// finally sort by name
return asc
? fileInfo1[key].localeCompare(fileInfo2[key], OC.getLanguage())
: -fileInfo1[key].localeCompare(fileInfo2[key], OC.getLanguage())
}
/**
* Generate a fileinfo object based on the full dav properties
* It will flatten everything and put all keys to camelCase
*
* @param {Object} obj the object
* @returns {Object}
*/
const genFileInfo = function(obj) {
const fileInfo = {}
Object.keys(obj).forEach(key => {
const data = obj[key]
// flatten object if any
if (!!data && typeof data === 'object' && !Array.isArray(data)) {
Object.assign(fileInfo, genFileInfo(data))
} else {
// format key and add it to the fileInfo
if (data === 'false') {
fileInfo[camelcase(key)] = false
} else if (data === 'true') {
fileInfo[camelcase(key)] = true
} else {
fileInfo[camelcase(key)] = isNumber(data)
? Number(data)
: data
}
}
})
return fileInfo
}
export { encodeFilePath, extractFilePaths, sortCompare, genFileInfo }

@ -38,11 +38,12 @@
<!-- Comments -->
<Comment v-for="comment in comments"
v-else
:key="comment.id"
v-bind="comment"
:key="comment.props.id"
v-bind="comment.props"
:auto-complete="autoComplete"
:message.sync="comment.props.message"
:ressource-id="ressourceId"
:message.sync="comment.message"
:user-data="genMentionsData(comment.props.mentions)"
class="comments__list"
@delete="onDelete" />
@ -148,6 +149,26 @@ export default {
this.getComments()
},
/**
* Make sure we have all mentions as Array of objects
* @param {Array} mentions the mentions list
* @returns {Object[]}
*/
genMentionsData(mentions) {
const list = Object.values(mentions).flat()
return list.reduce((mentions, mention) => {
mentions[mention.mentionId] = {
// TODO: support groups
icon: 'icon-user',
id: mention.mentionId,
label: mention.mentionDisplayName,
source: 'users',
primary: getCurrentUser().uid === mention.mentionId,
}
return mentions
}, {})
},
/**
* Get the existing shares infos
*/
@ -224,7 +245,7 @@ export default {
* @param {number} id the deleted comment
*/
onDelete(id) {
const index = this.comments.findIndex(comment => comment.id === id)
const index = this.comments.findIndex(comment => comment.props.id === id)
if (index > -1) {
this.comments.splice(index, 1)
} else {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -640,7 +640,7 @@ describe('Core base tests', function() {
expect($row.length).toEqual(0);
});
it('hides itself after a given time', function() {
OC.Notification.showTemporary('', {timeout: 10});
OC.Notification.showTemporary('', {timeout: 10000});
var $row = $('body .toastify');
expect($row.length).toEqual(1);
@ -660,7 +660,7 @@ describe('Core base tests', function() {
});
describe('show', function() {
it('hides itself after a given time', function() {
OC.Notification.show('', {timeout: 10});
OC.Notification.show('', {timeout: 10000});
var $row = $('body .toastify');
expect($row.length).toEqual(1);
@ -685,7 +685,7 @@ describe('Core base tests', function() {
});
describe('showHtml', function() {
it('hides itself after a given time', function() {
OC.Notification.showHtml('<p></p>', {timeout: 10});
OC.Notification.showHtml('<p></p>', {timeout: 10000});
var $row = $('body .toastify');
expect($row.length).toEqual(1);
@ -730,7 +730,7 @@ describe('Core base tests', function() {
it('hides a notification before its timeout expires', function() {
var hideCallback = sinon.spy();
var notification = OC.Notification.show('', {timeout: 10});
var notification = OC.Notification.show('', {timeout: 10000});
var $row = $('body .toastify');
expect($row.length).toEqual(1);
@ -766,7 +766,7 @@ describe('Core base tests', function() {
});
it('cumulates several notifications', function() {
var $row1 = OC.Notification.showTemporary('One');
var $row2 = OC.Notification.showTemporary('Two', {timeout: 2});
var $row2 = OC.Notification.showTemporary('Two', {timeout: 2000});
var $row3 = OC.Notification.showTemporary('Three');
var $el = $('body');

@ -21,7 +21,7 @@
import _ from 'underscore'
import $ from 'jquery'
import { showMessage } from '@nextcloud/dialogs'
import { showMessage, TOAST_DEFAULT_TIMEOUT, TOAST_PERMANENT_TIMEOUT } from '@nextcloud/dialogs'
/**
* @todo Write documentation
@ -98,7 +98,7 @@ export default {
showHtml(html, options) {
options = options || {}
options.isHTML = true
options.timeout = (!options.timeout) ? -1 : options.timeout
options.timeout = (!options.timeout) ? TOAST_PERMANENT_TIMEOUT : options.timeout
const toast = showMessage(html, options)
toast.toastElement.toastify = toast
return $(toast.toastElement)
@ -116,7 +116,7 @@ export default {
*/
show(text, options) {
options = options || {}
options.timeout = (!options.timeout) ? -1 : options.timeout
options.timeout = (!options.timeout) ? TOAST_PERMANENT_TIMEOUT : options.timeout
const toast = showMessage(text, options)
toast.toastElement.toastify = toast
return $(toast.toastElement)
@ -133,7 +133,7 @@ export default {
if (this.updatableNotification) {
this.updatableNotification.hideToast()
}
this.updatableNotification = showMessage(text, { timeout: -1 })
this.updatableNotification = showMessage(text, { timeout: TOAST_PERMANENT_TIMEOUT })
this.updatableNotification.toastElement.toastify = this.updatableNotification
return $(this.updatableNotification.toastElement)
},
@ -152,7 +152,7 @@ export default {
*/
showTemporary(text, options) {
options = options || {}
options.timeout = options.timeout || 7
options.timeout = options.timeout || TOAST_DEFAULT_TIMEOUT
const toast = showMessage(text, options)
toast.toastElement.toastify = toast
return $(toast.toastElement)

18
package-lock.json generated

@ -1761,14 +1761,24 @@
}
},
"@nextcloud/dialogs": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@nextcloud/dialogs/-/dialogs-2.0.1.tgz",
"integrity": "sha512-Bme8vcs8n4XT5spBgkDEv1z9zNOE23AIbr5jF1WJ1A2XNMNj5Zvy29RosIh0k7H+1lN0PlU38u+eMV1Ets3E4A==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@nextcloud/dialogs/-/dialogs-3.0.0.tgz",
"integrity": "sha512-5FVP0RSxIpKTKdSUlQ4osDDz/oCx2/4+InliB5MX2EcrjDe6q3fZMabSGnFTnIAu0CXRTzBk7RpneaIFGv+d5A==",
"requires": {
"@nextcloud/l10n": "^1.3.0",
"@nextcloud/typings": "^0.2.2",
"@nextcloud/typings": "^1.0.0",
"core-js": "^3.6.4",
"toastify-js": "^1.9.1"
},
"dependencies": {
"@nextcloud/typings": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@nextcloud/typings/-/typings-1.0.0.tgz",
"integrity": "sha512-r8SRvXszWTyKWEhVd3gx7eBAcCKwdoLlr+ZrR8hrSxs2nfH00de/QoGdo0n/Rcv/9mMtX/haJNd71KwODM2+uQ==",
"requires": {
"@types/jquery": "2.0.54"
}
}
}
},
"@nextcloud/eslint-config": {

@ -29,7 +29,7 @@
"@nextcloud/auth": "^1.3.0",
"@nextcloud/axios": "^1.3.3",
"@nextcloud/capabilities": "^1.0.2",
"@nextcloud/dialogs": "^2.0.1",
"@nextcloud/dialogs": "^3.0.0",
"@nextcloud/event-bus": "^1.2.0",
"@nextcloud/files": "^1.1.0",
"@nextcloud/initial-state": "^1.2.0",