Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[GH-730] Add link preview for PR and issue. #779

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
157 changes: 157 additions & 0 deletions webapp/src/components/link_embed_preview/embed_preview.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
$light-gray: #6a737d;
$light-blue: #eff7ff;
$github-merged: #6f42c1;
$github-closed: #cb2431;
$github-open: #28a745;
$github-not-planned: #6e7681;

@media (min-width: 544px) {
.github-preview--large {
min-width: 320px;
}
}

/* Github Preview */
.github-preview {
background-color: var(--center-channel-bg-rgb);
box-shadow: 0 2px 3px rgba(0,0,0,.08);
position: relative;
width: 100%;
max-width: 700px;
border-radius: 4px;
border: 1px solid rgba(var(--center-channel-color-rgb), 0.16);

/* Header */
.header {
color: rgba(var(--center-channel-color-rgb), 0.64);
font-size: 11px;
line-height: 16px;
white-space: nowrap;

a {
text-decoration: none;
color: rgba(var(--center-channel-color-rgb), 0.64);
display: inline-block;

.repo {
color: var(--center-channel-color-rgb);
}
}
}

/* Body */
.body > span {
line-height: 1.25;
}

/* Info */
.preview-info {
line-height: 1.25;
display: flex;
flex-direction: column;

> a,
> a:hover {
display: block;
text-decoration: none;
color: var(--link-color);
}

> a span {
color: $light-gray;

h5 {
font-weight: 600;
font-size: 14px;
display: inline;

span.github-preview-icon-opened {
color: $github-open;
}

span.github-preview-icon-closed {
color: $github-closed;
}

span.github-preview-icon-merged {
color: $github-merged;
}

span.github-preview-icon-not-planned {
color: $github-not-planned;
}
}

.markdown-text {
max-height: 150px;
line-height: 1.25;
overflow: hidden;
word-break: break-word;
word-wrap: break-word;
overflow-wrap: break-word;
font-size: 12px;

&::-webkit-scrollbar {
display: none;
}
}
}
}

.sub-info {
display: flex;
width: 100%;
flex-wrap: wrap;
gap: 4px 0;

.sub-info-block {
display: flex;
flex-direction: column;
width: 50%;
}
}

/* Labels */
.labels {
display: flex;
justify-content: flex-start;
align-items: center;
flex-wrap: wrap;
gap: 4px;
}

.label {
height: 20px;
padding: .15em 4px;
font-size: 12px;
font-weight: 600;
line-height: 15px;
border-radius: 2px;
box-shadow: inset 0 -1px 0 rgba(27,31,35,.12);
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
max-width: 125px;
}

.base-head {
display: flex;
line-height: 1;
align-items: center;
}

.commit-ref {
position: relative;
display: inline-block;
padding: 0 5px;
font: .75em/2 SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;
color: var(--blue);
background-color: $light-blue;
border-radius: 3px;
max-width: 140px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}

}
14 changes: 14 additions & 0 deletions webapp/src/components/link_embed_preview/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

import {connect} from 'react-redux';

import manifest from 'manifest';

import {LinkEmbedPreview} from './link_embed_preview';

const mapStateToProps = (state) => {
return {connected: state[`plugins-${manifest.id}`].connected};
};

export default connect(mapStateToProps, null)(LinkEmbedPreview);
183 changes: 183 additions & 0 deletions webapp/src/components/link_embed_preview/link_embed_preview.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import {GitMergeIcon, GitPullRequestIcon, IssueClosedIcon, IssueOpenedIcon, SkipIcon} from '@primer/octicons-react';
import PropTypes from 'prop-types';
import React, {useEffect, useState} from 'react';
import ReactMarkdown from 'react-markdown';
import './embed_preview.scss';

import Client from 'client';
import {getLabelFontColor} from '../../utils/styles';
import {isUrlCanPreview} from '../../utils/github_utils';

const maxTicketDescriptionLength = 160;

