Skip to content

Commit dacd54b

Browse files
committed
Revert "Revert "List post links on the sidebar""
This reverts commit dae60ff.
1 parent 0711473 commit dacd54b

File tree

4 files changed

+214
-4
lines changed

4 files changed

+214
-4
lines changed

src/components/LinksMenu/LinksMenu.jsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import Panel from '../Panel/Panel'
66
import AddLink from './AddLink'
77
import DeleteLinkModal from './DeleteLinkModal'
88
import EditLinkModal from './EditLinkModal'
9+
import LinksMenuAccordion from './LinksMenuAccordion'
910
import uncontrollable from 'uncontrollable'
1011
import MobileExpandable from '../MobileExpandable/MobileExpandable'
1112
import cn from 'classnames'
@@ -100,7 +101,9 @@ const LinksMenu = ({
100101
}
101102
const onEditCancel = () => onEditIntent(-1)
102103
const handleEditClick = () => onEditIntent(idx)
103-
if (linkToDelete === idx) {
104+
if (Array.isArray(link.children) && link.children.length > 0) {
105+
return (<LinksMenuAccordion key={`link-menu-accordion-${idx}`} link={ link } renderLink={ renderLink } />)
106+
} else if (linkToDelete === idx) {
104107
return (
105108
<li className="delete-confirmation-modal" key={ 'delete-confirmation-' + idx }>
106109
<DeleteLinkModal
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import React from 'react'
2+
import PropTypes from 'prop-types'
3+
import IconCarretDownNormal from '../../assets/icons/arrow-6px-carret-down-normal.svg'
4+
import './LinksMenuAccordion.scss'
5+
6+
7+
class LinksMenuAccordion extends React.Component {
8+
constructor (props) {
9+
super(props)
10+
this.state = {
11+
isOpen: false,
12+
}
13+
14+
this.toggleAccordion = this.toggleAccordion.bind(this)
15+
}
16+
17+
toggleAccordion() {
18+
this.setState({
19+
isOpen: !this.state.isOpen
20+
})
21+
}
22+
23+
render() {
24+
const { link, renderLink } = this.props
25+
const { isOpen } = this.state
26+
const iconClasses = `icon ${isOpen ? 'active' : ''}`
27+
return (
28+
<div styleName="link-accordion">
29+
<div styleName="link-accordion-head" onClick={this.toggleAccordion}>
30+
<span><IconCarretDownNormal styleName={iconClasses}/></span>
31+
<span styleName="link-accordion-title">{link.title}</span>
32+
</div>
33+
{isOpen && <div styleName="link-accordion-content">
34+
<ul>
35+
{
36+
link.children.map((childLink, i) => {
37+
return <li key={`childlink-${childLink.address}-${i}`}>{renderLink(childLink)}</li>
38+
})
39+
}
40+
</ul>
41+
</div>}
42+
</div>
43+
)
44+
}
45+
}
46+
47+
LinksMenuAccordion.propTypes = {
48+
link: PropTypes.object.isRequired,
49+
renderLink: PropTypes.func.isRequired
50+
}
51+
52+
export default LinksMenuAccordion
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
@import '~tc-ui/src/styles/tc-includes';
2+
@import '../../styles/includes';
3+
4+
.link-accordion {
5+
padding: 0 20px;
6+
font-weight: 400;
7+
font-size: 13px;
8+
}
9+
10+
.link-accordion-content li {
11+
padding-left: 15px;
12+
}
13+
14+
.link-accordion-head {
15+
padding: 10px 0;
16+
cursor: pointer;
17+
display: flex;
18+
}
19+
20+
.link-accordion-head .icon {
21+
transform: rotate(-90deg);
22+
}
23+
24+
.link-accordion-head .icon.active {
25+
transform: rotate(0);
26+
}
27+
28+
.link-accordion-title {
29+
margin-left: 5px;
30+
}
31+
32+
.link-accordion-head:hover {
33+
background: $tc-gray-neutral-light;
34+
}
35+
36+
@media screen and (max-width: $screen-md - 1px) {
37+
.link-accordion-head {
38+
padding: 20px 0;
39+
}
40+
}

src/projects/detail/containers/ProjectInfoContainer.js

Lines changed: 118 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { PROJECT_ROLE_OWNER, PROJECT_ROLE_COPILOT, PROJECT_ROLE_MANAGER,
1616
import PERMISSIONS from '../../../config/permissions'
1717
import { checkPermission } from '../../../helpers/permissions'
1818
import ProjectInfo from '../../../components/ProjectInfo/ProjectInfo'
19-
import {
19+
import {
2020
addProjectAttachment, updateProjectAttachment, uploadProjectAttachments, discardAttachments, changeAttachmentPermission,
2121
removeProjectAttachment
2222
} from '../../actions/projectAttachment'
@@ -38,6 +38,10 @@ class ProjectInfoContainer extends React.Component {
3838
this.onUploadAttachment = this.onUploadAttachment.bind(this)
3939
this.removeAttachment = this.removeAttachment.bind(this)
4040
this.onSubmitForReview = this.onSubmitForReview.bind(this)
41+
this.extractLinksFromPosts = this.extractLinksFromPosts.bind(this)
42+
this.extractMarkdownLink = this.extractMarkdownLink.bind(this)
43+
this.extractHtmlLink = this.extractHtmlLink.bind(this)
44+
this.extractRawLink = this.extractRawLink.bind(this)
4145
}
4246

4347
shouldComponentUpdate(nextProps, nextState) { // eslint-disable-line no-unused-vars
@@ -156,13 +160,110 @@ class ProjectInfoContainer extends React.Component {
156160
updateProject(project.id, { status: 'in_review'})
157161
}
158162

163+
extractHtmlLink(str) {
164+
const links = []
165+
const regex = /<a[^>]+href="(.*?)"[^>]*>([\s\S]*?)<\/a>/gm
166+
const urlRegex = /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$/gm // eslint-disable-line no-useless-escape
167+
const rawLinks = regex.exec(str)
168+
169+
if (Array.isArray(rawLinks)) {
170+
let i = 0
171+
while (i < rawLinks.length) {
172+
const title = rawLinks[i + 2]
173+
const address = rawLinks[i + 1]
174+
175+
if (urlRegex.test(address)) {
176+
links.push({
177+
title,
178+
address
179+
})
180+
}
181+
182+
i = i + 3
183+
}
184+
}
185+
186+
return links
187+
}
188+
189+
extractMarkdownLink(str) {
190+
const links = []
191+
const regex = /(?:__|[*#])|\[(.*?)\]\((.*?)\)/gm
192+
const urlRegex = /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$/gm // eslint-disable-line no-useless-escape
193+
const rawLinks = regex.exec(str)
194+
195+
if (Array.isArray(rawLinks)) {
196+
let i = 0
197+
while (i < rawLinks.length) {
198+
const title = rawLinks[i + 1]
199+
const address = rawLinks[i + 2]
200+
201+
if (urlRegex.test(address)) {
202+
links.push({
203+
title,
204+
address
205+
})
206+
}
207+
208+
i = i + 3
209+
}
210+
}
211+
212+
return links
213+
}
214+
215+
extractRawLink(str) {
216+
let links = []
217+
const regex = /(\s|^)(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,}[\s])(\s|$)/igm // eslint-disable-line no-useless-escape
218+
const rawLinks = str.match(regex)
219+
220+
if (Array.isArray(rawLinks)) {
221+
links = rawLinks
222+
.filter(link => !link.includes(']'))
223+
.map(link => {
224+
const name = link.trim()
225+
const url = !/^https?:\/\//i.test(name) ? 'http://' + name : name
226+
227+
return {
228+
title: name,
229+
address: url
230+
}
231+
})
232+
}
233+
234+
return links
235+
}
236+
237+
extractLinksFromPosts(feeds) {
238+
const links = []
239+
feeds.forEach(feed => {
240+
let childrenLinks = []
241+
feed.posts.forEach(post => {
242+
childrenLinks = childrenLinks.concat([
243+
...this.extractHtmlLink(post.rawContent),
244+
...this.extractMarkdownLink(post.rawContent),
245+
...this.extractRawLink(post.rawContent)
246+
])
247+
})
248+
249+
if (childrenLinks.length > 0) {
250+
links.push({
251+
title: feed.title,
252+
children: childrenLinks
253+
})
254+
}
255+
})
256+
257+
return links
258+
}
259+
159260
render() {
160261
const { duration } = this.state
161262
const { project, currentMemberRole, isSuperUser, phases, feeds,
162263
hideInfo, hideLinks, hideMembers, onChannelClick, activeChannelId, productsTimelines,
163264
isManageUser, phasesTopics, isProjectPlan, isProjectProcessing, projectTemplates,
164265
attachmentsAwaitingPermission, addProjectAttachment, discardAttachments, attachmentPermissions,
165-
changeAttachmentPermission, projectMembers, loggedInUser, isSharingAttachment } = this.props
266+
changeAttachmentPermission, projectMembers, loggedInUser, isSharingAttachment, canAccessPrivatePosts } = this.props
166267
let directLinks = null
167268
// check if direct links need to be added
168269
const isMemberOrCopilot = _.indexOf([PROJECT_ROLE_COPILOT, PROJECT_ROLE_MANAGER], currentMemberRole) > -1
@@ -251,6 +352,20 @@ class ProjectInfoContainer extends React.Component {
251352
})
252353
}
253354

355+
// extract links from posts
356+
const topicLinks = this.extractLinksFromPosts(feeds)
357+
const publicTopicLinks = topicLinks.filter(link => link.tag !== PROJECT_FEED_TYPE_MESSAGES)
358+
const privateTopicLinks = topicLinks.filter(link => link.tag === PROJECT_FEED_TYPE_MESSAGES)
359+
const phaseLinks = this.extractLinksFromPosts(phaseFeeds)
360+
361+
let links = []
362+
links = links.concat(project.bookmarks)
363+
links = links.concat(publicTopicLinks)
364+
if (canAccessPrivatePosts) {
365+
links = links.concat(privateTopicLinks)
366+
}
367+
links = links.concat(phaseLinks)
368+
254369
return (
255370
<div>
256371
<div className="sideAreaWrapper">
@@ -300,7 +415,7 @@ class ProjectInfoContainer extends React.Component {
300415
}
301416
{!hideLinks &&
302417
<LinksMenu
303-
links={project.bookmarks || []}
418+
links={links}
304419
canDelete={canManageLinks}
305420
canEdit={canManageLinks}
306421
canAdd={canManageLinks}

0 commit comments

Comments
 (0)