forked from github/plane
fix: minor bugs and ux improvements (#322)
* fix: ellipsis added to issue title * feat: toolttip added * feat: assignees tooltip added * fix: build fix * fix: build fix * fix: build error * fix: minor bugs and ux improvements --------- Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia@caravel.tech>
This commit is contained in:
parent
702cfeb4ee
commit
92f717962c
@ -165,19 +165,15 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
const handleCopyText = () => {
|
||||
const originURL =
|
||||
typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||
copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`)
|
||||
.then(() => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Issue link copied to clipboard",
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Some error occurred",
|
||||
});
|
||||
copyTextToClipboard(
|
||||
`${originURL}/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`
|
||||
).then(() => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Link Copied!",
|
||||
message: "Issue link copied to clipboard.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@ -201,14 +197,14 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
<div className="absolute top-1.5 right-1.5 z-10 opacity-0 group-hover/card:opacity-100">
|
||||
{type && !isNotAllowed && (
|
||||
<CustomMenu width="auto" ellipsis>
|
||||
<CustomMenu.MenuItem onClick={editIssue}>Edit</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={editIssue}>Edit issue</CustomMenu.MenuItem>
|
||||
{type !== "issue" && removeIssue && (
|
||||
<CustomMenu.MenuItem onClick={removeIssue}>
|
||||
<>Remove from {type}</>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
<CustomMenu.MenuItem onClick={() => handleDeleteIssue(issue)}>
|
||||
Delete permanently
|
||||
Delete issue
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={handleCopyText}>Copy issue link</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
@ -236,7 +232,6 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
isNotAllowed={isNotAllowed}
|
||||
position="left"
|
||||
/>
|
||||
)}
|
||||
{properties.state && selectedGroup !== "state_detail.name" && (
|
||||
@ -258,6 +253,24 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
{issue.sub_issues_count} {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
|
||||
</div>
|
||||
)}
|
||||
{properties.labels && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{issue.label_details.map((label) => (
|
||||
<span
|
||||
key={label.id}
|
||||
className="group flex items-center gap-1 rounded-2xl border px-2 py-0.5 text-xs"
|
||||
>
|
||||
<span
|
||||
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: label?.color && label.color !== "" ? label.color : "#000",
|
||||
}}
|
||||
/>
|
||||
{label.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{properties.assignee && (
|
||||
<ViewAssigneeSelect
|
||||
issue={issue}
|
||||
|
@ -82,21 +82,6 @@ export const LinkModal: React.FC<Props> = ({ isOpen, handleClose, onFormSubmit }
|
||||
Add Link
|
||||
</Dialog.Title>
|
||||
<div className="mt-2 space-y-3">
|
||||
<div>
|
||||
<Input
|
||||
id="title"
|
||||
label="Title"
|
||||
name="title"
|
||||
type="text"
|
||||
placeholder="Enter title"
|
||||
autoComplete="off"
|
||||
error={errors.title}
|
||||
register={register}
|
||||
validations={{
|
||||
required: "Title is required",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Input
|
||||
id="url"
|
||||
@ -112,6 +97,21 @@ export const LinkModal: React.FC<Props> = ({ isOpen, handleClose, onFormSubmit }
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Input
|
||||
id="title"
|
||||
label="Title"
|
||||
name="title"
|
||||
type="text"
|
||||
placeholder="Enter title"
|
||||
autoComplete="off"
|
||||
error={errors.title}
|
||||
register={register}
|
||||
validations={{
|
||||
required: "Title is required",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -124,19 +124,15 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
const handleCopyText = () => {
|
||||
const originURL =
|
||||
typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||
copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`)
|
||||
.then(() => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Issue link copied to clipboard",
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Some error occurred",
|
||||
});
|
||||
copyTextToClipboard(
|
||||
`${originURL}/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`
|
||||
).then(() => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Link Copied!",
|
||||
message: "Issue link copied to clipboard.",
|
||||
});
|
||||
});
|
||||
};
|
||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
||||
|
||||
@ -196,6 +192,24 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
{issue.sub_issues_count} {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
|
||||
</div>
|
||||
)}
|
||||
{properties.labels && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{issue.label_details.map((label) => (
|
||||
<span
|
||||
key={label.id}
|
||||
className="group flex items-center gap-1 rounded-2xl border px-2 py-0.5 text-xs"
|
||||
>
|
||||
<span
|
||||
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: label?.color && label.color !== "" ? label.color : "#000",
|
||||
}}
|
||||
/>
|
||||
{label.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{properties.assignee && (
|
||||
<ViewAssigneeSelect
|
||||
issue={issue}
|
||||
@ -205,14 +219,14 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
)}
|
||||
{type && !isNotAllowed && (
|
||||
<CustomMenu width="auto" ellipsis>
|
||||
<CustomMenu.MenuItem onClick={editIssue}>Edit</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={editIssue}>Edit issue</CustomMenu.MenuItem>
|
||||
{type !== "issue" && removeIssue && (
|
||||
<CustomMenu.MenuItem onClick={removeIssue}>
|
||||
<>Remove from {type}</>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
<CustomMenu.MenuItem onClick={() => handleDeleteIssue(issue)}>
|
||||
Delete permanently
|
||||
Delete issue
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={handleCopyText}>Copy issue link</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
|
@ -2,6 +2,7 @@ import Link from "next/link";
|
||||
|
||||
// icons
|
||||
import { LinkIcon, TrashIcon } from "@heroicons/react/24/outline";
|
||||
import { ExternalLinkIcon } from "components/icons";
|
||||
// helpers
|
||||
import { timeAgo } from "helpers/date-time.helper";
|
||||
// types
|
||||
@ -26,9 +27,17 @@ export const LinksList: React.FC<Props> = ({ links, handleDeleteLink, userAuth }
|
||||
return (
|
||||
<>
|
||||
{links.map((link) => (
|
||||
<div key={link.id} className="group relative">
|
||||
<div key={link.id} className="relative">
|
||||
{!isNotAllowed && (
|
||||
<div className="absolute top-1.5 right-1.5 z-10 opacity-0 group-hover:opacity-100">
|
||||
<div className="absolute top-1.5 right-1.5 z-10 flex items-center gap-1">
|
||||
<Link href={link.url}>
|
||||
<a
|
||||
className="grid h-7 w-7 place-items-center rounded bg-gray-100 p-1 outline-none"
|
||||
target="_blank"
|
||||
>
|
||||
<ExternalLinkIcon width="14" height="14" />
|
||||
</a>
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
className="grid h-7 w-7 place-items-center rounded bg-gray-100 p-1 text-red-500 outline-none duration-300 hover:bg-red-50"
|
||||
@ -38,16 +47,18 @@ export const LinksList: React.FC<Props> = ({ links, handleDeleteLink, userAuth }
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<Link href={link.url} target="_blank">
|
||||
<a className="group relative flex gap-2 rounded-md border bg-gray-100 p-2">
|
||||
<Link href={link.url}>
|
||||
<a className="relative flex gap-2 rounded-md border bg-gray-50 p-2" target="_blank">
|
||||
<div className="mt-0.5">
|
||||
<LinkIcon className="h-3.5 w-3.5" />
|
||||
</div>
|
||||
<div>
|
||||
<h5>{link.title}</h5>
|
||||
{/* <p className="mt-0.5 text-gray-500">
|
||||
Added {timeAgo(link.created_at)} ago by {link.created_by_detail.email}
|
||||
</p> */}
|
||||
<h5 className="w-4/5">{link.title}</h5>
|
||||
<p className="mt-0.5 text-gray-500">
|
||||
Added {timeAgo(link.created_at)}
|
||||
<br />
|
||||
by {link.created_by_detail.email}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
|
@ -70,19 +70,16 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = (props) => {
|
||||
const handleCopyText = () => {
|
||||
const originURL =
|
||||
typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||
copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`)
|
||||
.then(() => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Cycle link copied to clipboard",
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Some error occurred",
|
||||
});
|
||||
|
||||
copyTextToClipboard(
|
||||
`${originURL}/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`
|
||||
).then(() => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Link Copied!",
|
||||
message: "Cycle link copied to clipboard.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
@ -99,11 +96,9 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = (props) => {
|
||||
</a>
|
||||
</Link>
|
||||
<CustomMenu width="auto" ellipsis>
|
||||
<CustomMenu.MenuItem onClick={handleCopyText}>Copy cycle link</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={handleEditCycle}>Edit cycle</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={handleDeleteCycle}>
|
||||
Delete cycle permanently
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={handleDeleteCycle}>Delete cycle</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={handleCopyText}>Copy cycle link</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-x-2 gap-y-3 text-xs">
|
||||
|
38
apps/app/components/icons/external-link-icon.tsx
Normal file
38
apps/app/components/icons/external-link-icon.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import React from "react";
|
||||
|
||||
import type { Props } from "./types";
|
||||
|
||||
export const ExternalLinkIcon: React.FC<Props> = ({
|
||||
width = "24",
|
||||
height = "24",
|
||||
className,
|
||||
color = "black",
|
||||
}) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
className={className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 122.6 122.88"
|
||||
>
|
||||
<g>
|
||||
<path
|
||||
d="M110.6,72.58c0-3.19,2.59-5.78,5.78-5.78c3.19,0,5.78,2.59,5.78,5.78v33.19c0,4.71-1.92,8.99-5.02,12.09 c-3.1,3.1-7.38,5.02-12.09,5.02H17.11c-4.71,0-8.99-1.92-12.09-5.02c-3.1-3.1-5.02-7.38-5.02-12.09V17.19 C0,12.48,1.92,8.2,5.02,5.1C8.12,2,12.4,0.08,17.11,0.08h32.98c3.19,0,5.78,2.59,5.78,5.78c0,3.19-2.59,5.78-5.78,5.78H17.11 c-1.52,0-2.9,0.63-3.91,1.63c-1.01,1.01-1.63,2.39-1.63,3.91v88.58c0,1.52,0.63,2.9,1.63,3.91c1.01,1.01,2.39,1.63,3.91,1.63h87.95 c1.52,0,2.9-0.63,3.91-1.63s1.63-2.39,1.63-3.91V72.58L110.6,72.58z M112.42,17.46L54.01,76.6c-2.23,2.27-5.89,2.3-8.16,0.07 c-2.27-2.23-2.3-5.89-0.07-8.16l56.16-56.87H78.56c-3.19,0-5.78-2.59-5.78-5.78c0-3.19,2.59-5.78,5.78-5.78h26.5 c5.12,0,11.72-0.87,15.65,3.1c2.48,2.51,1.93,22.52,1.61,34.11c-0.08,3-0.15,5.29-0.15,6.93c0,3.19-2.59,5.78-5.78,5.78 c-3.19,0-5.78-2.59-5.78-5.78c0-0.31,0.08-3.32,0.19-7.24C110.96,30.94,111.93,22.94,112.42,17.46L112.42,17.46z"
|
||||
fill={color}
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
// <svg
|
||||
// width={width}
|
||||
// height={height}
|
||||
// className={className}
|
||||
// viewBox="0 0 24 24"
|
||||
// fill="none"
|
||||
// xmlns="http://www.w3.org/2000/svg"
|
||||
// >
|
||||
// <path
|
||||
// d="M5.2 13.1998C4.86667 13.1998 4.58333 13.0831 4.35 12.8498C4.11667 12.6165 4 12.3331 4 11.9998C4 11.6665 4.11667 11.3831 4.35 11.1498C4.58333 10.9165 4.86667 10.7998 5.2 10.7998C5.53333 10.7998 5.81667 10.9165 6.05 11.1498C6.28333 11.3831 6.4 11.6665 6.4 11.9998C6.4 12.3331 6.28333 12.6165 6.05 12.8498C5.81667 13.0831 5.53333 13.1998 5.2 13.1998ZM12 13.1998C11.6667 13.1998 11.3833 13.0831 11.15 12.8498C10.9167 12.6165 10.8 12.3331 10.8 11.9998C10.8 11.6665 10.9167 11.3831 11.15 11.1498C11.3833 10.9165 11.6667 10.7998 12 10.7998C12.3333 10.7998 12.6167 10.9165 12.85 11.1498C13.0833 11.3831 13.2 11.6665 13.2 11.9998C13.2 12.3331 13.0833 12.6165 12.85 12.8498C12.6167 13.0831 12.3333 13.1998 12 13.1998ZM18.8 13.1998C18.4667 13.1998 18.1833 13.0831 17.95 12.8498C17.7167 12.6165 17.6 12.3331 17.6 11.9998C17.6 11.6665 17.7167 11.3831 17.95 11.1498C18.1833 10.9165 18.4667 10.7998 18.8 10.7998C19.1333 10.7998 19.4167 10.9165 19.65 11.1498C19.8833 11.3831 20 11.6665 20 11.9998C20 12.3331 19.8833 12.6165 19.65 12.8498C19.4167 13.0831 19.1333 13.1998 18.8 13.1998Z"
|
||||
// fill="black"
|
||||
// />
|
||||
// </svg>
|
||||
);
|
@ -1,19 +1,26 @@
|
||||
export * from "./attachment-icon";
|
||||
export * from "./blocked-icon";
|
||||
export * from "./blocker-icon";
|
||||
export * from "./bolt-icon";
|
||||
export * from "./calendar-month-icon";
|
||||
export * from "./cancel-icon";
|
||||
export * from "./clipboard-icon";
|
||||
export * from "./comment-icon";
|
||||
export * from "./completed-cycle-icon";
|
||||
export * from "./current-cycle-icon";
|
||||
export * from "./cycle-icon";
|
||||
export * from "./discord-icon";
|
||||
export * from "./document-icon";
|
||||
export * from "./edit-icon";
|
||||
export * from "./ellipsis-horizontal-icon";
|
||||
export * from "./external-link-icon";
|
||||
export * from "./github-icon";
|
||||
export * from "./heartbeat-icon";
|
||||
export * from "./layer-diagonal-icon";
|
||||
export * from "./lock-icon";
|
||||
export * from "./menu-icon";
|
||||
export * from "./plus-icon";
|
||||
export * from "./question-mark-circle-icon";
|
||||
export * from "./setting-icon";
|
||||
export * from "./signal-cellular-icon";
|
||||
export * from "./tag-icon";
|
||||
@ -22,9 +29,3 @@ export * from "./upcoming-cycle-icon";
|
||||
export * from "./user-group-icon";
|
||||
export * from "./user-icon-circle";
|
||||
export * from "./user-icon";
|
||||
export * from "./question-mark-circle-icon";
|
||||
export * from "./bolt-icon";
|
||||
export * from "./document-icon";
|
||||
export * from "./discord-icon";
|
||||
export * from "./github-icon";
|
||||
export * from "./comment-icon";
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { FC, useCallback, useEffect, useMemo } from "react";
|
||||
import { FC, useCallback, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
@ -18,7 +18,6 @@ const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor
|
||||
});
|
||||
// types
|
||||
import { IIssue, UserAuth } from "types";
|
||||
import useToast from "hooks/use-toast";
|
||||
|
||||
export interface IssueDescriptionFormValues {
|
||||
name: string;
|
||||
@ -37,7 +36,7 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
|
||||
handleFormSubmit,
|
||||
userAuth,
|
||||
}) => {
|
||||
const { setToastAlert } = useToast();
|
||||
const [characterLimit, setCharacterLimit] = useState(false);
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
@ -55,23 +54,7 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
|
||||
|
||||
const handleDescriptionFormSubmit = useCallback(
|
||||
(formData: Partial<IIssue>) => {
|
||||
if (!formData.name || formData.name === "") {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error in saving!",
|
||||
message: "Title is required.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (formData.name.length > 255) {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error in saving!",
|
||||
message: "Title cannot have more than 255 characters.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!formData.name || formData.name.length === 0 || formData.name.length > 255) return;
|
||||
|
||||
handleFormSubmit({
|
||||
name: formData.name ?? "",
|
||||
@ -79,7 +62,7 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
|
||||
description_html: formData.description_html ?? "<p></p>",
|
||||
});
|
||||
},
|
||||
[handleFormSubmit, setToastAlert]
|
||||
[handleFormSubmit]
|
||||
);
|
||||
|
||||
const debounceHandler = useMemo(
|
||||
@ -105,21 +88,37 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<TextArea
|
||||
id="name"
|
||||
placeholder="Enter issue name"
|
||||
name="name"
|
||||
value={watch("name")}
|
||||
onChange={(e) => {
|
||||
setValue("name", e.target.value);
|
||||
debounceHandler();
|
||||
}}
|
||||
required={true}
|
||||
className="block px-3 py-2 text-xl
|
||||
<div className="relative">
|
||||
<TextArea
|
||||
id="name"
|
||||
name="name"
|
||||
placeholder="Enter issue name"
|
||||
value={watch("name")}
|
||||
onFocus={() => setCharacterLimit(true)}
|
||||
onBlur={() => setCharacterLimit(false)}
|
||||
onChange={(e) => {
|
||||
setValue("name", e.target.value);
|
||||
debounceHandler();
|
||||
}}
|
||||
required={true}
|
||||
className="block px-3 py-2 text-xl
|
||||
w-full overflow-hidden resize-none min-h-10
|
||||
rounded border-none bg-transparent ring-0 focus:ring-1 focus:ring-theme outline-none "
|
||||
role="textbox "
|
||||
/>
|
||||
rounded border-none bg-transparent ring-0 focus:ring-1 focus:ring-theme outline-none"
|
||||
role="textbox"
|
||||
/>
|
||||
{characterLimit && (
|
||||
<div className="absolute bottom-0 right-0 text-xs bg-white p-1 rounded pointer-events-none z-[2]">
|
||||
<span
|
||||
className={`${
|
||||
watch("name").length === 0 || watch("name").length > 255 ? "text-red-500" : ""
|
||||
}`}
|
||||
>
|
||||
{watch("name").length}
|
||||
</span>
|
||||
/255
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<span>{errors.name ? errors.name.message : null}</span>
|
||||
<RemirrorRichTextEditor
|
||||
value={watch("description")}
|
||||
|
@ -45,6 +45,9 @@ const defaultValues: Partial<IIssue> = {
|
||||
state: "",
|
||||
cycle: null,
|
||||
priority: null,
|
||||
assignees: [],
|
||||
assignees_list: [],
|
||||
labels: [],
|
||||
labels_list: [],
|
||||
};
|
||||
|
||||
@ -89,6 +92,7 @@ export const IssueForm: FC<IssueFormProps> = ({
|
||||
watch,
|
||||
control,
|
||||
setValue,
|
||||
setFocus,
|
||||
} = useForm<IIssue>({
|
||||
defaultValues,
|
||||
mode: "all",
|
||||
@ -113,12 +117,14 @@ export const IssueForm: FC<IssueFormProps> = ({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setFocus("name");
|
||||
|
||||
reset({
|
||||
...defaultValues,
|
||||
...initialData,
|
||||
project: projectId,
|
||||
});
|
||||
}, [initialData, reset, projectId]);
|
||||
}, [setFocus, initialData, reset, projectId]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -204,13 +210,13 @@ export const IssueForm: FC<IssueFormProps> = ({
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p className="text-sm text-gray-500">
|
||||
<Link
|
||||
href={`/${workspaceSlug}/projects/${projectId}/issues/${mostSimilarIssue}`}
|
||||
href={`/${workspaceSlug}/projects/${projectId}/issues/${mostSimilarIssue.id}`}
|
||||
>
|
||||
<a target="_blank" type="button" className="inline text-left">
|
||||
<span>Did you mean </span>
|
||||
<span className="italic">
|
||||
{mostSimilarIssue?.project_detail.identifier}-
|
||||
{mostSimilarIssue?.sequence_id}: {mostSimilarIssue?.name}{" "}
|
||||
{mostSimilarIssue.project_detail.identifier}-
|
||||
{mostSimilarIssue.sequence_id}: {mostSimilarIssue.name}{" "}
|
||||
</span>
|
||||
?
|
||||
</a>
|
||||
@ -363,7 +369,7 @@ export const IssueForm: FC<IssueFormProps> = ({
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button theme="secondary" onClick={handleClose}>
|
||||
<Button type="button" theme="secondary" onClick={handleClose}>
|
||||
Discard
|
||||
</Button>
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
|
@ -115,6 +115,24 @@ export const MyIssuesListItem: React.FC<Props> = ({
|
||||
{issue?.sub_issues_count} {issue?.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
|
||||
</div>
|
||||
)}
|
||||
{properties.labels && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{issue.label_details.map((label) => (
|
||||
<span
|
||||
key={label.id}
|
||||
className="group flex items-center gap-1 rounded-2xl border px-2 py-0.5 text-xs"
|
||||
>
|
||||
<span
|
||||
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: label?.color && label.color !== "" ? label.color : "#000",
|
||||
}}
|
||||
/>
|
||||
{label.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{properties.assignee && (
|
||||
<div className="flex items-center gap-1">
|
||||
<AssigneesList userIds={issue.assignees ?? []} />
|
||||
|
@ -188,6 +188,21 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
||||
});
|
||||
};
|
||||
|
||||
const handleCopyText = () => {
|
||||
const originURL =
|
||||
typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||
|
||||
copyTextToClipboard(
|
||||
`${originURL}/${workspaceSlug}/projects/${projectId}/issues/${issueDetail?.id}`
|
||||
).then(() => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Link Copied!",
|
||||
message: "Issue link copied to clipboard.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!createLabelForm) return;
|
||||
|
||||
@ -217,23 +232,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-md border p-2 shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
|
||||
onClick={() =>
|
||||
copyTextToClipboard(
|
||||
`https://app.plane.so/${workspaceSlug}/projects/${issueDetail?.project_detail?.id}/issues/${issueDetail?.id}`
|
||||
)
|
||||
.then(() => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Issue link copied to clipboard",
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Some error occurred",
|
||||
});
|
||||
})
|
||||
}
|
||||
onClick={handleCopyText}
|
||||
>
|
||||
<LinkIcon className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
@ -373,7 +372,10 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
||||
>
|
||||
<span
|
||||
className="h-2 w-2 flex-shrink-0 rounded-full"
|
||||
style={{ backgroundColor: label?.color ?? "black" }}
|
||||
style={{
|
||||
backgroundColor:
|
||||
label?.color && label.color !== "" ? label.color : "#000",
|
||||
}}
|
||||
/>
|
||||
{label.name}
|
||||
<XMarkIcon className="h-2 w-2 group-hover:text-red-500" />
|
||||
|
@ -203,12 +203,12 @@ export const SubIssuesList: FC<Props> = ({ parentIssue, userAuth }) => {
|
||||
>
|
||||
<Disclosure.Panel className="mt-3 flex flex-col gap-y-1">
|
||||
{subIssues.map((issue) => (
|
||||
<div
|
||||
<Link
|
||||
key={issue.id}
|
||||
className="group flex items-center justify-between gap-2 rounded p-2 hover:bg-gray-100"
|
||||
href={`/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`}
|
||||
>
|
||||
<Link href={`/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`}>
|
||||
<a className="flex items-center gap-2 rounded text-xs">
|
||||
<a className="group flex items-center justify-between gap-2 rounded p-2 hover:bg-gray-100">
|
||||
<div className="flex items-center gap-2 rounded text-xs">
|
||||
<span
|
||||
className="block flex-shrink-0 h-1.5 w-1.5 rounded-full"
|
||||
style={{
|
||||
@ -219,18 +219,23 @@ export const SubIssuesList: FC<Props> = ({ parentIssue, userAuth }) => {
|
||||
{issue.project_detail.identifier}-{issue.sequence_id}
|
||||
</span>
|
||||
<span className="max-w-sm break-all font-medium">{issue.name}</span>
|
||||
</a>
|
||||
</Link>
|
||||
{!isNotAllowed && (
|
||||
<button
|
||||
type="button"
|
||||
className="opacity-0 group-hover:opacity-100 cursor-pointer"
|
||||
onClick={() => handleSubIssueRemove(issue.id)}
|
||||
>
|
||||
<XMarkIcon className="h-4 w-4 text-gray-500 hover:text-gray-900" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isNotAllowed && (
|
||||
<button
|
||||
type="button"
|
||||
className="opacity-0 group-hover:opacity-100 cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleSubIssueRemove(issue.id);
|
||||
}}
|
||||
>
|
||||
<XMarkIcon className="h-4 w-4 text-gray-500 hover:text-gray-900" />
|
||||
</button>
|
||||
)}
|
||||
</a>
|
||||
</Link>
|
||||
))}
|
||||
</Disclosure.Panel>
|
||||
</Transition>
|
||||
|
@ -57,14 +57,14 @@ export const ViewAssigneeSelect: React.FC<Props> = ({
|
||||
<div>
|
||||
<Listbox.Button>
|
||||
<Tooltip
|
||||
tooltipHeading="Assignee"
|
||||
tooltipHeading="Assignees"
|
||||
tooltipContent={
|
||||
issue.assignee_details.length > 0
|
||||
? issue.assignee_details
|
||||
.map((assingee) =>
|
||||
assingee.first_name !== "" ? assingee.first_name : assingee.email
|
||||
.map((assignee) =>
|
||||
assignee?.first_name !== "" ? assignee?.first_name : assignee?.email
|
||||
)
|
||||
.toString()
|
||||
.join(", ")
|
||||
: "No Assignee"
|
||||
}
|
||||
>
|
||||
|
@ -202,7 +202,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-center items-center gap-2 rounded-md border bg-transparent h-full p-2 px-4 text-xs font-medium text-gray-900 hover:bg-gray-100 hover:text-gray-900 focus:outline-none">
|
||||
<Popover className="flex justify-center items-center relative rounded-lg">
|
||||
<Popover className="flex justify-center items-center relative rounded-lg">
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Popover.Button
|
||||
@ -372,7 +372,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center w-full gap-2 ">
|
||||
<div className="flex flex-col items-center justify-center w-full gap-2">
|
||||
{isStartValid && isEndValid ? (
|
||||
<ProgressChart
|
||||
issues={issues}
|
||||
|
@ -8,7 +8,7 @@ import { DeleteModuleModal } from "components/modules";
|
||||
// ui
|
||||
import { AssigneesList, Avatar, CustomMenu } from "components/ui";
|
||||
// icons
|
||||
import { CalendarDaysIcon, TrashIcon } from "@heroicons/react/24/outline";
|
||||
import { CalendarDaysIcon } from "@heroicons/react/24/outline";
|
||||
// helpers
|
||||
import { renderShortNumericDateFormat } from "helpers/date-time.helper";
|
||||
// types
|
||||
@ -39,19 +39,16 @@ export const SingleModuleCard: React.FC<Props> = ({ module, handleEditModule })
|
||||
const handleCopyText = () => {
|
||||
const originURL =
|
||||
typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||
copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/modules/${module.id}`)
|
||||
.then(() => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Module link copied to clipboard",
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Some error occurred",
|
||||
});
|
||||
|
||||
copyTextToClipboard(
|
||||
`${originURL}/${workspaceSlug}/projects/${projectId}/modules/${module.id}`
|
||||
).then(() => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Link Copied!",
|
||||
message: "Module link copied to clipboard.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
@ -64,11 +61,9 @@ export const SingleModuleCard: React.FC<Props> = ({ module, handleEditModule })
|
||||
<div className="group/card h-full w-full relative select-none p-2">
|
||||
<div className="absolute top-4 right-4 ">
|
||||
<CustomMenu width="auto" ellipsis>
|
||||
<CustomMenu.MenuItem onClick={handleCopyText}>Copy module link</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={handleEditModule}>Edit module</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={handleDeleteModule}>
|
||||
Delete module permanently
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={handleDeleteModule}>Delete module</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={handleCopyText}>Copy module link</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
</div>
|
||||
<Link href={`/${workspaceSlug}/projects/${module.project}/modules/${module.id}`}>
|
||||
|
@ -66,6 +66,18 @@ export const ProjectSidebarList: FC = () => {
|
||||
() => (workspaceSlug ? projectService.getProjects(workspaceSlug as string) : null)
|
||||
);
|
||||
|
||||
const handleCopyText = (projectId: string) => {
|
||||
const originURL =
|
||||
typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||
copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/issues`).then(() => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Link Copied!",
|
||||
message: "Project link copied to clipboard.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateProjectModal isOpen={isCreateProjectModal} setIsOpen={setCreateProjectModal} />
|
||||
@ -112,20 +124,8 @@ export const ProjectSidebarList: FC = () => {
|
||||
</Disclosure.Button>
|
||||
{!sidebarCollapse && (
|
||||
<CustomMenu ellipsis>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() =>
|
||||
copyTextToClipboard(
|
||||
`https://app.plane.so/${workspaceSlug}/projects/${project?.id}/issues/`
|
||||
).then(() => {
|
||||
setToastAlert({
|
||||
title: "Link Copied",
|
||||
message: "Link copied to clipboard",
|
||||
type: "success",
|
||||
});
|
||||
})
|
||||
}
|
||||
>
|
||||
Copy link
|
||||
<CustomMenu.MenuItem onClick={() => handleCopyText(project.id)}>
|
||||
Copy project link
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
)}
|
||||
|
@ -4,14 +4,13 @@ import {
|
||||
ToggleItalicButton,
|
||||
ToggleUnderlineButton,
|
||||
ToggleStrikeButton,
|
||||
ToggleOrderedListButton,
|
||||
ToggleBulletListButton,
|
||||
RedoButton,
|
||||
UndoButton,
|
||||
} from "@remirror/react";
|
||||
// headings
|
||||
import HeadingControls from "./heading-controls";
|
||||
// list
|
||||
import { OrderedListButton } from "./ordered-list";
|
||||
import { UnorderedListButton } from "./unordered-list";
|
||||
|
||||
export const RichTextToolbar: React.FC = () => (
|
||||
<div className="flex items-center gap-y-2 divide-x">
|
||||
@ -29,11 +28,8 @@ export const RichTextToolbar: React.FC = () => (
|
||||
<ToggleStrikeButton />
|
||||
</div>
|
||||
<div className="flex items-center gap-x-1 px-2">
|
||||
<OrderedListButton />
|
||||
<UnorderedListButton />
|
||||
<ToggleOrderedListButton />
|
||||
<ToggleBulletListButton />
|
||||
</div>
|
||||
{/* <div className="flex items-center gap-x-1 px-2">
|
||||
<LinkButton />
|
||||
</div> */}
|
||||
</div>
|
||||
);
|
||||
|
@ -138,8 +138,10 @@ const DelayAutoFocusInput = ({ autoFocus, ...rest }: HTMLProps<HTMLInputElement>
|
||||
export const FloatingLinkToolbar = () => {
|
||||
const { isEditing, linkPositioner, clickEdit, onRemove, submitHref, href, setHref, cancelHref } =
|
||||
useFloatingLinkState();
|
||||
|
||||
const active = useActive();
|
||||
const activeLink = active.link();
|
||||
|
||||
const { empty } = useCurrentSelection();
|
||||
|
||||
const handleClickEdit = useCallback(() => {
|
||||
@ -148,6 +150,14 @@ export const FloatingLinkToolbar = () => {
|
||||
|
||||
const linkEditButtons = activeLink ? (
|
||||
<>
|
||||
<CommandButton
|
||||
commandName="openLink"
|
||||
onSelect={() => {
|
||||
window.open(href, "_blank");
|
||||
}}
|
||||
icon="externalLinkFill"
|
||||
enabled
|
||||
/>
|
||||
<CommandButton
|
||||
commandName="updateLink"
|
||||
onSelect={handleClickEdit}
|
||||
@ -164,7 +174,9 @@ export const FloatingLinkToolbar = () => {
|
||||
<>
|
||||
{!isEditing && <FloatingToolbar>{linkEditButtons}</FloatingToolbar>}
|
||||
{!isEditing && empty && (
|
||||
<FloatingToolbar positioner={linkPositioner}>{linkEditButtons}</FloatingToolbar>
|
||||
<FloatingToolbar positioner={linkPositioner} className="shadow-lg rounded bg-white p-1">
|
||||
{linkEditButtons}
|
||||
</FloatingToolbar>
|
||||
)}
|
||||
|
||||
<FloatingWrapper
|
||||
|
@ -1,28 +0,0 @@
|
||||
import { useCommands, useActive } from "@remirror/react";
|
||||
|
||||
export const OrderedListButton = () => {
|
||||
const { toggleOrderedList, focus } = useCommands();
|
||||
|
||||
const active = useActive();
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
toggleOrderedList();
|
||||
focus();
|
||||
}}
|
||||
className={`${active.orderedList() ? "bg-gray-200" : "hover:bg-gray-100"} rounded p-1`}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 48 48"
|
||||
fill="black"
|
||||
>
|
||||
<path d="M6 40v-1.7h4.2V37H8.1v-1.7h2.1V34H6v-1.7h5.9V40Zm10.45-2.45v-3H42v3ZM6 27.85v-1.6l3.75-4.4H6v-1.7h5.9v1.6l-3.8 4.4h3.8v1.7Zm10.45-2.45v-3H42v3ZM8.1 15.8V9.7H6V8h3.8v7.8Zm8.35-2.55v-3H42v3Z" />
|
||||
</svg>
|
||||
</button>
|
||||
);
|
||||
};
|
@ -1,28 +0,0 @@
|
||||
import { useCommands, useActive } from "@remirror/react";
|
||||
|
||||
export const UnorderedListButton = () => {
|
||||
const { toggleBulletList, focus } = useCommands();
|
||||
|
||||
const active = useActive();
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
toggleBulletList();
|
||||
focus();
|
||||
}}
|
||||
className={`${active.bulletList() ? "bg-gray-200" : "hover:bg-gray-100"} rounded p-1`}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height="18"
|
||||
width="18"
|
||||
fill="black"
|
||||
viewBox="0 0 48 48"
|
||||
>
|
||||
<path d="M8.55 39q-1.05 0-1.8-.725T6 36.55q0-1.05.75-1.8t1.8-.75q1 0 1.725.75.725.75.725 1.8 0 1-.725 1.725Q9.55 39 8.55 39ZM16 38v-3h26v3ZM8.55 26.5q-1.05 0-1.8-.725T6 24q0-1.05.75-1.775.75-.725 1.8-.725 1 0 1.725.75Q11 23 11 24t-.725 1.75q-.725.75-1.725.75Zm7.45-1v-3h26v3ZM8.5 14q-1.05 0-1.775-.725Q6 12.55 6 11.5q0-1.05.725-1.775Q7.45 9 8.5 9q1.05 0 1.775.725Q11 10.45 11 11.5q0 1.05-.725 1.775Q9.55 14 8.5 14Zm7.5-1v-3h26v3Z" />
|
||||
</svg>
|
||||
</button>
|
||||
);
|
||||
};
|
@ -136,11 +136,7 @@ export const SingleState: React.FC<Props> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`group flex items-center justify-between gap-2 border-b bg-gray-50 p-3 ${
|
||||
activeGroup !== state.group ? "last:border-0" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="group flex items-center justify-between gap-2 bg-gray-50 p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="h-3 w-3 flex-shrink-0 rounded-full"
|
||||
|
@ -8,12 +8,12 @@ import useUser from "hooks/use-user";
|
||||
import { IssuePriorities, Properties } from "types";
|
||||
|
||||
const initialValues: Properties = {
|
||||
key: true,
|
||||
state: true,
|
||||
assignee: true,
|
||||
priority: false,
|
||||
due_date: false,
|
||||
// cycle: false,
|
||||
key: true,
|
||||
labels: false,
|
||||
priority: false,
|
||||
state: true,
|
||||
sub_issue_count: false,
|
||||
};
|
||||
|
||||
@ -83,12 +83,12 @@ const useIssuesProperties = (workspaceSlug?: string, projectId?: string) => {
|
||||
);
|
||||
|
||||
const newProperties: Properties = {
|
||||
key: properties.key,
|
||||
state: properties.state,
|
||||
assignee: properties.assignee,
|
||||
priority: properties.priority,
|
||||
due_date: properties.due_date,
|
||||
// cycle: properties.cycle,
|
||||
key: properties.key,
|
||||
labels: properties.labels,
|
||||
priority: properties.priority,
|
||||
state: properties.state,
|
||||
sub_issue_count: properties.sub_issue_count,
|
||||
};
|
||||
|
||||
|
@ -17,12 +17,12 @@ import { STATE_LIST } from "constants/fetch-keys";
|
||||
import { PRIORITIES } from "constants/project";
|
||||
|
||||
const initialValues: Properties = {
|
||||
key: true,
|
||||
state: true,
|
||||
assignee: true,
|
||||
priority: false,
|
||||
due_date: false,
|
||||
// cycle: false,
|
||||
key: true,
|
||||
labels: true,
|
||||
priority: false,
|
||||
state: true,
|
||||
sub_issue_count: false,
|
||||
};
|
||||
|
||||
|
@ -99,7 +99,7 @@ const StatesSettings: NextPage<UserAuth> = (props) => {
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-1 rounded-xl border p-1 md:w-2/3">
|
||||
<div className="space-y-1 rounded-xl border divide-y p-1 md:w-2/3">
|
||||
{key === activeGroup && (
|
||||
<CreateUpdateStateInline
|
||||
onClose={() => {
|
||||
|
8
apps/app/types/issues.d.ts
vendored
8
apps/app/types/issues.d.ts
vendored
@ -161,12 +161,12 @@ export type IssuePriorities = {
|
||||
};
|
||||
|
||||
export type Properties = {
|
||||
key: boolean;
|
||||
state: boolean;
|
||||
assignee: boolean;
|
||||
priority: boolean;
|
||||
due_date: boolean;
|
||||
// cycle: boolean;
|
||||
labels: boolean;
|
||||
key: boolean;
|
||||
priority: boolean;
|
||||
state: boolean;
|
||||
sub_issue_count: boolean;
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user