mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
feat: select blocker, blocking, and parent (#2121)
* feat: update, delete link refactor: using old fetch-key * feat: issue activity with ability to view & add comment feat: click on view more to view more options in the issue detail * fix: upload image not working on mobile * feat: select blocker, blocking, and parent dev: auth layout for web-view, console.log callback for web-view * style: made design consistant * fix: displaying page only on web-view * style: removed overflow hidden
This commit is contained in:
parent
6d52707ff5
commit
faa6a2bcbc
@ -120,7 +120,11 @@ export const AddComment: React.FC<Props> = ({ disabled = false, onSubmit }) => {
|
||||
</div>
|
||||
|
||||
<div className="inline">
|
||||
<PrimaryButton type="submit" disabled={isSubmitting || disabled} className="mt-2">
|
||||
<PrimaryButton
|
||||
type="submit"
|
||||
disabled={isSubmitting || disabled}
|
||||
className="mt-2 w-10 h-10 flex items-center justify-center"
|
||||
>
|
||||
<Send className="w-4 h-4" />
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
|
@ -12,3 +12,6 @@ export * from "./issue-activity";
|
||||
export * from "./select-assignee";
|
||||
export * from "./select-estimate";
|
||||
export * from "./add-comment";
|
||||
export * from "./select-parent";
|
||||
export * from "./select-blocker";
|
||||
export * from "./select-blocked";
|
||||
|
@ -44,7 +44,6 @@ export const IssueActivity: React.FC<Props> = (props) => {
|
||||
const { workspaceSlug, projectId, issueId } = router.query;
|
||||
|
||||
const { user } = useUser();
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { data: issueActivities, mutate: mutateIssueActivity } = useSWR(
|
||||
workspaceSlug && projectId && issueId ? PROJECT_ISSUES_ACTIVITY(issueId.toString()) : null,
|
||||
@ -104,11 +103,14 @@ export const IssueActivity: React.FC<Props> = (props) => {
|
||||
mutate(PROJECT_ISSUES_ACTIVITY(issueDetails.id));
|
||||
})
|
||||
.catch(() =>
|
||||
setToastAlert({
|
||||
console.log(
|
||||
"toast",
|
||||
JSON.stringify({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Comment could not be posted. Please try again.",
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -17,11 +17,8 @@ import { useDropzone } from "react-dropzone";
|
||||
// fetch key
|
||||
import { ISSUE_ATTACHMENTS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
|
||||
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
|
||||
// icons
|
||||
import { ChevronRightIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||
import { FileText, ChevronRight, X, Image as ImageIcon } from "lucide-react";
|
||||
|
||||
// components
|
||||
import { Label, WebViewModal } from "components/web-view";
|
||||
@ -34,6 +31,8 @@ type Props = {
|
||||
allowed: boolean;
|
||||
};
|
||||
|
||||
const isImage = (fileName: string) => /\.(gif|jpe?g|tiff?|png|webp|bmp)$/i.test(fileName);
|
||||
|
||||
export const IssueAttachments: React.FC<Props> = (props) => {
|
||||
const { allowed } = props;
|
||||
|
||||
@ -46,8 +45,6 @@ export const IssueAttachments: React.FC<Props> = (props) => {
|
||||
const [deleteAttachment, setDeleteAttachment] = useState<IIssueAttachment | null>(null);
|
||||
const [attachmentDeleteModal, setAttachmentDeleteModal] = useState<boolean>(false);
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const onDrop = useCallback(
|
||||
(acceptedFiles: File[]) => {
|
||||
if (!acceptedFiles[0] || !workspaceSlug) return;
|
||||
@ -77,23 +74,30 @@ export const IssueAttachments: React.FC<Props> = (props) => {
|
||||
false
|
||||
);
|
||||
mutate(PROJECT_ISSUES_ACTIVITY(issueId as string));
|
||||
setToastAlert({
|
||||
console.log(
|
||||
"toast",
|
||||
JSON.stringify({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
message: "File added successfully.",
|
||||
});
|
||||
})
|
||||
);
|
||||
setIsOpen(false);
|
||||
setIsLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
setIsLoading(false);
|
||||
setToastAlert({
|
||||
console.log(
|
||||
"toast",
|
||||
JSON.stringify({
|
||||
type: "error",
|
||||
title: "error!",
|
||||
message: "Something went wrong. please check file type & size (max 5 MB)",
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
},
|
||||
[issueId, projectId, setToastAlert, workspaceSlug]
|
||||
[issueId, projectId, workspaceSlug]
|
||||
);
|
||||
|
||||
const { getRootProps, getInputProps } = useDropzone({
|
||||
@ -136,7 +140,7 @@ export const IssueAttachments: React.FC<Props> = (props) => {
|
||||
) : (
|
||||
<>
|
||||
<h3 className="text-lg">Upload</h3>
|
||||
<ChevronRightIcon className="w-5 h-5" />
|
||||
<ChevronRight className="w-5 h-5" />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@ -151,8 +155,13 @@ export const IssueAttachments: React.FC<Props> = (props) => {
|
||||
className="px-3 border border-custom-border-200 rounded-[4px] py-2 flex justify-between items-center bg-custom-background-100"
|
||||
>
|
||||
<Link href={attachment.asset}>
|
||||
<a target="_blank" className="text-custom-text-200 truncate">
|
||||
{attachment.attributes.name}
|
||||
<a target="_blank" className="text-custom-text-200 truncate flex items-center">
|
||||
{isImage(attachment.attributes.name) ? (
|
||||
<ImageIcon className="w-5 h-5 mr-2 flex-shrink-0 text-custom-text-400" />
|
||||
) : (
|
||||
<FileText className="w-5 h-5 mr-2 flex-shrink-0 text-custom-text-400" />
|
||||
)}
|
||||
<span className="truncate">{attachment.attributes.name}</span>
|
||||
</a>
|
||||
</Link>
|
||||
{allowed && (
|
||||
@ -163,7 +172,7 @@ export const IssueAttachments: React.FC<Props> = (props) => {
|
||||
setAttachmentDeleteModal(true);
|
||||
}}
|
||||
>
|
||||
<XMarkIcon className="w-5 h-5 text-custom-text-100" />
|
||||
<X className="w-[18px] h-[18px] text-custom-text-400" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@ -171,7 +180,7 @@ export const IssueAttachments: React.FC<Props> = (props) => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsOpen(true)}
|
||||
className="bg-custom-primary-100/10 border border-dotted border-custom-primary-100 text-center py-2 w-full text-custom-primary-100"
|
||||
className="bg-custom-primary-100/10 border border-dotted rounded-[4px] border-custom-primary-100 text-center py-2 w-full text-custom-primary-100"
|
||||
>
|
||||
Click to upload file here
|
||||
</button>
|
||||
|
@ -12,7 +12,8 @@ import { mutate } from "swr";
|
||||
import issuesService from "services/issues.service";
|
||||
|
||||
// icons
|
||||
import { LinkIcon, PlusIcon, PencilIcon, TrashIcon } from "@heroicons/react/24/outline";
|
||||
// import { LinkIcon, PlusIcon, PencilIcon, TrashIcon } from "@heroicons/react/24/outline";
|
||||
import { Link as LinkIcon, Plus, Pencil, X } from "lucide-react";
|
||||
|
||||
// components
|
||||
import { Label, WebViewModal, CreateUpdateLinkForm } from "components/web-view";
|
||||
@ -108,7 +109,7 @@ export const IssueLinks: React.FC<Props> = (props) => {
|
||||
setSelectedLink(link.id);
|
||||
}}
|
||||
>
|
||||
<PencilIcon className="w-5 h-5 text-custom-text-100" />
|
||||
<Pencil className="w-[18px] h-[18px] text-custom-text-400" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@ -116,7 +117,7 @@ export const IssueLinks: React.FC<Props> = (props) => {
|
||||
handleDeleteLink(link.id);
|
||||
}}
|
||||
>
|
||||
<TrashIcon className="w-5 h-5 text-red-500 hover:bg-red-500/20" />
|
||||
<X className="w-[18px] h-[18px] text-custom-text-400" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@ -128,7 +129,7 @@ export const IssueLinks: React.FC<Props> = (props) => {
|
||||
onClick={() => setIsOpen(true)}
|
||||
className="w-full !py-2 text-custom-text-300 !text-base flex items-center justify-center"
|
||||
>
|
||||
<PlusIcon className="w-4 h-4 inline-block mr-1" />
|
||||
<Plus className="w-[18px] h-[18px] inline-block mr-1" />
|
||||
<span>Add</span>
|
||||
</SecondaryButton>
|
||||
</div>
|
||||
|
@ -1,17 +1,21 @@
|
||||
// react
|
||||
import React, { useState } from "react";
|
||||
|
||||
// next
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// react hook forms
|
||||
import { Control, Controller } from "react-hook-form";
|
||||
import { Control, Controller, useWatch } from "react-hook-form";
|
||||
|
||||
// icons
|
||||
import { ChevronDownIcon, PlayIcon } from "lucide-react";
|
||||
import { BlockedIcon, BlockerIcon } from "components/icons";
|
||||
import { ChevronDown, PlayIcon, User, X, CalendarDays, LayoutGrid, Users } from "lucide-react";
|
||||
|
||||
// hooks
|
||||
import useEstimateOption from "hooks/use-estimate-option";
|
||||
|
||||
// ui
|
||||
import { Icon, SecondaryButton } from "components/ui";
|
||||
import { SecondaryButton, CustomDatePicker } from "components/ui";
|
||||
|
||||
// components
|
||||
import {
|
||||
@ -20,6 +24,8 @@ import {
|
||||
PrioritySelect,
|
||||
AssigneeSelect,
|
||||
EstimateSelect,
|
||||
ParentSelect,
|
||||
BlockerSelect,
|
||||
} from "components/web-view";
|
||||
|
||||
// types
|
||||
@ -33,6 +39,24 @@ type Props = {
|
||||
export const IssuePropertiesDetail: React.FC<Props> = (props) => {
|
||||
const { control, submitChanges } = props;
|
||||
|
||||
const blockerIssue = useWatch({
|
||||
control,
|
||||
name: "blocker_issues",
|
||||
});
|
||||
|
||||
const blockedIssue = useWatch({
|
||||
control,
|
||||
name: "blocked_issues",
|
||||
});
|
||||
|
||||
const startDate = useWatch({
|
||||
control,
|
||||
name: "start_date",
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const [isViewAllOpen, setIsViewAllOpen] = useState(false);
|
||||
|
||||
const { isEstimateActive } = useEstimateOption();
|
||||
@ -43,8 +67,8 @@ export const IssuePropertiesDetail: React.FC<Props> = (props) => {
|
||||
<div className="mb-[6px]">
|
||||
<div className="border border-custom-border-200 rounded-[4px] p-2 flex justify-between items-center">
|
||||
<div className="flex items-center gap-1">
|
||||
<Icon iconName="grid_view" />
|
||||
<span className="text-sm text-custom-text-200">State</span>
|
||||
<LayoutGrid className="h-4 w-4 flex-shrink-0 text-custom-text-400" />
|
||||
<span className="text-sm text-custom-text-400">State</span>
|
||||
</div>
|
||||
<div>
|
||||
<Controller
|
||||
@ -63,8 +87,20 @@ export const IssuePropertiesDetail: React.FC<Props> = (props) => {
|
||||
<div className="mb-[6px]">
|
||||
<div className="border border-custom-border-200 rounded-[4px] p-2 flex justify-between items-center">
|
||||
<div className="flex items-center gap-1">
|
||||
<Icon iconName="signal_cellular_alt" />
|
||||
<span className="text-sm text-custom-text-200">Priority</span>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M13.5862 14.5239C13.3459 14.5239 13.1416 14.4398 12.9733 14.2715C12.805 14.1032 12.7209 13.8989 12.7209 13.6585V3.76429C12.7209 3.52391 12.805 3.31958 12.9733 3.15132C13.1416 2.98306 13.3459 2.89893 13.5862 2.89893C13.8266 2.89893 14.031 2.98306 14.1992 3.15132C14.3675 3.31958 14.4516 3.52391 14.4516 3.76429V13.6585C14.4516 13.8989 14.3675 14.1032 14.1992 14.2715C14.031 14.4398 13.8266 14.5239 13.5862 14.5239ZM5.1629 14.5239C5.04676 14.5239 4.93557 14.5018 4.82932 14.4576C4.72308 14.4133 4.63006 14.3513 4.55025 14.2715C4.47045 14.1917 4.40843 14.0986 4.36419 13.9922C4.31996 13.8858 4.29785 13.7746 4.29785 13.6585V11.2643C4.29785 11.0239 4.38198 10.8196 4.55025 10.6513C4.71851 10.4831 4.92283 10.3989 5.16322 10.3989C5.40359 10.3989 5.60791 10.4831 5.77618 10.6513C5.94445 10.8196 6.02859 11.0239 6.02859 11.2643V13.6585C6.02859 13.7746 6.00647 13.8858 5.96223 13.9922C5.91801 14.0986 5.85599 14.1917 5.77618 14.2715C5.69638 14.3513 5.60325 14.4133 5.49678 14.4576C5.39033 14.5018 5.27904 14.5239 5.1629 14.5239ZM9.37473 14.5239C9.13436 14.5239 8.93003 14.4398 8.76176 14.2715C8.59349 14.1032 8.50936 13.8989 8.50936 13.6585V7.5143C8.50936 7.27391 8.59349 7.06958 8.76176 6.90132C8.93003 6.73306 9.13436 6.64893 9.37473 6.64893C9.61511 6.64893 9.81943 6.73306 9.98771 6.90132C10.156 7.06958 10.2401 7.27391 10.2401 7.5143V13.6585C10.2401 13.8989 10.156 14.1032 9.98771 14.2715C9.81943 14.4398 9.61511 14.5239 9.37473 14.5239Z"
|
||||
fill="#A3A3A3"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<span className="text-sm text-custom-text-400">Priority</span>
|
||||
</div>
|
||||
<div>
|
||||
<Controller
|
||||
@ -83,8 +119,8 @@ export const IssuePropertiesDetail: React.FC<Props> = (props) => {
|
||||
<div className="mb-[6px]">
|
||||
<div className="border border-custom-border-200 rounded-[4px] p-2 flex justify-between items-center">
|
||||
<div className="flex items-center gap-1">
|
||||
<Icon iconName="person" />
|
||||
<span className="text-sm text-custom-text-200">Assignee</span>
|
||||
<Users className="h-4 w-4 flex-shrink-0 text-custom-text-400" />
|
||||
<span className="text-sm text-custom-text-400">Assignee</span>
|
||||
</div>
|
||||
<div>
|
||||
<Controller
|
||||
@ -106,8 +142,8 @@ export const IssuePropertiesDetail: React.FC<Props> = (props) => {
|
||||
<div className="mb-[6px]">
|
||||
<div className="border border-custom-border-200 rounded-[4px] p-2 flex justify-between items-center">
|
||||
<div className="flex items-center gap-1">
|
||||
<PlayIcon className="h-4 w-4 flex-shrink-0 -rotate-90" />
|
||||
<span className="text-sm text-custom-text-200">Estimate</span>
|
||||
<PlayIcon className="h-4 w-4 flex-shrink-0 -rotate-90 text-custom-text-400" />
|
||||
<span className="text-sm text-custom-text-400">Estimate</span>
|
||||
</div>
|
||||
<div>
|
||||
<Controller
|
||||
@ -124,6 +160,179 @@ export const IssuePropertiesDetail: React.FC<Props> = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-[6px]">
|
||||
<div className="border border-custom-border-200 rounded-[4px] p-2 flex justify-between items-center">
|
||||
<div className="flex items-center gap-1">
|
||||
<User className="h-4 w-4 flex-shrink-0 text-custom-text-400" />
|
||||
<span className="text-sm text-custom-text-400">Parent</span>
|
||||
</div>
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="parent"
|
||||
render={({ field: { value } }) => (
|
||||
<ParentSelect
|
||||
value={value}
|
||||
onChange={(val) => submitChanges({ parent: val })}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-[6px]">
|
||||
<div className="border border-custom-border-200 rounded-[4px] p-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-1">
|
||||
<BlockerIcon height={16} width={16} />
|
||||
<span className="text-sm text-custom-text-400">Blocking</span>
|
||||
</div>
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="blocker_issues"
|
||||
render={({ field: { value } }) => (
|
||||
<BlockerSelect
|
||||
value={value}
|
||||
onChange={(val) =>
|
||||
submitChanges({
|
||||
blocker_issues: val,
|
||||
blockers_list: val?.map((i: any) => i.blocker_issue_detail?.id ?? ""),
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{blockerIssue &&
|
||||
blockerIssue.map((issue) => (
|
||||
<div
|
||||
key={issue.blocker_issue_detail?.id}
|
||||
className="group inline-flex mr-1 cursor-pointer items-center gap-1 rounded-2xl border border-custom-border-200 px-1.5 py-0.5 text-xs text-yellow-500 duration-300 hover:border-yellow-500/20 hover:bg-yellow-500/20"
|
||||
>
|
||||
<a
|
||||
href={`/${workspaceSlug}/projects/${issue.blocker_issue_detail?.project_detail.id}/issues/${issue.blocker_issue_detail?.id}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<BlockerIcon height={10} width={10} />
|
||||
{`${issue.blocker_issue_detail?.project_detail.identifier}-${issue.blocker_issue_detail?.sequence_id}`}
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
className="duration-300"
|
||||
onClick={() => {
|
||||
const updatedBlockers = blockerIssue.filter(
|
||||
(i) => i.blocker_issue_detail?.id !== issue.blocker_issue_detail?.id
|
||||
);
|
||||
|
||||
submitChanges({
|
||||
blocker_issues: updatedBlockers,
|
||||
blockers_list: updatedBlockers.map(
|
||||
(i) => i.blocker_issue_detail?.id ?? ""
|
||||
),
|
||||
});
|
||||
}}
|
||||
>
|
||||
<X className="h-2 w-2" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-[6px]">
|
||||
<div className="border border-custom-border-200 rounded-[4px] p-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-1">
|
||||
<BlockedIcon height={16} width={16} />
|
||||
<span className="text-sm text-custom-text-400">Blocked by</span>
|
||||
</div>
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="blocked_issues"
|
||||
render={({ field: { value } }) => (
|
||||
<BlockerSelect
|
||||
value={value}
|
||||
onChange={(val) =>
|
||||
submitChanges({
|
||||
blocked_issues: val,
|
||||
blocks_list: val?.map((i: any) => i.blocker_issue_detail?.id ?? ""),
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{blockedIssue &&
|
||||
blockedIssue.map((issue) => (
|
||||
<div
|
||||
key={issue.blocked_issue_detail?.id}
|
||||
className="group inline-flex mr-1 cursor-pointer items-center gap-1 rounded-2xl border border-custom-border-200 px-1.5 py-0.5 text-xs text-red-500 duration-300 hover:border-red-500/20 hover:bg-red-500/20"
|
||||
>
|
||||
<a
|
||||
href={`/${workspaceSlug}/projects/${issue.blocked_issue_detail?.project_detail.id}/issues/${issue.blocked_issue_detail?.id}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<BlockedIcon height={10} width={10} />
|
||||
{`${issue?.blocked_issue_detail?.project_detail?.identifier}-${issue?.blocked_issue_detail?.sequence_id}`}
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
className="duration-300"
|
||||
onClick={() => {
|
||||
const updatedBlocked = blockedIssue.filter(
|
||||
(i) => i.blocked_issue_detail?.id !== issue.blocked_issue_detail?.id
|
||||
);
|
||||
|
||||
submitChanges({
|
||||
blocked_issues: updatedBlocked,
|
||||
blocks_list: updatedBlocked.map((i) => i.blocked_issue_detail?.id ?? ""),
|
||||
});
|
||||
}}
|
||||
>
|
||||
<X className="h-2 w-2" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-[6px]">
|
||||
<div className="border border-custom-border-200 rounded-[4px] p-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-1">
|
||||
<CalendarDays className="w-4 h-4 text-custom-text-400" />
|
||||
<span className="text-sm text-custom-text-400">Due date</span>
|
||||
</div>
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="target_date"
|
||||
render={({ field: { value } }) => (
|
||||
<CustomDatePicker
|
||||
placeholder="Due date"
|
||||
value={value}
|
||||
wrapperClassName="w-full"
|
||||
onChange={(val) =>
|
||||
submitChanges({
|
||||
target_date: val,
|
||||
})
|
||||
}
|
||||
className="border-transparent !shadow-none !w-[6.75rem]"
|
||||
minDate={startDate ? new Date(startDate) : undefined}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="mb-[6px]">
|
||||
@ -135,7 +344,7 @@ export const IssuePropertiesDetail: React.FC<Props> = (props) => {
|
||||
<span className="text-base text-custom-primary-100">
|
||||
{isViewAllOpen ? "View less" : "View all"}
|
||||
</span>
|
||||
<ChevronDownIcon
|
||||
<ChevronDown
|
||||
size={16}
|
||||
className={`ml-1 text-custom-primary-100 ${isViewAllOpen ? "-rotate-180" : ""}`}
|
||||
/>
|
||||
|
@ -8,7 +8,7 @@ import { useRouter } from "next/router";
|
||||
import useSWR from "swr";
|
||||
|
||||
// icons
|
||||
import { ChevronDownIcon } from "@heroicons/react/24/outline";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
|
||||
// services
|
||||
import projectService from "services/project.service";
|
||||
@ -87,8 +87,7 @@ export const AssigneeSelect: React.FC<Props> = (props) => {
|
||||
) : (
|
||||
"No assignees"
|
||||
)}
|
||||
{/* {selectedAssignee?.member.display_name || "Select assignee"} */}
|
||||
<ChevronDownIcon className="w-5 h-5" />
|
||||
<ChevronDown className="w-5 h-5" />
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
|
87
web/components/web-view/select-blocked.tsx
Normal file
87
web/components/web-view/select-blocked.tsx
Normal file
@ -0,0 +1,87 @@
|
||||
// react
|
||||
import React, { useState } from "react";
|
||||
|
||||
// next
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
|
||||
// icons
|
||||
import { ChevronDown } from "lucide-react";
|
||||
|
||||
// components
|
||||
import { ExistingIssuesListModal } from "components/core";
|
||||
|
||||
// types
|
||||
import { BlockeIssueDetail, ISearchIssueResponse } from "types";
|
||||
|
||||
type Props = {
|
||||
value: any;
|
||||
onChange: (value: any) => void;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const BlockedSelect: React.FC<Props> = (props) => {
|
||||
const { value, onChange, disabled = false } = props;
|
||||
|
||||
const [isBlockedModalOpen, setIsBlockedModalOpen] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { issueId } = router.query;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const onSubmit = async (data: ISearchIssueResponse[]) => {
|
||||
if (data.length === 0) {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Please select at least one issue.",
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedIssues: { blocker_issue_detail: BlockeIssueDetail }[] = data.map((i) => ({
|
||||
blocker_issue_detail: {
|
||||
id: i.id,
|
||||
name: i.name,
|
||||
sequence_id: i.sequence_id,
|
||||
project_detail: {
|
||||
id: i.project_id,
|
||||
identifier: i.project__identifier,
|
||||
name: i.project__name,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
onChange([...(value || []), ...selectedIssues]);
|
||||
|
||||
setIsBlockedModalOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ExistingIssuesListModal
|
||||
isOpen={isBlockedModalOpen}
|
||||
handleClose={() => setIsBlockedModalOpen(false)}
|
||||
searchParams={{ blocker_blocked_by: true, issue_id: issueId!.toString() }}
|
||||
handleOnSubmit={onSubmit}
|
||||
workspaceLevelToggle
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={() => setIsBlockedModalOpen(true)}
|
||||
className={
|
||||
"relative w-full px-2.5 py-0.5 text-base flex justify-between items-center gap-0.5 text-custom-text-100"
|
||||
}
|
||||
>
|
||||
<span className="text-custom-text-200">Select issue</span>
|
||||
<ChevronDown className="w-4 h-4 text-custom-text-200" />
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
87
web/components/web-view/select-blocker.tsx
Normal file
87
web/components/web-view/select-blocker.tsx
Normal file
@ -0,0 +1,87 @@
|
||||
// react
|
||||
import React, { useState } from "react";
|
||||
|
||||
// next
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
|
||||
// icons
|
||||
import { ChevronDown } from "lucide-react";
|
||||
|
||||
// components
|
||||
import { ExistingIssuesListModal } from "components/core";
|
||||
|
||||
// types
|
||||
import { BlockeIssueDetail, ISearchIssueResponse } from "types";
|
||||
|
||||
type Props = {
|
||||
value: any;
|
||||
onChange: (value: any) => void;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const BlockerSelect: React.FC<Props> = (props) => {
|
||||
const { value, onChange, disabled = false } = props;
|
||||
|
||||
const [isBlockerModalOpen, setIsBlockerModalOpen] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { issueId } = router.query;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const onSubmit = async (data: ISearchIssueResponse[]) => {
|
||||
if (data.length === 0) {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Please select at least one issue.",
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedIssues: { blocker_issue_detail: BlockeIssueDetail }[] = data.map((i) => ({
|
||||
blocker_issue_detail: {
|
||||
id: i.id,
|
||||
name: i.name,
|
||||
sequence_id: i.sequence_id,
|
||||
project_detail: {
|
||||
id: i.project_id,
|
||||
identifier: i.project__identifier,
|
||||
name: i.project__name,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
onChange([...(value || []), ...selectedIssues]);
|
||||
|
||||
setIsBlockerModalOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ExistingIssuesListModal
|
||||
isOpen={isBlockerModalOpen}
|
||||
handleClose={() => setIsBlockerModalOpen(false)}
|
||||
searchParams={{ blocker_blocked_by: true, issue_id: issueId!.toString() }}
|
||||
handleOnSubmit={onSubmit}
|
||||
workspaceLevelToggle
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={() => setIsBlockerModalOpen(true)}
|
||||
className={
|
||||
"relative w-full px-2.5 py-0.5 text-base flex justify-between items-center gap-0.5 text-custom-text-100"
|
||||
}
|
||||
>
|
||||
<span className="text-custom-text-200">Select issue</span>
|
||||
<ChevronDown className="w-4 h-4 text-custom-text-200" />
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
@ -2,7 +2,7 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
// icons
|
||||
import { ChevronDownIcon, PlayIcon } from "lucide-react";
|
||||
import { ChevronDown, PlayIcon } from "lucide-react";
|
||||
|
||||
// hooks
|
||||
import useEstimateOption from "hooks/use-estimate-option";
|
||||
@ -76,7 +76,7 @@ export const EstimateSelect: React.FC<Props> = (props) => {
|
||||
) : (
|
||||
"No estimate"
|
||||
)}
|
||||
<ChevronDownIcon className="w-5 h-5" />
|
||||
<ChevronDown className="w-5 h-5" />
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
|
76
web/components/web-view/select-parent.tsx
Normal file
76
web/components/web-view/select-parent.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
// react
|
||||
import React, { useState } from "react";
|
||||
|
||||
// next
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// swr
|
||||
import useSWR from "swr";
|
||||
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
|
||||
// fetch key
|
||||
import { ISSUE_DETAILS } from "constants/fetch-keys";
|
||||
|
||||
// components
|
||||
import { ParentIssuesListModal } from "components/issues";
|
||||
|
||||
// types
|
||||
import { ISearchIssueResponse } from "types";
|
||||
|
||||
type Props = {
|
||||
value: string | null;
|
||||
onChange: (value: any) => void;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const ParentSelect: React.FC<Props> = (props) => {
|
||||
const { value, onChange, disabled = false } = props;
|
||||
|
||||
const [isParentModalOpen, setIsParentModalOpen] = useState(false);
|
||||
const [selectedParentIssue, setSelectedParentIssue] = useState<ISearchIssueResponse | null>(null);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, issueId } = router.query;
|
||||
|
||||
const { data: issueDetails } = useSWR(
|
||||
workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId.toString()) : null,
|
||||
workspaceSlug && projectId && issueId
|
||||
? () =>
|
||||
issuesService.retrieve(workspaceSlug.toString(), projectId.toString(), issueId.toString())
|
||||
: null
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ParentIssuesListModal
|
||||
isOpen={isParentModalOpen}
|
||||
handleClose={() => setIsParentModalOpen(false)}
|
||||
onChange={(issue) => {
|
||||
onChange(issue.id);
|
||||
setSelectedParentIssue(issue);
|
||||
}}
|
||||
issueId={issueId as string}
|
||||
projectId={projectId as string}
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={() => setIsParentModalOpen(true)}
|
||||
className={
|
||||
"relative w-full px-2.5 py-0.5 text-base flex justify-between items-center gap-0.5 text-custom-text-100"
|
||||
}
|
||||
>
|
||||
{selectedParentIssue && issueDetails?.parent ? (
|
||||
`${selectedParentIssue.project__identifier}-${selectedParentIssue.sequence_id}`
|
||||
) : !selectedParentIssue && issueDetails?.parent ? (
|
||||
`${issueDetails.parent_detail?.project_detail.identifier}-${issueDetails.parent_detail?.sequence_id}`
|
||||
) : (
|
||||
<span className="text-custom-text-200">Select issue</span>
|
||||
)}
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
@ -2,7 +2,7 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
// icons
|
||||
import { ChevronDownIcon } from "lucide-react";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
|
||||
// constants
|
||||
import { PRIORITIES } from "constants/project";
|
||||
@ -76,7 +76,7 @@ export const PrioritySelect: React.FC<Props> = (props) => {
|
||||
}
|
||||
>
|
||||
{value ? capitalizeFirstLetter(value) : "None"}
|
||||
<ChevronDownIcon className="w-5 h-5" />
|
||||
<ChevronDown className="w-5 h-5" />
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
|
@ -8,7 +8,7 @@ import { useRouter } from "next/router";
|
||||
import useSWR from "swr";
|
||||
|
||||
// icons
|
||||
import { ChevronDownIcon } from "@heroicons/react/24/outline";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
|
||||
// services
|
||||
import stateService from "services/state.service";
|
||||
@ -82,7 +82,7 @@ export const StateSelect: React.FC<Props> = (props) => {
|
||||
}
|
||||
>
|
||||
{selectedState?.name || "Select a state"}
|
||||
<ChevronDownIcon className="w-5 h-5" />
|
||||
<ChevronDown className="w-5 h-5" />
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
|
@ -8,7 +8,7 @@ import { useRouter } from "next/router";
|
||||
import useSWR, { mutate } from "swr";
|
||||
|
||||
// icons
|
||||
import { XMarkIcon } from "@heroicons/react/24/outline";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
@ -98,7 +98,7 @@ export const SubIssueList: React.FC<Props> = (props) => {
|
||||
<p className="text-sm font-normal">{subIssue.name}</p>
|
||||
</div>
|
||||
<button type="button" onClick={() => handleSubIssueRemove(subIssue)}>
|
||||
<XMarkIcon className="w-5 h-5 text-custom-text-200" />
|
||||
<X className="w-[18px] h-[18px] text-custom-text-400" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
|
@ -47,7 +47,7 @@ export const WebViewModal = (props: Props) => {
|
||||
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="relative transform overflow-hidden rounded-none rounded-tr-[4px] rounded-tl-[4px] bg-custom-background-100 p-6 text-left shadow-xl transition-all sm:mt-8 w-full">
|
||||
<Dialog.Panel className="relative transform overflow-hidden rounded-none rounded-tr-[20px] rounded-tl-[20px] bg-custom-background-100 p-6 text-left shadow-xl transition-all sm:mt-8 w-full">
|
||||
<div className="flex justify-between items-center w-full">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
@ -84,9 +84,9 @@ type OptionsProps = {
|
||||
};
|
||||
|
||||
const Options: React.FC<OptionsProps> = ({ options }) => (
|
||||
<div className="space-y-6">
|
||||
<div className="divide-y">
|
||||
{options.map((option) => (
|
||||
<div key={option.value} className="flex items-center justify-between gap-2 py-2">
|
||||
<div key={option.value} className="flex items-center justify-between gap-2 py-[14px]">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
|
60
web/layouts/web-view-layout/index.tsx
Normal file
60
web/layouts/web-view-layout/index.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
// swr
|
||||
import useSWR from "swr";
|
||||
|
||||
// services
|
||||
import userService from "services/user.service";
|
||||
|
||||
// fetch keys
|
||||
import { CURRENT_USER } from "constants/fetch-keys";
|
||||
|
||||
// icons
|
||||
import { AlertCircle } from "lucide-react";
|
||||
|
||||
// ui
|
||||
import { Spinner } from "components/ui";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const getIfInWebview = (userAgent: NavigatorID["userAgent"]) => {
|
||||
if (/iphone|ipod|ipad/.test(userAgent) || userAgent.includes("wv")) return true;
|
||||
else return false;
|
||||
};
|
||||
|
||||
const useMobileDetect = () => {
|
||||
const userAgent = typeof navigator === "undefined" ? "SSR" : navigator.userAgent;
|
||||
return getIfInWebview(userAgent);
|
||||
};
|
||||
|
||||
const WebViewLayout: React.FC<Props> = ({ children }) => {
|
||||
const { data: currentUser, error } = useSWR(CURRENT_USER, () => userService.currentUser());
|
||||
|
||||
const isWebview = useMobileDetect();
|
||||
|
||||
if (!currentUser && !error) {
|
||||
return (
|
||||
<div className="h-screen grid place-items-center p-4">
|
||||
<div className="flex flex-col items-center gap-3 text-center">
|
||||
<h3 className="text-xl">Loading your profile...</h3>
|
||||
<Spinner />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-screen w-full bg-custom-background-100">
|
||||
{error || !isWebview ? (
|
||||
<div className="flex flex-col items-center justify-center gap-y-3 h-full text-center text-custom-text-200">
|
||||
<AlertCircle size={64} />
|
||||
<h2 className="text-2xl font-semibold">You are not authorized to view this page.</h2>
|
||||
</div>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WebViewLayout;
|
@ -21,7 +21,7 @@ import useUser from "hooks/use-user";
|
||||
import useProjectMembers from "hooks/use-project-members";
|
||||
|
||||
// layouts
|
||||
import DefaultLayout from "layouts/default-layout";
|
||||
import WebViewLayout from "layouts/web-view-layout";
|
||||
|
||||
// ui
|
||||
import { Spinner } from "components/ui";
|
||||
@ -128,25 +128,25 @@ const MobileWebViewIssueDetail = () => {
|
||||
|
||||
if (!error && !issueDetails)
|
||||
return (
|
||||
<DefaultLayout>
|
||||
<WebViewLayout>
|
||||
<div className="px-4 py-2 h-full">
|
||||
<div className="h-full flex justify-center items-center">
|
||||
<Spinner />
|
||||
Loading...
|
||||
</div>
|
||||
</div>
|
||||
</DefaultLayout>
|
||||
</WebViewLayout>
|
||||
);
|
||||
|
||||
if (error)
|
||||
return (
|
||||
<DefaultLayout>
|
||||
<WebViewLayout>
|
||||
<div className="px-4 py-2">{error?.response?.data || "Something went wrong"}</div>
|
||||
</DefaultLayout>
|
||||
</WebViewLayout>
|
||||
);
|
||||
|
||||
return (
|
||||
<DefaultLayout>
|
||||
<WebViewLayout>
|
||||
<div className="px-6 py-2 h-full overflow-auto space-y-3">
|
||||
<IssueWebViewForm
|
||||
isAllowed={isAllowed}
|
||||
@ -168,7 +168,7 @@ const MobileWebViewIssueDetail = () => {
|
||||
|
||||
<IssueActivity allowed={isAllowed} issueDetails={issueDetails!} />
|
||||
</div>
|
||||
</DefaultLayout>
|
||||
</WebViewLayout>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -8,11 +8,19 @@ const nonValidatedRoutes = [
|
||||
"/reset-password",
|
||||
"/workspace-member-invitation",
|
||||
"/sign-up",
|
||||
"/m/",
|
||||
];
|
||||
|
||||
const validateRouteCheck = (route: string): boolean => {
|
||||
let validationToggle = false;
|
||||
const routeCheck = nonValidatedRoutes.find((_route: string) => _route === route);
|
||||
|
||||
let routeCheck = false;
|
||||
nonValidatedRoutes.forEach((_route: string) => {
|
||||
if (route.includes(_route)) {
|
||||
routeCheck = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (routeCheck) validationToggle = true;
|
||||
return validationToggle;
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user