diff --git a/apiserver/plane/app/views/analytic.py b/apiserver/plane/app/views/analytic.py index 04a77f789..6eb914b23 100644 --- a/apiserver/plane/app/views/analytic.py +++ b/apiserver/plane/app/views/analytic.py @@ -1,6 +1,7 @@ # Django imports from django.db.models import Count, Sum, F, Q from django.db.models.functions import ExtractMonth +from django.utils import timezone # Third party imports from rest_framework import status @@ -331,8 +332,9 @@ class DefaultAnalyticsEndpoint(BaseAPIView): .order_by("state_group") ) + current_year = timezone.now().year issue_completed_month_wise = ( - base_issues.filter(completed_at__isnull=False) + base_issues.filter(completed_at__year=current_year) .annotate(month=ExtractMonth("completed_at")) .values("month") .annotate(count=Count("*")) diff --git a/apiserver/plane/app/views/issue.py b/apiserver/plane/app/views/issue.py index c8845150a..edefade16 100644 --- a/apiserver/plane/app/views/issue.py +++ b/apiserver/plane/app/views/issue.py @@ -1209,13 +1209,13 @@ class IssueArchiveViewSet(BaseViewSet): return Response(issues, status=status.HTTP_200_OK) def retrieve(self, request, slug, project_id, pk=None): - issue = Issue.objects.get( - workspace__slug=slug, - project_id=project_id, - archived_at__isnull=False, - pk=pk, + issue = self.get_queryset().filter(pk=pk).first() + return Response( + IssueDetailSerializer( + issue, fields=self.fields, expand=self.expand + ).data, + status=status.HTTP_200_OK, ) - return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK) def unarchive(self, request, slug, project_id, pk=None): issue = Issue.objects.get( diff --git a/packages/editor/rich-text-editor/src/ui/index.tsx b/packages/editor/rich-text-editor/src/ui/index.tsx index 43c3f8f34..4bcb340fd 100644 --- a/packages/editor/rich-text-editor/src/ui/index.tsx +++ b/packages/editor/rich-text-editor/src/ui/index.tsx @@ -15,6 +15,7 @@ import { EditorBubbleMenu } from "src/ui/menus/bubble-menu"; export type IRichTextEditor = { value: string; + initialValue?: string; dragDropEnabled?: boolean; uploadFile: UploadImage; restoreFile: RestoreImage; @@ -54,6 +55,7 @@ const RichTextEditor = ({ setShouldShowAlert, editorContentCustomClassNames, value, + initialValue, uploadFile, deleteFile, noBorder, @@ -97,6 +99,10 @@ const RichTextEditor = ({ customClassName, }); + React.useEffect(() => { + if (editor && initialValue && editor.getHTML() != initialValue) editor.commands.setContent(initialValue); + }, [editor, initialValue]); + if (!editor) return null; return ( diff --git a/web/components/core/index.ts b/web/components/core/index.ts index 4f99f3606..f68ff5f3c 100644 --- a/web/components/core/index.ts +++ b/web/components/core/index.ts @@ -4,3 +4,4 @@ export * from "./sidebar"; export * from "./theme"; export * from "./activity"; export * from "./image-picker-popover"; +export * from "./page-title"; diff --git a/web/components/core/page-title.tsx b/web/components/core/page-title.tsx new file mode 100644 index 000000000..f9f4e94b2 --- /dev/null +++ b/web/components/core/page-title.tsx @@ -0,0 +1,18 @@ +import Head from "next/head"; + +type PageHeadTitleProps = { + title?: string; + description?: string; +}; + +export const PageHead: React.FC = (props) => { + const { title } = props; + + if (!title) return null; + + return ( + + {title} + + ); +}; diff --git a/web/components/core/theme/custom-theme-selector.tsx b/web/components/core/theme/custom-theme-selector.tsx index bd6f43569..fdb7a6483 100644 --- a/web/components/core/theme/custom-theme-selector.tsx +++ b/web/components/core/theme/custom-theme-selector.tsx @@ -66,7 +66,6 @@ export const CustomThemeSelector: React.FC = observer(() => { const handleValueChange = (val: string | undefined, onChange: any) => { let hex = val; - // prepend a hashtag if it doesn't exist if (val && val[0] !== "#") hex = `#${val}`; @@ -94,7 +93,7 @@ export const CustomThemeSelector: React.FC = observer(() => { placeholder="#0d101b" className="w-full" style={{ - backgroundColor: value, + backgroundColor: watch("background"), color: watch("text"), }} hasError={Boolean(errors?.background)} @@ -120,8 +119,8 @@ export const CustomThemeSelector: React.FC = observer(() => { placeholder="#c5c5c5" className="w-full" style={{ - backgroundColor: watch("background"), - color: value, + backgroundColor: watch("text"), + color: watch("background"), }} hasError={Boolean(errors?.text)} /> @@ -146,7 +145,7 @@ export const CustomThemeSelector: React.FC = observer(() => { placeholder="#3f76ff" className="w-full" style={{ - backgroundColor: value, + backgroundColor: watch("primary"), color: watch("text"), }} hasError={Boolean(errors?.primary)} @@ -172,7 +171,7 @@ export const CustomThemeSelector: React.FC = observer(() => { placeholder="#0d101b" className="w-full" style={{ - backgroundColor: value, + backgroundColor: watch("sidebarBackground"), color: watch("sidebarText"), }} hasError={Boolean(errors?.sidebarBackground)} @@ -200,8 +199,8 @@ export const CustomThemeSelector: React.FC = observer(() => { placeholder="#c5c5c5" className="w-full" style={{ - backgroundColor: watch("sidebarBackground"), - color: value, + backgroundColor: watch("sidebarText"), + color: watch("sidebarBackground"), }} hasError={Boolean(errors?.sidebarText)} /> diff --git a/web/components/dashboard/widgets/recent-projects.tsx b/web/components/dashboard/widgets/recent-projects.tsx index eb7a5e4e5..79be92333 100644 --- a/web/components/dashboard/widgets/recent-projects.tsx +++ b/web/components/dashboard/widgets/recent-projects.tsx @@ -96,7 +96,7 @@ export const RecentProjectsWidget: React.FC = observer((props) => { href={`/${workspaceSlug}/projects`} className="text-lg font-semibold text-custom-text-300 mx-7 hover:underline" > - Your projects + Recent projects
{canCreateProject && ( diff --git a/web/components/dropdowns/cycle.tsx b/web/components/dropdowns/cycle.tsx index e3aa6df11..5086d2d26 100644 --- a/web/components/dropdowns/cycle.tsx +++ b/web/components/dropdowns/cycle.tsx @@ -61,6 +61,7 @@ export const CycleDropdown: React.FC = observer((props) => { const [isOpen, setIsOpen] = useState(false); // refs const dropdownRef = useRef(null); + const inputRef = useRef(null); // popper-js refs const [referenceElement, setReferenceElement] = useState(null); const [popperElement, setPopperElement] = useState(null); @@ -111,23 +112,15 @@ export const CycleDropdown: React.FC = observer((props) => { const filteredOptions = query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase())); - // fetch cycles of the project if not already present in the store - useEffect(() => { - if (!workspaceSlug) return; - - if (!cycleIds) fetchAllCycles(workspaceSlug, projectId); - }, [cycleIds, fetchAllCycles, projectId, workspaceSlug]); - const selectedCycle = value ? getCycleById(value) : null; const onOpen = () => { - if (referenceElement) referenceElement.focus(); + if (workspaceSlug && !cycleIds) fetchAllCycles(workspaceSlug, projectId); }; const handleClose = () => { if (!isOpen) return; setIsOpen(false); - if (referenceElement) referenceElement.blur(); onClose && onClose(); }; @@ -151,6 +144,12 @@ export const CycleDropdown: React.FC = observer((props) => { useOutsideClickDetector(dropdownRef, handleClose); + useEffect(() => { + if (isOpen && inputRef.current) { + inputRef.current.focus(); + } + }, [isOpen]); + return ( = observer((props) => {
setQuery(e.target.value)} diff --git a/web/components/dropdowns/estimate.tsx b/web/components/dropdowns/estimate.tsx index 48558a683..2674fa902 100644 --- a/web/components/dropdowns/estimate.tsx +++ b/web/components/dropdowns/estimate.tsx @@ -1,4 +1,4 @@ -import { Fragment, ReactNode, useRef, useState } from "react"; +import { Fragment, ReactNode, useEffect, useRef, useState } from "react"; import { observer } from "mobx-react-lite"; import { Combobox } from "@headlessui/react"; import { usePopper } from "react-popper"; @@ -60,6 +60,7 @@ export const EstimateDropdown: React.FC = observer((props) => { const [isOpen, setIsOpen] = useState(false); // refs const dropdownRef = useRef(null); + const inputRef = useRef(null); // popper-js refs const [referenceElement, setReferenceElement] = useState(null); const [popperElement, setPopperElement] = useState(null); @@ -110,13 +111,11 @@ export const EstimateDropdown: React.FC = observer((props) => { const onOpen = () => { if (!activeEstimate && workspaceSlug) fetchProjectEstimates(workspaceSlug, projectId); - if (referenceElement) referenceElement.focus(); }; const handleClose = () => { if (!isOpen) return; setIsOpen(false); - if (referenceElement) referenceElement.blur(); onClose && onClose(); }; @@ -140,6 +139,12 @@ export const EstimateDropdown: React.FC = observer((props) => { useOutsideClickDetector(dropdownRef, handleClose); + useEffect(() => { + if (isOpen && inputRef.current) { + inputRef.current.focus(); + } + }, [isOpen]); + return ( = observer((props) => {
setQuery(e.target.value)} diff --git a/web/components/dropdowns/member/project-member.tsx b/web/components/dropdowns/member/project-member.tsx index 44cd3a701..d1f285aa5 100644 --- a/web/components/dropdowns/member/project-member.tsx +++ b/web/components/dropdowns/member/project-member.tsx @@ -1,4 +1,4 @@ -import { Fragment, useRef, useState } from "react"; +import { Fragment, useEffect, useRef, useState } from "react"; import { observer } from "mobx-react-lite"; import { Combobox } from "@headlessui/react"; import { usePopper } from "react-popper"; @@ -50,6 +50,7 @@ export const ProjectMemberDropdown: React.FC = observer((props) => { const [isOpen, setIsOpen] = useState(false); // refs const dropdownRef = useRef(null); + const inputRef = useRef(null); // popper-js refs const [referenceElement, setReferenceElement] = useState(null); const [popperElement, setPopperElement] = useState(null); @@ -103,13 +104,11 @@ export const ProjectMemberDropdown: React.FC = observer((props) => { const onOpen = () => { if (!projectMemberIds && workspaceSlug) fetchProjectMembers(workspaceSlug, projectId); - if (referenceElement) referenceElement.focus(); }; const handleClose = () => { if (!isOpen) return; setIsOpen(false); - if (referenceElement) referenceElement.blur(); onClose && onClose(); }; @@ -133,6 +132,12 @@ export const ProjectMemberDropdown: React.FC = observer((props) => { useOutsideClickDetector(dropdownRef, handleClose); + useEffect(() => { + if (isOpen && inputRef.current) { + inputRef.current.focus(); + } + }, [isOpen]); + return ( = observer((props) => {
setQuery(e.target.value)} diff --git a/web/components/dropdowns/member/workspace-member.tsx b/web/components/dropdowns/member/workspace-member.tsx index d126b60f7..7a2628cca 100644 --- a/web/components/dropdowns/member/workspace-member.tsx +++ b/web/components/dropdowns/member/workspace-member.tsx @@ -1,4 +1,4 @@ -import { Fragment, useRef, useState } from "react"; +import { Fragment, useEffect, useRef, useState } from "react"; import { observer } from "mobx-react-lite"; import { Combobox } from "@headlessui/react"; import { usePopper } from "react-popper"; @@ -44,6 +44,7 @@ export const WorkspaceMemberDropdown: React.FC = observer(( const [isOpen, setIsOpen] = useState(false); // refs const dropdownRef = useRef(null); + const inputRef = useRef(null); // popper-js refs const [referenceElement, setReferenceElement] = useState(null); const [popperElement, setPopperElement] = useState(null); @@ -91,19 +92,13 @@ export const WorkspaceMemberDropdown: React.FC = observer(( }; if (multiple) comboboxProps.multiple = true; - const onOpen = () => { - if (referenceElement) referenceElement.focus(); - }; - const handleClose = () => { if (!isOpen) return; setIsOpen(false); - if (referenceElement) referenceElement.blur(); onClose && onClose(); }; const toggleDropdown = () => { - if (!isOpen) onOpen(); setIsOpen((prevIsOpen) => !prevIsOpen); }; @@ -122,6 +117,12 @@ export const WorkspaceMemberDropdown: React.FC = observer(( useOutsideClickDetector(dropdownRef, handleClose); + useEffect(() => { + if (isOpen && inputRef.current) { + inputRef.current.focus(); + } + }, [isOpen]); + return ( = observer((
setQuery(e.target.value)} diff --git a/web/components/dropdowns/module.tsx b/web/components/dropdowns/module.tsx index a9b64c1f1..c05eeb97e 100644 --- a/web/components/dropdowns/module.tsx +++ b/web/components/dropdowns/module.tsx @@ -166,6 +166,7 @@ export const ModuleDropdown: React.FC = observer((props) => { const [isOpen, setIsOpen] = useState(false); // refs const dropdownRef = useRef(null); + const inputRef = useRef(null); // popper-js refs const [referenceElement, setReferenceElement] = useState(null); const [popperElement, setPopperElement] = useState(null); @@ -216,21 +217,13 @@ export const ModuleDropdown: React.FC = observer((props) => { const filteredOptions = query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase())); - // fetch modules of the project if not already present in the store - useEffect(() => { - if (!workspaceSlug) return; - - if (!moduleIds) fetchModules(workspaceSlug, projectId); - }, [moduleIds, fetchModules, projectId, workspaceSlug]); - const onOpen = () => { - if (referenceElement) referenceElement.focus(); + if (!moduleIds && workspaceSlug) fetchModules(workspaceSlug, projectId); }; const handleClose = () => { if (!isOpen) return; setIsOpen(false); - if (referenceElement) referenceElement.blur(); onClose && onClose(); }; @@ -261,6 +254,12 @@ export const ModuleDropdown: React.FC = observer((props) => { }; if (multiple) comboboxProps.multiple = true; + useEffect(() => { + if (isOpen && inputRef.current) { + inputRef.current.focus(); + } + }, [isOpen]); + return ( = observer((props) => {
setQuery(e.target.value)} diff --git a/web/components/dropdowns/priority.tsx b/web/components/dropdowns/priority.tsx index 1bab9a21e..d519ad9f1 100644 --- a/web/components/dropdowns/priority.tsx +++ b/web/components/dropdowns/priority.tsx @@ -1,4 +1,4 @@ -import { Fragment, ReactNode, useRef, useState } from "react"; +import { Fragment, ReactNode, useEffect, useRef, useState } from "react"; import { Combobox } from "@headlessui/react"; import { usePopper } from "react-popper"; import { Check, ChevronDown, Search } from "lucide-react"; @@ -272,6 +272,7 @@ export const PriorityDropdown: React.FC = (props) => { const [isOpen, setIsOpen] = useState(false); // refs const dropdownRef = useRef(null); + const inputRef = useRef(null); // popper-js refs const [referenceElement, setReferenceElement] = useState(null); const [popperElement, setPopperElement] = useState(null); @@ -305,19 +306,13 @@ export const PriorityDropdown: React.FC = (props) => { const filteredOptions = query === "" ? options : options.filter((o) => o.query.toLowerCase().includes(query.toLowerCase())); - const onOpen = () => { - if (referenceElement) referenceElement.focus(); - }; - const handleClose = () => { if (!isOpen) return; setIsOpen(false); - if (referenceElement) referenceElement.blur(); onClose && onClose(); }; const toggleDropdown = () => { - if (!isOpen) onOpen(); setIsOpen((prevIsOpen) => !prevIsOpen); }; @@ -342,6 +337,12 @@ export const PriorityDropdown: React.FC = (props) => { ? BackgroundButton : TransparentButton; + useEffect(() => { + if (isOpen && inputRef.current) { + inputRef.current.focus(); + } + }, [isOpen]); + return ( = (props) => {
setQuery(e.target.value)} diff --git a/web/components/dropdowns/project.tsx b/web/components/dropdowns/project.tsx index 7991c4402..f6fb9205e 100644 --- a/web/components/dropdowns/project.tsx +++ b/web/components/dropdowns/project.tsx @@ -1,4 +1,4 @@ -import { Fragment, ReactNode, useRef, useState } from "react"; +import { Fragment, ReactNode, useEffect, useRef, useState } from "react"; import { observer } from "mobx-react-lite"; import { Combobox } from "@headlessui/react"; import { usePopper } from "react-popper"; @@ -50,6 +50,7 @@ export const ProjectDropdown: React.FC = observer((props) => { const [isOpen, setIsOpen] = useState(false); // refs const dropdownRef = useRef(null); + const inputRef = useRef(null); // popper-js refs const [referenceElement, setReferenceElement] = useState(null); const [popperElement, setPopperElement] = useState(null); @@ -94,19 +95,13 @@ export const ProjectDropdown: React.FC = observer((props) => { const selectedProject = value ? getProjectById(value) : null; - const onOpen = () => { - if (referenceElement) referenceElement.focus(); - }; - const handleClose = () => { if (!isOpen) return; setIsOpen(false); onClose && onClose(); - if (referenceElement) referenceElement.blur(); }; const toggleDropdown = () => { - if (!isOpen) onOpen(); setIsOpen((prevIsOpen) => !prevIsOpen); }; @@ -125,6 +120,12 @@ export const ProjectDropdown: React.FC = observer((props) => { useOutsideClickDetector(dropdownRef, handleClose); + useEffect(() => { + if (isOpen && inputRef.current) { + inputRef.current.focus(); + } + }, [isOpen]); + return ( = observer((props) => {
setQuery(e.target.value)} diff --git a/web/components/dropdowns/state.tsx b/web/components/dropdowns/state.tsx index a7f54adfb..fa068fdd0 100644 --- a/web/components/dropdowns/state.tsx +++ b/web/components/dropdowns/state.tsx @@ -1,4 +1,4 @@ -import { Fragment, ReactNode, useRef, useState } from "react"; +import { Fragment, ReactNode, useEffect, useRef, useState } from "react"; import { observer } from "mobx-react-lite"; import { Combobox } from "@headlessui/react"; import { usePopper } from "react-popper"; @@ -52,6 +52,7 @@ export const StateDropdown: React.FC = observer((props) => { const [isOpen, setIsOpen] = useState(false); // refs const dropdownRef = useRef(null); + const inputRef = useRef(null); // popper-js refs const [referenceElement, setReferenceElement] = useState(null); const [popperElement, setPopperElement] = useState(null); @@ -92,14 +93,12 @@ export const StateDropdown: React.FC = observer((props) => { const onOpen = () => { if (!statesList && workspaceSlug) fetchProjectStates(workspaceSlug, projectId); - if (referenceElement) referenceElement.focus(); }; const handleClose = () => { if (!isOpen) return; setIsOpen(false); onClose && onClose(); - if (referenceElement) referenceElement.blur(); }; const toggleDropdown = () => { @@ -122,6 +121,12 @@ export const StateDropdown: React.FC = observer((props) => { useOutsideClickDetector(dropdownRef, handleClose); + useEffect(() => { + if (isOpen && inputRef.current) { + inputRef.current.focus(); + } + }, [isOpen]); + return ( = observer((props) => {
setQuery(e.target.value)} diff --git a/web/components/headers/project-issues.tsx b/web/components/headers/project-issues.tsx index 5c44a84d6..033196758 100644 --- a/web/components/headers/project-issues.tsx +++ b/web/components/headers/project-issues.tsx @@ -1,8 +1,7 @@ import { useCallback, useState } from "react"; -import Link from "next/link"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -import { Briefcase, Circle, ExternalLink, Plus, Inbox } from "lucide-react"; +import { Briefcase, Circle, ExternalLink, Plus } from "lucide-react"; // hooks import { useApplication, @@ -11,7 +10,6 @@ import { useProject, useProjectState, useUser, - useInbox, useMember, } from "hooks/store"; // components @@ -54,7 +52,6 @@ export const ProjectIssuesHeader: React.FC = observer(() => { const { currentProjectDetails } = useProject(); const { projectStates } = useProjectState(); const { projectLabels } = useLabel(); - const { getInboxesByProjectId, getInboxById } = useInbox(); const activeLayout = issueFilters?.displayFilters?.layout; @@ -101,9 +98,6 @@ export const ProjectIssuesHeader: React.FC = observer(() => { [workspaceSlug, projectId, updateFilters] ); - const inboxesMap = currentProjectDetails?.inbox_view ? getInboxesByProjectId(currentProjectDetails.id) : undefined; - const inboxDetails = inboxesMap && inboxesMap.length > 0 ? getInboxById(inboxesMap[0]) : undefined; - const deployUrl = process.env.NEXT_PUBLIC_DEPLOY_URL; const canUserCreateIssue = currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); @@ -154,7 +148,9 @@ export const ProjectIssuesHeader: React.FC = observer(() => { } />} + link={ + } /> + } />
@@ -201,24 +197,15 @@ export const ProjectIssuesHeader: React.FC = observer(() => { />
- {currentProjectDetails?.inbox_view && inboxDetails && ( - - - - - - - )} + {canUserCreateIssue && ( <> - + + + {currentMode && ( +
+ setPeekMode(val)} + customButton={ + + } + > + {PEEK_OPTIONS.map((mode) => ( + +
+ + {mode.title} +
+
+ ))} +
+
+ )} +
+
+ +
+ {currentUser && !isArchived && ( + + )} + + {!disabled && ( + + )} +
+
+
+ ); +}); diff --git a/web/components/issues/peek-overview/index.ts b/web/components/issues/peek-overview/index.ts index 6d602e45b..aa341b939 100644 --- a/web/components/issues/peek-overview/index.ts +++ b/web/components/issues/peek-overview/index.ts @@ -2,3 +2,4 @@ export * from "./issue-detail"; export * from "./properties"; export * from "./root"; export * from "./view"; +export * from "./header"; diff --git a/web/components/issues/peek-overview/issue-detail.tsx b/web/components/issues/peek-overview/issue-detail.tsx index 8c5101938..7f540874c 100644 --- a/web/components/issues/peek-overview/issue-detail.tsx +++ b/web/components/issues/peek-overview/issue-detail.tsx @@ -1,10 +1,14 @@ -import { FC } from "react"; -// hooks -import { useIssueDetail, useProject, useUser } from "hooks/store"; -// components -import { IssueDescriptionForm, TIssueOperations } from "components/issues"; -import { IssueReaction } from "../issue-detail/reactions"; +import { FC, useEffect } from "react"; import { observer } from "mobx-react"; +// store hooks +import { useIssueDetail, useProject, useUser } from "hooks/store"; +// hooks +import useReloadConfirmations from "hooks/use-reload-confirmation"; +// components +import { TIssueOperations } from "components/issues"; +import { IssueReaction } from "../issue-detail/reactions"; +import { IssueTitleInput } from "../title-input"; +import { IssueDescriptionInput } from "../description-input"; interface IPeekOverviewIssueDetails { workspaceSlug: string; @@ -17,38 +21,70 @@ interface IPeekOverviewIssueDetails { } export const PeekOverviewIssueDetails: FC = observer((props) => { - const { workspaceSlug, projectId, issueId, issueOperations, disabled, isSubmitting, setIsSubmitting } = props; + const { workspaceSlug, issueId, issueOperations, disabled, isSubmitting, setIsSubmitting } = props; // store hooks const { getProjectById } = useProject(); const { currentUser } = useUser(); const { issue: { getIssueById }, } = useIssueDetail(); + // hooks + const { setShowAlert } = useReloadConfirmations(isSubmitting === "submitting"); - // derived values - const issue = getIssueById(issueId); + useEffect(() => { + if (isSubmitting === "submitted") { + setShowAlert(false); + setTimeout(async () => { + setIsSubmitting("saved"); + }, 2000); + } else if (isSubmitting === "submitting") { + setShowAlert(true); + } + }, [isSubmitting, setShowAlert, setIsSubmitting]); + + const issue = issueId ? getIssueById(issueId) : undefined; if (!issue) return <>; + const projectDetails = getProjectById(issue?.project_id); + const issueDescription = + issue.description_html !== undefined || issue.description_html !== null + ? issue.description_html != "" + ? issue.description_html + : "

" + : undefined; + return ( <> {projectDetails?.identifier}-{issue?.sequence_id} - setIsSubmitting(value)} + projectId={issue.project_id} + issueId={issue.id} isSubmitting={isSubmitting} - issue={issue} + setIsSubmitting={(value) => setIsSubmitting(value)} issueOperations={issueOperations} disabled={disabled} + value={issue.name} /> + + setIsSubmitting(value)} + /> + {currentUser && ( diff --git a/web/components/issues/peek-overview/root.tsx b/web/components/issues/peek-overview/root.tsx index b491ebe36..c49c0a503 100644 --- a/web/components/issues/peek-overview/root.tsx +++ b/web/components/issues/peek-overview/root.tsx @@ -69,20 +69,11 @@ export const IssuePeekOverview: FC = observer((props) => { // state const [loader, setLoader] = useState(false); - useEffect(() => { - if (peekIssue) { - setLoader(true); - fetchIssue(peekIssue.workspaceSlug, peekIssue.projectId, peekIssue.issueId).finally(() => { - setLoader(false); - }); - } - }, [peekIssue, fetchIssue]); - const issueOperations: TIssuePeekOperations = useMemo( () => ({ fetch: async (workspaceSlug: string, projectId: string, issueId: string) => { try { - await fetchIssue(workspaceSlug, projectId, issueId); + await fetchIssue(workspaceSlug, projectId, issueId, is_archived); } catch (error) { console.error("Error fetching the parent issue"); } @@ -324,9 +315,20 @@ export const IssuePeekOverview: FC = observer((props) => { removeModulesFromIssue, setToastAlert, onIssueUpdate, + captureIssueEvent, + router.asPath, ] ); + useEffect(() => { + if (peekIssue) { + setLoader(true); + issueOperations.fetch(peekIssue.workspaceSlug, peekIssue.projectId, peekIssue.issueId).finally(() => { + setLoader(false); + }); + } + }, [peekIssue, issueOperations]); + if (!peekIssue?.workspaceSlug || !peekIssue?.projectId || !peekIssue?.issueId) return <>; const issue = getIssueById(peekIssue.issueId) || undefined; diff --git a/web/components/issues/peek-overview/view.tsx b/web/components/issues/peek-overview/view.tsx index 82bda41d5..4e80c4938 100644 --- a/web/components/issues/peek-overview/view.tsx +++ b/web/components/issues/peek-overview/view.tsx @@ -1,28 +1,25 @@ import { FC, useRef, useState } from "react"; -import { useRouter } from "next/router"; + import { observer } from "mobx-react-lite"; -import { MoveRight, MoveDiagonal, Link2, Trash2 } from "lucide-react"; + // hooks import useOutsideClickDetector from "hooks/use-outside-click-detector"; import useKeypress from "hooks/use-keypress"; // store hooks -import { useIssueDetail, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; +import { useIssueDetail } from "hooks/store"; // components import { DeleteArchivedIssueModal, DeleteIssueModal, - IssueSubscription, - IssueUpdateStatus, + IssuePeekOverviewHeader, + TPeekModes, PeekOverviewIssueDetails, PeekOverviewProperties, TIssueOperations, } from "components/issues"; import { IssueActivity } from "../issue-detail/issue-activity"; // ui -import { CenterPanelIcon, CustomSelect, FullScreenPanelIcon, SidePanelIcon, Spinner } from "@plane/ui"; -// helpers -import { copyUrlToClipboard } from "helpers/string.helper"; +import { Spinner } from "@plane/ui"; interface IIssueView { workspaceSlug: string; @@ -34,72 +31,28 @@ interface IIssueView { issueOperations: TIssueOperations; } -type TPeekModes = "side-peek" | "modal" | "full-screen"; - -const PEEK_OPTIONS: { key: TPeekModes; icon: any; title: string }[] = [ - { - key: "side-peek", - icon: SidePanelIcon, - title: "Side Peek", - }, - { - key: "modal", - icon: CenterPanelIcon, - title: "Modal", - }, - { - key: "full-screen", - icon: FullScreenPanelIcon, - title: "Full Screen", - }, -]; - export const IssueView: FC = observer((props) => { const { workspaceSlug, projectId, issueId, isLoading, is_archived, disabled = false, issueOperations } = props; - // router - const router = useRouter(); // states const [peekMode, setPeekMode] = useState("side-peek"); const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved"); // ref const issuePeekOverviewRef = useRef(null); // store hooks - const { setPeekIssue, isAnyModalOpen, isDeleteIssueModalOpen, toggleDeleteIssueModal } = useIssueDetail(); - const { currentUser } = useUser(); const { + setPeekIssue, + isAnyModalOpen, + isDeleteIssueModalOpen, + toggleDeleteIssueModal, issue: { getIssueById }, } = useIssueDetail(); - const { setToastAlert } = useToast(); - // derived values - const currentMode = PEEK_OPTIONS.find((m) => m.key === peekMode); const issue = getIssueById(issueId); - + // remove peek id const removeRoutePeekId = () => { setPeekIssue(undefined); }; + // hooks useOutsideClickDetector(issuePeekOverviewRef, () => !isAnyModalOpen && removeRoutePeekId()); - - const redirectToIssueDetail = () => { - router.push({ - pathname: `/${workspaceSlug}/projects/${projectId}/${is_archived ? "archived-issues" : "issues"}/${issueId}`, - }); - removeRoutePeekId(); - }; - - const handleCopyText = (e: React.MouseEvent) => { - e.stopPropagation(); - e.preventDefault(); - copyUrlToClipboard( - `${workspaceSlug}/projects/${projectId}/${is_archived ? "archived-issues" : "issues"}/${issueId}` - ).then(() => { - setToastAlert({ - type: "success", - title: "Link Copied!", - message: "Issue link copied to clipboard.", - }); - }); - }; - const handleKeyDown = () => !isAnyModalOpen && removeRoutePeekId(); useKeypress("Escape", handleKeyDown); @@ -126,7 +79,7 @@ export const IssueView: FC = observer((props) => { /> )} -
+
{issueId && (
= observer((props) => { }} > {/* header */} -
-
- - - - {currentMode && ( -
- setPeekMode(val)} - customButton={ - - } - > - {PEEK_OPTIONS.map((mode) => ( - -
- - {mode.title} -
-
- ))} -
-
- )} -
-
- -
- {currentUser && !is_archived && ( - - )} - - {!disabled && ( - - )} -
-
-
- + { + setPeekMode(value); + }} + removeRoutePeekId={removeRoutePeekId} + toggleDeleteIssueModal={toggleDeleteIssueModal} + isArchived={is_archived} + issueId={issueId} + workspaceSlug={workspaceSlug} + projectId={projectId} + isSubmitting={isSubmitting} + disabled={disabled} + /> {/* content */}
{isLoading && !issue ? ( @@ -230,11 +137,7 @@ export const IssueView: FC = observer((props) => { disabled={disabled} /> - +
) : (
@@ -250,11 +153,7 @@ export const IssueView: FC = observer((props) => { setIsSubmitting={(value) => setIsSubmitting(value)} /> - +
= observer((props) => { const [isDropdownOpen, setIsDropdownOpen] = useState(false); // refs const dropdownRef = useRef(null); + const inputRef = useRef(null); // popper const { styles, attributes } = usePopper(referenceElement, popperElement, { placement: "bottom-start", @@ -76,6 +77,12 @@ export const IssueLabelSelect: React.FC = observer((props) => { useOutsideClickDetector(dropdownRef, handleClose); + useEffect(() => { + if (isDropdownOpen && inputRef.current) { + inputRef.current.focus(); + } + }, [isDropdownOpen]); + return ( = observer((props) => {
setQuery(event.target.value)} placeholder="Search" diff --git a/web/components/issues/title-input.tsx b/web/components/issues/title-input.tsx new file mode 100644 index 000000000..55dd80b87 --- /dev/null +++ b/web/components/issues/title-input.tsx @@ -0,0 +1,69 @@ +import { FC, useState, useEffect, useCallback } from "react"; +import { observer } from "mobx-react"; +// components +import { TextArea } from "@plane/ui"; +// types +import { TIssueOperations } from "./issue-detail"; +// hooks +import useDebounce from "hooks/use-debounce"; + +export type IssueTitleInputProps = { + disabled?: boolean; + value: string | undefined | null; + workspaceSlug: string; + isSubmitting: "submitting" | "submitted" | "saved"; + setIsSubmitting: (value: "submitting" | "submitted" | "saved") => void; + issueOperations: TIssueOperations; + projectId: string; + issueId: string; +}; + +export const IssueTitleInput: FC = observer((props) => { + const { disabled, value, workspaceSlug, setIsSubmitting, issueId, issueOperations, projectId } = props; + // states + const [title, setTitle] = useState(""); + // hooks + + const debouncedValue = useDebounce(title, 1500); + + useEffect(() => { + if (value) setTitle(value); + }, [value]); + + useEffect(() => { + if (debouncedValue && debouncedValue !== value) { + issueOperations.update(workspaceSlug, projectId, issueId, { name: debouncedValue }, false).finally(() => { + setIsSubmitting("saved"); + }); + } + // DO NOT Add more dependencies here. It will cause multiple requests to be sent. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [debouncedValue]); + + const handleTitleChange = useCallback( + (e: React.ChangeEvent) => { + setIsSubmitting("submitting"); + setTitle(e.target.value); + }, + [setIsSubmitting] + ); + + return ( +
+