diff --git a/.circleci/config.yml b/.circleci/config.yml index bc2544019..f340988ea 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -127,7 +127,7 @@ workflows: - build-dev filters: branches: - only: ['dev', 'dev-msinteg', 'feature/form-redesign'] + only: ['dev', 'dev-msinteg', 'feature/form-redesign', 'feature/postAttachments'] - deployProd: context : org-global diff --git a/package-lock.json b/package-lock.json index c45353d00..90d089252 100644 --- a/package-lock.json +++ b/package-lock.json @@ -440,7 +440,6 @@ "react-textarea-autosize": "^5.2.1", "react-transition-group": "^2.2.1", "redux-thunk": "^2.1.0", - "tc-ui": "git+https://github.com/appirio-tech/tc-ui.git#e577a0e704136f1e9ecce92ce4c0626aab932691", "uncontrollable": "^4.0.1" }, "dependencies": { @@ -449,6 +448,64 @@ "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.12.7.tgz", "integrity": "sha512-fLeEhqwymYat/MpTPUjSKHVYYl0ec2mOyALEMLmzr5i1isuG+6jfI2j2d5oBO3VIzgUXgBVIcOT9uH1TFxBckw==" }, + "fbjs": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.6.1.tgz", + "integrity": "sha1-lja3cF9bqWhNRLcveDISVK/IYPc=", + "requires": { + "core-js": "^1.0.0", + "loose-envify": "^1.0.0", + "promise": "^7.0.3", + "ua-parser-js": "^0.7.9", + "whatwg-fetch": "^0.9.0" + } + }, + "history": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/history/-/history-2.1.2.tgz", + "integrity": "sha1-SqLeiXoOSGfkU5hDvm7Nsphr/ew=", + "requires": { + "deep-equal": "^1.0.0", + "invariant": "^2.0.0", + "query-string": "^3.0.0", + "warning": "^2.0.0" + }, + "dependencies": { + "warning": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/warning/-/warning-2.1.0.tgz", + "integrity": "sha1-ISINnGOvx3qMkhEeARr3Bc4MaQE=", + "requires": { + "loose-envify": "^1.0.0" + } + } + } + }, + "hoist-non-react-statics": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz", + "integrity": "sha1-qkSM8JhtVcxAdzsXF0t90GbLfPs=" + }, + "query-string": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-3.0.3.tgz", + "integrity": "sha1-ri4UtNBQcdTpuetIc8NbDc1C5jg=", + "requires": { + "strict-uri-encode": "^1.0.0" + } + }, + "react-router": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-2.8.1.tgz", + "integrity": "sha1-c+lJH2zrMW0Pd5gpCBhj43juTtc=", + "requires": { + "history": "^2.1.2", + "hoist-non-react-statics": "^1.2.0", + "invariant": "^2.2.1", + "loose-envify": "^1.2.0", + "warning": "^3.0.0" + } + }, "react-select": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/react-select/-/react-select-0.9.1.tgz", @@ -458,6 +515,40 @@ "react-input-autosize": "^0.6.2" } }, + "tc-ui": { + "version": "git+https://github.com/appirio-tech/tc-ui.git#e577a0e704136f1e9ecce92ce4c0626aab932691", + "from": "git+https://github.com/appirio-tech/tc-ui.git#e577a0e704136f1e9ecce92ce4c0626aab932691", + "requires": { + "classnames": "^2.2.3", + "lodash": "^4.0.0", + "moment": "^2.11.2", + "node-neat": "~1.7.1-beta1", + "react": "^0.14.7", + "react-datetime": "^2.0.2", + "react-dom": "^0.14.7", + "react-dropzone": "^3.3.2", + "react-redux": "^4.2.1", + "react-router": "^2.0.0-rc6", + "react-select": "^0.9.1", + "redux": "^3.3.1" + }, + "dependencies": { + "react": { + "version": "0.14.9", + "resolved": "https://registry.npmjs.org/react/-/react-0.14.9.tgz", + "integrity": "sha1-kRCmSXxJ1EuhwO3TF67CnC4NkdE=", + "requires": { + "envify": "^3.0.0", + "fbjs": "^0.6.1" + } + }, + "react-dom": { + "version": "0.14.9", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-0.14.9.tgz", + "integrity": "sha1-BQZKPc8PsYgKOyv8nVjFXY2fYpM=" + } + } + }, "uncontrollable": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-4.1.0.tgz", @@ -465,6 +556,19 @@ "requires": { "invariant": "^2.1.0" } + }, + "warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/warning/-/warning-3.0.0.tgz", + "integrity": "sha1-MuU3fLVy3kqwR1O9+IIcAe1gW3w=", + "requires": { + "loose-envify": "^1.0.0" + } + }, + "whatwg-fetch": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-0.9.0.tgz", + "integrity": "sha1-DjaExsuZlbQ+/J3wPkw2XZX9nMA=" } } }, @@ -6053,12 +6157,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -6073,17 +6179,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -6200,7 +6309,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -6212,6 +6322,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -6226,6 +6337,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -6233,12 +6345,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -6257,6 +6371,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -6337,7 +6452,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -6349,6 +6465,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -6470,6 +6587,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -17152,17 +17270,10 @@ "Base64": "~0.1.3", "json-fallback": "0.0.1", "jsonp": "~0.0.4", - "qs": "git+https://github.com/jfromaniello/node-querystring.git#5d96513991635e3e22d7aa54a8584d6ce97cace8", "reqwest": "^1.1.4", "trim": "~0.0.1", "winchan": "^0.1.1", "xtend": "~2.1.1" - }, - "dependencies": { - "qs": { - "version": "git+https://github.com/jfromaniello/node-querystring.git#5d96513991635e3e22d7aa54a8584d6ce97cace8", - "from": "git+https://github.com/jfromaniello/node-querystring.git#fix_ie7_bug_with_arrays" - } } }, "auto-config-fake-server": { @@ -19810,6 +19921,7 @@ "version": "0.0.9", "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", + "optional": true, "requires": { "inherits": "~2.0.0" } @@ -19818,6 +19930,7 @@ "version": "2.10.1", "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", + "optional": true, "requires": { "hoek": "2.x.x" } @@ -19834,7 +19947,8 @@ "buffer-shims": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", - "integrity": "sha1-mXjOMXOIxkmth5MCjDR37wRKi1E=" + "integrity": "sha1-mXjOMXOIxkmth5MCjDR37wRKi1E=", + "optional": true }, "caseless": { "version": "0.12.0", @@ -19851,12 +19965,14 @@ "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "optional": true }, "combined-stream": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", + "optional": true, "requires": { "delayed-stream": "~1.0.0" } @@ -19869,12 +19985,14 @@ "console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "optional": true }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "optional": true }, "cryptiles": { "version": "2.0.5", @@ -19920,7 +20038,8 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "optional": true }, "delegates": { "version": "1.0.0", @@ -19946,7 +20065,8 @@ "extsprintf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz", - "integrity": "sha1-4QgOBljjALBilJkMxw4VAiNf1VA=" + "integrity": "sha1-4QgOBljjALBilJkMxw4VAiNf1VA=", + "optional": true }, "forever-agent": { "version": "0.6.1", @@ -20117,6 +20237,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -20130,7 +20251,8 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "optional": true }, "isstream": { "version": "0.1.2", @@ -20203,12 +20325,14 @@ "mime-db": { "version": "1.27.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.27.0.tgz", - "integrity": "sha1-gg9XIpa70g7CXtVeW13oaeVDbrE=" + "integrity": "sha1-gg9XIpa70g7CXtVeW13oaeVDbrE=", + "optional": true }, "mime-types": { "version": "2.1.15", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.15.tgz", "integrity": "sha1-pOv1BkCUVpI3uM9wBGd20J/JKu0=", + "optional": true, "requires": { "mime-db": "~1.27.0" } @@ -20282,7 +20406,8 @@ "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "optional": true }, "oauth-sign": { "version": "0.8.2", @@ -20340,7 +20465,8 @@ "process-nextick-args": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", + "optional": true }, "punycode": { "version": "1.4.1", @@ -20378,6 +20504,7 @@ "version": "2.2.9", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.9.tgz", "integrity": "sha1-z3jsb0ptHrQ9JkiMrJfwQudLf8g=", + "optional": true, "requires": { "buffer-shims": "~1.0.0", "core-util-is": "~1.0.0", @@ -20429,7 +20556,8 @@ "safe-buffer": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.0.1.tgz", - "integrity": "sha1-0mPKVGls2KMGtcplUekt5XkY++c=" + "integrity": "sha1-0mPKVGls2KMGtcplUekt5XkY++c=", + "optional": true }, "semver": { "version": "5.3.0", @@ -20487,6 +20615,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -20497,6 +20626,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.1.tgz", "integrity": "sha1-YuIA8DmVWmgQ2N8KM//A8BNmLZg=", + "optional": true, "requires": { "safe-buffer": "^5.0.1" } @@ -20525,6 +20655,7 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", + "optional": true, "requires": { "block-stream": "*", "fstream": "^1.0.2", @@ -20580,7 +20711,8 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "optional": true }, "uuid": { "version": "3.0.1", @@ -23173,6 +23305,10 @@ "resolved": "https://registry.npmjs.org/q/-/q-1.5.0.tgz", "integrity": "sha512-VVMcd+HnuWZalHPycK7CsbVJ+sSrrrnCvHcW38YJVK9Tywnb5DUWJjONi81bLUj7aqDjIXnePxBl5t1r/F/ncg==" }, + "qs": { + "version": "git+https://github.com/jfromaniello/node-querystring.git#5d96513991635e3e22d7aa54a8584d6ce97cace8", + "from": "git+https://github.com/jfromaniello/node-querystring.git#5d96513991635e3e22d7aa54a8584d6ce97cace8" + }, "query-string": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", diff --git a/src/api/messages.js b/src/api/messages.js index 554b32b43..941c5e1bd 100644 --- a/src/api/messages.js +++ b/src/api/messages.js @@ -76,7 +76,11 @@ export function deleteTopic(topicId) { } export function addTopicPost(topicId, post) { - return axios.post(`${apiBaseUrl}/topics/${topicId}/posts/create`, { post: post.content }, { timeout } ) + const payload = { + post: post.content, + attachmentIds: post.attachmentIds + } + return axios.post(`${apiBaseUrl}/topics/${topicId}/posts/create`, payload, { timeout } ) .then( resp => { return { topicId, @@ -86,7 +90,11 @@ export function addTopicPost(topicId, post) { } export function saveTopicPost(topicId, post) { - return axios.post(`${apiBaseUrl}/topics/${topicId}/posts/${post.id}/edit`, { post: post.content }, { timeout } ) + const payload = { + post: post.content, + attachmentIds: post.attachmentIds + } + return axios.post(`${apiBaseUrl}/topics/${topicId}/posts/${post.id}/edit`, payload, { timeout } ) .then( resp => { return { topicId, @@ -149,3 +157,28 @@ export function getTopicsWithComments(reference, referenceId, tag, removeCoderBo }) } + +export function createTopicAttachment(attachment) { + return axios.post(`${apiBaseUrl}/topics/attachments/create`, attachment, { timeout } ) + .then( resp => { + return { + result : _.get(resp.data, 'result.content', {}) + } + }) +} + +export function getTopicAttachment(attachmentId) { + return axios.get(`${apiBaseUrl}/topics/attachments/${attachmentId}/read`, { timeout } ) + .then( resp => { + return _.get(resp.data, 'result.content.url', '') + }) +} + +export function deleteTopicAttachment(attachmentId) { + return axios.delete(`${apiBaseUrl}/topics/attachments/${attachmentId}/delete`, { timeout } ) + .then( resp => { + return { + result : _.get(resp.data, 'result.content', {}) + } + }) +} diff --git a/src/components/ActionCard/ActionCard.jsx b/src/components/ActionCard/ActionCard.jsx index 2ca005e78..f9de21360 100644 --- a/src/components/ActionCard/ActionCard.jsx +++ b/src/components/ActionCard/ActionCard.jsx @@ -42,8 +42,8 @@ class Header extends React.Component{ onTopicChange(title, content) { this.props.onTopicChange(this.props.topicMessage.id, title, content, true) } - onSaveTopic({title, content}) { - this.props.onSaveTopic(this.props.topicMessage.id, title, content) + onSaveTopic({title, content, onSaveTopic}) { + this.props.onSaveTopic(this.props.topicMessage.id, title, content, onSaveTopic) } render() { @@ -67,6 +67,7 @@ class Header extends React.Component{ avatarUrl={this.props.avatarUrl} authorName={this.props.authorName} cancelEdit={this.cancelEditTopic} + canUploadAttachment /> ) } diff --git a/src/components/ActionCard/AddComment.jsx b/src/components/ActionCard/AddComment.jsx index 94326165c..72fbab580 100644 --- a/src/components/ActionCard/AddComment.jsx +++ b/src/components/ActionCard/AddComment.jsx @@ -16,8 +16,8 @@ export default class AddComment extends React.Component { } } - onPost({content}) { - this.props.onAdd(content) + onPost({ content, attachmentIds }) { + this.props.onAdd(content, attachmentIds) } onChange(title, content) { @@ -40,6 +40,7 @@ export default class AddComment extends React.Component { authorName={authorName} allMembers={allMembers} projectMembers={projectMembers} + canUploadAttachment /> ) } diff --git a/src/components/ActionCard/Comment.jsx b/src/components/ActionCard/Comment.jsx index 331e903ee..01f31b98d 100644 --- a/src/components/ActionCard/Comment.jsx +++ b/src/components/ActionCard/Comment.jsx @@ -3,17 +3,18 @@ import PropTypes from 'prop-types' import cn from 'classnames' import UserTooltip from '../User/UserTooltip' import RichTextArea from '../RichTextArea/RichTextArea' -import { Link } from 'react-router-dom' +import { Link, withRouter } from 'react-router-dom' import CommentEditToggle from './CommentEditToggle' import _ from 'lodash' import moment from 'moment' import NotificationsReader from '../../components/NotificationsReader' -import { +import { POST_TIME_FORMAT, - EVENT_TYPE, + EVENT_TYPE, } from '../../config/constants.js' import './Comment.scss' +import { PROJECT_ATTACHMENTS_FOLDER } from '../../config/constants' class Comment extends React.Component { @@ -25,18 +26,24 @@ class Comment extends React.Component { this.edit = this.edit.bind(this) this.delete = this.delete.bind(this) this.cancelEdit = this.cancelEdit.bind(this) + this.getDownloadAttachmentUrl = this.getDownloadAttachmentUrl.bind(this) + this.getDownloadAttachmentFilename = this.getDownloadAttachmentFilename.bind(this) } componentWillMount() { - this.setState({editMode: this.props.message && this.props.message.editMode || this.props.isSaving}) + const projectId = this.props.match.params.projectId + this.setState({ + editMode: this.props.message && this.props.message.editMode || this.props.isSaving, + attachmentsStorePath: `${PROJECT_ATTACHMENTS_FOLDER}/${projectId}/` + }) } componentWillReceiveProps(nextProps) { this.setState({editMode: nextProps.message && nextProps.message.editMode || nextProps.isSaving}) } - onSave({content}) { - this.props.onSave(this.props.message, content) + onSave({content, attachmentIds}) { + this.props.onSave(this.props.message, content, attachmentIds) } onChange(title, content) { @@ -57,6 +64,16 @@ class Comment extends React.Component { this.props.onChange(null, false) } + getDownloadAttachmentUrl(attachmentId) { + return `/projects/messages/attachments/${attachmentId}` + } + + getDownloadAttachmentFilename(attachmentOriginalFilename) { + const regex = new RegExp(`^${_.escapeRegExp(this.state.attachmentsStorePath)}.[a-zA-Z0-9]*.(.*.)`, 'g') + const match = regex.exec(attachmentOriginalFilename) + return match[1] + } + render() { const {message, author, date, edited, children, noInfo, self, isSaving, hasError, readonly, allMembers, canDelete, projectMembers, commentAnchorPrefix} = this.props const messageAnchor = commentAnchorPrefix + message.id @@ -86,6 +103,8 @@ class Comment extends React.Component { allMembers={allMembers} projectMembers={projectMembers} editingTopic = {false} + canUploadAttachment + attachments={message.attachments} /> ) @@ -93,11 +112,11 @@ class Comment extends React.Component { return (
- @@ -130,6 +149,19 @@ class Comment extends React.Component {
{children}
+ { message.attachments && +
+ +
+ } {isDeleting &&
Deleting post ...
@@ -214,4 +246,4 @@ Comment.propTypes = { commentAnchorPrefix: PropTypes.string, } -export default Comment +export default withRouter(Comment) diff --git a/src/components/ActionCard/Comment.scss b/src/components/ActionCard/Comment.scss index 4d3972578..d459ba765 100644 --- a/src/components/ActionCard/Comment.scss +++ b/src/components/ActionCard/Comment.scss @@ -35,7 +35,7 @@ min-width: 0; } -.text { +.text { a, a:hover { color:$tc-dark-blue-110; @@ -120,3 +120,83 @@ line-height: 20px; } } + +.download-attachment-files { + margin-top: 10px; + width: 50%; + li{ + @include roboto; + font-size: $tc-label-md; + color: $tc-dark-blue-110; + line-height: 1; + padding: $base-unit 0 $base-unit 0; + display: flex; + + &.clickable { + cursor: pointer; + } + + &.is-active { + background-color: $tc-gray-neutral-dark; + + &:not(.delete-confirmation-modal):hover { + background-color: $tc-gray-neutral-dark; + } + } + + .button-group { + margin-left: auto; + padding-left:20px; + width: 110px; + } + + .buttons{ + visibility: hidden; + margin-right: 5px; + margin-left: auto; + button { + padding: 0; + background: transparent; + border: none; + cursor: pointer; + opacity: 0.4; + height: 16px; + width: 16px; + background-size: 16px; + display: inline-block; + margin-left: 15px; + } + } + a { + color: $tc-dark-blue-110; + line-height: $base-unit*4; + max-width: 220px; + text-overflow: ellipsis; + overflow-x: hidden; + + &:visited { + color: $tc-dark-blue-110; + } + } + } + @media screen and (min-width: $screen-md) { + li:hover{ + background: $tc-gray-neutral-light; + color: $tc-dark-blue; + + .buttons{ + display: block; + } + .link-buttons{ + visibility: visible; + } + } + } + + li:before { + content: '\b7\a0'; + font-size: 200%; + color: $tc-dark-blue-110; + line-height: $base-unit*3; + } +} diff --git a/src/components/Feed/Feed.jsx b/src/components/Feed/Feed.jsx index 6d5f201fd..1a61f8ef1 100644 --- a/src/components/Feed/Feed.jsx +++ b/src/components/Feed/Feed.jsx @@ -67,8 +67,8 @@ class Feed extends React.Component { this.props.onTopicChange(this.props.topicMessage.id, title, content, true) } - onSaveTopic({title, content}) { - this.props.onSaveTopic(this.props.topicMessage.id, title, content) + onSaveTopic({title, content, attachmentIds}) { + this.props.onSaveTopic(this.props.topicMessage.id, title, content, attachmentIds) } updateHeaderHeight() { diff --git a/src/components/Feed/FeedComments.jsx b/src/components/Feed/FeedComments.jsx index c4aa2ef10..99cd122e0 100644 --- a/src/components/Feed/FeedComments.jsx +++ b/src/components/Feed/FeedComments.jsx @@ -425,7 +425,7 @@ class FeedComments extends React.Component { submitText="Post" nextStepText="Add a post" onClose={this.toggleNewCommentMobile} - onPost={({ content }) => onAddNewComment(content)} + onPost={({ content, attachmentIds }) => onAddNewComment(content, attachmentIds)} isCreating={isAddingComment} hasError={error} onNewPostChange={this.onNewCommentChange} diff --git a/src/components/Feed/NewPost.jsx b/src/components/Feed/NewPost.jsx index 482c67794..165896a25 100644 --- a/src/components/Feed/NewPost.jsx +++ b/src/components/Feed/NewPost.jsx @@ -40,6 +40,7 @@ class NewPost extends React.Component { allMembers={allMembers} projectMembers={projectMembers} hasPrivateSwitch={canAccessPrivatePosts} + canUploadAttachment /> ) } diff --git a/src/components/Feed/NewPostMobile.jsx b/src/components/Feed/NewPostMobile.jsx index 1367aaa85..37acea72f 100644 --- a/src/components/Feed/NewPostMobile.jsx +++ b/src/components/Feed/NewPostMobile.jsx @@ -16,12 +16,27 @@ import MobilePage from '../MobilePage/MobilePage' import SwitchButton from 'appirio-tech-react-components/components/SwitchButton/SwitchButton' import XMartIcon from '../../assets/icons/x-mark.svg' import './NewPostMobile.scss' +import * as filepicker from 'filestack-js' +import { + FILE_PICKER_API_KEY, + FILE_PICKER_CNAME, + FILE_PICKER_FROM_SOURCES, + FILE_PICKER_SUBMISSION_CONTAINER_NAME, PROJECT_ATTACHMENTS_FOLDER +} from '../../config/constants' +import BtnRemove from '../../assets/icons/ui-16px-1_trash-simple.svg' +import _ from 'lodash' +import { withRouter } from 'react-router-dom' +import { createTopicAttachment } from '../../api/messages' export const NEW_POST_STEP = { STATUS: 'STATUS', COMMENT: 'COMMENT' } +const fileUploadClient = filepicker.init(FILE_PICKER_API_KEY, { + cname: FILE_PICKER_CNAME +}) + // we need it to calulate body height based on the actual mobile browser viewport height const HEADER_HEIGHT = 50 @@ -29,17 +44,25 @@ class NewPostMobile extends React.Component { constructor(props) { super(props) + const projectId = props.match.params.projectId this.state = { step: props.step, statusValue: '', commentValue: '', browserActualViewportHeigth: document.documentElement.clientHeight, - isPrivate: false + isPrivate: false, + isAttachmentUploaderOpen: false, + rawFiles: [], + attachmentsStorePath: `${PROJECT_ATTACHMENTS_FOLDER}/${projectId}/`, } this.setStep = this.setStep.bind(this) this.onValueChange = this.onValueChange.bind(this) this.updateBrowserHeight = this.updateBrowserHeight.bind(this) + this.openFileUpload = this.openFileUpload.bind(this) + this.processUploadedFiles = this.processUploadedFiles.bind(this) + this.removeRawFile = this.removeRawFile.bind(this) + this.onPost = this.onPost.bind(this) } componentWillMount() { @@ -79,12 +102,85 @@ class NewPostMobile extends React.Component { } } + openFileUpload() { + if (fileUploadClient) { + if (this.state.isAttachmentUploaderOpen) return + const picker = fileUploadClient.picker({ + storeTo: { + location: 's3', + path: this.state.attachmentsStorePath, + container: FILE_PICKER_SUBMISSION_CONTAINER_NAME, + region: 'us-east-1' + }, + maxFiles: 4, + fromSources: FILE_PICKER_FROM_SOURCES, + uploadInBackground: false, + onFileUploadFinished: (files) => { + this.processUploadedFiles(files) + }, + onOpen: () => { + this.setState({isAttachmentUploaderOpen: true}) + }, + onClose: () => { + this.setState({isAttachmentUploaderOpen: false}) + } + }) + + picker.open() + } + } + + processUploadedFiles(fpFiles) { + fpFiles = _.isArray(fpFiles) ? fpFiles : [fpFiles] + const rawFiles = fpFiles.map(f => ({ + filename: f.key, + bucket: f.container, + title: f.filename + })) + + this.setState({ rawFiles }) + } + + removeRawFile(index) { + const rawFiles = _.cloneDeep(this.state.rawFiles) + rawFiles.splice(index, 1) + this.setState({ + rawFiles + }) + } + + onPost() { + const { statusValue, commentValue, isPrivate, rawFiles } = this.state + if (rawFiles.length > 0) { + const promises = rawFiles.map(f => createTopicAttachment(_.omit(f, ['title']))) + Promise.all(promises) + .then(results => { + const rawFilesattachmentIds = results.map(content => content.result.id) + const attachmentIds = [ + ...rawFilesattachmentIds + ] + this.props.onPost({ + title: statusValue, + content: commentValue, + isPrivate, + attachmentIds + }) + }) + } else { + this.props.onPost({ + title: statusValue, + content: commentValue, + isPrivate + }) + } + } + render() { const { - statusTitle, commentTitle, commentPlaceholder, submitText, onPost, onClose, + statusTitle, commentTitle, commentPlaceholder, submitText, onClose, isCreating, nextStepText, statusPlaceholder, canAccessPrivatePosts } = this.props - const { step, statusValue, commentValue, browserActualViewportHeigth, isPrivate } = this.state + const { step, statusValue, commentValue, browserActualViewportHeigth, isPrivate, rawFiles } = this.state let value let title @@ -104,11 +200,7 @@ class NewPostMobile extends React.Component { value = commentValue title = commentTitle placeholder = commentPlaceholder - onBtnClick = () => onPost({ - title: statusValue, - content: commentValue, - isPrivate - }) + onBtnClick = () => { this.onPost() } btnText = submitText } @@ -136,12 +228,37 @@ class NewPostMobile extends React.Component { onChange={this.onValueChange} disabled={isCreating} /> -
- +
+
+
+
    + { + rawFiles.map((f, index) => ( +
  • + {f.title} +
    +
    + +
    +
    +
  • + )) + } +
+
+ +
+
+ +
@@ -167,4 +284,4 @@ NewPostMobile.propTypes = { canAccessPrivatePosts: PropTypes.bool, } -export default NewPostMobile +export default withRouter(NewPostMobile) diff --git a/src/components/Feed/NewPostMobile.scss b/src/components/Feed/NewPostMobile.scss index 13f66dbc8..12d9f78e2 100644 --- a/src/components/Feed/NewPostMobile.scss +++ b/src/components/Feed/NewPostMobile.scss @@ -73,7 +73,7 @@ padding: 4 * $base-unit; resize: none; - &::placholder { + &::placeholder { color: $tc-gray-40; } } @@ -134,3 +134,82 @@ } } } + +.attachment-files-mobile { + width: 50%; + li{ + @include roboto; + font-size: $tc-label-md; + color: $tc-dark-blue-110; + line-height: 1; + padding: $base-unit 0 $base-unit 0; + display: flex; + + &.clickable { + cursor: pointer; + } + + &.is-active { + background-color: $tc-gray-neutral-dark; + } + + .button-group { + margin-left: auto; + padding-left:20px; + width: 110px; + } + + .buttons{ + margin-right: 5px; + margin-left: auto; + button { + padding: 0; + background: transparent; + border: none; + cursor: pointer; + opacity: 0.4; + height: 16px; + width: 16px; + background-size: 16px; + display: inline-block; + margin-left: 15px; + } + } + a { + color: $tc-dark-blue-110; + line-height: $base-unit*4; + max-width: 220px; + text-overflow: ellipsis; + overflow-x: hidden; + + &:visited { + color: $tc-dark-blue-110; + } + } + } + + li:before { + content: '\b7\a0'; + font-size: 200%; + color: $tc-dark-blue-110; + line-height: $base-unit*3; + } +} + +.tc-attachment-button { + width: 50%; + a{ + float: right; + cursor: pointer; + color: $tc-dark-blue; + &:hover { + text-decoration: underline; + } + } + margin-right: 20px; +} + +.attachment-wrapper { + padding: 10px; + display: flex; +} diff --git a/src/components/FileDownload.jsx b/src/components/FileDownload.jsx index 26741cde5..691cc88cf 100644 --- a/src/components/FileDownload.jsx +++ b/src/components/FileDownload.jsx @@ -1,5 +1,6 @@ import React from 'react' import { getProjectAttachment } from '../api/projectAttachments' +import { getTopicAttachment } from '../api/messages' import { withRouter } from 'react-router-dom' class FileDownload extends React.Component { @@ -15,13 +16,22 @@ class FileDownload extends React.Component { } download() { - const projectId = this.props.match.params.projectId - const attachmentId = this.props.match.params.attachmentId - getProjectAttachment(projectId, attachmentId).then((url) => { - window.location = url - }).catch(() => { - this.setState({loaded:true, error:'File unavailable'}) - }) + if (this.props.match.params.messageAttachmentId){ + const attachmentId = this.props.match.params.messageAttachmentId + getTopicAttachment(attachmentId).then((url) => { + window.location = url + }).catch(() => { + this.setState({loaded:true, error:'File unavailable'}) + }) + } else { + const projectId = this.props.match.params.projectId + const attachmentId = this.props.match.params.attachmentId + getProjectAttachment(projectId, attachmentId).then((url) => { + window.location = url + }).catch(() => { + this.setState({loaded:true, error:'File unavailable'}) + }) + } } render() { diff --git a/src/components/RichTextArea/RichTextArea.jsx b/src/components/RichTextArea/RichTextArea.jsx index fe9f9354f..39ee0af5d 100644 --- a/src/components/RichTextArea/RichTextArea.jsx +++ b/src/components/RichTextArea/RichTextArea.jsx @@ -22,6 +22,17 @@ import _ from 'lodash' import { getAvatarResized } from '../../helpers/tcHelpers' import SwitchButton from 'appirio-tech-react-components/components/SwitchButton/SwitchButton' +import { + FILE_PICKER_API_KEY, + FILE_PICKER_CNAME, FILE_PICKER_FROM_SOURCES, + FILE_PICKER_SUBMISSION_CONTAINER_NAME, + PROJECT_ATTACHMENTS_FOLDER +} from '../../config/constants' +import * as filepicker from 'filestack-js' +import BtnRemove from '../../assets/icons/ui-16px-1_trash-simple.svg' +import { createTopicAttachment } from '../../api/messages' +import { withRouter } from 'react-router-dom' + const linkPlugin = createLinkPlugin() const blockDndPlugin = createBlockDndPlugin() @@ -52,18 +63,22 @@ const blocks = [ {className: 'code', style: 'code-block'} ] +const fileUploadClient = filepicker.init(FILE_PICKER_API_KEY, { + cname: FILE_PICKER_CNAME +}) + class RichTextArea extends React.Component { constructor(props) { super(props) this.state = { - editorExpanded: false, - editorState: EditorState.createEmpty(), - titleValue: '', - suggestions: [], - allSuggestions:[], + editorExpanded: false, + editorState: EditorState.createEmpty(), + titleValue: '', + suggestions: [], + allSuggestions:[], isPrivate: false } - + this.onTitleChange = this.onTitleChange.bind(this) this.onEditorChange = this.onEditorChange.bind(this) this.handleKeyCommand = this.handleKeyCommand.bind(this) @@ -78,6 +93,11 @@ class RichTextArea extends React.Component { this.setUploadState = this.setUploadState.bind(this) this.onSearchChange = this.onSearchChange.bind(this) this.onAddMention = this.onAddMention.bind(this) + this.openFileUpload = this.openFileUpload.bind(this) + this.processUploadedFiles = this.processUploadedFiles.bind(this) + this.getDownloadAttachmentFilename = this.getDownloadAttachmentFilename.bind(this) + this.removeRawFile = this.removeRawFile.bind(this) + this.removeFile = this.removeFile.bind(this) this.mentionPlugin = createMentionPlugin({mentionPrefix: '@'}) this.plugins = plugins.slice(0) this.plugins.push(this.mentionPlugin) @@ -90,6 +110,7 @@ class RichTextArea extends React.Component { componentWillMount() { const suggestions = _.map(_.values(this.props.projectMembers), (e) => { return {name: e.firstName + ' ' + e.lastName, handle: e.handle, userId: e.userId, link:'/users/'+e.handle} }) + const projectId = this.props.match.params.projectId this.setState({ editorExpanded: this.props.editMode, titleValue: this.props.title || '', @@ -97,7 +118,11 @@ class RichTextArea extends React.Component { currentMDContent: this.props.content, oldMDContent: this.props.oldContent, suggestions, - allSuggestions:suggestions + allSuggestions:suggestions, + isAttachmentUploaderOpen: false, + rawFiles: [], + attachmentsStorePath: `${PROJECT_ATTACHMENTS_FOLDER}/${projectId}/`, + files: _.cloneDeep(this.props.attachments || []) }) } @@ -106,7 +131,6 @@ class RichTextArea extends React.Component { } componentWillReceiveProps(nextProps) { - if (nextProps.isCreating !== this.props.isCreating && !nextProps.isCreating && !nextProps.hasError) { this.clearState() } else if ((nextProps.isGettingComment !== this.props.isGettingComment && !nextProps.isGettingComment) @@ -117,7 +141,8 @@ class RichTextArea extends React.Component { titleValue: nextProps.title || '', editorState, currentMDContent: nextProps.content, - oldMDContent: nextProps.oldContent + oldMDContent: nextProps.oldContent, + files: _.cloneDeep(nextProps.attachments || []) }) } } @@ -184,11 +209,11 @@ class RichTextArea extends React.Component { const editorExpanded = isEditor && !isCloseButton const isPrivate = isEditor && !isCloseButton ? this.state.isPrivate : false - + // to avoid unnecessary re-rendering on every click, only update state if any of the values is updated if (editorExpanded !== this.state.editorExpanded || isPrivate !== this.state.isPrivate) { this.setState({ - editorExpanded, + editorExpanded, isPrivate, }) } @@ -241,19 +266,32 @@ class RichTextArea extends React.Component { this.props.onPostChange(this.refs.title.value, this.state.currentMDContent) } } - onPost() { + const { isCreating, disableTitle, disableContent, onPost, canUploadAttachment } = this.props + const { titleValue: title, currentMDContent: content, isPrivate, rawFiles, files } = this.state // if post creation is already in progress - if (this.props.isCreating) { + if (isCreating) { return } - const title = this.state.titleValue - const content = this.state.currentMDContent - const isPrivate = this.state.isPrivate - - if ((this.props.disableTitle || title) && (this.props.disableContent || content)) { - this.props.onPost({title, content, isPrivate}) + if (canUploadAttachment && rawFiles.length > 0) { + const promises = rawFiles.map(f => createTopicAttachment(_.omit(f, ['title']))) + Promise.all(promises) + .then(results => { + const rawFilesattachmentIds = results.map(content => content.result.id) + const filesattachmentIds = files.map(f => f.id) + const attachmentIds = [ + ...filesattachmentIds, + ...rawFilesattachmentIds + ] + if ((disableTitle || title) && (disableContent || content)) { + onPost({ title, content, isPrivate, attachmentIds }) + } + }) + } else { + if ((disableTitle || title) && (disableContent || content)) { + onPost({ title, content, isPrivate, attachmentIds: files.map(f => f.id) }) + } } } onSearchChange({value}){ @@ -264,6 +302,9 @@ class RichTextArea extends React.Component { onAddMention() { } cancelEdit() { + this.setState({ + rawFiles: [] + }) this.props.cancelEdit() } getEditorState() { @@ -275,15 +316,77 @@ class RichTextArea extends React.Component { setUploadState(uploading) { this.setState({uploading}) } + openFileUpload() { + if (fileUploadClient) { + if (this.state.isAttachmentUploaderOpen) return + const picker = fileUploadClient.picker({ + storeTo: { + location: 's3', + path: this.state.attachmentsStorePath, + container: FILE_PICKER_SUBMISSION_CONTAINER_NAME, + region: 'us-east-1' + }, + maxFiles: 4, + fromSources: FILE_PICKER_FROM_SOURCES, + uploadInBackground: false, + onFileUploadFinished: (files) => { + this.processUploadedFiles(files) + }, + onOpen: () => { + this.setState({isAttachmentUploaderOpen: true}) + }, + onClose: () => { + this.setState({isAttachmentUploaderOpen: false}) + } + }) + + picker.open() + } + } + processUploadedFiles(fpFiles) { + fpFiles = _.isArray(fpFiles) ? fpFiles : [fpFiles] + let rawFiles = fpFiles.map(f => ({ + filename: f.key, + bucket: f.container, + title: f.filename + })) + if (this.state.rawFiles){ + rawFiles = this.state.rawFiles.concat(rawFiles) + } + + this.setState({ rawFiles, editorExpanded: true }) + } + removeRawFile(index) { + const rawFiles = _.cloneDeep(this.state.rawFiles) + rawFiles.splice(index, 1) + this.setState({ + editorExpanded: true, + rawFiles + }) + } + removeFile(index) { + const files = _.cloneDeep(this.state.files) + files.splice(index, 1) + this.setState({ + editorExpanded: true, + files + }) + } + getDownloadAttachmentFilename(attachmentOriginalFilename) { + const regex = new RegExp(`^${_.escapeRegExp(this.state.attachmentsStorePath)}.[a-zA-Z0-9]*.(.*.)`, 'g') + const match = regex.exec(attachmentOriginalFilename) + return match[1] + } render() { const {MentionSuggestions} = this.mentionPlugin const {className, avatarUrl, authorName, titlePlaceholder, contentPlaceholder, editMode, isCreating, - isGettingComment, disableTitle, disableContent, expandedTitlePlaceholder, editingTopic, hasPrivateSwitch } = this.props - const {editorExpanded, editorState, titleValue, oldMDContent, currentMDContent, uploading, isPrivate} = this.state + isGettingComment, disableTitle, disableContent, expandedTitlePlaceholder, editingTopic, hasPrivateSwitch, canUploadAttachment } = this.props + const {editorExpanded, editorState, titleValue, oldMDContent, currentMDContent, uploading, isPrivate, rawFiles, files} = this.state let canSubmit = (disableTitle || titleValue.trim()) && (disableContent || editorState.getCurrentContent().hasText()) if (editMode && canSubmit) { canSubmit = (!disableTitle && titleValue !== this.props.oldTitle) || (!disableContent && oldMDContent !== currentMDContent) + || rawFiles.length > 0 } const currentStyle = editorState.getCurrentInlineStyle() const blockType = RichUtils.getCurrentBlockType(editorState) @@ -361,83 +464,120 @@ class RichTextArea extends React.Component { }
- {!disableContent && -
- {styles.map((item) => ( - - ))} -
- {blocks.map((item) => ( - - ))} - - { allowImages &&
} - { allowImages && +
+ {!disableContent && +
+ {styles.map((item) => ( + + ))} +
+ {blocks.map((item) => ( + + ))} - } -
- } -
- {hasPrivateSwitch && - this.setState({isPrivate: evt.target.checked})} - checked={isPrivate} - label="Private" - /> + { allowImages &&
} + { allowImages && + + } +
} - {!editMode && - - } - {editMode && !isCreating && - + } + {editMode && !isCreating && + + } + { editMode && + - } - { editMode && - - } - { !editMode && - - } + } + { !editMode && + + } +
+ {canUploadAttachment &&
+
    + { + files.map((f, index) => ( +
  • + {this.getDownloadAttachmentFilename(f.originalFileName)} +
    +
    + +
    +
    +
  • + )) + } + { + rawFiles.map((f, index) => ( +
  • + {f.title} +
    +
    + +
    +
    +
  • + )) + } +
+
}
@@ -471,6 +611,8 @@ RichTextArea.propTypes = { projectMembers: PropTypes.object, editingTopic: PropTypes.bool, hasPrivateSwitch: PropTypes.bool, + canUploadAttachment: PropTypes.bool, + attachments: PropTypes.array } -export default RichTextArea +export default withRouter(RichTextArea) diff --git a/src/components/RichTextArea/RichTextArea.scss b/src/components/RichTextArea/RichTextArea.scss index 21bc5848f..a651ecd03 100644 --- a/src/components/RichTextArea/RichTextArea.scss +++ b/src/components/RichTextArea/RichTextArea.scss @@ -8,10 +8,13 @@ &.expanded { .tc-textarea .textarea-footer .textarea-footer-inner { - display: flex; + display: block; - @media screen and (max-width: $screen-rg - 1px) { - display: block; + .textarea-footer-inner-top { + display: flex; + @media screen and (max-width: $screen-rg - 1px) { + display: block; + } } } @@ -212,6 +215,19 @@ margin-left: auto; } + .tc-attachment { + &-button { + a{ + cursor: pointer; + color: $tc-dark-blue; + &:hover { + text-decoration: underline; + } + } + margin-right: 20px; + } + } + .tc-btns { display: flex; align-items: center; @@ -294,4 +310,84 @@ .tool-inactive { background-color: transparent; } + + .attachment-files { + margin-top: 10px; + width: 50%; + li{ + @include roboto; + font-size: $tc-label-md; + color: $tc-dark-blue-110; + line-height: 1; + padding: $base-unit 0 $base-unit 0; + display: flex; + + &.clickable { + cursor: pointer; + } + + &.is-active { + background-color: $tc-gray-neutral-dark; + + &:not(.delete-confirmation-modal):hover { + background-color: $tc-gray-neutral-dark; + } + } + + .button-group { + margin-left: auto; + padding-left:20px; + width: 110px; + } + + .buttons{ + visibility: hidden; + margin-right: 5px; + margin-left: auto; + button { + padding: 0; + background: transparent; + border: none; + cursor: pointer; + opacity: 0.4; + height: 16px; + width: 16px; + background-size: 16px; + display: inline-block; + margin-left: 15px; + } + } + a { + color: $tc-dark-blue-110; + line-height: $base-unit*4; + max-width: 220px; + text-overflow: ellipsis; + overflow-x: hidden; + + &:visited { + color: $tc-dark-blue-110; + } + } + } + @media screen and (min-width: $screen-md) { + li:hover{ + background: $tc-gray-neutral-light; + color: $tc-dark-blue; + + .buttons{ + display: block; + } + .link-buttons{ + visibility: visible; + } + } + } + + li:before { + content: '\b7\a0'; + font-size: 200%; + color: $tc-dark-blue-110; + line-height: $base-unit*3; + } + } } diff --git a/src/projects/detail/containers/FeedContainer.js b/src/projects/detail/containers/FeedContainer.js index 017dfcbdc..624477588 100644 --- a/src/projects/detail/containers/FeedContainer.js +++ b/src/projects/detail/containers/FeedContainer.js @@ -149,7 +149,8 @@ class FeedView extends React.Component { date, createdAt: p.date, edited, - author: isSystemUser(p.userId) ? SYSTEM_USER : commentAuthor + author: isSystemUser(p.userId) ? SYSTEM_USER : commentAuthor, + attachments: p.attachments || [] } const prevComment = prevFeed ? _.find(prevFeed.posts, t => p.id === t.id) : null if (prevComment && prevComment.isSavingComment && !comment.isSavingComment && !comment.error) { @@ -236,13 +237,16 @@ class FeedView extends React.Component { }) } - onNewPost({title, content, isPrivate = false}) { + onNewPost({title, content, isPrivate = false, attachmentIds}) { const { project } = this.props const newFeed = { title, body: content, tag: isPrivate ? PROJECT_FEED_TYPE_MESSAGES : PROJECT_FEED_TYPE_PRIMARY } + if (attachmentIds) { + Object.assign(newFeed, { attachmentIds }) + } this.props.createProjectTopic(project.id, newFeed) } @@ -284,13 +288,16 @@ class FeedView extends React.Component { } } - onAddNewComment(feedId, content) { + onAddNewComment(feedId, content, attachmentIds) { const { currentUser, feeds } = this.props const feed = _.find(feeds, { id: feedId }) const newComment = { date: new Date(), userId: parseInt(currentUser.id), - content + content, + } + if (attachmentIds) { + Object.assign(newComment, { attachmentIds }) } this.props.addFeedComment(feedId, feed.tag, newComment) } @@ -312,11 +319,11 @@ class FeedView extends React.Component { }) } - onSaveMessage(feedId, message, content) { + onSaveMessage(feedId, message, content, attachmentIds) { const newMessage = {...message} const { feeds } = this.state const feed = _.find(feeds, { id: feedId }) - newMessage.content = content + Object.assign(newMessage, {content, attachmentIds}) this.props.saveFeedComment(feedId, feed.tag, newMessage) } @@ -363,7 +370,8 @@ class FeedView extends React.Component { onSaveTopic(feedId, postId, title, content) { const { feeds } = this.state const feed = _.find(feeds, { id: feedId }) - this.props.saveProjectTopic(feedId, feed.tag, {postId, title, content}) + const newTopic = { postId, title, content } + this.props.saveProjectTopic(feedId, feed.tag, newTopic) } onDeleteTopic(feedId) { diff --git a/src/projects/detail/containers/MessagesContainer.js b/src/projects/detail/containers/MessagesContainer.js index 5103d0bcb..1267a4d40 100644 --- a/src/projects/detail/containers/MessagesContainer.js +++ b/src/projects/detail/containers/MessagesContainer.js @@ -319,13 +319,16 @@ class MessagesView extends React.Component { }) } - onAddNewMessage(threadId, content) { + onAddNewMessage(threadId, content, attachmentIds) { const { currentUser } = this.props const newMessage = { date: new Date(), userId: parseInt(currentUser.id), content } + if (attachmentIds) { + Object.assign(newMessage, { attachmentIds }) + } this.props.addFeedComment(threadId, PROJECT_FEED_TYPE_MESSAGES, newMessage) } @@ -346,9 +349,9 @@ class MessagesView extends React.Component { }) } - onSaveMessage(threadId, message, content) { + onSaveMessage(threadId, message, content, attachmentIds) { const newMessage = {...message} - newMessage.content = content + Object.assign(newMessage, {content, attachmentIds}) this.props.saveFeedComment(threadId, PROJECT_FEED_TYPE_MESSAGES, newMessage) } @@ -389,20 +392,24 @@ class MessagesView extends React.Component { } onSaveTopic(threadId, postId, title, content) { - this.props.saveProjectTopic(threadId, PROJECT_FEED_TYPE_MESSAGES, {postId, title, content}) + const newTopic = { postId, title, content } + this.props.saveProjectTopic(threadId, PROJECT_FEED_TYPE_MESSAGES, newTopic) } onDeleteTopic(threadId) { this.props.deleteProjectTopic(threadId, PROJECT_FEED_TYPE_MESSAGES) } - onNewThread({title, content}) { + onNewThread({title, content, attachmentIds}) { const { project } = this.props const newThread = { title, body: content, tag: PROJECT_FEED_TYPE_MESSAGES } + if (attachmentIds) { + Object.assign(newThread, { attachmentIds }) + } this.props.createProjectTopic(project.id, newThread).then(() => { this.setState({ isCreateNewMessage : false diff --git a/src/projects/detail/containers/PhaseFeedHOC.jsx b/src/projects/detail/containers/PhaseFeedHOC.jsx index 64f218ecb..74e1127d0 100644 --- a/src/projects/detail/containers/PhaseFeedHOC.jsx +++ b/src/projects/detail/containers/PhaseFeedHOC.jsx @@ -125,7 +125,7 @@ const phaseFeedHOC = (Component) => { }) } - onAddNewComment(content) { + onAddNewComment(content, attachmentIds) { const { phase, topic, currentUser, addPhaseFeedComment } = this.props const newComment = { @@ -134,6 +134,10 @@ const phaseFeedHOC = (Component) => { content, } + if (attachmentIds) { + Object.assign(newComment, { attachmentIds }) + } + addPhaseFeedComment(phase.id, topic.id, newComment) } @@ -143,10 +147,10 @@ const phaseFeedHOC = (Component) => { deletePhaseFeedComment(phase.id, topic.id, postId) } - onSaveMessage(message, content) { + onSaveMessage(message, content, attachmentIds) { const { phase, topic, savePhaseFeedComment } = this.props const updatedMessage = {...message} - updatedMessage.content = content + Object.assign(updatedMessage, {content, attachmentIds}) savePhaseFeedComment(phase.id, topic.id, updatedMessage) } diff --git a/src/projects/reducers/phasesTopics.js b/src/projects/reducers/phasesTopics.js index 7579f659c..d9c09d563 100644 --- a/src/projects/reducers/phasesTopics.js +++ b/src/projects/reducers/phasesTopics.js @@ -245,6 +245,7 @@ export const phasesTopics = function (state=initialState, action) { error: { $set: false }, rawContent: { $set : rawContent }, body: { $set : savedComment.body }, + attachments: { $set : savedComment.attachments }, updatedDate: { $set : savedComment.updatedDate }, edited: {$set : true } }) diff --git a/src/projects/reducers/projectTopics.js b/src/projects/reducers/projectTopics.js index 268bcad60..c8c7957cb 100644 --- a/src/projects/reducers/projectTopics.js +++ b/src/projects/reducers/projectTopics.js @@ -462,6 +462,7 @@ export const projectTopics = function (state=initialState, action) { error: { $set : false }, rawContent: { $set : rawContent }, body: { $set : savedComment.body }, + attachments: { $set : savedComment.attachments }, updatedDate: { $set : savedComment.updatedDate }, edited: {$set : true } }) diff --git a/src/projects/routes.jsx b/src/projects/routes.jsx index 14fe6d4d9..81c6ec80e 100644 --- a/src/projects/routes.jsx +++ b/src/projects/routes.jsx @@ -41,6 +41,7 @@ const projectRoutes = ( path="/projects" render={() => ( + , null)} /> , null)} /> , )} /> , )} />