Skip to content

Commit 450d0d9

Browse files
Github integration (outline#6414)
Co-authored-by: Tom Moor <[email protected]>
1 parent a648625 commit 450d0d9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+1710
-93
lines changed

.env.sample

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,16 @@ OIDC_DISPLAY_NAME=OpenID Connect
122122
# Space separated auth scopes.
123123
OIDC_SCOPES=openid profile email
124124

125+
# To configure the GitHub integration, you'll need to create a GitHub App at
126+
# => https://github.com/settings/apps
127+
#
128+
# When configuring the Client ID, add a redirect URL under "Permissions & events":
129+
# https://<URL>/api/github.callback
130+
GITHUB_CLIENT_ID=
131+
GITHUB_CLIENT_SECRET=
132+
GITHUB_APP_NAME=
133+
GITHUB_APP_ID=
134+
GITHUB_APP_PRIVATE_KEY=
125135

126136
# –––––––––––––––– OPTIONAL ––––––––––––––––
127137

.env.test

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ GOOGLE_CLIENT_SECRET=123
1313
SLACK_CLIENT_ID=123
1414
SLACK_CLIENT_SECRET=123
1515

16+
GITHUB_CLIENT_ID=123;
17+
GITHUB_CLIENT_SECRET=123;
18+
GITHUB_APP_NAME=outline-test;
19+
1620
OIDC_CLIENT_ID=client-id
1721
OIDC_CLIENT_SECRET=client-secret
1822
OIDC_AUTH_URI=http://localhost/authorize

app/components/HoverPreview/Components.tsx

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { transparentize } from "polished";
22
import { Link } from "react-router-dom";
33
import styled, { css } from "styled-components";
44
import { s } from "@shared/styles";
5+
import { getTextColor } from "@shared/utils/color";
56
import Text from "~/components/Text";
67

78
export const CARD_MARGIN = 10;
@@ -28,10 +29,12 @@ export const Preview = styled(Link)`
2829
max-width: 375px;
2930
`;
3031

31-
export const Title = styled.h2`
32-
font-size: 1.25em;
33-
margin: 0;
34-
color: ${s("text")};
32+
export const Title = styled(Text).attrs({ as: "h2", size: "large" })`
33+
margin-bottom: 4px;
34+
display: flex;
35+
align-items: flex-start;
36+
justify-content: flex-start;
37+
gap: 4px;
3538
`;
3639

3740
export const Info = styled(StyledText).attrs(() => ({
@@ -46,6 +49,7 @@ export const Description = styled(StyledText)`
4649
margin-top: 0.5em;
4750
line-height: var(--line-height);
4851
max-height: calc(var(--line-height) * ${NUMBER_OF_LINES});
52+
overflow: hidden;
4953
`;
5054

5155
export const Thumbnail = styled.img`
@@ -54,6 +58,20 @@ export const Thumbnail = styled.img`
5458
background: ${s("menuBackground")};
5559
`;
5660

61+
export const Label = styled(Text).attrs({ size: "xsmall", weight: "bold" })<{
62+
color?: string;
63+
}>`
64+
background-color: ${(props) =>
65+
props.color ?? props.theme.secondaryBackground};
66+
color: ${(props) =>
67+
props.color ? getTextColor(props.color) : props.theme.text};
68+
width: fit-content;
69+
border-radius: 2em;
70+
padding: 0 8px;
71+
margin-right: 0.5em;
72+
margin-top: 0.5em;
73+
`;
74+
5775
export const CardContent = styled.div`
5876
overflow: hidden;
5977
user-select: none;

app/components/HoverPreview/HoverPreview.tsx

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@ import useStores from "~/hooks/useStores";
1515
import { client } from "~/utils/ApiClient";
1616
import { CARD_MARGIN } from "./Components";
1717
import HoverPreviewDocument from "./HoverPreviewDocument";
18+
import HoverPreviewIssue from "./HoverPreviewIssue";
1819
import HoverPreviewLink from "./HoverPreviewLink";
1920
import HoverPreviewMention from "./HoverPreviewMention";
21+
import HoverPreviewPullRequest from "./HoverPreviewPullRequest";
2022

2123
const DELAY_CLOSE = 600;
2224
const POINTER_HEIGHT = 22;
@@ -111,7 +113,11 @@ function HoverPreviewDesktop({ element, onClose }: Props) {
111113
{(data) => (
112114
<Animate
113115
initial={{ opacity: 0, y: -20, pointerEvents: "none" }}
114-
animate={{ opacity: 1, y: 0, pointerEvents: "auto" }}
116+
animate={{
117+
opacity: 1,
118+
y: 0,
119+
transitionEnd: { pointerEvents: "auto" },
120+
}}
115121
>
116122
{data.type === UnfurlType.Mention ? (
117123
<HoverPreviewMention
@@ -128,6 +134,27 @@ function HoverPreviewDesktop({ element, onClose }: Props) {
128134
description={data.description}
129135
info={data.meta.info}
130136
/>
137+
) : data.type === UnfurlType.Issue ? (
138+
<HoverPreviewIssue
139+
url={data.url}
140+
title={data.title}
141+
description={data.description}
142+
author={data.author}
143+
createdAt={data.createdAt}
144+
identifier={data.meta.identifier}
145+
labels={data.meta.labels}
146+
status={data.meta.status}
147+
/>
148+
) : data.type === UnfurlType.Pull ? (
149+
<HoverPreviewPullRequest
150+
url={data.url}
151+
title={data.title}
152+
description={data.description}
153+
author={data.author}
154+
createdAt={data.createdAt}
155+
identifier={data.meta.identifier}
156+
status={data.meta.status}
157+
/>
131158
) : (
132159
<HoverPreviewLink
133160
url={data.url}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import * as React from "react";
2+
import { Trans } from "react-i18next";
3+
import Flex from "~/components/Flex";
4+
import Avatar from "../Avatar";
5+
import { IssueStatusIcon } from "../Icons/IssueStatusIcon";
6+
import Text from "../Text";
7+
import Time from "../Time";
8+
import {
9+
Preview,
10+
Title,
11+
Description,
12+
Card,
13+
CardContent,
14+
Label,
15+
Info,
16+
} from "./Components";
17+
18+
type Props = {
19+
/** Issue url */
20+
url: string;
21+
/** Issue title */
22+
title: string;
23+
/** Issue description */
24+
description: string;
25+
/** Wehn the issue was created */
26+
createdAt: string;
27+
/** Author of the issue */
28+
author: { name: string; avatarUrl: string };
29+
/** Labels attached to the issue */
30+
labels: Array<{ name: string; color: string }>;
31+
/** Issue status */
32+
status: { name: string; color: string };
33+
/** Issue identifier */
34+
identifier: string;
35+
};
36+
37+
const HoverPreviewIssue = React.forwardRef(function _HoverPreviewIssue(
38+
{
39+
url,
40+
title,
41+
identifier,
42+
description,
43+
author,
44+
labels,
45+
status,
46+
createdAt,
47+
}: Props,
48+
ref: React.Ref<HTMLDivElement>
49+
) {
50+
const authorName = author.name;
51+
52+
return (
53+
<Preview as="a" href={url} target="_blank" rel="noopener noreferrer">
54+
<Flex column ref={ref}>
55+
<Card fadeOut={false}>
56+
<CardContent>
57+
<Flex gap={2} column>
58+
<Title>
59+
<IssueStatusIcon status={status.name} color={status.color} />
60+
<span>
61+
{title}&nbsp;<Text type="tertiary">{identifier}</Text>
62+
</span>
63+
</Title>
64+
<Flex align="center" gap={4}>
65+
<Avatar src={author.avatarUrl} />
66+
<Info>
67+
<Trans>
68+
{{ authorName }} created{" "}
69+
<Time dateTime={createdAt} addSuffix />
70+
</Trans>
71+
</Info>
72+
</Flex>
73+
<Description>{description}</Description>
74+
75+
<Flex wrap>
76+
{labels.map((label, index) => (
77+
<Label key={index} color={label.color}>
78+
{label.name}
79+
</Label>
80+
))}
81+
</Flex>
82+
</Flex>
83+
</CardContent>
84+
</Card>
85+
</Flex>
86+
</Preview>
87+
);
88+
});
89+
90+
export default HoverPreviewIssue;
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import * as React from "react";
2+
import { Trans } from "react-i18next";
3+
import Flex from "~/components/Flex";
4+
import Avatar from "../Avatar";
5+
import { PullRequestIcon } from "../Icons/PullRequestIcon";
6+
import Text from "../Text";
7+
import Time from "../Time";
8+
import {
9+
Preview,
10+
Title,
11+
Description,
12+
Card,
13+
CardContent,
14+
Info,
15+
} from "./Components";
16+
17+
type Props = {
18+
/** Pull request url */
19+
url: string;
20+
/** Pull request title */
21+
title: string;
22+
/** Pull request description */
23+
description: string;
24+
/** When the pull request was opened */
25+
createdAt: string;
26+
/** Author of the pull request */
27+
author: { name: string; avatarUrl: string };
28+
/** Pull request status */
29+
status: { name: string; color: string };
30+
/** Pull request identifier */
31+
identifier: string;
32+
};
33+
34+
const HoverPreviewPullRequest = React.forwardRef(
35+
function _HoverPreviewPullRequest(
36+
{ url, title, identifier, description, author, status, createdAt }: Props,
37+
ref: React.Ref<HTMLDivElement>
38+
) {
39+
const authorName = author.name;
40+
41+
return (
42+
<Preview as="a" href={url} target="_blank" rel="noopener noreferrer">
43+
<Flex column ref={ref}>
44+
<Card fadeOut={false}>
45+
<CardContent>
46+
<Flex gap={2} column>
47+
<Title>
48+
<PullRequestIcon status={status.name} color={status.color} />
49+
<span>
50+
{title}&nbsp;<Text type="tertiary">{identifier}</Text>
51+
</span>
52+
</Title>
53+
<Flex align="center" gap={4}>
54+
<Avatar src={author.avatarUrl} />
55+
<Info>
56+
<Trans>
57+
{{ authorName }} opened{" "}
58+
<Time dateTime={createdAt} addSuffix />
59+
</Trans>
60+
</Info>
61+
</Flex>
62+
<Description>{description}</Description>
63+
</Flex>
64+
</CardContent>
65+
</Card>
66+
</Flex>
67+
</Preview>
68+
);
69+
}
70+
);
71+
72+
export default HoverPreviewPullRequest;
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import * as React from "react";
2+
import styled from "styled-components";
3+
4+
type Props = {
5+
status: string;
6+
color: string;
7+
size?: number;
8+
className?: string;
9+
};
10+
11+
/**
12+
* Issue status icon based on GitHub issue status, but can be used for any git-style integration.
13+
*/
14+
export function IssueStatusIcon({ size, ...rest }: Props) {
15+
return (
16+
<Icon size={size}>
17+
<BaseIcon {...rest} />
18+
</Icon>
19+
);
20+
}
21+
22+
const Icon = styled.span<{ size?: number }>`
23+
display: inline-flex;
24+
flex-shrink: 0;
25+
width: ${(props) => props.size ?? 24}px;
26+
height: ${(props) => props.size ?? 24}px;
27+
align-items: center;
28+
justify-content: center;
29+
`;
30+
31+
function BaseIcon(props: Props) {
32+
switch (props.status) {
33+
case "open":
34+
return (
35+
<svg
36+
viewBox="0 0 16 16"
37+
width="16"
38+
height="16"
39+
fill={props.color}
40+
className={props.className}
41+
>
42+
<path d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z" />
43+
<path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Z" />
44+
</svg>
45+
);
46+
case "closed":
47+
return (
48+
<svg
49+
viewBox="0 0 16 16"
50+
width="16"
51+
height="16"
52+
fill={props.color}
53+
className={props.className}
54+
>
55+
<path d="M11.28 6.78a.75.75 0 0 0-1.06-1.06L7.25 8.69 5.78 7.22a.75.75 0 0 0-1.06 1.06l2 2a.75.75 0 0 0 1.06 0l3.5-3.5Z" />
56+
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0Zm-1.5 0a6.5 6.5 0 1 0-13 0 6.5 6.5 0 0 0 13 0Z" />
57+
</svg>
58+
);
59+
case "canceled":
60+
return (
61+
<svg
62+
viewBox="0 0 16 16"
63+
width="16"
64+
height="16"
65+
fill={props.color}
66+
className={props.className}
67+
>
68+
<path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Zm9.78-2.22-5.5 5.5a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734l5.5-5.5a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042Z" />
69+
</svg>
70+
);
71+
default:
72+
return null;
73+
}
74+
}

0 commit comments

Comments
 (0)