export const LinkEmbedPreview = ({embed: {url}, connected}) => {
const [data, setData] = useState(null);
useEffect(() => {
const initData = async () => {
if (isUrlCanPreview(url)) {
const [owner, repo, type, number] = url.split('github.com/')[1].split('/');
Sn-Kinos marked this conversation as resolved.
Show resolved Hide resolved

let res;
switch (type) {
case 'issues':
res = await Client.getIssue(owner, repo, number);
break;
case 'pull':
res = await Client.getPullRequest(owner, repo, number);
break;
}
if (res) {
res.owner = owner;
res.repo = repo;
res.type = type;
}
setData(res);
}
};

if (!connected || data) {
return;
}

initData();
}, [connected, data, url]);

const getIconElement = () => {
const iconProps = {
size: 'small',
verticalAlign: 'text-bottom',
};

let icon;
let colorClass;
switch (data.type) {
case 'pull':
icon = <GitPullRequestIcon {...iconProps}/>;

colorClass = 'github-preview-icon-open';
if (data.state === 'closed') {
if (data.merged) {
colorClass = 'github-preview-icon-merged';
icon = <GitMergeIcon {...iconProps}/>;
} else {
colorClass = 'github-preview-icon-closed';
}
}

break;
case 'issues':
if (data.state === 'open') {
colorClass = 'github-preview-icon-open';
icon = <IssueOpenedIcon {...iconProps}/>;
} else if (data.state_reason === 'not_planned') {
colorClass = 'github-preview-icon-not-planned';
icon = <SkipIcon {...iconProps}/>;
} else {
colorClass = 'github-preview-icon-merged';
icon = <IssueClosedIcon {...iconProps}/>;
}
break;
}
return (
<span className={`pr-2 ${colorClass}`}>
{icon}
</span>
);
};

if (!data) {
return null;
}
let date = new Date(data.created_at);
date = date.toDateString();

let description = '';
if (data.body) {
description = data.body.substring(0, maxTicketDescriptionLength).trim();
if (data.body.length > maxTicketDescriptionLength) {
description += '...';
}
}

return (
<div className='github-preview github-preview--large p-4 mt-1 mb-1'>
<div className='header'>
<span className='repo'>
{data.repo}
</span>
{' on '}
<span>{date}</span>
</div>

<div className='body d-flex'>

{/* info */}
<div className='preview-info mt-1'>
<a
href={url}
target='_blank'
rel='noopener noreferrer'
>
<h5 className='mr-1'>
{ getIconElement() }
{data.title}
</h5>
<span>{'#' + data.number}</span>
</a>
<div className='markdown-text mt-1 mb-1'>
<ReactMarkdown linkTarget='_blank'>{description}</ReactMarkdown>
</div>

<div className='sub-info mt-1'>
{/* base <- head */}
{data.type === 'pull' && (
<div className='sub-info-block'>
<h6 className='mt-0 mb-1'>{'Base ← Head'}</h6>
<div className='base-head'>
<span
title={data.base.ref}
className='commit-ref'
>{data.base.ref}
</span> <span className='mx-1'>{'←'}</span>{' '}
<span
title={data.head.ref}
className='commit-ref'
>{data.head.ref}
</span>
</div>
</div>
)}

{/* Labels */}
{data.labels && data.labels.length > 0 && (
<div className='sub-info-block'>
<h6 className='mt-0 mb-1'>{'Labels'}</h6>
<div className='labels'>
{data.labels.map((label, idx) => {
return (
<span
key={`${label.name}-${idx}`}
className='label'
title={label.description}
style={{backgroundColor: '#' + label.color, color: getLabelFontColor(label.color)}}
>
<span>{label.name}</span>
</span>
);
})}
</div>
</div>
)}
</div>
</div>
</div>
</div>
);
};

LinkEmbedPreview.propTypes = {
embed: {
url: PropTypes.string.isRequired,
},
connected: PropTypes.bool.isRequired,
};
6 changes: 2 additions & 4 deletions webapp/src/components/link_tooltip/link_tooltip.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,16 @@ import ReactMarkdown from 'react-markdown';

import Client from 'client';
import {getLabelFontColor, hexToRGB} from '../../utils/styles';
import {isUrlCanPreview} from '../../utils/github_utils';

const maxTicketDescriptionLength = 160;

export const LinkTooltip = ({href, connected, show, theme}) => {
const [data, setData] = useState(null);
useEffect(() => {
const initData = async () => {
if (href.includes('github.com/')) {
if (isUrlCanPreview(href)) {
const [owner, repo, type, number] = href.split('github.com/')[1].split('/');
if (!owner | !repo | !type | !number) {
return;
}

let res;
switch (type) {
Expand Down
1 change: 1 addition & 0 deletions webapp/src/components/link_tooltip/tooltip.css
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@

/* Labels */
.github-tooltip .labels {
display: flex;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What effect does this have on the tooltip's styling?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.labels is not flex element so the properties below doesn't working. So I added it and flex works.

Before After
image image
.labels = height: 23px; (auto) display: block;
( flex properties not working )
.labels = height: 20px; (auto) display: flex;
( flex properties working )
.labels > * = height: 20px; .labels > * = height: 20px;

justify-content: flex-start;
align-items: center;
}
Expand Down
Loading
Loading