mirror of
https://github.com/makeplane/plane.git
synced 2025-12-23 07:09:34 +01:00
[WEB-3892] chore: link item improvements (#6944)
* chore: code refactor * chore: global link block component added * chore: link item improvement and code refactor
This commit is contained in:
committed by
GitHub
parent
18fb3b8450
commit
be5d77d978
@@ -32,3 +32,4 @@ export * from "./tag";
|
|||||||
export * from "./tabs";
|
export * from "./tabs";
|
||||||
export * from "./calendar";
|
export * from "./calendar";
|
||||||
export * from "./color-picker";
|
export * from "./color-picker";
|
||||||
|
export * from "./link";
|
||||||
|
|||||||
68
packages/ui/src/link/block.tsx
Normal file
68
packages/ui/src/link/block.tsx
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import React, { FC } from "react";
|
||||||
|
// plane utils
|
||||||
|
import { calculateTimeAgo, cn, getIconForLink } from "@plane/utils";
|
||||||
|
// plane ui
|
||||||
|
import { CustomMenu, TContextMenuItem } from "@plane/ui";
|
||||||
|
|
||||||
|
export type TLinkItemBlockProps = {
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
createdAt?: Date | string;
|
||||||
|
menuItems?: TContextMenuItem[];
|
||||||
|
onClick?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LinkItemBlock: FC<TLinkItemBlockProps> = (props) => {
|
||||||
|
// props
|
||||||
|
const { title, url, createdAt, menuItems, onClick } = props;
|
||||||
|
// icons
|
||||||
|
const Icon = getIconForLink(url);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onClick={onClick}
|
||||||
|
className="cursor-pointer group flex items-center bg-custom-background-100 px-4 w-[230px] h-[56px] border-[0.5px] border-custom-border-200 rounded-md gap-4"
|
||||||
|
>
|
||||||
|
<div className="flex-shrink-0 size-8 rounded p-2 bg-custom-background-90 grid place-items-center">
|
||||||
|
<Icon className="size-4 stroke-2 text-custom-text-350 group-hover:text-custom-text-100" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 truncate">
|
||||||
|
<div className="text-sm font-medium truncate">{title}</div>
|
||||||
|
{createdAt && <div className="text-xs font-medium text-custom-text-400">{calculateTimeAgo(createdAt)}</div>}
|
||||||
|
</div>
|
||||||
|
{menuItems && (
|
||||||
|
<div className="hidden group-hover:block">
|
||||||
|
<CustomMenu placement="bottom-end" menuItemsClassName="z-20" closeOnSelect verticalEllipsis>
|
||||||
|
{menuItems.map((item) => (
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
key={item.key}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
item.action();
|
||||||
|
}}
|
||||||
|
className={cn("flex items-center gap-2 w-full ", {
|
||||||
|
"text-custom-text-400": item.disabled,
|
||||||
|
})}
|
||||||
|
disabled={item.disabled}
|
||||||
|
>
|
||||||
|
{item.icon && <item.icon className={cn("h-3 w-3", item.iconClassName)} />}
|
||||||
|
<div>
|
||||||
|
<h5>{item.title}</h5>
|
||||||
|
{item.description && (
|
||||||
|
<p
|
||||||
|
className={cn("text-custom-text-300 whitespace-pre-line", {
|
||||||
|
"text-custom-text-400": item.disabled,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{item.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
))}
|
||||||
|
</CustomMenu>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
1
packages/ui/src/link/index.ts
Normal file
1
packages/ui/src/link/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./block";
|
||||||
@@ -1,109 +1,64 @@
|
|||||||
import {
|
import {
|
||||||
Github,
|
Github,
|
||||||
Linkedin,
|
Linkedin,
|
||||||
Twitter,
|
Twitter,
|
||||||
Facebook,
|
Facebook,
|
||||||
Instagram,
|
Instagram,
|
||||||
Youtube,
|
Youtube,
|
||||||
Dribbble,
|
Dribbble,
|
||||||
Figma,
|
Figma,
|
||||||
FileText,
|
FileText,
|
||||||
FileImage,
|
FileImage,
|
||||||
FileVideo,
|
FileVideo,
|
||||||
FileAudio,
|
FileAudio,
|
||||||
FileArchive,
|
FileArchive,
|
||||||
FileSpreadsheet,
|
FileSpreadsheet,
|
||||||
FileCode,
|
FileCode,
|
||||||
Mail,
|
Mail,
|
||||||
Chrome,
|
Chrome,
|
||||||
Link2,
|
Link2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
|
type IconMatcher = {
|
||||||
|
pattern: RegExp;
|
||||||
|
icon: typeof Github;
|
||||||
|
};
|
||||||
|
|
||||||
// get-icon-for-link.ts
|
const SOCIAL_MEDIA_MATCHERS: IconMatcher[] = [
|
||||||
|
{ pattern: /github\.com/, icon: Github },
|
||||||
|
{ pattern: /linkedin\.com/, icon: Linkedin },
|
||||||
|
{ pattern: /(twitter\.com|x\.com)/, icon: Twitter },
|
||||||
|
{ pattern: /facebook\.com/, icon: Facebook },
|
||||||
|
{ pattern: /instagram\.com/, icon: Instagram },
|
||||||
|
{ pattern: /youtube\.com/, icon: Youtube },
|
||||||
|
{ pattern: /dribbble\.com/, icon: Dribbble },
|
||||||
|
];
|
||||||
|
|
||||||
export const getIconForLink = (url: string) => {
|
const PRODUCTIVITY_MATCHERS: IconMatcher[] = [
|
||||||
const lowerUrl = url.toLowerCase();
|
{ pattern: /figma\.com/, icon: Figma },
|
||||||
|
{ pattern: /(google\.com|docs\.|doc\.)/, icon: FileText },
|
||||||
|
];
|
||||||
|
|
||||||
// Social Media
|
const FILE_TYPE_MATCHERS: IconMatcher[] = [
|
||||||
if (lowerUrl.indexOf("github.com") !== -1) return Github;
|
{ pattern: /\.(jpg|jpeg|png|gif|bmp|svg|webp)$/, icon: FileImage },
|
||||||
if (lowerUrl.indexOf("linkedin.com") !== -1) return Linkedin;
|
{ pattern: /\.(mp4|mov|avi|wmv|flv|mkv)$/, icon: FileVideo },
|
||||||
if (lowerUrl.indexOf("twitter.com") !== -1 || lowerUrl.indexOf("x.com") !== -1) return Twitter;
|
{ pattern: /\.(mp3|wav|ogg)$/, icon: FileAudio },
|
||||||
if (lowerUrl.indexOf("facebook.com") !== -1) return Facebook;
|
{ pattern: /\.(zip|rar|7z|tar|gz)$/, icon: FileArchive },
|
||||||
if (lowerUrl.indexOf("instagram.com") !== -1) return Instagram;
|
{ pattern: /\.(xls|xlsx|csv)$/, icon: FileSpreadsheet },
|
||||||
if (lowerUrl.indexOf("youtube.com") !== -1 || lowerUrl.indexOf("youtu.be") !== -1) return Youtube;
|
{ pattern: /\.(pdf|doc|docx|txt)$/, icon: FileText },
|
||||||
if (lowerUrl.indexOf("dribbble.com") !== -1) return Dribbble;
|
{ pattern: /\.(html|js|ts|jsx|tsx|css|scss)$/, icon: FileCode },
|
||||||
|
];
|
||||||
|
|
||||||
// Productivity / Tools
|
const OTHER_MATCHERS: IconMatcher[] = [
|
||||||
if (lowerUrl.indexOf("figma.com") !== -1) return Figma;
|
{ pattern: /^mailto:/, icon: Mail },
|
||||||
|
{ pattern: /^http/, icon: Chrome },
|
||||||
|
];
|
||||||
|
|
||||||
if (
|
export const getIconForLink = (url: string) => {
|
||||||
lowerUrl.indexOf("google.com") !== -1 ||
|
const lowerUrl = url.toLowerCase();
|
||||||
lowerUrl.indexOf("docs.") !== -1 ||
|
|
||||||
lowerUrl.indexOf("doc.") !== -1
|
|
||||||
) return FileText;
|
|
||||||
|
|
||||||
// File types
|
const allMatchers = [...SOCIAL_MEDIA_MATCHERS, ...PRODUCTIVITY_MATCHERS, ...FILE_TYPE_MATCHERS, ...OTHER_MATCHERS];
|
||||||
if (
|
|
||||||
lowerUrl.indexOf(".jpg") !== -1 ||
|
|
||||||
lowerUrl.indexOf(".jpeg") !== -1 ||
|
|
||||||
lowerUrl.indexOf(".png") !== -1 ||
|
|
||||||
lowerUrl.indexOf(".gif") !== -1 ||
|
|
||||||
lowerUrl.indexOf(".bmp") !== -1 ||
|
|
||||||
lowerUrl.indexOf(".svg") !== -1 ||
|
|
||||||
lowerUrl.indexOf(".webp") !== -1
|
|
||||||
) return FileImage;
|
|
||||||
|
|
||||||
if (
|
|
||||||
lowerUrl.indexOf(".mp4") !== -1 ||
|
|
||||||
lowerUrl.indexOf(".mov") !== -1 ||
|
|
||||||
lowerUrl.indexOf(".avi") !== -1 ||
|
|
||||||
lowerUrl.indexOf(".wmv") !== -1 ||
|
|
||||||
lowerUrl.indexOf(".flv") !== -1 ||
|
|
||||||
lowerUrl.indexOf(".mkv") !== -1
|
|
||||||
) return FileVideo;
|
|
||||||
|
|
||||||
if (
|
|
||||||
lowerUrl.indexOf(".mp3") !== -1 ||
|
|
||||||
lowerUrl.indexOf(".wav") !== -1 ||
|
|
||||||
lowerUrl.indexOf(".ogg") !== -1
|
|
||||||
) return FileAudio;
|
|
||||||
|
|
||||||
if (
|
|
||||||
lowerUrl.indexOf(".zip") !== -1 ||
|
|
||||||
lowerUrl.indexOf(".rar") !== -1 ||
|
|
||||||
lowerUrl.indexOf(".7z") !== -1 ||
|
|
||||||
lowerUrl.indexOf(".tar") !== -1 ||
|
|
||||||
lowerUrl.indexOf(".gz") !== -1
|
|
||||||
) return FileArchive;
|
|
||||||
|
|
||||||
if (
|
|
||||||
lowerUrl.indexOf(".xls") !== -1 ||
|
|
||||||
lowerUrl.indexOf(".xlsx") !== -1 ||
|
|
||||||
lowerUrl.indexOf(".csv") !== -1
|
|
||||||
) return FileSpreadsheet;
|
|
||||||
|
|
||||||
if (
|
|
||||||
lowerUrl.indexOf(".pdf") !== -1 ||
|
|
||||||
lowerUrl.indexOf(".doc") !== -1 ||
|
|
||||||
lowerUrl.indexOf(".docx") !== -1 ||
|
|
||||||
lowerUrl.indexOf(".txt") !== -1
|
|
||||||
) return FileText;
|
|
||||||
|
|
||||||
if (
|
|
||||||
lowerUrl.indexOf(".html") !== -1 ||
|
|
||||||
lowerUrl.indexOf(".js") !== -1 ||
|
|
||||||
lowerUrl.indexOf(".ts") !== -1 ||
|
|
||||||
lowerUrl.indexOf(".jsx") !== -1 ||
|
|
||||||
lowerUrl.indexOf(".tsx") !== -1 ||
|
|
||||||
lowerUrl.indexOf(".css") !== -1 ||
|
|
||||||
lowerUrl.indexOf(".scss") !== -1
|
|
||||||
) return FileCode;
|
|
||||||
|
|
||||||
// Other
|
|
||||||
if (lowerUrl.indexOf("mailto:") !== -1) return Mail;
|
|
||||||
if (lowerUrl.indexOf("http") === 0) return Chrome;
|
|
||||||
|
|
||||||
return Link2;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
const matchedIcon = allMatchers.find(({ pattern }) => pattern.test(lowerUrl));
|
||||||
|
return matchedIcon?.icon ?? Link2;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,21 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { FC } from "react";
|
import { FC, useCallback, useMemo } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import {
|
import { Pencil, ExternalLink, Link, Trash2 } from "lucide-react";
|
||||||
Pencil,
|
|
||||||
ExternalLink,
|
|
||||||
Link,
|
|
||||||
Trash2
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
import { TOAST_TYPE, setToast, CustomMenu, TContextMenuItem } from "@plane/ui";
|
import { TOAST_TYPE, setToast, TContextMenuItem, LinkItemBlock } from "@plane/ui";
|
||||||
// plane utils
|
// plane utils
|
||||||
import { cn, copyTextToClipboard,getIconForLink } from "@plane/utils";
|
import { copyTextToClipboard } from "@plane/utils";
|
||||||
|
|
||||||
|
|
||||||
// helpers
|
|
||||||
import { calculateTimeAgo } from "@/helpers/date-time.helper";
|
|
||||||
// hooks
|
// hooks
|
||||||
import { useHome } from "@/hooks/store/use-home";
|
import { useHome } from "@/hooks/store/use-home";
|
||||||
// types
|
// types
|
||||||
@@ -34,101 +25,76 @@ export const ProjectLinkDetail: FC<TProjectLinkDetail> = observer((props) => {
|
|||||||
quickLinks: { getLinkById, toggleLinkModal, setLinkData },
|
quickLinks: { getLinkById, toggleLinkModal, setLinkData },
|
||||||
} = useHome();
|
} = useHome();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
// derived values
|
||||||
const linkDetail = getLinkById(linkId);
|
const linkDetail = getLinkById(linkId);
|
||||||
if (!linkDetail) return <></>;
|
|
||||||
|
|
||||||
const viewLink = linkDetail.url;
|
if (!linkDetail) return null;
|
||||||
|
|
||||||
const Icon = getIconForLink(linkDetail.url);
|
// handlers
|
||||||
|
const handleEdit = useCallback(
|
||||||
|
(modalToggle: boolean) => {
|
||||||
|
toggleLinkModal(modalToggle);
|
||||||
|
setLinkData(linkDetail);
|
||||||
|
},
|
||||||
|
[linkDetail, setLinkData, toggleLinkModal]
|
||||||
|
);
|
||||||
|
|
||||||
const handleEdit = (modalToggle: boolean) => {
|
const handleCopyText = useCallback(() => {
|
||||||
toggleLinkModal(modalToggle);
|
copyTextToClipboard(linkDetail.url).then(() => {
|
||||||
setLinkData(linkDetail);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCopyText = () =>
|
|
||||||
copyTextToClipboard(viewLink).then(() => {
|
|
||||||
setToast({
|
setToast({
|
||||||
type: TOAST_TYPE.SUCCESS,
|
type: TOAST_TYPE.SUCCESS,
|
||||||
title: t("link_copied"),
|
title: t("link_copied"),
|
||||||
message: t("view_link_copied_to_clipboard"),
|
message: t("view_link_copied_to_clipboard"),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
const handleOpenInNewTab = () => window.open(`${viewLink}`, "_blank");
|
}, [linkDetail.url, t]);
|
||||||
|
|
||||||
|
const handleOpenInNewTab = useCallback(() => {
|
||||||
|
window.open(linkDetail.url, "_blank", "noopener,noreferrer");
|
||||||
|
}, [linkDetail.url]);
|
||||||
|
|
||||||
const MENU_ITEMS: TContextMenuItem[] = [
|
const handleDelete = useCallback(() => {
|
||||||
{
|
linkOperations.remove(linkId);
|
||||||
key: "edit",
|
}, [linkId, linkOperations]);
|
||||||
action: () => handleEdit(true),
|
|
||||||
title: t("edit"),
|
// derived values
|
||||||
icon: Pencil,
|
const menuItems = useMemo<TContextMenuItem[]>(
|
||||||
},
|
() => [
|
||||||
{
|
{
|
||||||
key: "open-new-tab",
|
key: "edit",
|
||||||
action: handleOpenInNewTab,
|
action: () => handleEdit(true),
|
||||||
title: t("open_in_new_tab"),
|
title: t("edit"),
|
||||||
icon: ExternalLink,
|
icon: Pencil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "copy-link",
|
key: "open-new-tab",
|
||||||
action: handleCopyText,
|
action: handleOpenInNewTab,
|
||||||
title: t("copy_link"),
|
title: t("open_in_new_tab"),
|
||||||
icon: Link,
|
icon: ExternalLink,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "delete",
|
key: "copy-link",
|
||||||
action: () => linkOperations.remove(linkId),
|
action: handleCopyText,
|
||||||
title: t("delete"),
|
title: t("copy_link"),
|
||||||
icon: Trash2,
|
icon: Link,
|
||||||
},
|
},
|
||||||
];
|
{
|
||||||
|
key: "delete",
|
||||||
|
action: handleDelete,
|
||||||
|
title: t("delete"),
|
||||||
|
icon: Trash2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[handleEdit, handleOpenInNewTab, handleCopyText, handleDelete, t]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<LinkItemBlock
|
||||||
|
title={linkDetail.title || linkDetail.url}
|
||||||
|
url={linkDetail.url}
|
||||||
|
createdAt={linkDetail.created_at}
|
||||||
|
menuItems={menuItems}
|
||||||
onClick={handleOpenInNewTab}
|
onClick={handleOpenInNewTab}
|
||||||
className="cursor-pointer group flex items-center bg-custom-background-100 px-4 w-[230px] h-[56px] border-[0.5px] border-custom-border-200 rounded-md gap-4 hover:shadow-md transition-shadow"
|
/>
|
||||||
>
|
|
||||||
<div className="flex-shrink-0 size-8 rounded p-2 bg-custom-background-80 grid place-items-center">
|
|
||||||
<Icon className="size-4 stroke-2 text-custom-text-350" />
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 truncate">
|
|
||||||
<div className="text-sm font-medium truncate">{linkDetail.title || linkDetail.url}</div>
|
|
||||||
<div className="text-xs font-medium text-custom-text-400">{calculateTimeAgo(linkDetail.created_at)}</div>
|
|
||||||
</div>
|
|
||||||
<div className="hidden group-hover:block">
|
|
||||||
<CustomMenu placement="bottom-end" menuItemsClassName="z-20" closeOnSelect verticalEllipsis>
|
|
||||||
{MENU_ITEMS.map((item) => (
|
|
||||||
<CustomMenu.MenuItem
|
|
||||||
key={item.key}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
item.action();
|
|
||||||
}}
|
|
||||||
className={cn("flex items-center gap-2 w-full ", {
|
|
||||||
"text-custom-text-400": item.disabled,
|
|
||||||
})}
|
|
||||||
disabled={item.disabled}
|
|
||||||
>
|
|
||||||
{item.icon && <item.icon className={cn("h-3 w-3", item.iconClassName)} />}
|
|
||||||
<div>
|
|
||||||
<h5>{item.title}</h5>
|
|
||||||
{item.description && (
|
|
||||||
<p
|
|
||||||
className={cn("text-custom-text-300 whitespace-pre-line", {
|
|
||||||
"text-custom-text-400": item.disabled,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{item.description}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
))}
|
|
||||||
</CustomMenu>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,8 +3,9 @@
|
|||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
// hooks
|
// hooks
|
||||||
// ui
|
// ui
|
||||||
import { Pencil, Trash2, LinkIcon, ExternalLink } from "lucide-react";
|
import { Pencil, Trash2, ExternalLink } from "lucide-react";
|
||||||
import { Tooltip, TOAST_TYPE, setToast } from "@plane/ui";
|
import { Tooltip, TOAST_TYPE, setToast } from "@plane/ui";
|
||||||
|
import { getIconForLink } from "@plane/utils";
|
||||||
// icons
|
// icons
|
||||||
// types
|
// types
|
||||||
// helpers
|
// helpers
|
||||||
@@ -34,6 +35,8 @@ export const IssueLinkDetail: FC<TIssueLinkDetail> = (props) => {
|
|||||||
const linkDetail = getLinkById(linkId);
|
const linkDetail = getLinkById(linkId);
|
||||||
if (!linkDetail) return <></>;
|
if (!linkDetail) return <></>;
|
||||||
|
|
||||||
|
const Icon = getIconForLink(linkDetail.url);
|
||||||
|
|
||||||
const toggleIssueLinkModal = (modalToggle: boolean) => {
|
const toggleIssueLinkModal = (modalToggle: boolean) => {
|
||||||
toggleIssueLinkModalStore(modalToggle);
|
toggleIssueLinkModalStore(modalToggle);
|
||||||
setIssueLinkData(linkDetail);
|
setIssueLinkData(linkDetail);
|
||||||
@@ -57,7 +60,7 @@ export const IssueLinkDetail: FC<TIssueLinkDetail> = (props) => {
|
|||||||
>
|
>
|
||||||
<div className="flex items-start gap-2 truncate">
|
<div className="flex items-start gap-2 truncate">
|
||||||
<span className="py-1">
|
<span className="py-1">
|
||||||
<LinkIcon className="h-3 w-3 flex-shrink-0" />
|
<Icon className="size-3 stroke-2 text-custom-text-350 group-hover:text-custom-text-100 flex-shrink-0" />
|
||||||
</span>
|
</span>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
tooltipContent={linkDetail.title && linkDetail.title !== "" ? linkDetail.title : linkDetail.url}
|
tooltipContent={linkDetail.title && linkDetail.title !== "" ? linkDetail.title : linkDetail.url}
|
||||||
|
|||||||
@@ -2,14 +2,14 @@
|
|||||||
|
|
||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { Pencil, Trash2, LinkIcon, Copy } from "lucide-react";
|
import { Pencil, Trash2, Copy } from "lucide-react";
|
||||||
import { EIssueServiceType } from "@plane/constants";
|
import { EIssueServiceType } from "@plane/constants";
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
import { TIssueServiceType } from "@plane/types";
|
import { TIssueServiceType } from "@plane/types";
|
||||||
// ui
|
// ui
|
||||||
import { Tooltip, TOAST_TYPE, setToast, CustomMenu } from "@plane/ui";
|
import { Tooltip, TOAST_TYPE, setToast, CustomMenu } from "@plane/ui";
|
||||||
|
import { calculateTimeAgo, getIconForLink } from "@plane/utils";
|
||||||
// helpers
|
// helpers
|
||||||
import { calculateTimeAgoShort } from "@/helpers/date-time.helper";
|
|
||||||
import { copyTextToClipboard } from "@/helpers/string.helper";
|
import { copyTextToClipboard } from "@/helpers/string.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useIssueDetail } from "@/hooks/store";
|
import { useIssueDetail } from "@/hooks/store";
|
||||||
@@ -37,6 +37,8 @@ export const IssueLinkItem: FC<TIssueLinkItem> = observer((props) => {
|
|||||||
const linkDetail = getLinkById(linkId);
|
const linkDetail = getLinkById(linkId);
|
||||||
if (!linkDetail) return <></>;
|
if (!linkDetail) return <></>;
|
||||||
|
|
||||||
|
const Icon = getIconForLink(linkDetail.url);
|
||||||
|
|
||||||
const toggleIssueLinkModal = (modalToggle: boolean) => {
|
const toggleIssueLinkModal = (modalToggle: boolean) => {
|
||||||
toggleIssueLinkModalStore(modalToggle);
|
toggleIssueLinkModalStore(modalToggle);
|
||||||
setIssueLinkData(linkDetail);
|
setIssueLinkData(linkDetail);
|
||||||
@@ -48,7 +50,7 @@ export const IssueLinkItem: FC<TIssueLinkItem> = observer((props) => {
|
|||||||
className="group col-span-12 lg:col-span-6 xl:col-span-4 2xl:col-span-3 3xl:col-span-2 flex items-center justify-between gap-3 h-10 flex-shrink-0 px-3 bg-custom-background-90 hover:bg-custom-background-80 border-[0.5px] border-custom-border-200 rounded"
|
className="group col-span-12 lg:col-span-6 xl:col-span-4 2xl:col-span-3 3xl:col-span-2 flex items-center justify-between gap-3 h-10 flex-shrink-0 px-3 bg-custom-background-90 hover:bg-custom-background-80 border-[0.5px] border-custom-border-200 rounded"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2.5 truncate flex-grow">
|
<div className="flex items-center gap-2.5 truncate flex-grow">
|
||||||
<LinkIcon className="size-4 flex-shrink-0 text-custom-text-400 group-hover:text-custom-text-200" />
|
<Icon className="size-4 flex-shrink-0 stroke-2 text-custom-text-350 group-hover:text-custom-text-100" />
|
||||||
<Tooltip tooltipContent={linkDetail.url} isMobile={isMobile}>
|
<Tooltip tooltipContent={linkDetail.url} isMobile={isMobile}>
|
||||||
<a
|
<a
|
||||||
href={linkDetail.url}
|
href={linkDetail.url}
|
||||||
@@ -62,7 +64,7 @@ export const IssueLinkItem: FC<TIssueLinkItem> = observer((props) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1 flex-shrink-0">
|
<div className="flex items-center gap-1 flex-shrink-0">
|
||||||
<p className="p-1 text-xs align-bottom leading-5 text-custom-text-400 group-hover-text-custom-text-200">
|
<p className="p-1 text-xs align-bottom leading-5 text-custom-text-400 group-hover-text-custom-text-200">
|
||||||
{calculateTimeAgoShort(linkDetail.created_at)}
|
{calculateTimeAgo(linkDetail.created_at)}
|
||||||
</p>
|
</p>
|
||||||
<span
|
<span
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { Copy, LinkIcon, Pencil, Trash2 } from "lucide-react";
|
import { Copy, Pencil, Trash2 } from "lucide-react";
|
||||||
// plane types
|
// plane types
|
||||||
import { ILinkDetails } from "@plane/types";
|
import { ILinkDetails } from "@plane/types";
|
||||||
// plane ui
|
// plane ui
|
||||||
import { setToast, TOAST_TYPE, Tooltip } from "@plane/ui";
|
import { setToast, TOAST_TYPE, Tooltip } from "@plane/ui";
|
||||||
|
import { getIconForLink } from "@plane/utils";
|
||||||
// helpers
|
// helpers
|
||||||
import { calculateTimeAgo } from "@/helpers/date-time.helper";
|
import { calculateTimeAgo } from "@/helpers/date-time.helper";
|
||||||
import { copyTextToClipboard } from "@/helpers/string.helper";
|
import { copyTextToClipboard } from "@/helpers/string.helper";
|
||||||
@@ -27,6 +28,8 @@ export const ModulesLinksListItem: React.FC<Props> = observer((props) => {
|
|||||||
// platform os
|
// platform os
|
||||||
const { isMobile } = usePlatformOS();
|
const { isMobile } = usePlatformOS();
|
||||||
|
|
||||||
|
const Icon = getIconForLink(link.url);
|
||||||
|
|
||||||
const copyToClipboard = (text: string) => {
|
const copyToClipboard = (text: string) => {
|
||||||
copyTextToClipboard(text).then(() =>
|
copyTextToClipboard(text).then(() =>
|
||||||
setToast({
|
setToast({
|
||||||
@@ -42,7 +45,7 @@ export const ModulesLinksListItem: React.FC<Props> = observer((props) => {
|
|||||||
<div className="flex w-full items-start justify-between gap-2">
|
<div className="flex w-full items-start justify-between gap-2">
|
||||||
<div className="flex items-start gap-2 truncate">
|
<div className="flex items-start gap-2 truncate">
|
||||||
<span className="py-1">
|
<span className="py-1">
|
||||||
<LinkIcon className="h-3 w-3 flex-shrink-0" />
|
<Icon className="size-3 stroke-2 text-custom-text-350 group-hover:text-custom-text-100 flex-shrink-0" />
|
||||||
</span>
|
</span>
|
||||||
<Tooltip tooltipContent={link.title && link.title !== "" ? link.title : link.url} isMobile={isMobile}>
|
<Tooltip tooltipContent={link.title && link.title !== "" ? link.title : link.url} isMobile={isMobile}>
|
||||||
<a href={link.url} target="_blank" rel="noopener noreferrer" className="cursor-pointer truncate text-xs">
|
<a href={link.url} target="_blank" rel="noopener noreferrer" className="cursor-pointer truncate text-xs">
|
||||||
@@ -87,9 +90,8 @@ export const ModulesLinksListItem: React.FC<Props> = observer((props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-5">
|
<div className="px-5">
|
||||||
<p className="mt-0.5 stroke-[1.5] text-xs text-custom-text-300">
|
<p className="flex items-center gap-1.5 mt-0.5 stroke-[1.5] text-xs text-custom-text-300">
|
||||||
Added {calculateTimeAgo(link.created_at)}
|
Added {calculateTimeAgo(link.created_at)}{" "}
|
||||||
<br />
|
|
||||||
{createdByDetails && (
|
{createdByDetails && (
|
||||||
<>by {createdByDetails?.is_bot ? createdByDetails?.first_name + " Bot" : createdByDetails?.display_name}</>
|
<>by {createdByDetails?.is_bot ? createdByDetails?.first_name + " Bot" : createdByDetails?.display_name}</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user