From faa6a2bcbcdcc1c800a6152caf087ec2a5ea0777 Mon Sep 17 00:00:00 2001 From: Dakshesh Jain <65905942+dakshesh14@users.noreply.github.com> Date: Thu, 7 Sep 2023 18:42:24 +0530 Subject: [PATCH] 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 --- web/components/web-view/add-comment.tsx | 6 +- web/components/web-view/index.ts | 3 + web/components/web-view/issue-activity.tsx | 14 +- web/components/web-view/issue-attachments.tsx | 53 ++-- web/components/web-view/issue-link-list.tsx | 9 +- .../web-view/issue-properties-detail.tsx | 233 +++++++++++++++++- web/components/web-view/select-assignee.tsx | 5 +- web/components/web-view/select-blocked.tsx | 87 +++++++ web/components/web-view/select-blocker.tsx | 87 +++++++ web/components/web-view/select-estimate.tsx | 4 +- web/components/web-view/select-parent.tsx | 76 ++++++ web/components/web-view/select-priority.tsx | 4 +- web/components/web-view/select-state.tsx | 4 +- web/components/web-view/sub-issues.tsx | 4 +- web/components/web-view/web-view-modal.tsx | 6 +- web/layouts/web-view-layout/index.tsx | 60 +++++ .../projects/[projectId]/issues/[issueId].tsx | 14 +- web/services/api.service.ts | 10 +- 18 files changed, 612 insertions(+), 67 deletions(-) create mode 100644 web/components/web-view/select-blocked.tsx create mode 100644 web/components/web-view/select-blocker.tsx create mode 100644 web/components/web-view/select-parent.tsx create mode 100644 web/layouts/web-view-layout/index.tsx diff --git a/web/components/web-view/add-comment.tsx b/web/components/web-view/add-comment.tsx index b5bff0cb5..b4f49d7be 100644 --- a/web/components/web-view/add-comment.tsx +++ b/web/components/web-view/add-comment.tsx @@ -120,7 +120,11 @@ export const AddComment: React.FC = ({ disabled = false, onSubmit }) => {
- +
diff --git a/web/components/web-view/index.ts b/web/components/web-view/index.ts index 2b87ad820..342bc4838 100644 --- a/web/components/web-view/index.ts +++ b/web/components/web-view/index.ts @@ -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"; diff --git a/web/components/web-view/issue-activity.tsx b/web/components/web-view/issue-activity.tsx index 39f6036c2..55089d60d 100644 --- a/web/components/web-view/issue-activity.tsx +++ b/web/components/web-view/issue-activity.tsx @@ -44,7 +44,6 @@ export const IssueActivity: React.FC = (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) => { mutate(PROJECT_ISSUES_ACTIVITY(issueDetails.id)); }) .catch(() => - setToastAlert({ - type: "error", - title: "Error!", - message: "Comment could not be posted. Please try again.", - }) + console.log( + "toast", + JSON.stringify({ + type: "error", + title: "Error!", + message: "Comment could not be posted. Please try again.", + }) + ) ); }; diff --git a/web/components/web-view/issue-attachments.tsx b/web/components/web-view/issue-attachments.tsx index 838ee8e44..ba4540318 100644 --- a/web/components/web-view/issue-attachments.tsx +++ b/web/components/web-view/issue-attachments.tsx @@ -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) => { const { allowed } = props; @@ -46,8 +45,6 @@ export const IssueAttachments: React.FC = (props) => { const [deleteAttachment, setDeleteAttachment] = useState(null); const [attachmentDeleteModal, setAttachmentDeleteModal] = useState(false); - const { setToastAlert } = useToast(); - const onDrop = useCallback( (acceptedFiles: File[]) => { if (!acceptedFiles[0] || !workspaceSlug) return; @@ -77,23 +74,30 @@ export const IssueAttachments: React.FC = (props) => { false ); mutate(PROJECT_ISSUES_ACTIVITY(issueId as string)); - setToastAlert({ - type: "success", - title: "Success!", - message: "File added successfully.", - }); + console.log( + "toast", + JSON.stringify({ + type: "success", + title: "Success!", + message: "File added successfully.", + }) + ); + setIsOpen(false); setIsLoading(false); }) .catch((err) => { setIsLoading(false); - setToastAlert({ - type: "error", - title: "error!", - message: "Something went wrong. please check file type & size (max 5 MB)", - }); + 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) => { ) : ( <>

Upload

- + )} @@ -151,8 +155,13 @@ export const IssueAttachments: React.FC = (props) => { className="px-3 border border-custom-border-200 rounded-[4px] py-2 flex justify-between items-center bg-custom-background-100" > - - {attachment.attributes.name} + + {isImage(attachment.attributes.name) ? ( + + ) : ( + + )} + {attachment.attributes.name} {allowed && ( @@ -163,7 +172,7 @@ export const IssueAttachments: React.FC = (props) => { setAttachmentDeleteModal(true); }} > - + )} @@ -171,7 +180,7 @@ export const IssueAttachments: React.FC = (props) => { diff --git a/web/components/web-view/issue-link-list.tsx b/web/components/web-view/issue-link-list.tsx index e7922ba8c..5f1da6a62 100644 --- a/web/components/web-view/issue-link-list.tsx +++ b/web/components/web-view/issue-link-list.tsx @@ -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) => { setSelectedLink(link.id); }} > - + )} @@ -128,7 +129,7 @@ export const IssueLinks: React.FC = (props) => { onClick={() => setIsOpen(true)} className="w-full !py-2 text-custom-text-300 !text-base flex items-center justify-center" > - + Add diff --git a/web/components/web-view/issue-properties-detail.tsx b/web/components/web-view/issue-properties-detail.tsx index 6fb03baa7..6a247365b 100644 --- a/web/components/web-view/issue-properties-detail.tsx +++ b/web/components/web-view/issue-properties-detail.tsx @@ -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) => { 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) => {
- - State + + State
= (props) => {
- - Priority + + + + + Priority
= (props) => {
- - Assignee + + Assignee
= (props) => {
- - Estimate + + Estimate
= (props) => {
)} +
+
+
+ + Parent +
+
+ ( + submitChanges({ parent: val })} + /> + )} + /> +
+
+
+
+
+
+
+ + Blocking +
+
+ ( + + submitChanges({ + blocker_issues: val, + blockers_list: val?.map((i: any) => i.blocker_issue_detail?.id ?? ""), + }) + } + /> + )} + /> +
+
+ {blockerIssue && + blockerIssue.map((issue) => ( +
+ + + {`${issue.blocker_issue_detail?.project_detail.identifier}-${issue.blocker_issue_detail?.sequence_id}`} + + +
+ ))} +
+
+
+
+
+
+ + Blocked by +
+
+ ( + + submitChanges({ + blocked_issues: val, + blocks_list: val?.map((i: any) => i.blocker_issue_detail?.id ?? ""), + }) + } + /> + )} + /> +
+
+ {blockedIssue && + blockedIssue.map((issue) => ( +
+ + + {`${issue?.blocked_issue_detail?.project_detail?.identifier}-${issue?.blocked_issue_detail?.sequence_id}`} + + +
+ ))} +
+
+ +
+
+
+
+ + Due date +
+
+ ( + + submitChanges({ + target_date: val, + }) + } + className="border-transparent !shadow-none !w-[6.75rem]" + minDate={startDate ? new Date(startDate) : undefined} + /> + )} + /> +
+
+
+
)}
@@ -135,7 +344,7 @@ export const IssuePropertiesDetail: React.FC = (props) => { {isViewAllOpen ? "View less" : "View all"} - diff --git a/web/components/web-view/select-assignee.tsx b/web/components/web-view/select-assignee.tsx index 13ebd377f..27eae5db1 100644 --- a/web/components/web-view/select-assignee.tsx +++ b/web/components/web-view/select-assignee.tsx @@ -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) => { ) : ( "No assignees" )} - {/* {selectedAssignee?.member.display_name || "Select assignee"} */} - + ); diff --git a/web/components/web-view/select-blocked.tsx b/web/components/web-view/select-blocked.tsx new file mode 100644 index 000000000..0056cad5c --- /dev/null +++ b/web/components/web-view/select-blocked.tsx @@ -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) => { + 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 ( + <> + setIsBlockedModalOpen(false)} + searchParams={{ blocker_blocked_by: true, issue_id: issueId!.toString() }} + handleOnSubmit={onSubmit} + workspaceLevelToggle + /> + + + + ); +}; diff --git a/web/components/web-view/select-blocker.tsx b/web/components/web-view/select-blocker.tsx new file mode 100644 index 000000000..a46cdfcaa --- /dev/null +++ b/web/components/web-view/select-blocker.tsx @@ -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) => { + 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 ( + <> + setIsBlockerModalOpen(false)} + searchParams={{ blocker_blocked_by: true, issue_id: issueId!.toString() }} + handleOnSubmit={onSubmit} + workspaceLevelToggle + /> + + + + ); +}; diff --git a/web/components/web-view/select-estimate.tsx b/web/components/web-view/select-estimate.tsx index 751375d7b..765e6187d 100644 --- a/web/components/web-view/select-estimate.tsx +++ b/web/components/web-view/select-estimate.tsx @@ -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) => { ) : ( "No estimate" )} - + ); diff --git a/web/components/web-view/select-parent.tsx b/web/components/web-view/select-parent.tsx new file mode 100644 index 000000000..e5975b7b5 --- /dev/null +++ b/web/components/web-view/select-parent.tsx @@ -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) => { + const { value, onChange, disabled = false } = props; + + const [isParentModalOpen, setIsParentModalOpen] = useState(false); + const [selectedParentIssue, setSelectedParentIssue] = useState(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 ( + <> + setIsParentModalOpen(false)} + onChange={(issue) => { + onChange(issue.id); + setSelectedParentIssue(issue); + }} + issueId={issueId as string} + projectId={projectId as string} + /> + + + + ); +}; diff --git a/web/components/web-view/select-priority.tsx b/web/components/web-view/select-priority.tsx index adb12714a..87067bc7d 100644 --- a/web/components/web-view/select-priority.tsx +++ b/web/components/web-view/select-priority.tsx @@ -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) => { } > {value ? capitalizeFirstLetter(value) : "None"} - + ); diff --git a/web/components/web-view/select-state.tsx b/web/components/web-view/select-state.tsx index c28d30f19..3d530dd48 100644 --- a/web/components/web-view/select-state.tsx +++ b/web/components/web-view/select-state.tsx @@ -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) => { } > {selectedState?.name || "Select a state"} - + ); diff --git a/web/components/web-view/sub-issues.tsx b/web/components/web-view/sub-issues.tsx index 8110e5895..4299d9e3a 100644 --- a/web/components/web-view/sub-issues.tsx +++ b/web/components/web-view/sub-issues.tsx @@ -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) => {

{subIssue.name}

))} diff --git a/web/components/web-view/web-view-modal.tsx b/web/components/web-view/web-view-modal.tsx index 93f9ab46d..2be28a01c 100644 --- a/web/components/web-view/web-view-modal.tsx +++ b/web/components/web-view/web-view-modal.tsx @@ -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" > - +
= ({ options }) => ( -
+
{options.map((option) => ( -
+
{ + 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 = ({ children }) => { + const { data: currentUser, error } = useSWR(CURRENT_USER, () => userService.currentUser()); + + const isWebview = useMobileDetect(); + + if (!currentUser && !error) { + return ( +
+
+

Loading your profile...

+ +
+
+ ); + } + + return ( +
+ {error || !isWebview ? ( +
+ +

You are not authorized to view this page.

+
+ ) : ( + children + )} +
+ ); +}; + +export default WebViewLayout; diff --git a/web/pages/m/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx b/web/pages/m/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx index 637c953e7..ede258c73 100644 --- a/web/pages/m/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx +++ b/web/pages/m/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx @@ -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 ( - +
Loading...
-
+ ); if (error) return ( - +
{error?.response?.data || "Something went wrong"}
-
+ ); return ( - +
{
-
+ ); }; diff --git a/web/services/api.service.ts b/web/services/api.service.ts index 361fea03e..c71c75ba8 100644 --- a/web/services/api.service.ts +++ b/web/services/api.service.ts @@ -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; };