mirror of
https://github.com/outline/outline.git
synced 2026-06-13 11:25:03 +03:00
Add Linear project unfurling support (#11525)
* Initial plan * Add Project type and unfurl implementation for Linear projects Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com> * Fix linter issues - remove unused import and rename unused parameter Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com> * Make actor parameter optional in unfurl helper methods Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com> * fix: Resolve type errors in Linear project unfurl Use project.status (ProjectStatus object) instead of the deprecated project.state (string) field, add satisfies constraint, and fix exhaustive return in unfurl switch. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Determine mention type * styling --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com> Co-authored-by: Tom Moor <tom@getoutline.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -43,9 +43,9 @@ export const Info = styled(StyledText).attrs(() => ({
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
export const Description = styled(StyledText)`
|
||||
export const Description = styled(StyledText)<{ $margin?: string }>`
|
||||
${sharedVars}
|
||||
margin-top: 0.5em;
|
||||
margin-top: ${(props) => props.$margin ?? "0.5em"};
|
||||
line-height: var(--line-height);
|
||||
max-height: calc(var(--line-height) * ${NUMBER_OF_LINES});
|
||||
overflow: hidden;
|
||||
@@ -64,8 +64,6 @@ export const Label = styled(Text).attrs({ size: "xsmall", weight: "bold" })<{
|
||||
width: fit-content;
|
||||
border-radius: 2em;
|
||||
padding: 1px 8px 1px 20px;
|
||||
margin-right: 0.5em;
|
||||
margin-top: 0.5em;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
|
||||
@@ -75,8 +73,8 @@ export const Label = styled(Text).attrs({ size: "xsmall", weight: "bold" })<{
|
||||
left: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: ${(props) =>
|
||||
props.color || props.theme.backgroundSecondary};
|
||||
|
||||
@@ -17,6 +17,7 @@ import HoverPreviewGroup from "./HoverPreviewGroup";
|
||||
import HoverPreviewIssue from "./HoverPreviewIssue";
|
||||
import HoverPreviewLink from "./HoverPreviewLink";
|
||||
import HoverPreviewMention from "./HoverPreviewMention";
|
||||
import HoverPreviewProject from "./HoverPreviewProject";
|
||||
import HoverPreviewPullRequest from "./HoverPreviewPullRequest";
|
||||
|
||||
const DELAY_CLOSE = 500;
|
||||
@@ -192,6 +193,18 @@ const HoverPreviewDesktop = observer(
|
||||
createdAt={data.createdAt}
|
||||
state={data.state}
|
||||
/>
|
||||
) : data.type === UnfurlResourceType.Project ? (
|
||||
<HoverPreviewProject
|
||||
ref={cardRef}
|
||||
url={data.url}
|
||||
name={data.name}
|
||||
color={data.color}
|
||||
lead={data.lead}
|
||||
labels={data.labels}
|
||||
description={data.description}
|
||||
state={data.state}
|
||||
targetDate={data.targetDate}
|
||||
/>
|
||||
) : (
|
||||
<HoverPreviewLink
|
||||
ref={cardRef}
|
||||
|
||||
@@ -75,7 +75,7 @@ const HoverPreviewIssue = React.forwardRef(function HoverPreviewIssue_(
|
||||
</Description>
|
||||
)}
|
||||
|
||||
<Flex wrap>
|
||||
<Flex wrap gap={6} style={{ marginTop: 8 }}>
|
||||
{labels.map((label, index) => (
|
||||
<Label key={index} color={label.color}>
|
||||
{label.name}
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import { s } from "@shared/styles";
|
||||
import { Backticks } from "@shared/components/Backticks";
|
||||
import Squircle from "@shared/components/Squircle";
|
||||
import Editor from "~/components/Editor";
|
||||
import type { UnfurlResourceType, UnfurlResponse } from "@shared/types";
|
||||
import { Avatar, AvatarSize } from "~/components/Avatar";
|
||||
import Flex from "~/components/Flex";
|
||||
import Text from "../Text";
|
||||
import Time from "../Time";
|
||||
import {
|
||||
Preview,
|
||||
Title,
|
||||
Card,
|
||||
CardContent,
|
||||
Label,
|
||||
Description,
|
||||
} from "./Components";
|
||||
import { richExtensions } from "@shared/editor/nodes";
|
||||
|
||||
type Props = Pick<
|
||||
UnfurlResponse[UnfurlResourceType.Project],
|
||||
| "url"
|
||||
| "name"
|
||||
| "color"
|
||||
| "lead"
|
||||
| "labels"
|
||||
| "state"
|
||||
| "targetDate"
|
||||
| "description"
|
||||
>;
|
||||
|
||||
const HoverPreviewProject = React.forwardRef(function HoverPreviewProject_(
|
||||
{ url, name, color, lead, labels, state, description, targetDate }: Props,
|
||||
ref: React.Ref<HTMLDivElement>
|
||||
) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Preview as="a" href={url} target="_blank" rel="noopener noreferrer">
|
||||
<Flex column ref={ref}>
|
||||
<Card fadeOut={false}>
|
||||
<CardContent>
|
||||
<Flex gap={4} column>
|
||||
<Title>
|
||||
<StyledSquircle color={color} size={16} />
|
||||
<span>
|
||||
<Backticks content={name} />
|
||||
</span>
|
||||
</Title>
|
||||
{description && (
|
||||
<Description as="div" $margin="0">
|
||||
<React.Suspense fallback={<div />}>
|
||||
<Editor
|
||||
extensions={richExtensions}
|
||||
defaultValue={description}
|
||||
embedsDisabled
|
||||
readOnly
|
||||
/>
|
||||
</React.Suspense>
|
||||
</Description>
|
||||
)}
|
||||
<Text type="tertiary" size="small">
|
||||
{state.name}
|
||||
</Text>
|
||||
|
||||
{(lead || targetDate) && (
|
||||
<>
|
||||
<Divider />
|
||||
|
||||
{lead && (
|
||||
<MetadataRow>
|
||||
<MetadataLabel>{t("Lead")}</MetadataLabel>
|
||||
<Flex align="center" gap={6}>
|
||||
<Avatar src={lead.avatarUrl} size={AvatarSize.Toast} />
|
||||
<Text size="small">{lead.name}</Text>
|
||||
</Flex>
|
||||
</MetadataRow>
|
||||
)}
|
||||
|
||||
{targetDate && (
|
||||
<MetadataRow>
|
||||
<MetadataLabel>{t("Target date")}</MetadataLabel>
|
||||
<Text size="small">
|
||||
<Time dateTime={targetDate} addSuffix />
|
||||
</Text>
|
||||
</MetadataRow>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{labels.length > 0 && (
|
||||
<>
|
||||
<Divider />
|
||||
<MetadataRow>
|
||||
<MetadataLabel>{t("Labels")}</MetadataLabel>
|
||||
<Flex wrap gap={6}>
|
||||
{labels.map((label, index) => (
|
||||
<Label key={index} color={label.color}>
|
||||
{label.name}
|
||||
</Label>
|
||||
))}
|
||||
</Flex>
|
||||
</MetadataRow>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Flex>
|
||||
</Preview>
|
||||
);
|
||||
});
|
||||
|
||||
const StyledSquircle = styled(Squircle)`
|
||||
flex-shrink: 0;
|
||||
margin-top: 4px;
|
||||
`;
|
||||
|
||||
const Divider = styled.div`
|
||||
height: 1px;
|
||||
background: ${s("divider")};
|
||||
margin: 4px 0;
|
||||
`;
|
||||
|
||||
const MetadataRow = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
min-height: 28px;
|
||||
`;
|
||||
|
||||
const MetadataLabel = styled(Text).attrs({
|
||||
type: "tertiary",
|
||||
size: "small",
|
||||
})`
|
||||
flex-shrink: 0;
|
||||
min-width: 80px;
|
||||
`;
|
||||
|
||||
export default HoverPreviewProject;
|
||||
@@ -70,7 +70,11 @@ export const determineMentionType = ({
|
||||
|
||||
case IntegrationService.Linear: {
|
||||
const type = pathParts[2];
|
||||
return type === "issue" ? MentionType.Issue : undefined;
|
||||
return type === "issue"
|
||||
? MentionType.Issue
|
||||
: type === "project"
|
||||
? MentionType.Project
|
||||
: undefined;
|
||||
}
|
||||
|
||||
case IntegrationService.GitLab: {
|
||||
|
||||
+121
-49
@@ -8,7 +8,11 @@ import { IntegrationService, UnfurlResourceType } from "@shared/types";
|
||||
import Logger from "@server/logging/Logger";
|
||||
import { Integration } from "@server/models";
|
||||
import type User from "@server/models/User";
|
||||
import type { UnfurlIssueOrPR, UnfurlSignature } from "@server/types";
|
||||
import type {
|
||||
UnfurlIssueOrPR,
|
||||
UnfurlProject,
|
||||
UnfurlSignature,
|
||||
} from "@server/types";
|
||||
import { LinearUtils } from "../shared/LinearUtils";
|
||||
import env from "./env";
|
||||
import { Minute } from "@shared/utils/time";
|
||||
@@ -26,7 +30,10 @@ const AccessTokenResponseSchema = z.object({
|
||||
});
|
||||
|
||||
export class Linear {
|
||||
private static supportedUnfurls = [UnfurlResourceType.Issue];
|
||||
private static supportedUnfurls = [
|
||||
UnfurlResourceType.Issue,
|
||||
UnfurlResourceType.Project,
|
||||
];
|
||||
|
||||
static async oauthAccess(code: string) {
|
||||
const headers = {
|
||||
@@ -103,7 +110,7 @@ export class Linear {
|
||||
*
|
||||
* @param url Linear resource url
|
||||
* @param actor User attempting to unfurl resource url
|
||||
* @returns An object containing resource details e.g, a Linear issue details
|
||||
* @returns An object containing resource details e.g, a Linear issue or project details
|
||||
*/
|
||||
static unfurl: UnfurlSignature = async (url: string, actor?: User) => {
|
||||
const resource = Linear.parseUrl(url);
|
||||
@@ -138,60 +145,125 @@ export class Linear {
|
||||
);
|
||||
|
||||
const client = new LinearClient({ accessToken });
|
||||
const issue = await client.issue(resource.id);
|
||||
|
||||
if (!issue) {
|
||||
return { error: "Resource not found" };
|
||||
switch (resource.type) {
|
||||
case UnfurlResourceType.Issue:
|
||||
return Linear.unfurlIssue(client, resource.id, actor);
|
||||
case UnfurlResourceType.Project:
|
||||
return Linear.unfurlProject(client, resource.id, actor);
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
const [author, state, labels] = await Promise.all([
|
||||
issue.creator,
|
||||
issue.state,
|
||||
issue.paginate(issue.labels, {}),
|
||||
]);
|
||||
|
||||
if (!state || !labels) {
|
||||
return { error: "Failed to fetch auxiliary data from Linear" };
|
||||
}
|
||||
|
||||
const completionPercentage = await Linear.completionPercentage(
|
||||
client,
|
||||
issue,
|
||||
state
|
||||
);
|
||||
|
||||
return {
|
||||
type: UnfurlResourceType.Issue,
|
||||
url: issue.url,
|
||||
id: issue.identifier,
|
||||
title: issue.title,
|
||||
description: issue.description ?? null,
|
||||
author: {
|
||||
name:
|
||||
author?.name ??
|
||||
issue.botActor?.userDisplayName ??
|
||||
issue.botActor?.name ??
|
||||
t("Unknown", opts(actor)),
|
||||
avatarUrl: author?.avatarUrl ?? "",
|
||||
},
|
||||
labels: labels.map((label) => ({
|
||||
name: label.name,
|
||||
color: label.color,
|
||||
})),
|
||||
state: {
|
||||
type: state.type,
|
||||
name: state.name,
|
||||
color: state.color,
|
||||
completionPercentage,
|
||||
},
|
||||
createdAt: issue.createdAt.toISOString(),
|
||||
} satisfies UnfurlIssueOrPR;
|
||||
} catch (err) {
|
||||
Logger.warn("Failed to fetch resource from Linear", err);
|
||||
return { error: err.message || "Unknown error" };
|
||||
}
|
||||
};
|
||||
|
||||
private static async unfurlIssue(
|
||||
client: LinearClient,
|
||||
id: string,
|
||||
actor: User | undefined
|
||||
) {
|
||||
const issue = await client.issue(id);
|
||||
|
||||
if (!issue) {
|
||||
return { error: "Resource not found" };
|
||||
}
|
||||
|
||||
const [author, state, labels] = await Promise.all([
|
||||
issue.creator,
|
||||
issue.state,
|
||||
issue.paginate(issue.labels, {}),
|
||||
]);
|
||||
|
||||
if (!state || !labels) {
|
||||
return { error: "Failed to fetch auxiliary data from Linear" };
|
||||
}
|
||||
|
||||
const completionPercentage = await Linear.completionPercentage(
|
||||
client,
|
||||
issue,
|
||||
state
|
||||
);
|
||||
|
||||
return {
|
||||
type: UnfurlResourceType.Issue,
|
||||
url: issue.url,
|
||||
id: issue.identifier,
|
||||
title: issue.title,
|
||||
description: issue.description ?? null,
|
||||
author: {
|
||||
name:
|
||||
author?.name ??
|
||||
issue.botActor?.userDisplayName ??
|
||||
issue.botActor?.name ??
|
||||
t("Unknown", opts(actor)),
|
||||
avatarUrl: author?.avatarUrl ?? "",
|
||||
},
|
||||
labels: labels.map((label) => ({
|
||||
name: label.name,
|
||||
color: label.color,
|
||||
})),
|
||||
state: {
|
||||
type: state.type,
|
||||
name: state.name,
|
||||
color: state.color,
|
||||
completionPercentage,
|
||||
},
|
||||
createdAt: issue.createdAt.toISOString(),
|
||||
} satisfies UnfurlIssueOrPR;
|
||||
}
|
||||
|
||||
private static async unfurlProject(
|
||||
client: LinearClient,
|
||||
id: string,
|
||||
_actor: User | undefined
|
||||
) {
|
||||
const project = await client.project(id);
|
||||
|
||||
if (!project) {
|
||||
return { error: "Resource not found" };
|
||||
}
|
||||
|
||||
const [lead, status, labels] = await Promise.all([
|
||||
project.lead,
|
||||
project.status,
|
||||
project.paginate(project.labels, {}),
|
||||
]);
|
||||
|
||||
if (!status || !labels) {
|
||||
return { error: "Failed to fetch auxiliary data from Linear" };
|
||||
}
|
||||
|
||||
return {
|
||||
type: UnfurlResourceType.Project,
|
||||
url: project.url,
|
||||
id: project.id,
|
||||
name: project.name,
|
||||
color: project.color ?? status.color,
|
||||
description: project.description ?? null,
|
||||
lead: lead
|
||||
? {
|
||||
name: lead.name,
|
||||
avatarUrl: lead.avatarUrl ?? "",
|
||||
}
|
||||
: null,
|
||||
state: {
|
||||
type: status.type,
|
||||
name: status.name,
|
||||
color: status.color,
|
||||
},
|
||||
labels: labels.map((label) => ({
|
||||
name: label.name,
|
||||
color: label.color,
|
||||
})),
|
||||
progress: project.progress,
|
||||
createdAt: project.createdAt.toISOString(),
|
||||
targetDate: project.targetDate ?? null,
|
||||
} satisfies UnfurlProject;
|
||||
}
|
||||
|
||||
private static async completionPercentage(
|
||||
client: LinearClient,
|
||||
issue: Issue,
|
||||
|
||||
@@ -22,6 +22,8 @@ async function presentUnfurl(
|
||||
return presentPR(data);
|
||||
case UnfurlResourceType.Issue:
|
||||
return presentIssue(data);
|
||||
case UnfurlResourceType.Project:
|
||||
return presentProject(data);
|
||||
default:
|
||||
return presentURL(data);
|
||||
}
|
||||
@@ -111,6 +113,11 @@ const presentIssue = (
|
||||
): UnfurlResponse[UnfurlResourceType.Issue] =>
|
||||
data as UnfurlResponse[UnfurlResourceType.Issue]; // this would have been transformed by the unfurl plugin.
|
||||
|
||||
const presentProject = (
|
||||
data: Record<string, any>
|
||||
): UnfurlResponse[UnfurlResourceType.Project] =>
|
||||
data as UnfurlResponse[UnfurlResourceType.Project]; // this would have been transformed by the unfurl plugin.
|
||||
|
||||
const presentLastOnlineInfoFor = (user: User) => {
|
||||
const locale = dateLocale(user.language);
|
||||
|
||||
|
||||
@@ -581,18 +581,22 @@ export type UnfurlIssueOrPR =
|
||||
| UnfurlResponse[UnfurlResourceType.Issue]
|
||||
| UnfurlResponse[UnfurlResourceType.PR];
|
||||
|
||||
export type UnfurlProject = UnfurlResponse[UnfurlResourceType.Project];
|
||||
|
||||
export type UnfurlURL = UnfurlResponse[UnfurlResourceType.URL] & {
|
||||
transformedUnfurl: true;
|
||||
};
|
||||
|
||||
export type Unfurl =
|
||||
| UnfurlIssueOrPR
|
||||
| UnfurlProject
|
||||
| UnfurlURL
|
||||
| {
|
||||
type: Exclude<
|
||||
UnfurlResourceType,
|
||||
| UnfurlResourceType.Issue
|
||||
| UnfurlResourceType.PR
|
||||
| UnfurlResourceType.Project
|
||||
| UnfurlResourceType.URL
|
||||
>;
|
||||
[x: string]: JSONValue;
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
import { cn } from "../styles/utils";
|
||||
import type { ComponentProps } from "../types";
|
||||
import { toDisplayUrl, cdnPath } from "../../utils/urls";
|
||||
import Squircle from "../../components/Squircle";
|
||||
|
||||
type Attrs = {
|
||||
className: string;
|
||||
@@ -348,6 +349,79 @@ export const MentionIssue = observer((props: IssuePrProps) => {
|
||||
);
|
||||
});
|
||||
|
||||
type ProjectProps = ComponentProps & {
|
||||
onChangeUnfurl: (unfurl: UnfurlResponse[UnfurlResourceType.Project]) => void;
|
||||
};
|
||||
|
||||
export const MentionProject = observer((props: ProjectProps) => {
|
||||
const { unfurls } = useStores();
|
||||
const isMounted = useIsMounted();
|
||||
const [loaded, setLoaded] = React.useState(false);
|
||||
const onChangeUnfurl = React.useRef(props.onChangeUnfurl).current;
|
||||
|
||||
const { isSelected, node } = props;
|
||||
const {
|
||||
className,
|
||||
unfurl: unfurlAttr,
|
||||
...attrs
|
||||
} = getAttributesFromNode(node);
|
||||
|
||||
const unfurl = unfurls.get(attrs.href)?.data ?? unfurlAttr;
|
||||
|
||||
React.useEffect(() => {
|
||||
const fetchProject = async () => {
|
||||
const unfurlModel = await unfurls.fetchUnfurl({ url: attrs.href });
|
||||
|
||||
if (!isMounted()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (unfurlModel) {
|
||||
onChangeUnfurl({
|
||||
...unfurlModel.data,
|
||||
description: null,
|
||||
} satisfies UnfurlResponse[UnfurlResourceType.Project]);
|
||||
}
|
||||
|
||||
setLoaded(true);
|
||||
};
|
||||
|
||||
void fetchProject();
|
||||
}, [unfurls, attrs.href, isMounted, onChangeUnfurl]);
|
||||
|
||||
if (!unfurl) {
|
||||
return !loaded ? (
|
||||
<MentionLoading className={className} />
|
||||
) : (
|
||||
<MentionError className={className} />
|
||||
);
|
||||
}
|
||||
|
||||
const project = unfurl as UnfurlResponse[UnfurlResourceType.Project];
|
||||
|
||||
return (
|
||||
<a
|
||||
{...attrs}
|
||||
className={cn(className, {
|
||||
"ProseMirror-selectednode": isSelected,
|
||||
})}
|
||||
href={attrs.href as string}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
>
|
||||
<Flex align="center" gap={6}>
|
||||
<Squircle color={project.color} size={12} />
|
||||
<Flex align="center" gap={4}>
|
||||
<Text>
|
||||
<Backticks content={project.name} />
|
||||
</Text>
|
||||
<Text type="tertiary">{Math.round(project.progress * 100)}%</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</a>
|
||||
);
|
||||
});
|
||||
|
||||
export const MentionPullRequest = observer((props: IssuePrProps) => {
|
||||
const { unfurls } = useStores();
|
||||
const isMounted = useIsMounted();
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
MentionDocument,
|
||||
MentionGroup,
|
||||
MentionIssue,
|
||||
MentionProject,
|
||||
MentionPullRequest,
|
||||
MentionURL,
|
||||
MentionUser,
|
||||
@@ -110,7 +111,8 @@ export default class Mention extends Node {
|
||||
"data-actorid": node.attrs.actorId,
|
||||
"data-url":
|
||||
node.attrs.type === MentionType.PullRequest ||
|
||||
node.attrs.type === MentionType.Issue
|
||||
node.attrs.type === MentionType.Issue ||
|
||||
node.attrs.type === MentionType.Project
|
||||
? node.attrs.href
|
||||
: `mention://${node.attrs.id}/${node.attrs.type}/${node.attrs.modelId}`,
|
||||
"data-unfurl": JSON.stringify(node.attrs.unfurl),
|
||||
@@ -145,6 +147,13 @@ export default class Mention extends Node {
|
||||
onChangeUnfurl={this.handleChangeUnfurl(props)}
|
||||
/>
|
||||
);
|
||||
case MentionType.Project:
|
||||
return (
|
||||
<MentionProject
|
||||
{...props}
|
||||
onChangeUnfurl={this.handleChangeUnfurl(props)}
|
||||
/>
|
||||
);
|
||||
case MentionType.URL:
|
||||
return (
|
||||
<MentionURL
|
||||
@@ -199,6 +208,7 @@ export default class Mention extends Node {
|
||||
MentionType.Document,
|
||||
MentionType.Issue,
|
||||
MentionType.PullRequest,
|
||||
MentionType.Project,
|
||||
];
|
||||
|
||||
return {
|
||||
@@ -215,7 +225,8 @@ export default class Mention extends Node {
|
||||
|
||||
if (
|
||||
mentionType === MentionType.Issue ||
|
||||
mentionType === MentionType.PullRequest
|
||||
mentionType === MentionType.PullRequest ||
|
||||
mentionType === MentionType.Project
|
||||
) {
|
||||
link = selection.node.attrs.href;
|
||||
} else {
|
||||
@@ -340,7 +351,9 @@ export default class Mention extends Node {
|
||||
unfurl.type === UnfurlResourceType.PR ||
|
||||
unfurl.type === UnfurlResourceType.URL
|
||||
? unfurl.title
|
||||
: undefined;
|
||||
: unfurl.type === UnfurlResourceType.Project
|
||||
? unfurl.name
|
||||
: undefined;
|
||||
|
||||
const overrides: Record<string, unknown> = label ? { label } : {};
|
||||
overrides.unfurl = unfurl;
|
||||
|
||||
@@ -341,6 +341,9 @@
|
||||
"{{ count }} members": "{{ count }} member",
|
||||
"{{ count }} members_plural": "{{ count }} members",
|
||||
"{{authorName}} created <3></3>": "{{authorName}} created <3></3>",
|
||||
"Lead": "Lead",
|
||||
"Target date": "Target date",
|
||||
"Labels": "Labels",
|
||||
"{{authorName}} opened <3></3>": "{{authorName}} opened <3></3>",
|
||||
"Search emoji": "Search emoji",
|
||||
"Search icons": "Search icons",
|
||||
|
||||
@@ -97,6 +97,7 @@ export enum MentionType {
|
||||
Group = "group",
|
||||
Issue = "issue",
|
||||
PullRequest = "pull_request",
|
||||
Project = "project",
|
||||
URL = "url",
|
||||
}
|
||||
|
||||
@@ -504,6 +505,7 @@ export enum UnfurlResourceType {
|
||||
Document = "document",
|
||||
Issue = "issue",
|
||||
PR = "pull",
|
||||
Project = "project",
|
||||
}
|
||||
|
||||
export type UnfurlResponse = {
|
||||
@@ -514,6 +516,8 @@ export type UnfurlResponse = {
|
||||
url: string;
|
||||
/** A text title, describing the resource */
|
||||
title: string;
|
||||
/** A color representing the resource */
|
||||
color?: string;
|
||||
/** A brief description about the resource */
|
||||
description: string;
|
||||
/** A URL to a thumbnail image representing the resource */
|
||||
@@ -609,6 +613,36 @@ export type UnfurlResponse = {
|
||||
/** Pull Request creation time */
|
||||
createdAt: string;
|
||||
};
|
||||
[UnfurlResourceType.Project]: {
|
||||
/** The resource type */
|
||||
type: UnfurlResourceType.Project;
|
||||
/** Project link */
|
||||
url: string;
|
||||
/** Project identifier */
|
||||
id: string;
|
||||
/** Project name */
|
||||
name: string;
|
||||
/** Project color */
|
||||
color: string;
|
||||
/** Project description */
|
||||
description: string | null;
|
||||
/** Project lead */
|
||||
lead: { name: string; avatarUrl: string } | null;
|
||||
/** Project state */
|
||||
state: {
|
||||
name: string;
|
||||
color: string;
|
||||
type: string;
|
||||
};
|
||||
/** Project labels */
|
||||
labels: Array<{ name: string; color: string }>;
|
||||
/** Project progress (0-1) */
|
||||
progress: number;
|
||||
/** Project creation time */
|
||||
createdAt: string;
|
||||
/** Project target date */
|
||||
targetDate: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
export enum QueryNotices {
|
||||
|
||||
Reference in New Issue
Block a user