Github integration (#6414)

Co-authored-by: Tom Moor <tom.moor@gmail.com>
This commit is contained in:
Apoorv Mishra
2024-03-23 19:39:28 +05:30
committed by GitHub
parent a648625700
commit 450d0d9355
47 changed files with 1710 additions and 93 deletions

View File

@@ -2,6 +2,7 @@ import { transparentize } from "polished";
import { Link } from "react-router-dom";
import styled, { css } from "styled-components";
import { s } from "@shared/styles";
import { getTextColor } from "@shared/utils/color";
import Text from "~/components/Text";
export const CARD_MARGIN = 10;
@@ -28,10 +29,12 @@ export const Preview = styled(Link)`
max-width: 375px;
`;
export const Title = styled.h2`
font-size: 1.25em;
margin: 0;
color: ${s("text")};
export const Title = styled(Text).attrs({ as: "h2", size: "large" })`
margin-bottom: 4px;
display: flex;
align-items: flex-start;
justify-content: flex-start;
gap: 4px;
`;
export const Info = styled(StyledText).attrs(() => ({
@@ -46,6 +49,7 @@ export const Description = styled(StyledText)`
margin-top: 0.5em;
line-height: var(--line-height);
max-height: calc(var(--line-height) * ${NUMBER_OF_LINES});
overflow: hidden;
`;
export const Thumbnail = styled.img`
@@ -54,6 +58,20 @@ export const Thumbnail = styled.img`
background: ${s("menuBackground")};
`;
export const Label = styled(Text).attrs({ size: "xsmall", weight: "bold" })<{
color?: string;
}>`
background-color: ${(props) =>
props.color ?? props.theme.secondaryBackground};
color: ${(props) =>
props.color ? getTextColor(props.color) : props.theme.text};
width: fit-content;
border-radius: 2em;
padding: 0 8px;
margin-right: 0.5em;
margin-top: 0.5em;
`;
export const CardContent = styled.div`
overflow: hidden;
user-select: none;

View File

@@ -15,8 +15,10 @@ import useStores from "~/hooks/useStores";
import { client } from "~/utils/ApiClient";
import { CARD_MARGIN } from "./Components";
import HoverPreviewDocument from "./HoverPreviewDocument";
import HoverPreviewIssue from "./HoverPreviewIssue";
import HoverPreviewLink from "./HoverPreviewLink";
import HoverPreviewMention from "./HoverPreviewMention";
import HoverPreviewPullRequest from "./HoverPreviewPullRequest";
const DELAY_CLOSE = 600;
const POINTER_HEIGHT = 22;
@@ -111,7 +113,11 @@ function HoverPreviewDesktop({ element, onClose }: Props) {
{(data) => (
<Animate
initial={{ opacity: 0, y: -20, pointerEvents: "none" }}
animate={{ opacity: 1, y: 0, pointerEvents: "auto" }}
animate={{
opacity: 1,
y: 0,
transitionEnd: { pointerEvents: "auto" },
}}
>
{data.type === UnfurlType.Mention ? (
<HoverPreviewMention
@@ -128,6 +134,27 @@ function HoverPreviewDesktop({ element, onClose }: Props) {
description={data.description}
info={data.meta.info}
/>
) : data.type === UnfurlType.Issue ? (
<HoverPreviewIssue
url={data.url}
title={data.title}
description={data.description}
author={data.author}
createdAt={data.createdAt}
identifier={data.meta.identifier}
labels={data.meta.labels}
status={data.meta.status}
/>
) : data.type === UnfurlType.Pull ? (
<HoverPreviewPullRequest
url={data.url}
title={data.title}
description={data.description}
author={data.author}
createdAt={data.createdAt}
identifier={data.meta.identifier}
status={data.meta.status}
/>
) : (
<HoverPreviewLink
url={data.url}

View File

@@ -0,0 +1,90 @@
import * as React from "react";
import { Trans } from "react-i18next";
import Flex from "~/components/Flex";
import Avatar from "../Avatar";
import { IssueStatusIcon } from "../Icons/IssueStatusIcon";
import Text from "../Text";
import Time from "../Time";
import {
Preview,
Title,
Description,
Card,
CardContent,
Label,
Info,
} from "./Components";
type Props = {
/** Issue url */
url: string;
/** Issue title */
title: string;
/** Issue description */
description: string;
/** Wehn the issue was created */
createdAt: string;
/** Author of the issue */
author: { name: string; avatarUrl: string };
/** Labels attached to the issue */
labels: Array<{ name: string; color: string }>;
/** Issue status */
status: { name: string; color: string };
/** Issue identifier */
identifier: string;
};
const HoverPreviewIssue = React.forwardRef(function _HoverPreviewIssue(
{
url,
title,
identifier,
description,
author,
labels,
status,
createdAt,
}: Props,
ref: React.Ref<HTMLDivElement>
) {
const authorName = author.name;
return (
<Preview as="a" href={url} target="_blank" rel="noopener noreferrer">
<Flex column ref={ref}>
<Card fadeOut={false}>
<CardContent>
<Flex gap={2} column>
<Title>
<IssueStatusIcon status={status.name} color={status.color} />
<span>
{title}&nbsp;<Text type="tertiary">{identifier}</Text>
</span>
</Title>
<Flex align="center" gap={4}>
<Avatar src={author.avatarUrl} />
<Info>
<Trans>
{{ authorName }} created{" "}
<Time dateTime={createdAt} addSuffix />
</Trans>
</Info>
</Flex>
<Description>{description}</Description>
<Flex wrap>
{labels.map((label, index) => (
<Label key={index} color={label.color}>
{label.name}
</Label>
))}
</Flex>
</Flex>
</CardContent>
</Card>
</Flex>
</Preview>
);
});
export default HoverPreviewIssue;

View File

@@ -0,0 +1,72 @@
import * as React from "react";
import { Trans } from "react-i18next";
import Flex from "~/components/Flex";
import Avatar from "../Avatar";
import { PullRequestIcon } from "../Icons/PullRequestIcon";
import Text from "../Text";
import Time from "../Time";
import {
Preview,
Title,
Description,
Card,
CardContent,
Info,
} from "./Components";
type Props = {
/** Pull request url */
url: string;
/** Pull request title */
title: string;
/** Pull request description */
description: string;
/** When the pull request was opened */
createdAt: string;
/** Author of the pull request */
author: { name: string; avatarUrl: string };
/** Pull request status */
status: { name: string; color: string };
/** Pull request identifier */
identifier: string;
};
const HoverPreviewPullRequest = React.forwardRef(
function _HoverPreviewPullRequest(
{ url, title, identifier, description, author, status, createdAt }: Props,
ref: React.Ref<HTMLDivElement>
) {
const authorName = author.name;
return (
<Preview as="a" href={url} target="_blank" rel="noopener noreferrer">
<Flex column ref={ref}>
<Card fadeOut={false}>
<CardContent>
<Flex gap={2} column>
<Title>
<PullRequestIcon status={status.name} color={status.color} />
<span>
{title}&nbsp;<Text type="tertiary">{identifier}</Text>
</span>
</Title>
<Flex align="center" gap={4}>
<Avatar src={author.avatarUrl} />
<Info>
<Trans>
{{ authorName }} opened{" "}
<Time dateTime={createdAt} addSuffix />
</Trans>
</Info>
</Flex>
<Description>{description}</Description>
</Flex>
</CardContent>
</Card>
</Flex>
</Preview>
);
}
);
export default HoverPreviewPullRequest;

View File

@@ -0,0 +1,74 @@
import * as React from "react";
import styled from "styled-components";
type Props = {
status: string;
color: string;
size?: number;
className?: string;
};
/**
* Issue status icon based on GitHub issue status, but can be used for any git-style integration.
*/
export function IssueStatusIcon({ size, ...rest }: Props) {
return (
<Icon size={size}>
<BaseIcon {...rest} />
</Icon>
);
}
const Icon = styled.span<{ size?: number }>`
display: inline-flex;
flex-shrink: 0;
width: ${(props) => props.size ?? 24}px;
height: ${(props) => props.size ?? 24}px;
align-items: center;
justify-content: center;
`;
function BaseIcon(props: Props) {
switch (props.status) {
case "open":
return (
<svg
viewBox="0 0 16 16"
width="16"
height="16"
fill={props.color}
className={props.className}
>
<path d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z" />
<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" />
</svg>
);
case "closed":
return (
<svg
viewBox="0 0 16 16"
width="16"
height="16"
fill={props.color}
className={props.className}
>
<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" />
<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" />
</svg>
);
case "canceled":
return (
<svg
viewBox="0 0 16 16"
width="16"
height="16"
fill={props.color}
className={props.className}
>
<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" />
</svg>
);
default:
return null;
}
}

View File

@@ -0,0 +1,72 @@
import * as React from "react";
import styled from "styled-components";
type Props = {
status: string;
color: string;
size?: number;
className?: string;
};
/**
* Issue status icon based on GitHub pull requests, but can be used for any git-style integration.
*/
export function PullRequestIcon({ size, ...rest }: Props) {
return (
<Icon size={size}>
<BaseIcon {...rest} />
</Icon>
);
}
const Icon = styled.span<{ size?: number }>`
display: inline-flex;
flex-shrink: 0;
width: ${(props) => props.size ?? 24}px;
height: ${(props) => props.size ?? 24}px;
align-items: center;
justify-content: center;
`;
function BaseIcon(props: Props) {
switch (props.status) {
case "open":
return (
<svg
viewBox="0 0 16 16"
width="16"
height="16"
fill={props.color}
className={props.className}
>
<path d="M1.5 3.25a2.25 2.25 0 1 1 3 2.122v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.25 2.25 0 0 1 1.5 3.25Zm5.677-.177L9.573.677A.25.25 0 0 1 10 .854V2.5h1A2.5 2.5 0 0 1 13.5 5v5.628a2.251 2.251 0 1 1-1.5 0V5a1 1 0 0 0-1-1h-1v1.646a.25.25 0 0 1-.427.177L7.177 3.427a.25.25 0 0 1 0-.354ZM3.75 2.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm0 9.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm8.25.75a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0Z" />
</svg>
);
case "merged":
return (
<svg
viewBox="0 0 16 16"
width="16"
height="16"
fill={props.color}
className={props.className}
>
<path d="M5.45 5.154A4.25 4.25 0 0 0 9.25 7.5h1.378a2.251 2.251 0 1 1 0 1.5H9.25A5.734 5.734 0 0 1 5 7.123v3.505a2.25 2.25 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.95-.218ZM4.25 13.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm8.5-4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5ZM5 3.25a.75.75 0 1 0 0 .005V3.25Z" />
</svg>
);
case "closed":
return (
<svg
viewBox="0 0 16 16"
width="16"
height="16"
fill={props.color}
className={props.className}
>
<path d="M3.25 1A2.25 2.25 0 0 1 4 5.372v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.251 2.251 0 0 1 3.25 1Zm9.5 5.5a.75.75 0 0 1 .75.75v3.378a2.251 2.251 0 1 1-1.5 0V7.25a.75.75 0 0 1 .75-.75Zm-2.03-5.273a.75.75 0 0 1 1.06 0l.97.97.97-.97a.748.748 0 0 1 1.265.332.75.75 0 0 1-.205.729l-.97.97.97.97a.751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018l-.97-.97-.97.97a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734l.97-.97-.97-.97a.75.75 0 0 1 0-1.06ZM2.5 3.25a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0ZM3.25 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm9.5 0a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Z" />
</svg>
);
default:
return null;
}
}