forked from github/plane
feat: peek overview for spreadsheet issues (#1979)
* feat: peak overview for issues * fix: peek spelling * chore: truncate issue property labels * style: full screen view designed * chore: add comment section * chore: copy link and delete options added * chore: update icons --------- Co-authored-by: Aaryan Khandelwal <aaryan610@Aaryans-MacBook-Pro.local>
This commit is contained in:
parent
93fa093a79
commit
2b168edd99
@ -548,7 +548,7 @@ export const IssuesView: React.FC<Props> = ({
|
||||
}}
|
||||
handleOnDragEnd={handleOnDragEnd}
|
||||
handleIssueAction={handleIssueAction}
|
||||
openIssuesListModal={openIssuesListModal ? openIssuesListModal : null}
|
||||
openIssuesListModal={openIssuesListModal ?? null}
|
||||
removeIssue={cycleId ? removeIssueFromCycle : moduleId ? removeIssueFromModule : null}
|
||||
trashBox={trashBox}
|
||||
setTrashBox={setTrashBox}
|
||||
|
@ -1,12 +1,12 @@
|
||||
import React, { useCallback, useState } from "react";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { mutate } from "swr";
|
||||
|
||||
// components
|
||||
import {
|
||||
IssuePeekOverview,
|
||||
ViewAssigneeSelect,
|
||||
ViewDueDateSelect,
|
||||
ViewEstimateSelect,
|
||||
@ -75,6 +75,10 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
||||
nestingLevel,
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
// issue peek overview
|
||||
const [issuePeekOverview, setIssuePeekOverview] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
||||
@ -95,7 +99,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
||||
? VIEW_ISSUES(viewId.toString(), params)
|
||||
: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params);
|
||||
|
||||
if (issue.parent) {
|
||||
if (issue.parent)
|
||||
mutate<ISubIssueResponse>(
|
||||
SUB_ISSUES(issue.parent.toString()),
|
||||
(prevData) => {
|
||||
@ -116,7 +120,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
||||
},
|
||||
false
|
||||
);
|
||||
} else {
|
||||
else
|
||||
mutate<IIssue[]>(
|
||||
fetchKey,
|
||||
(prevData) =>
|
||||
@ -131,7 +135,6 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
||||
}),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
issuesService
|
||||
.patchIssue(
|
||||
@ -179,6 +182,16 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
||||
|
||||
return (
|
||||
<>
|
||||
<IssuePeekOverview
|
||||
handleDeleteIssue={() => handleDeleteIssue(issue)}
|
||||
handleUpdateIssue={async (formData) => partialUpdateIssue(formData, issue)}
|
||||
issue={issue}
|
||||
isOpen={issuePeekOverview}
|
||||
onClose={() => setIssuePeekOverview(false)}
|
||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||
readOnly={isNotAllowed}
|
||||
/>
|
||||
<div
|
||||
className="relative group grid auto-rows-[minmax(44px,1fr)] hover:rounded-sm hover:bg-custom-background-80 border-b border-custom-border-200 w-full min-w-max"
|
||||
style={{ gridTemplateColumns }}
|
||||
@ -264,11 +277,13 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Link href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}>
|
||||
<a className="truncate text-custom-text-100 cursor-pointer w-full text-[0.825rem]">
|
||||
<button
|
||||
type="button"
|
||||
className="truncate text-custom-text-100 text-left cursor-pointer w-full text-[0.825rem]"
|
||||
onClick={() => setIssuePeekOverview(true)}
|
||||
>
|
||||
{issue.name}
|
||||
</a>
|
||||
</Link>
|
||||
</button>
|
||||
</div>
|
||||
{properties.state && (
|
||||
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||
@ -364,5 +379,6 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -5,7 +5,7 @@ import { useRouter } from "next/router";
|
||||
|
||||
// components
|
||||
import { SpreadsheetColumns, SpreadsheetIssues } from "components/core";
|
||||
import { CustomMenu, Icon, Spinner } from "components/ui";
|
||||
import { CustomMenu, Spinner } from "components/ui";
|
||||
// hooks
|
||||
import useIssuesProperties from "hooks/use-issue-properties";
|
||||
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
|
||||
|
106
apps/app/components/inbox/inbox-issue-activity.tsx
Normal file
106
apps/app/components/inbox/inbox-issue-activity.tsx
Normal file
@ -0,0 +1,106 @@
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR, { mutate } from "swr";
|
||||
|
||||
// components
|
||||
import { AddComment, IssueActivitySection } from "components/issues";
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
// hooks
|
||||
import useUser from "hooks/use-user";
|
||||
import useToast from "hooks/use-toast";
|
||||
// types
|
||||
import { IIssue, IIssueComment } from "types";
|
||||
// fetch-keys
|
||||
import { PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
|
||||
|
||||
type Props = { issueDetails: IIssue };
|
||||
|
||||
export const InboxIssueActivity: React.FC<Props> = ({ issueDetails }) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, inboxIssueId } = router.query;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { user } = useUser();
|
||||
|
||||
const { data: issueActivity, mutate: mutateIssueActivity } = useSWR(
|
||||
workspaceSlug && projectId && inboxIssueId
|
||||
? PROJECT_ISSUES_ACTIVITY(inboxIssueId.toString())
|
||||
: null,
|
||||
workspaceSlug && projectId && inboxIssueId
|
||||
? () =>
|
||||
issuesService.getIssueActivities(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
inboxIssueId.toString()
|
||||
)
|
||||
: null
|
||||
);
|
||||
|
||||
const handleCommentUpdate = async (comment: IIssueComment) => {
|
||||
if (!workspaceSlug || !projectId || !inboxIssueId) return;
|
||||
|
||||
await issuesService
|
||||
.patchIssueComment(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
inboxIssueId as string,
|
||||
comment.id,
|
||||
comment,
|
||||
user
|
||||
)
|
||||
.then(() => mutateIssueActivity());
|
||||
};
|
||||
|
||||
const handleCommentDelete = async (commentId: string) => {
|
||||
if (!workspaceSlug || !projectId || !inboxIssueId) return;
|
||||
|
||||
mutateIssueActivity((prevData: any) => prevData?.filter((p: any) => p.id !== commentId), false);
|
||||
|
||||
await issuesService
|
||||
.deleteIssueComment(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
inboxIssueId as string,
|
||||
commentId,
|
||||
user
|
||||
)
|
||||
.then(() => mutateIssueActivity());
|
||||
};
|
||||
|
||||
const handleAddComment = async (formData: IIssueComment) => {
|
||||
if (!workspaceSlug || !issueDetails) return;
|
||||
|
||||
await issuesService
|
||||
.createIssueComment(
|
||||
workspaceSlug.toString(),
|
||||
issueDetails.project,
|
||||
issueDetails.id,
|
||||
formData,
|
||||
user
|
||||
)
|
||||
.then(() => {
|
||||
mutate(PROJECT_ISSUES_ACTIVITY(issueDetails.id));
|
||||
})
|
||||
.catch(() =>
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Comment could not be posted. Please try again.",
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<h3 className="text-lg text-custom-text-100">Comments/Activity</h3>
|
||||
<IssueActivitySection
|
||||
activity={issueActivity}
|
||||
handleCommentUpdate={handleCommentUpdate}
|
||||
handleCommentDelete={handleCommentDelete}
|
||||
/>
|
||||
<AddComment onSubmit={handleAddComment} />
|
||||
</div>
|
||||
);
|
||||
};
|
@ -14,13 +14,8 @@ import inboxServices from "services/inbox.service";
|
||||
import useInboxView from "hooks/use-inbox-view";
|
||||
import useUserAuth from "hooks/use-user-auth";
|
||||
// components
|
||||
import {
|
||||
AddComment,
|
||||
IssueActivitySection,
|
||||
IssueDescriptionForm,
|
||||
IssueDetailsSidebar,
|
||||
IssueReaction,
|
||||
} from "components/issues";
|
||||
import { IssueDescriptionForm, IssueDetailsSidebar, IssueReaction } from "components/issues";
|
||||
import { InboxIssueActivity } from "components/inbox";
|
||||
// ui
|
||||
import { Loader } from "components/ui";
|
||||
// icons
|
||||
@ -42,7 +37,6 @@ import { INBOX_ISSUES, INBOX_ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "cons
|
||||
|
||||
const defaultValues = {
|
||||
name: "",
|
||||
description: "",
|
||||
description_html: "",
|
||||
estimate_point: null,
|
||||
assignees_list: [],
|
||||
@ -296,7 +290,6 @@ export const InboxMainContent: React.FC = () => {
|
||||
workspaceSlug={workspaceSlug as string}
|
||||
issue={{
|
||||
name: issueDetails.name,
|
||||
description: issueDetails.description,
|
||||
description_html: issueDetails.description_html,
|
||||
}}
|
||||
handleFormSubmit={submitChanges}
|
||||
@ -312,11 +305,7 @@ export const InboxMainContent: React.FC = () => {
|
||||
issueId={issueDetails.id}
|
||||
/>
|
||||
|
||||
<div className="space-y-5">
|
||||
<h3 className="text-lg text-custom-text-100">Comments/Activity</h3>
|
||||
<IssueActivitySection issueId={issueDetails.id} user={user} />
|
||||
<AddComment issueId={issueDetails.id} user={user} />
|
||||
</div>
|
||||
<InboxIssueActivity issueDetails={issueDetails} />
|
||||
</div>
|
||||
|
||||
<div className="basis-1/3 space-y-5 border-custom-border-200 p-5">
|
||||
|
@ -4,6 +4,7 @@ export * from "./delete-issue-modal";
|
||||
export * from "./filters-dropdown";
|
||||
export * from "./filters-list";
|
||||
export * from "./inbox-action-headers";
|
||||
export * from "./inbox-issue-activity";
|
||||
export * from "./inbox-issue-card";
|
||||
export * from "./inbox-main-content";
|
||||
export * from "./issues-list-sidebar";
|
||||
|
@ -3,10 +3,6 @@ import React from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
// components
|
||||
import { ActivityIcon, ActivityMessage } from "components/core";
|
||||
import { CommentCard } from "components/issues/comment";
|
||||
@ -15,62 +11,23 @@ import { Icon, Loader } from "components/ui";
|
||||
// helpers
|
||||
import { timeAgo } from "helpers/date-time.helper";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssueComment } from "types";
|
||||
// fetch-keys
|
||||
import { PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
|
||||
import { IIssueActivity, IIssueComment } from "types";
|
||||
|
||||
type Props = {
|
||||
issueId: string;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
activity: IIssueActivity[] | undefined;
|
||||
handleCommentUpdate: (comment: IIssueComment) => Promise<void>;
|
||||
handleCommentDelete: (commentId: string) => Promise<void>;
|
||||
};
|
||||
|
||||
export const IssueActivitySection: React.FC<Props> = ({ issueId, user }) => {
|
||||
export const IssueActivitySection: React.FC<Props> = ({
|
||||
activity,
|
||||
handleCommentUpdate,
|
||||
handleCommentDelete,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { data: issueActivities, mutate: mutateIssueActivities } = useSWR(
|
||||
workspaceSlug && projectId ? PROJECT_ISSUES_ACTIVITY(issueId) : null,
|
||||
workspaceSlug && projectId
|
||||
? () =>
|
||||
issuesService.getIssueActivities(workspaceSlug as string, projectId as string, issueId)
|
||||
: null
|
||||
);
|
||||
|
||||
const handleCommentUpdate = async (comment: IIssueComment) => {
|
||||
if (!workspaceSlug || !projectId || !issueId) return;
|
||||
|
||||
await issuesService
|
||||
.patchIssueComment(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
issueId as string,
|
||||
comment.id,
|
||||
comment,
|
||||
user
|
||||
)
|
||||
.then((res) => mutateIssueActivities());
|
||||
};
|
||||
|
||||
const handleCommentDelete = async (commentId: string) => {
|
||||
if (!workspaceSlug || !projectId || !issueId) return;
|
||||
|
||||
mutateIssueActivities(
|
||||
(prevData: any) => prevData?.filter((p: any) => p.id !== commentId),
|
||||
false
|
||||
);
|
||||
|
||||
await issuesService
|
||||
.deleteIssueComment(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
issueId as string,
|
||||
commentId,
|
||||
user
|
||||
)
|
||||
.then(() => mutateIssueActivities());
|
||||
};
|
||||
|
||||
if (!issueActivities) {
|
||||
if (!activity)
|
||||
return (
|
||||
<Loader className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
@ -87,12 +44,11 @@ export const IssueActivitySection: React.FC<Props> = ({ issueId, user }) => {
|
||||
</div>
|
||||
</Loader>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flow-root">
|
||||
<ul role="list" className="-mb-4">
|
||||
{issueActivities.map((activityItem, index) => {
|
||||
{activity.map((activityItem, index) => {
|
||||
// determines what type of action is performed
|
||||
const message = activityItem.field ? (
|
||||
<ActivityMessage activity={activityItem} />
|
||||
@ -104,7 +60,7 @@ export const IssueActivitySection: React.FC<Props> = ({ issueId, user }) => {
|
||||
return (
|
||||
<li key={activityItem.id}>
|
||||
<div className="relative pb-1">
|
||||
{issueActivities.length > 1 && index !== issueActivities.length - 1 ? (
|
||||
{activity.length > 1 && index !== activity.length - 1 ? (
|
||||
<span
|
||||
className="absolute top-5 left-5 -ml-px h-full w-0.5 bg-custom-background-80"
|
||||
aria-hidden="true"
|
||||
|
@ -2,20 +2,13 @@ import React from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { mutate } from "swr";
|
||||
|
||||
// react-hook-form
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
// services
|
||||
import issuesServices from "services/issues.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// ui
|
||||
import { SecondaryButton } from "components/ui";
|
||||
// types
|
||||
import type { ICurrentUserResponse, IIssueComment } from "types";
|
||||
import type { IIssueComment } from "types";
|
||||
// fetch-keys
|
||||
import { PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
|
||||
import Tiptap, { ITiptapRichTextEditor } from "components/tiptap";
|
||||
|
||||
const TiptapEditor = React.forwardRef<ITiptapRichTextEditor, ITiptapRichTextEditor>(
|
||||
@ -30,63 +23,37 @@ const defaultValues: Partial<IIssueComment> = {
|
||||
};
|
||||
|
||||
type Props = {
|
||||
issueId: string;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
disabled?: boolean;
|
||||
onSubmit: (data: IIssueComment) => Promise<void>;
|
||||
};
|
||||
|
||||
export const AddComment: React.FC<Props> = ({ issueId, user, disabled = false }) => {
|
||||
export const AddComment: React.FC<Props> = ({ disabled = false, onSubmit }) => {
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
formState: { isSubmitting },
|
||||
handleSubmit,
|
||||
reset,
|
||||
setValue,
|
||||
watch,
|
||||
formState: { isSubmitting },
|
||||
reset,
|
||||
} = useForm<IIssueComment>({ defaultValues });
|
||||
|
||||
const editorRef = React.useRef<any>(null);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
const handleAddComment = async (formData: IIssueComment) => {
|
||||
if (!formData.comment_html || !formData.comment_json || isSubmitting) return;
|
||||
|
||||
const onSubmit = async (formData: IIssueComment) => {
|
||||
if (
|
||||
!workspaceSlug ||
|
||||
!projectId ||
|
||||
!issueId ||
|
||||
isSubmitting ||
|
||||
!formData.comment_html ||
|
||||
!formData.comment_json
|
||||
)
|
||||
return;
|
||||
await issuesServices
|
||||
.createIssueComment(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
issueId as string,
|
||||
formData,
|
||||
user
|
||||
)
|
||||
.then(() => {
|
||||
mutate(PROJECT_ISSUES_ACTIVITY(issueId as string));
|
||||
await onSubmit(formData).then(() => {
|
||||
reset(defaultValues);
|
||||
editorRef.current?.clearEditor();
|
||||
})
|
||||
.catch(() =>
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Comment could not be posted. Please try again.",
|
||||
})
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<form onSubmit={handleSubmit(handleAddComment)}>
|
||||
<div className="issue-comments-section">
|
||||
<Controller
|
||||
name="comment_html"
|
||||
|
@ -4,24 +4,21 @@ import { FC, useCallback, useEffect, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// hooks
|
||||
import useReloadConfirmations from "hooks/use-reload-confirmation";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
// components
|
||||
import { TextArea } from "components/ui";
|
||||
|
||||
import Tiptap from "components/tiptap";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
import Tiptap from "components/tiptap";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
|
||||
export interface IssueDescriptionFormValues {
|
||||
name: string;
|
||||
description: any;
|
||||
description_html: string;
|
||||
}
|
||||
|
||||
export interface IssueDetailsProps {
|
||||
issue: {
|
||||
name: string;
|
||||
description: string;
|
||||
description_html: string;
|
||||
};
|
||||
workspaceSlug: string;
|
||||
@ -43,7 +40,6 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
|
||||
const {
|
||||
handleSubmit,
|
||||
watch,
|
||||
setValue,
|
||||
reset,
|
||||
register,
|
||||
control,
|
||||
@ -51,7 +47,6 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
|
||||
} = useForm<IIssue>({
|
||||
defaultValues: {
|
||||
name: "",
|
||||
description: "",
|
||||
description_html: "",
|
||||
},
|
||||
});
|
||||
@ -62,7 +57,6 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
|
||||
|
||||
await handleFormSubmit({
|
||||
name: formData.name ?? "",
|
||||
description: formData.description ?? "",
|
||||
description_html: formData.description_html ?? "<p></p>",
|
||||
});
|
||||
},
|
||||
@ -80,7 +74,6 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
|
||||
}
|
||||
}, [isSubmitting, setShowAlert]);
|
||||
|
||||
|
||||
// reset form values
|
||||
useEffect(() => {
|
||||
if (!issue) return;
|
||||
@ -99,6 +92,7 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="relative">
|
||||
{isAllowed ? (
|
||||
<TextArea
|
||||
id="name"
|
||||
name="name"
|
||||
@ -115,10 +109,14 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
|
||||
role="textbox"
|
||||
disabled={!isAllowed}
|
||||
/>
|
||||
{characterLimit && (
|
||||
) : (
|
||||
<h4 className="break-words text-2xl font-semibold">{issue.name}</h4>
|
||||
)}
|
||||
{characterLimit && isAllowed && (
|
||||
<div className="pointer-events-none absolute bottom-1 right-1 z-[2] rounded bg-custom-background-100 text-custom-text-200 p-0.5 text-xs">
|
||||
<span
|
||||
className={`${watch("name").length === 0 || watch("name").length > 255 ? "text-red-500" : ""
|
||||
className={`${
|
||||
watch("name").length === 0 || watch("name").length > 255 ? "text-red-500" : ""
|
||||
}`}
|
||||
>
|
||||
{watch("name").length}
|
||||
@ -133,7 +131,7 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
|
||||
name="description_html"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => {
|
||||
if (!value && !watch("description_html")) return <></>;
|
||||
if (!value) return <></>;
|
||||
|
||||
return (
|
||||
<Tiptap
|
||||
@ -141,30 +139,33 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
|
||||
!value ||
|
||||
value === "" ||
|
||||
(typeof value === "object" && Object.keys(value).length === 0)
|
||||
? watch("description_html")
|
||||
? "<p></p>"
|
||||
: value
|
||||
}
|
||||
workspaceSlug={workspaceSlug}
|
||||
debouncedUpdatesEnabled={true}
|
||||
setShouldShowAlert={setShowAlert}
|
||||
setIsSubmitting={setIsSubmitting}
|
||||
customClassName="min-h-[150px] shadow-sm"
|
||||
editorContentCustomClassNames="pb-9"
|
||||
customClassName={
|
||||
isAllowed ? "min-h-[150px] shadow-sm" : "!p-0 !pt-2 text-custom-text-200"
|
||||
}
|
||||
noBorder={!isAllowed}
|
||||
onChange={(description: Object, description_html: string) => {
|
||||
setShowAlert(true);
|
||||
setIsSubmitting("submitting");
|
||||
onChange(description_html);
|
||||
setValue("description", description);
|
||||
handleSubmit(handleDescriptionFormSubmit)().finally(() => {
|
||||
setIsSubmitting("submitted");
|
||||
});
|
||||
handleSubmit(handleDescriptionFormSubmit)().finally(() =>
|
||||
setIsSubmitting("submitted")
|
||||
);
|
||||
}}
|
||||
editable={isAllowed}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={`absolute right-5 bottom-5 text-xs text-custom-text-200 border border-custom-border-400 rounded-xl w-[6.5rem] py-1 z-10 flex items-center justify-center ${isSubmitting === "saved" ? "fadeOut" : "fadeIn"
|
||||
className={`absolute right-5 bottom-5 text-xs text-custom-text-200 border border-custom-border-400 rounded-xl w-[6.5rem] py-1 z-10 flex items-center justify-center ${
|
||||
isSubmitting === "saved" ? "fadeOut" : "fadeIn"
|
||||
}`}
|
||||
>
|
||||
{isSubmitting === "submitting" ? "Saving..." : "Saved"}
|
||||
|
@ -15,3 +15,4 @@ export * from "./sidebar";
|
||||
export * from "./sub-issues-list";
|
||||
export * from "./label";
|
||||
export * from "./issue-reaction";
|
||||
export * from "./peek-overview";
|
||||
|
@ -1,12 +1,13 @@
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
import useSWR, { mutate } from "swr";
|
||||
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
// hooks
|
||||
import useUserAuth from "hooks/use-user-auth";
|
||||
import useToast from "hooks/use-toast";
|
||||
// contexts
|
||||
import { useProjectMyMembership } from "contexts/project-member.context";
|
||||
// components
|
||||
@ -25,9 +26,9 @@ import { CustomMenu } from "components/ui";
|
||||
import { LayerDiagonalIcon } from "components/icons";
|
||||
import { MinusCircleIcon } from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
import { IIssue, IIssueComment } from "types";
|
||||
// fetch-keys
|
||||
import { SUB_ISSUES } from "constants/fetch-keys";
|
||||
import { PROJECT_ISSUES_ACTIVITY, SUB_ISSUES } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
issueDetails: IIssue;
|
||||
@ -43,6 +44,8 @@ export const IssueMainContent: React.FC<Props> = ({
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, issueId, archivedIssueId } = router.query;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { user } = useUserAuth();
|
||||
const { memberRole } = useProjectMyMembership();
|
||||
|
||||
@ -59,6 +62,72 @@ export const IssueMainContent: React.FC<Props> = ({
|
||||
);
|
||||
const siblingIssuesList = siblingIssues?.sub_issues.filter((i) => i.id !== issueDetails.id);
|
||||
|
||||
const { data: issueActivity, mutate: mutateIssueActivity } = useSWR(
|
||||
workspaceSlug && projectId && issueId ? PROJECT_ISSUES_ACTIVITY(issueId.toString()) : null,
|
||||
workspaceSlug && projectId && issueId
|
||||
? () =>
|
||||
issuesService.getIssueActivities(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
issueId.toString()
|
||||
)
|
||||
: null
|
||||
);
|
||||
|
||||
const handleCommentUpdate = async (comment: IIssueComment) => {
|
||||
if (!workspaceSlug || !projectId || !issueId) return;
|
||||
|
||||
await issuesService
|
||||
.patchIssueComment(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
issueId as string,
|
||||
comment.id,
|
||||
comment,
|
||||
user
|
||||
)
|
||||
.then(() => mutateIssueActivity());
|
||||
};
|
||||
|
||||
const handleCommentDelete = async (commentId: string) => {
|
||||
if (!workspaceSlug || !projectId || !issueId) return;
|
||||
|
||||
mutateIssueActivity((prevData: any) => prevData?.filter((p: any) => p.id !== commentId), false);
|
||||
|
||||
await issuesService
|
||||
.deleteIssueComment(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
issueId as string,
|
||||
commentId,
|
||||
user
|
||||
)
|
||||
.then(() => mutateIssueActivity());
|
||||
};
|
||||
|
||||
const handleAddComment = async (formData: IIssueComment) => {
|
||||
if (!workspaceSlug || !issueDetails) return;
|
||||
|
||||
await issuesService
|
||||
.createIssueComment(
|
||||
workspaceSlug.toString(),
|
||||
issueDetails.project,
|
||||
issueDetails.id,
|
||||
formData,
|
||||
user
|
||||
)
|
||||
.then(() => {
|
||||
mutate(PROJECT_ISSUES_ACTIVITY(issueDetails.id));
|
||||
})
|
||||
.catch(() =>
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Comment could not be posted. Please try again.",
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="rounded-lg">
|
||||
@ -97,7 +166,8 @@ export const IssueMainContent: React.FC<Props> = ({
|
||||
<CustomMenu.MenuItem
|
||||
key={issue.id}
|
||||
renderAs="a"
|
||||
href={`/${workspaceSlug}/projects/${projectId as string}/issues/${issue.id
|
||||
href={`/${workspaceSlug}/projects/${projectId as string}/issues/${
|
||||
issue.id
|
||||
}`}
|
||||
className="flex items-center gap-2 py-2"
|
||||
>
|
||||
@ -146,14 +216,11 @@ export const IssueMainContent: React.FC<Props> = ({
|
||||
<div className="space-y-5 pt-3">
|
||||
<h3 className="text-lg text-custom-text-100">Comments/Activity</h3>
|
||||
<IssueActivitySection
|
||||
issueId={(archivedIssueId as string) ?? (issueId as string)}
|
||||
user={user}
|
||||
/>
|
||||
<AddComment
|
||||
issueId={(archivedIssueId as string) ?? (issueId as string)}
|
||||
user={user}
|
||||
disabled={uneditable}
|
||||
activity={issueActivity}
|
||||
handleCommentUpdate={handleCommentUpdate}
|
||||
handleCommentDelete={handleCommentDelete}
|
||||
/>
|
||||
<AddComment onSubmit={handleAddComment} disabled={uneditable} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
@ -0,0 +1,79 @@
|
||||
import {
|
||||
PeekOverviewHeader,
|
||||
PeekOverviewIssueActivity,
|
||||
PeekOverviewIssueDetails,
|
||||
PeekOverviewIssueProperties,
|
||||
TPeekOverviewModes,
|
||||
} from "components/issues";
|
||||
import { IIssue } from "types";
|
||||
|
||||
type Props = {
|
||||
handleClose: () => void;
|
||||
handleDeleteIssue: () => void;
|
||||
handleUpdateIssue: (issue: Partial<IIssue>) => Promise<void>;
|
||||
issue: IIssue;
|
||||
mode: TPeekOverviewModes;
|
||||
readOnly: boolean;
|
||||
setMode: (mode: TPeekOverviewModes) => void;
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
export const FullScreenPeekView: React.FC<Props> = ({
|
||||
handleClose,
|
||||
handleDeleteIssue,
|
||||
handleUpdateIssue,
|
||||
issue,
|
||||
mode,
|
||||
readOnly,
|
||||
setMode,
|
||||
workspaceSlug,
|
||||
}) => (
|
||||
<div className="h-full w-full grid grid-cols-10 divide-x divide-custom-border-200 overflow-hidden">
|
||||
<div className="h-full w-full flex flex-col col-span-7 overflow-hidden">
|
||||
<div className="w-full p-5">
|
||||
<PeekOverviewHeader
|
||||
handleClose={handleClose}
|
||||
handleDeleteIssue={handleDeleteIssue}
|
||||
issue={issue}
|
||||
mode={mode}
|
||||
setMode={setMode}
|
||||
workspaceSlug={workspaceSlug}
|
||||
/>
|
||||
</div>
|
||||
<div className="h-full w-full px-6 overflow-y-auto">
|
||||
{/* issue title and description */}
|
||||
<div className="w-full">
|
||||
<PeekOverviewIssueDetails
|
||||
handleUpdateIssue={handleUpdateIssue}
|
||||
issue={issue}
|
||||
readOnly={readOnly}
|
||||
workspaceSlug={workspaceSlug}
|
||||
/>
|
||||
</div>
|
||||
{/* divider */}
|
||||
<div className="h-[1] w-full border-t border-custom-border-200 my-5" />
|
||||
{/* issue activity/comments */}
|
||||
<div className="w-full">
|
||||
<PeekOverviewIssueActivity
|
||||
workspaceSlug={workspaceSlug}
|
||||
issue={issue}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-3 h-full w-full overflow-y-auto">
|
||||
{/* issue properties */}
|
||||
<div className="w-full px-6 py-5">
|
||||
<PeekOverviewIssueProperties
|
||||
handleDeleteIssue={handleDeleteIssue}
|
||||
issue={issue}
|
||||
mode="full"
|
||||
onChange={handleUpdateIssue}
|
||||
readOnly={readOnly}
|
||||
workspaceSlug={workspaceSlug}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
133
apps/app/components/issues/peek-overview/header.tsx
Normal file
133
apps/app/components/issues/peek-overview/header.tsx
Normal file
@ -0,0 +1,133 @@
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// ui
|
||||
import { CustomSelect, Icon } from "components/ui";
|
||||
// helpers
|
||||
import { copyTextToClipboard } from "helpers/string.helper";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
import { TPeekOverviewModes } from "./layout";
|
||||
import { ArrowRightAlt, CloseFullscreen, East, OpenInFull } from "@mui/icons-material";
|
||||
|
||||
type Props = {
|
||||
handleClose: () => void;
|
||||
handleDeleteIssue: () => void;
|
||||
issue: IIssue;
|
||||
mode: TPeekOverviewModes;
|
||||
setMode: (mode: TPeekOverviewModes) => void;
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
const peekModes: {
|
||||
key: TPeekOverviewModes;
|
||||
icon: string;
|
||||
label: string;
|
||||
}[] = [
|
||||
{ key: "side", icon: "side_navigation", label: "Side Peek" },
|
||||
{
|
||||
key: "modal",
|
||||
icon: "dialogs",
|
||||
label: "Modal Peek",
|
||||
},
|
||||
{
|
||||
key: "full",
|
||||
icon: "nearby",
|
||||
label: "Full Screen Peek",
|
||||
},
|
||||
];
|
||||
|
||||
export const PeekOverviewHeader: React.FC<Props> = ({
|
||||
issue,
|
||||
handleClose,
|
||||
handleDeleteIssue,
|
||||
mode,
|
||||
setMode,
|
||||
workspaceSlug,
|
||||
}) => {
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const handleCopyLink = () => {
|
||||
const originURL =
|
||||
typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||
|
||||
copyTextToClipboard(
|
||||
`${originURL}/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`
|
||||
).then(() => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Link copied!",
|
||||
message: "Issue link copied to clipboard",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-4">
|
||||
{mode === "side" && (
|
||||
<button type="button" onClick={handleClose}>
|
||||
<East
|
||||
sx={{
|
||||
fontSize: "14px",
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
{mode === "modal" || mode === "full" ? (
|
||||
<button type="button" onClick={() => setMode("side")}>
|
||||
<CloseFullscreen
|
||||
sx={{
|
||||
fontSize: "14px",
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
) : (
|
||||
<button type="button" onClick={() => setMode("modal")}>
|
||||
<OpenInFull
|
||||
sx={{
|
||||
fontSize: "14px",
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
<CustomSelect
|
||||
value={mode}
|
||||
onChange={(val: TPeekOverviewModes) => setMode(val)}
|
||||
customButton={
|
||||
<button
|
||||
type="button"
|
||||
className={`grid place-items-center ${mode === "full" ? "rotate-45" : ""}`}
|
||||
>
|
||||
<Icon iconName={peekModes.find((m) => m.key === mode)?.icon ?? ""} />
|
||||
</button>
|
||||
}
|
||||
position="left"
|
||||
>
|
||||
{peekModes.map((mode) => (
|
||||
<CustomSelect.Option key={mode.key} value={mode.key}>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Icon
|
||||
iconName={mode.icon}
|
||||
className={`!text-base flex-shrink-0 -my-1 ${
|
||||
mode.key === "full" ? "rotate-45" : ""
|
||||
}`}
|
||||
/>
|
||||
{mode.label}
|
||||
</div>
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
</CustomSelect>
|
||||
</div>
|
||||
{(mode === "side" || mode === "modal") && (
|
||||
<div className="flex items-center gap-2">
|
||||
<button type="button" onClick={handleCopyLink} className="-rotate-45">
|
||||
<Icon iconName="link" />
|
||||
</button>
|
||||
<button type="button" onClick={handleDeleteIssue}>
|
||||
<Icon iconName="delete" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
7
apps/app/components/issues/peek-overview/index.ts
Normal file
7
apps/app/components/issues/peek-overview/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export * from "./full-screen-peek-view";
|
||||
export * from "./header";
|
||||
export * from "./issue-activity";
|
||||
export * from "./issue-details";
|
||||
export * from "./issue-properties";
|
||||
export * from "./layout";
|
||||
export * from "./side-peek-view";
|
90
apps/app/components/issues/peek-overview/issue-activity.tsx
Normal file
90
apps/app/components/issues/peek-overview/issue-activity.tsx
Normal file
@ -0,0 +1,90 @@
|
||||
import useSWR, { mutate } from "swr";
|
||||
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
// hooks
|
||||
import useUser from "hooks/use-user";
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { AddComment, IssueActivitySection } from "components/issues";
|
||||
// types
|
||||
import { IIssue, IIssueComment } from "types";
|
||||
// fetch-keys
|
||||
import { PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
issue: IIssue;
|
||||
readOnly: boolean;
|
||||
};
|
||||
|
||||
export const PeekOverviewIssueActivity: React.FC<Props> = ({ workspaceSlug, issue, readOnly }) => {
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { user } = useUser();
|
||||
|
||||
const { data: issueActivity, mutate: mutateIssueActivity } = useSWR(
|
||||
workspaceSlug && issue ? PROJECT_ISSUES_ACTIVITY(issue.id) : null,
|
||||
workspaceSlug && issue
|
||||
? () => issuesService.getIssueActivities(workspaceSlug.toString(), issue?.project, issue?.id)
|
||||
: null
|
||||
);
|
||||
|
||||
const handleCommentUpdate = async (comment: IIssueComment) => {
|
||||
if (!workspaceSlug || !issue) return;
|
||||
|
||||
await issuesService
|
||||
.patchIssueComment(
|
||||
workspaceSlug as string,
|
||||
issue.project,
|
||||
issue.id,
|
||||
comment.id,
|
||||
comment,
|
||||
user
|
||||
)
|
||||
.then(() => mutateIssueActivity());
|
||||
};
|
||||
|
||||
const handleCommentDelete = async (commentId: string) => {
|
||||
if (!workspaceSlug || !issue) return;
|
||||
|
||||
mutateIssueActivity((prevData: any) => prevData?.filter((p: any) => p.id !== commentId), false);
|
||||
|
||||
await issuesService
|
||||
.deleteIssueComment(workspaceSlug as string, issue.project, issue.id, commentId, user)
|
||||
.then(() => mutateIssueActivity());
|
||||
};
|
||||
|
||||
const handleAddComment = async (formData: IIssueComment) => {
|
||||
if (!workspaceSlug || !issue) return;
|
||||
|
||||
await issuesService
|
||||
.createIssueComment(workspaceSlug.toString(), issue.project, issue.id, formData, user)
|
||||
.then(() => {
|
||||
mutate(PROJECT_ISSUES_ACTIVITY(issue.id));
|
||||
})
|
||||
.catch(() =>
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Comment could not be posted. Please try again.",
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h4 className="font-medium">Activity</h4>
|
||||
<div className="mt-4">
|
||||
<IssueActivitySection
|
||||
activity={issueActivity}
|
||||
handleCommentUpdate={handleCommentUpdate}
|
||||
handleCommentDelete={handleCommentDelete}
|
||||
/>
|
||||
<div className="mt-4">
|
||||
<AddComment onSubmit={handleAddComment} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
34
apps/app/components/issues/peek-overview/issue-details.tsx
Normal file
34
apps/app/components/issues/peek-overview/issue-details.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
// components
|
||||
import { IssueDescriptionForm, IssueReaction } from "components/issues";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
|
||||
type Props = {
|
||||
handleUpdateIssue: (formData: Partial<IIssue>) => Promise<void>;
|
||||
issue: IIssue;
|
||||
readOnly: boolean;
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
export const PeekOverviewIssueDetails: React.FC<Props> = ({
|
||||
handleUpdateIssue,
|
||||
issue,
|
||||
readOnly,
|
||||
workspaceSlug,
|
||||
}) => (
|
||||
<div className="space-y-2">
|
||||
<h6 className="font-medium text-custom-text-200">
|
||||
{issue.project_detail.identifier}-{issue.sequence_id}
|
||||
</h6>
|
||||
<IssueDescriptionForm
|
||||
handleFormSubmit={handleUpdateIssue}
|
||||
isAllowed={!readOnly}
|
||||
issue={{
|
||||
name: issue.name,
|
||||
description_html: issue.description_html,
|
||||
}}
|
||||
workspaceSlug={workspaceSlug}
|
||||
/>
|
||||
<IssueReaction workspaceSlug={workspaceSlug} issueId={issue.id} projectId={issue.project} />
|
||||
</div>
|
||||
);
|
203
apps/app/components/issues/peek-overview/issue-properties.tsx
Normal file
203
apps/app/components/issues/peek-overview/issue-properties.tsx
Normal file
@ -0,0 +1,203 @@
|
||||
// headless ui
|
||||
import { Disclosure } from "@headlessui/react";
|
||||
import { getStateGroupIcon } from "components/icons";
|
||||
// components
|
||||
import {
|
||||
SidebarAssigneeSelect,
|
||||
SidebarEstimateSelect,
|
||||
SidebarPrioritySelect,
|
||||
SidebarStateSelect,
|
||||
TPeekOverviewModes,
|
||||
} from "components/issues";
|
||||
// icons
|
||||
import { CustomDatePicker, Icon } from "components/ui";
|
||||
import { copyTextToClipboard } from "helpers/string.helper";
|
||||
import useToast from "hooks/use-toast";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
|
||||
type Props = {
|
||||
handleDeleteIssue: () => void;
|
||||
issue: IIssue;
|
||||
mode: TPeekOverviewModes;
|
||||
onChange: (issueProperty: Partial<IIssue>) => void;
|
||||
readOnly: boolean;
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
export const PeekOverviewIssueProperties: React.FC<Props> = ({
|
||||
handleDeleteIssue,
|
||||
issue,
|
||||
mode,
|
||||
onChange,
|
||||
readOnly,
|
||||
workspaceSlug,
|
||||
}) => {
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const startDate = issue.start_date;
|
||||
const targetDate = issue.target_date;
|
||||
|
||||
const minDate = startDate ? new Date(startDate) : null;
|
||||
minDate?.setDate(minDate.getDate());
|
||||
|
||||
const maxDate = targetDate ? new Date(targetDate) : null;
|
||||
maxDate?.setDate(maxDate.getDate());
|
||||
|
||||
const handleCopyLink = () => {
|
||||
const originURL =
|
||||
typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||
|
||||
copyTextToClipboard(
|
||||
`${originURL}/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`
|
||||
).then(() => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Link copied!",
|
||||
message: "Issue link copied to clipboard",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={mode === "full" ? "divide-y divide-custom-border-200" : ""}>
|
||||
{mode === "full" && (
|
||||
<div className="flex justify-between gap-2 pb-3">
|
||||
<h6 className="flex items-center gap-2 font-medium">
|
||||
{getStateGroupIcon(issue.state_detail.group, "16", "16", issue.state_detail.color)}
|
||||
{issue.project_detail.identifier}-{issue.sequence_id}
|
||||
</h6>
|
||||
<div className="flex items-center gap-2">
|
||||
<button type="button" onClick={handleCopyLink} className="-rotate-45">
|
||||
<Icon iconName="link" />
|
||||
</button>
|
||||
<button type="button" onClick={handleDeleteIssue}>
|
||||
<Icon iconName="delete" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={`space-y-4 ${mode === "full" ? "pt-3" : ""}`}>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<div className="flex-shrink-0 w-1/4 flex items-center gap-2 font-medium">
|
||||
<Icon iconName="radio_button_checked" className="!text-base flex-shrink-0" />
|
||||
<span className="flex-grow truncate">State</span>
|
||||
</div>
|
||||
<div className="w-3/4">
|
||||
<SidebarStateSelect
|
||||
value={issue.state}
|
||||
onChange={(val: string) => onChange({ state: val })}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<div className="flex-shrink-0 w-1/4 flex items-center gap-2 font-medium">
|
||||
<Icon iconName="group" className="!text-base flex-shrink-0" />
|
||||
<span className="flex-grow truncate">Assignees</span>
|
||||
</div>
|
||||
<div className="w-3/4">
|
||||
<SidebarAssigneeSelect
|
||||
value={issue.assignees_list}
|
||||
onChange={(val: string[]) => onChange({ assignees_list: val })}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<div className="flex-shrink-0 w-1/4 flex items-center gap-2 font-medium">
|
||||
<Icon iconName="signal_cellular_alt" className="!text-base flex-shrink-0" />
|
||||
<span className="flex-grow truncate">Priority</span>
|
||||
</div>
|
||||
<div className="w-3/4">
|
||||
<SidebarPrioritySelect
|
||||
value={issue.priority}
|
||||
onChange={(val: string) => onChange({ priority: val })}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<div className="flex-shrink-0 w-1/4 flex items-center gap-2 font-medium">
|
||||
<Icon iconName="calendar_today" className="!text-base flex-shrink-0" />
|
||||
<span className="flex-grow truncate">Start date</span>
|
||||
</div>
|
||||
<div>
|
||||
{issue.start_date ? (
|
||||
<CustomDatePicker
|
||||
placeholder="Start date"
|
||||
value={issue.start_date}
|
||||
onChange={(val) =>
|
||||
onChange({
|
||||
start_date: val,
|
||||
})
|
||||
}
|
||||
className="bg-custom-background-100"
|
||||
wrapperClassName="w-full"
|
||||
maxDate={maxDate ?? undefined}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
) : (
|
||||
<span className="text-custom-text-200">Empty</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<div className="flex-shrink-0 w-1/4 flex items-center gap-2 font-medium">
|
||||
<Icon iconName="calendar_today" className="!text-base flex-shrink-0" />
|
||||
<span className="flex-grow truncate">Due date</span>
|
||||
</div>
|
||||
<div>
|
||||
{issue.target_date ? (
|
||||
<CustomDatePicker
|
||||
placeholder="Due date"
|
||||
value={issue.target_date}
|
||||
onChange={(val) =>
|
||||
onChange({
|
||||
target_date: val,
|
||||
})
|
||||
}
|
||||
className="bg-custom-background-100"
|
||||
wrapperClassName="w-full"
|
||||
minDate={minDate ?? undefined}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
) : (
|
||||
<span className="text-custom-text-200">Empty</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="flex items-center gap-2 text-sm">
|
||||
<div className="flex-shrink-0 w-1/4 flex items-center gap-2 font-medium">
|
||||
<Icon iconName="change_history" className="!text-base flex-shrink-0" />
|
||||
<span className="flex-grow truncate">Estimate</span>
|
||||
</div>
|
||||
<div className="w-3/4">
|
||||
<SidebarEstimateSelect
|
||||
value={issue.estimate_point}
|
||||
onChange={(val: number | null) => onChange({ estimate_point: val })}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
</div>
|
||||
</div> */}
|
||||
{/* <Disclosure as="div">
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Disclosure.Button
|
||||
as="button"
|
||||
type="button"
|
||||
className="flex items-center gap-1 text-sm text-custom-text-200"
|
||||
>
|
||||
Show {open ? "Less" : "More"}
|
||||
<Icon iconName={open ? "expand_less" : "expand_more"} className="!text-base" />
|
||||
</Disclosure.Button>
|
||||
<Disclosure.Panel as="div" className="mt-4 space-y-4">
|
||||
Disclosure Panel
|
||||
</Disclosure.Panel>
|
||||
</>
|
||||
)}
|
||||
</Disclosure> */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
107
apps/app/components/issues/peek-overview/layout.tsx
Normal file
107
apps/app/components/issues/peek-overview/layout.tsx
Normal file
@ -0,0 +1,107 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
// headless ui
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { FullScreenPeekView, SidePeekView } from "components/issues";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
|
||||
type Props = {
|
||||
handleDeleteIssue: () => void;
|
||||
handleUpdateIssue: (issue: Partial<IIssue>) => Promise<void>;
|
||||
issue: IIssue | null;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
workspaceSlug: string;
|
||||
readOnly: boolean;
|
||||
};
|
||||
|
||||
export type TPeekOverviewModes = "side" | "modal" | "full";
|
||||
|
||||
export const IssuePeekOverview: React.FC<Props> = ({
|
||||
handleDeleteIssue,
|
||||
handleUpdateIssue,
|
||||
issue,
|
||||
isOpen,
|
||||
onClose,
|
||||
workspaceSlug,
|
||||
readOnly,
|
||||
}) => {
|
||||
const [peekOverviewMode, setPeekOverviewMode] = useState<TPeekOverviewModes>("side");
|
||||
|
||||
const handleClose = () => {
|
||||
onClose();
|
||||
setPeekOverviewMode("side");
|
||||
};
|
||||
|
||||
if (!issue || !isOpen) return null;
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
||||
{/* add backdrop conditionally */}
|
||||
{(peekOverviewMode === "modal" || peekOverviewMode === "full") && (
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
|
||||
</Transition.Child>
|
||||
)}
|
||||
<div className="fixed inset-0 z-20 overflow-y-auto">
|
||||
<div className="relative h-full w-full">
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel
|
||||
className={`absolute z-20 bg-custom-background-100 ${
|
||||
peekOverviewMode === "side"
|
||||
? "top-0 right-0 h-full w-1/2 shadow-custom-shadow-md"
|
||||
: peekOverviewMode === "modal"
|
||||
? "top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 h-[70%] w-3/5 rounded-lg shadow-custom-shadow-xl"
|
||||
: "top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 h-[95%] w-[95%] rounded-lg shadow-custom-shadow-xl"
|
||||
}`}
|
||||
>
|
||||
{(peekOverviewMode === "side" || peekOverviewMode === "modal") && (
|
||||
<SidePeekView
|
||||
handleClose={handleClose}
|
||||
handleDeleteIssue={handleDeleteIssue}
|
||||
handleUpdateIssue={handleUpdateIssue}
|
||||
issue={issue}
|
||||
mode={peekOverviewMode}
|
||||
readOnly={readOnly}
|
||||
setMode={(mode) => setPeekOverviewMode(mode)}
|
||||
workspaceSlug={workspaceSlug}
|
||||
/>
|
||||
)}
|
||||
{peekOverviewMode === "full" && (
|
||||
<FullScreenPeekView
|
||||
handleClose={handleClose}
|
||||
handleDeleteIssue={handleDeleteIssue}
|
||||
handleUpdateIssue={handleUpdateIssue}
|
||||
issue={issue}
|
||||
mode={peekOverviewMode}
|
||||
readOnly={readOnly}
|
||||
setMode={(mode) => setPeekOverviewMode(mode)}
|
||||
workspaceSlug={workspaceSlug}
|
||||
/>
|
||||
)}
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
};
|
75
apps/app/components/issues/peek-overview/side-peek-view.tsx
Normal file
75
apps/app/components/issues/peek-overview/side-peek-view.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
import {
|
||||
PeekOverviewHeader,
|
||||
PeekOverviewIssueActivity,
|
||||
PeekOverviewIssueDetails,
|
||||
PeekOverviewIssueProperties,
|
||||
TPeekOverviewModes,
|
||||
} from "components/issues";
|
||||
import { IIssue } from "types";
|
||||
|
||||
type Props = {
|
||||
handleClose: () => void;
|
||||
handleDeleteIssue: () => void;
|
||||
handleUpdateIssue: (issue: Partial<IIssue>) => Promise<void>;
|
||||
issue: IIssue;
|
||||
mode: TPeekOverviewModes;
|
||||
readOnly: boolean;
|
||||
setMode: (mode: TPeekOverviewModes) => void;
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
export const SidePeekView: React.FC<Props> = ({
|
||||
handleClose,
|
||||
handleDeleteIssue,
|
||||
handleUpdateIssue,
|
||||
issue,
|
||||
mode,
|
||||
readOnly,
|
||||
setMode,
|
||||
workspaceSlug,
|
||||
}) => (
|
||||
<div className="h-full w-full flex flex-col overflow-hidden">
|
||||
<div className="w-full p-5">
|
||||
<PeekOverviewHeader
|
||||
handleClose={handleClose}
|
||||
handleDeleteIssue={handleDeleteIssue}
|
||||
issue={issue}
|
||||
mode={mode}
|
||||
setMode={setMode}
|
||||
workspaceSlug={workspaceSlug}
|
||||
/>
|
||||
</div>
|
||||
<div className="h-full w-full px-6 overflow-y-auto">
|
||||
{/* issue title and description */}
|
||||
<div className="w-full">
|
||||
<PeekOverviewIssueDetails
|
||||
handleUpdateIssue={handleUpdateIssue}
|
||||
issue={issue}
|
||||
readOnly={readOnly}
|
||||
workspaceSlug={workspaceSlug}
|
||||
/>
|
||||
</div>
|
||||
{/* issue properties */}
|
||||
<div className="w-full mt-10">
|
||||
<PeekOverviewIssueProperties
|
||||
handleDeleteIssue={handleDeleteIssue}
|
||||
issue={issue}
|
||||
mode={mode}
|
||||
onChange={handleUpdateIssue}
|
||||
readOnly={readOnly}
|
||||
workspaceSlug={workspaceSlug}
|
||||
/>
|
||||
</div>
|
||||
{/* divider */}
|
||||
<div className="h-[1] w-full border-t border-custom-border-200 my-5" />
|
||||
{/* issue activity/comments */}
|
||||
<div className="w-full pb-5">
|
||||
<PeekOverviewIssueActivity
|
||||
workspaceSlug={workspaceSlug}
|
||||
issue={issue}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
@ -9,26 +9,16 @@ import projectService from "services/project.service";
|
||||
// ui
|
||||
import { CustomSearchSelect } from "components/ui";
|
||||
import { AssigneesList, Avatar } from "components/ui/avatar";
|
||||
// icons
|
||||
import { UserGroupIcon } from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import { UserAuth } from "types";
|
||||
// fetch-keys
|
||||
import { PROJECT_MEMBERS } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
value: string[];
|
||||
onChange: (val: string[]) => void;
|
||||
userAuth: UserAuth;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const SidebarAssigneeSelect: React.FC<Props> = ({
|
||||
value,
|
||||
onChange,
|
||||
userAuth,
|
||||
disabled = false,
|
||||
}) => {
|
||||
export const SidebarAssigneeSelect: React.FC<Props> = ({ value, onChange, disabled = false }) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
@ -50,36 +40,27 @@ export const SidebarAssigneeSelect: React.FC<Props> = ({
|
||||
),
|
||||
}));
|
||||
|
||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled;
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center py-2">
|
||||
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:basis-1/2">
|
||||
<UserGroupIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<p>Assignees</p>
|
||||
</div>
|
||||
<div className="sm:basis-1/2">
|
||||
<CustomSearchSelect
|
||||
value={value}
|
||||
label={
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
customButton={
|
||||
<>
|
||||
{value && value.length > 0 && Array.isArray(value) ? (
|
||||
<div className="-my-0.5 flex items-center justify-center gap-2">
|
||||
<div className="-my-0.5 flex items-center gap-2">
|
||||
<AssigneesList userIds={value} length={3} showLength={false} />
|
||||
<span className="text-custom-text-100">{value.length} Assignees</span>
|
||||
<span className="text-custom-text-100 text-sm">{value.length} Assignees</span>
|
||||
</div>
|
||||
) : (
|
||||
"No assignees"
|
||||
<button type="button" className="bg-custom-background-80 px-2.5 py-0.5 text-sm rounded">
|
||||
No assignees
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
options={options}
|
||||
onChange={onChange}
|
||||
position="right"
|
||||
multiple
|
||||
disabled={isNotAllowed}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -6,53 +6,36 @@ import useEstimateOption from "hooks/use-estimate-option";
|
||||
import { CustomSelect } from "components/ui";
|
||||
// icons
|
||||
import { PlayIcon } from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import { UserAuth } from "types";
|
||||
|
||||
type Props = {
|
||||
value: number | null;
|
||||
onChange: (val: number | null) => void;
|
||||
userAuth: UserAuth;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const SidebarEstimateSelect: React.FC<Props> = ({
|
||||
value,
|
||||
onChange,
|
||||
userAuth,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled;
|
||||
|
||||
export const SidebarEstimateSelect: React.FC<Props> = ({ value, onChange, disabled = false }) => {
|
||||
const { isEstimateActive, estimatePoints } = useEstimateOption();
|
||||
|
||||
if (!isEstimateActive) return null;
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center py-2">
|
||||
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:basis-1/2">
|
||||
<PlayIcon className="h-4 w-4 flex-shrink-0 -rotate-90" />
|
||||
<p>Estimate</p>
|
||||
</div>
|
||||
<div className="sm:basis-1/2">
|
||||
<CustomSelect
|
||||
value={value}
|
||||
label={
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
customButton={
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-1.5 !text-sm bg-custom-background-80 rounded px-2.5 py-0.5"
|
||||
>
|
||||
<PlayIcon
|
||||
className={`h-4 w-4 -rotate-90 ${
|
||||
value !== null ? "text-custom-text-100" : "text-custom-text-200"
|
||||
}`}
|
||||
/>
|
||||
{estimatePoints?.find((e) => e.key === value)?.value ?? (
|
||||
<span className="text-custom-text-200">No estimates</span>
|
||||
)}
|
||||
</div>
|
||||
{estimatePoints?.find((e) => e.key === value)?.value ?? "No estimate"}
|
||||
</button>
|
||||
}
|
||||
onChange={onChange}
|
||||
position="right"
|
||||
width="w-full"
|
||||
disabled={isNotAllowed || disabled}
|
||||
disabled={disabled}
|
||||
>
|
||||
<CustomSelect.Option value={null}>
|
||||
<>
|
||||
@ -74,7 +57,5 @@ export const SidebarEstimateSelect: React.FC<Props> = ({
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
</CustomSelect>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -3,51 +3,43 @@ import React from "react";
|
||||
// ui
|
||||
import { CustomSelect } from "components/ui";
|
||||
// icons
|
||||
import { ChartBarIcon } from "@heroicons/react/24/outline";
|
||||
import { getPriorityIcon } from "components/icons/priority-icon";
|
||||
// types
|
||||
import { UserAuth } from "types";
|
||||
// constants
|
||||
import { PRIORITIES } from "constants/project";
|
||||
|
||||
type Props = {
|
||||
value: string | null;
|
||||
onChange: (val: string) => void;
|
||||
userAuth: UserAuth;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const SidebarPrioritySelect: React.FC<Props> = ({
|
||||
value,
|
||||
onChange,
|
||||
userAuth,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled;
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center py-2">
|
||||
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:basis-1/2">
|
||||
<ChartBarIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<p>Priority</p>
|
||||
</div>
|
||||
<div className="sm:basis-1/2">
|
||||
export const SidebarPrioritySelect: React.FC<Props> = ({ value, onChange, disabled = false }) => (
|
||||
<CustomSelect
|
||||
label={
|
||||
<div className="flex items-center gap-2 text-left capitalize">
|
||||
<span className={`${value ? "text-custom-text-100" : "text-custom-text-200"}`}>
|
||||
{getPriorityIcon(value ?? "None", "text-sm")}
|
||||
customButton={
|
||||
<button
|
||||
type="button"
|
||||
className={`flex items-center gap-1.5 text-left text-sm capitalize rounded px-2.5 py-0.5 ${
|
||||
value === "urgent"
|
||||
? "border-red-500/20 bg-red-500/20 text-red-500"
|
||||
: value === "high"
|
||||
? "border-orange-500/20 bg-orange-500/20 text-orange-500"
|
||||
: value === "medium"
|
||||
? "border-yellow-500/20 bg-yellow-500/20 text-yellow-500"
|
||||
: value === "low"
|
||||
? "border-green-500/20 bg-green-500/20 text-green-500"
|
||||
: "bg-custom-background-80 border-custom-border-200"
|
||||
}`}
|
||||
>
|
||||
<span className="grid place-items-center -my-1">
|
||||
{getPriorityIcon(value ?? "None", "!text-sm")}
|
||||
</span>
|
||||
<span className={`${value ? "text-custom-text-100" : "text-custom-text-200"}`}>
|
||||
{value ?? "None"}
|
||||
</span>
|
||||
</div>
|
||||
<span>{value ?? "None"}</span>
|
||||
</button>
|
||||
}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
width="w-full"
|
||||
position="right"
|
||||
disabled={isNotAllowed}
|
||||
optionsClassName="w-min"
|
||||
disabled={disabled}
|
||||
>
|
||||
{PRIORITIES.map((option) => (
|
||||
<CustomSelect.Option key={option} value={option} className="capitalize">
|
||||
@ -58,7 +50,4 @@ export const SidebarPrioritySelect: React.FC<Props> = ({
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
</CustomSelect>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -9,29 +9,20 @@ import stateService from "services/state.service";
|
||||
// ui
|
||||
import { Spinner, CustomSelect } from "components/ui";
|
||||
// icons
|
||||
import { Squares2X2Icon } from "@heroicons/react/24/outline";
|
||||
import { getStateGroupIcon } from "components/icons";
|
||||
// helpers
|
||||
import { getStatesList } from "helpers/state.helper";
|
||||
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
||||
// types
|
||||
import { UserAuth } from "types";
|
||||
// constants
|
||||
import { STATES_LIST } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
value: string;
|
||||
onChange: (val: string) => void;
|
||||
userAuth: UserAuth;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const SidebarStateSelect: React.FC<Props> = ({
|
||||
value,
|
||||
onChange,
|
||||
userAuth,
|
||||
disabled = false,
|
||||
}) => {
|
||||
export const SidebarStateSelect: React.FC<Props> = ({ value, onChange, disabled = false }) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, inboxIssueId } = router.query;
|
||||
|
||||
@ -45,41 +36,35 @@ export const SidebarStateSelect: React.FC<Props> = ({
|
||||
|
||||
const selectedState = states?.find((s) => s.id === value);
|
||||
|
||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled;
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center py-2">
|
||||
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:basis-1/2">
|
||||
<Squares2X2Icon className="h-4 w-4 flex-shrink-0" />
|
||||
<p>State</p>
|
||||
</div>
|
||||
<div className="sm:basis-1/2">
|
||||
<CustomSelect
|
||||
label={
|
||||
selectedState ? (
|
||||
<div className="flex items-center gap-2 text-left text-custom-text-100">
|
||||
customButton={
|
||||
<button type="button" className="bg-custom-background-80 text-sm rounded px-2.5 py-0.5">
|
||||
{selectedState ? (
|
||||
<div className="flex items-center gap-1.5 text-left text-custom-text-100">
|
||||
{getStateGroupIcon(
|
||||
selectedState?.group ?? "backlog",
|
||||
"16",
|
||||
"16",
|
||||
"14",
|
||||
"14",
|
||||
selectedState?.color ?? ""
|
||||
)}
|
||||
{addSpaceIfCamelCase(selectedState?.name ?? "")}
|
||||
</div>
|
||||
) : inboxIssueId ? (
|
||||
<div className="flex items-center gap-2 text-left text-custom-text-100">
|
||||
{getStateGroupIcon("backlog", "16", "16", "#ff7700")}
|
||||
<div className="flex items-center gap-1.5 text-left text-custom-text-100">
|
||||
{getStateGroupIcon("backlog", "14", "14", "#ff7700")}
|
||||
Triage
|
||||
</div>
|
||||
) : (
|
||||
"None"
|
||||
)
|
||||
)}
|
||||
</button>
|
||||
}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
width="w-full"
|
||||
position="right"
|
||||
disabled={isNotAllowed}
|
||||
optionsClassName="w-min"
|
||||
position="left"
|
||||
disabled={disabled}
|
||||
>
|
||||
{states ? (
|
||||
states.length > 0 ? (
|
||||
@ -98,7 +83,5 @@ export const SidebarStateSelect: React.FC<Props> = ({
|
||||
<Spinner />
|
||||
)}
|
||||
</CustomSelect>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -33,7 +33,16 @@ import {
|
||||
// ui
|
||||
import { CustomDatePicker, Icon } from "components/ui";
|
||||
// icons
|
||||
import { LinkIcon, CalendarDaysIcon, TrashIcon, PlusIcon } from "@heroicons/react/24/outline";
|
||||
import {
|
||||
LinkIcon,
|
||||
CalendarDaysIcon,
|
||||
TrashIcon,
|
||||
PlusIcon,
|
||||
Squares2X2Icon,
|
||||
ChartBarIcon,
|
||||
UserGroupIcon,
|
||||
PlayIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
// helpers
|
||||
import { copyTextToClipboard } from "helpers/string.helper";
|
||||
// types
|
||||
@ -332,6 +341,12 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
||||
{showFirstSection && (
|
||||
<div className="py-1">
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("state")) && (
|
||||
<div className="flex flex-wrap items-center py-2">
|
||||
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:basis-1/2">
|
||||
<Squares2X2Icon className="h-4 w-4 flex-shrink-0" />
|
||||
<p>State</p>
|
||||
</div>
|
||||
<div className="sm:basis-1/2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="state"
|
||||
@ -339,13 +354,20 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
||||
<SidebarStateSelect
|
||||
value={value}
|
||||
onChange={(val: string) => submitChanges({ state: val })}
|
||||
userAuth={memberRole}
|
||||
disabled={uneditable}
|
||||
disabled={memberRole.isGuest || memberRole.isViewer || uneditable}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("assignee")) && (
|
||||
<div className="flex flex-wrap items-center py-2">
|
||||
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:basis-1/2">
|
||||
<UserGroupIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<p>Assignees</p>
|
||||
</div>
|
||||
<div className="sm:basis-1/2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="assignees_list"
|
||||
@ -353,13 +375,20 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
||||
<SidebarAssigneeSelect
|
||||
value={value}
|
||||
onChange={(val: string[]) => submitChanges({ assignees_list: val })}
|
||||
userAuth={memberRole}
|
||||
disabled={uneditable}
|
||||
disabled={memberRole.isGuest || memberRole.isViewer || uneditable}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("priority")) && (
|
||||
<div className="flex flex-wrap items-center py-2">
|
||||
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:basis-1/2">
|
||||
<ChartBarIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<p>Priority</p>
|
||||
</div>
|
||||
<div className="sm:basis-1/2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="priority"
|
||||
@ -367,25 +396,35 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
||||
<SidebarPrioritySelect
|
||||
value={value}
|
||||
onChange={(val: string) => submitChanges({ priority: val })}
|
||||
userAuth={memberRole}
|
||||
disabled={uneditable}
|
||||
disabled={memberRole.isGuest || memberRole.isViewer || uneditable}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("estimate")) && (
|
||||
<div className="flex flex-wrap items-center py-2">
|
||||
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:basis-1/2">
|
||||
<PlayIcon className="h-4 w-4 flex-shrink-0 -rotate-90" />
|
||||
<p>Estimate</p>
|
||||
</div>
|
||||
<div className="sm:basis-1/2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="estimate_point"
|
||||
render={({ field: { value } }) => (
|
||||
<SidebarEstimateSelect
|
||||
value={value}
|
||||
onChange={(val: number | null) => submitChanges({ estimate_point: val })}
|
||||
userAuth={memberRole}
|
||||
disabled={uneditable}
|
||||
onChange={(val: number | null) =>
|
||||
submitChanges({ estimate_point: val })
|
||||
}
|
||||
disabled={memberRole.isGuest || memberRole.isViewer || uneditable}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
Loading…
Reference in New Issue
Block a user