diff --git a/.github/workflows/build-branch.yml b/.github/workflows/build-branch.yml index f02062189c4..0d3f970689f 100644 --- a/.github/workflows/build-branch.yml +++ b/.github/workflows/build-branch.yml @@ -28,7 +28,7 @@ jobs: - id: set_env_variables name: Set Environment Variables run: | - if [ "${{ env.TARGET_BRANCH }}" == "master" ]; then + if [ "${{ env.TARGET_BRANCH }}" == "master" ] || [ "${{ github.event_name }}" == "release" ]; then echo "BUILDX_DRIVER=cloud" >> $GITHUB_OUTPUT echo "BUILDX_VERSION=lab:latest" >> $GITHUB_OUTPUT echo "BUILDX_PLATFORMS=linux/amd64,linux/arm64" >> $GITHUB_OUTPUT diff --git a/apiserver/plane/app/views/inbox.py b/apiserver/plane/app/views/inbox.py index 01eee78e393..f76c74d9c1d 100644 --- a/apiserver/plane/app/views/inbox.py +++ b/apiserver/plane/app/views/inbox.py @@ -27,7 +27,7 @@ InboxSerializer, InboxIssueSerializer, IssueCreateSerializer, - IssueStateInboxSerializer, + IssueDetailSerializer, ) from plane.utils.issue_filters import issue_filters from plane.bgtasks.issue_activites_task import issue_activity @@ -333,7 +333,7 @@ def partial_update(self, request, slug, project_id, inbox_id, issue_id): def retrieve(self, request, slug, project_id, inbox_id, issue_id): issue = self.get_queryset().filter(pk=issue_id).first() - serializer = IssueSerializer(issue, expand=self.expand,) + serializer = IssueDetailSerializer(issue, expand=self.expand,) return Response(serializer.data, status=status.HTTP_200_OK) def destroy(self, request, slug, project_id, inbox_id, issue_id): diff --git a/apiserver/plane/app/views/issue.py b/apiserver/plane/app/views/issue.py index c8845150a52..edefade16e4 100644 --- a/apiserver/plane/app/views/issue.py +++ b/apiserver/plane/app/views/issue.py @@ -1209,13 +1209,13 @@ def list(self, request, slug, project_id): 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 43c3f8f3432..4bcb340fd80 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/theme/custom-theme-selector.tsx b/web/components/core/theme/custom-theme-selector.tsx index bd6f4356921..fdb7a648347 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/dropdowns/cycle.tsx b/web/components/dropdowns/cycle.tsx index e3aa6df11db..5086d2d26b7 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 48558a683f7..2674fa902d1 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 44cd3a7016a..d1f285aa555 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 d126b60f72b..7a2628ccaff 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 a9b64c1f1f7..c05eeb97e65 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 1bab9a21e9c..d519ad9f18c 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 7991c44024c..f6fb9205e86 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 a7f54adfbe0..fa068fdd01b 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/inbox/inbox-issue-status.tsx b/web/components/inbox/inbox-issue-status.tsx index 301583b4ba7..2d101f2aaf7 100644 --- a/web/components/inbox/inbox-issue-status.tsx +++ b/web/components/inbox/inbox-issue-status.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { observer } from "mobx-react"; // hooks import { useInboxIssues } from "hooks/store"; // constants @@ -13,7 +14,7 @@ type Props = { showDescription?: boolean; }; -export const InboxIssueStatus: React.FC = (props) => { +export const InboxIssueStatus: React.FC = observer((props) => { const { workspaceSlug, projectId, inboxId, issueId, iconSize = 18, showDescription = false } = props; // hooks const { @@ -52,4 +53,4 @@ export const InboxIssueStatus: React.FC = (props) => { )}
); -}; +}); diff --git a/web/components/issues/description-form.tsx b/web/components/issues/description-form.tsx index 4d20fd97858..c64c147ea90 100644 --- a/web/components/issues/description-form.tsx +++ b/web/components/issues/description-form.tsx @@ -108,7 +108,7 @@ export const IssueDescriptionForm: FC = observer((props) => { description_html: issue.description_html === "" ? "

" : issue.description_html, }); setLocalTitleValue(issue.name); - }, [issue, reset]); + }, [issue, issue.description_html, reset]); // ADDING handleDescriptionFormSubmit TO DEPENDENCY ARRAY PRODUCES ADVERSE EFFECTS // TODO: Verify the exhaustive-deps warning diff --git a/web/components/issues/description-input.tsx b/web/components/issues/description-input.tsx new file mode 100644 index 00000000000..79634fa84aa --- /dev/null +++ b/web/components/issues/description-input.tsx @@ -0,0 +1,91 @@ +import { FC, useState, useEffect } from "react"; +// components +import { Loader } from "@plane/ui"; +import { RichReadOnlyEditor, RichTextEditor } from "@plane/rich-text-editor"; +// store hooks +import { useMention, useWorkspace } from "hooks/store"; +// services +import { FileService } from "services/file.service"; +const fileService = new FileService(); +// types +import { TIssueOperations } from "./issue-detail"; +// hooks +import useDebounce from "hooks/use-debounce"; + +export type IssueDescriptionInputProps = { + workspaceSlug: string; + projectId: string; + issueId: string; + value: string | undefined; + initialValue: string | undefined; + disabled?: boolean; + issueOperations: TIssueOperations; + setIsSubmitting: (value: "submitting" | "submitted" | "saved") => void; +}; + +export const IssueDescriptionInput: FC = (props) => { + const { workspaceSlug, projectId, issueId, value, initialValue, disabled, issueOperations, setIsSubmitting } = props; + // states + const [descriptionHTML, setDescriptionHTML] = useState(value); + // store hooks + const { mentionHighlights, mentionSuggestions } = useMention(); + const { getWorkspaceBySlug } = useWorkspace(); + // hooks + const debouncedValue = useDebounce(descriptionHTML, 1500); + // computed values + const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id as string; + + useEffect(() => { + setDescriptionHTML(value); + }, [value]); + + useEffect(() => { + if (debouncedValue && debouncedValue !== value) { + issueOperations + .update(workspaceSlug, projectId, issueId, { description_html: debouncedValue }, false) + .finally(() => { + setIsSubmitting("submitted"); + }); + } + // DO NOT Add more dependencies here. It will cause multiple requests to be sent. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [debouncedValue]); + + if (!descriptionHTML) { + return ( + + + + ); + } + + if (disabled) { + return ( + + ); + } + + return ( + { + setIsSubmitting("submitting"); + setDescriptionHTML(description_html === "" ? "

" : description_html); + }} + mentionSuggestions={mentionSuggestions} + mentionHighlights={mentionHighlights} + /> + ); +}; diff --git a/web/components/issues/issue-detail/inbox/main-content.tsx b/web/components/issues/issue-detail/inbox/main-content.tsx index 4a1f79bee5f..d753be02fec 100644 --- a/web/components/issues/issue-detail/inbox/main-content.tsx +++ b/web/components/issues/issue-detail/inbox/main-content.tsx @@ -1,9 +1,12 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { observer } from "mobx-react-lite"; // hooks import { useIssueDetail, useProjectState, useUser } from "hooks/store"; +import useReloadConfirmations from "hooks/use-reload-confirmation"; // components -import { IssueDescriptionForm, IssueUpdateStatus, TIssueOperations } from "components/issues"; +import { IssueUpdateStatus, TIssueOperations } from "components/issues"; +import { IssueTitleInput } from "../../title-input"; +import { IssueDescriptionInput } from "../../description-input"; import { IssueReaction } from "../reactions"; import { IssueActivity } from "../issue-activity"; import { InboxIssueStatus } from "../../../inbox/inbox-issue-status"; @@ -29,12 +32,31 @@ export const InboxIssueMainContent: React.FC = observer((props) => { const { issue: { getIssueById }, } = useIssueDetail(); + const { setShowAlert } = useReloadConfirmations(isSubmitting === "submitting"); - const issue = getIssueById(issueId); + useEffect(() => { + if (isSubmitting === "submitted") { + setShowAlert(false); + setTimeout(async () => { + setIsSubmitting("saved"); + }, 3000); + } else if (isSubmitting === "submitting") { + setShowAlert(true); + } + }, [isSubmitting, setShowAlert, setIsSubmitting]); + + const issue = issueId ? getIssueById(issueId) : undefined; if (!issue) return <>; const currentIssueState = projectStates?.find((s) => s.id === issue.state_id); + const issueDescription = + issue.description_html !== undefined || issue.description_html !== null + ? issue.description_html != "" + ? issue.description_html + : "

" + : undefined; + return ( <>
@@ -57,15 +79,26 @@ export const InboxIssueMainContent: React.FC = observer((props) => {
- setIsSubmitting(value)} + projectId={issue.project_id} + issueId={issue.id} isSubmitting={isSubmitting} - issue={issue} + setIsSubmitting={(value) => setIsSubmitting(value)} issueOperations={issueOperations} disabled={!is_editable} + value={issue.name} + /> + + setIsSubmitting(value)} /> {currentUser && ( diff --git a/web/components/issues/issue-detail/issue-activity/comments/comment-create.tsx b/web/components/issues/issue-detail/issue-activity/comments/comment-create.tsx index bb79c981769..bf5b15266f1 100644 --- a/web/components/issues/issue-detail/issue-activity/comments/comment-create.tsx +++ b/web/components/issues/issue-detail/issue-activity/comments/comment-create.tsx @@ -81,7 +81,6 @@ export const IssueCommentCreate: FC = (props) => { render={({ field: { value, onChange } }) => ( { - console.log("yo"); handleSubmit(onSubmit)(e); }} cancelUploadImage={fileService.cancelUpload} diff --git a/web/components/issues/issue-detail/main-content.tsx b/web/components/issues/issue-detail/main-content.tsx index 07552580177..719129d98fe 100644 --- a/web/components/issues/issue-detail/main-content.tsx +++ b/web/components/issues/issue-detail/main-content.tsx @@ -1,9 +1,12 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { observer } from "mobx-react-lite"; // hooks import { useIssueDetail, useProjectState, useUser } from "hooks/store"; +import useReloadConfirmations from "hooks/use-reload-confirmation"; // components -import { IssueDescriptionForm, IssueAttachmentRoot, IssueUpdateStatus } from "components/issues"; +import { IssueAttachmentRoot, IssueUpdateStatus } from "components/issues"; +import { IssueTitleInput } from "../title-input"; +import { IssueDescriptionInput } from "../description-input"; import { IssueParentDetail } from "./parent"; import { IssueReaction } from "./reactions"; import { SubIssuesRoot } from "../sub-issues"; @@ -31,12 +34,31 @@ export const IssueMainContent: React.FC = observer((props) => { const { issue: { getIssueById }, } = useIssueDetail(); + const { setShowAlert } = useReloadConfirmations(isSubmitting === "submitting"); - 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 currentIssueState = projectStates?.find((s) => s.id === issue.state_id); + const issueDescription = + issue.description_html !== undefined || issue.description_html !== null + ? issue.description_html != "" + ? issue.description_html + : "

" + : undefined; + return ( <>
@@ -61,15 +83,26 @@ export const IssueMainContent: React.FC = observer((props) => {
- setIsSubmitting(value)} + projectId={issue.project_id} + issueId={issue.id} isSubmitting={isSubmitting} - issue={issue} + setIsSubmitting={(value) => setIsSubmitting(value)} issueOperations={issueOperations} disabled={!is_editable} + value={issue.name} + /> + + setIsSubmitting(value)} /> {currentUser && ( diff --git a/web/components/issues/issue-layouts/empty-states/draft-issues.tsx b/web/components/issues/issue-layouts/empty-states/draft-issues.tsx index 347778d8f25..c496cc5fe2d 100644 --- a/web/components/issues/issue-layouts/empty-states/draft-issues.tsx +++ b/web/components/issues/issue-layouts/empty-states/draft-issues.tsx @@ -42,7 +42,7 @@ export const ProjectDraftEmptyState: React.FC = observer(() => { const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; const currentLayoutEmptyStateImagePath = getEmptyStateImagePath("empty-filters", activeLayout ?? "list", isLightMode); - const EmptyStateImagePath = getEmptyStateImagePath("draft", "empty-issues", isLightMode); + const EmptyStateImagePath = getEmptyStateImagePath("draft", "draft-issues-empty", isLightMode); const issueFilterCount = size( Object.fromEntries( diff --git a/web/components/issues/issue-layouts/list/block.tsx b/web/components/issues/issue-layouts/list/block.tsx index ceec7b219cc..2e48e1f1c8c 100644 --- a/web/components/issues/issue-layouts/list/block.tsx +++ b/web/components/issues/issue-layouts/list/block.tsx @@ -50,9 +50,8 @@ export const IssueBlock: React.FC = observer((props: IssueBlock return (
{displayProperties && displayProperties?.key && ( diff --git a/web/components/issues/issue-layouts/list/list-view-types.d.ts b/web/components/issues/issue-layouts/list/list-view-types.d.ts index 1838316cb90..e369410af7f 100644 --- a/web/components/issues/issue-layouts/list/list-view-types.d.ts +++ b/web/components/issues/issue-layouts/list/list-view-types.d.ts @@ -1,3 +1,5 @@ +import { TIssue } from "@plane/types"; + export interface IQuickActionProps { issue: TIssue; handleDelete: () => Promise; diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx index f97f1ea86ce..bc6518911ae 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { useRouter } from "next/router"; import { CustomMenu } from "@plane/ui"; import { Copy, Link, Pencil, Trash2 } from "lucide-react"; +import omit from "lodash/omit"; // hooks import useToast from "hooks/use-toast"; import { useEventTracker } from "hooks/store"; @@ -30,7 +31,7 @@ export const AllIssueQuickActions: React.FC = (props) => { const { setToastAlert } = useToast(); const handleCopyIssueLink = () => { - copyUrlToClipboard(`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`).then(() => + copyUrlToClipboard(`/${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`).then(() => setToastAlert({ type: "success", title: "Link copied", @@ -39,11 +40,13 @@ export const AllIssueQuickActions: React.FC = (props) => { ); }; - const duplicateIssuePayload = { - ...issue, - name: `${issue.name} (copy)`, - }; - delete duplicateIssuePayload.id; + const duplicateIssuePayload = omit( + { + ...issue, + name: `${issue.name} (copy)`, + }, + ["id"] + ); return ( <> @@ -87,7 +90,7 @@ export const AllIssueQuickActions: React.FC = (props) => { { setTrackElement("Global issues"); - setIssueToEdit(issue); + setIssueToEdit(issue); setCreateUpdateIssueModal(true); }} > @@ -99,7 +102,7 @@ export const AllIssueQuickActions: React.FC = (props) => { { setTrackElement("Global issues"); - setCreateUpdateIssueModal(true); + setCreateUpdateIssueModal(true); }} >
@@ -110,7 +113,7 @@ export const AllIssueQuickActions: React.FC = (props) => { { setTrackElement("Global issues"); - setDeleteIssueModal(true); + setDeleteIssueModal(true); }} >
diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx index f962701769b..e331d718276 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx @@ -4,7 +4,7 @@ import { CustomMenu } from "@plane/ui"; import { Link, Trash2 } from "lucide-react"; // hooks import useToast from "hooks/use-toast"; -import { useEventTracker, useIssues ,useUser} from "hooks/store"; +import { useEventTracker, useIssues, useUser } from "hooks/store"; // components import { DeleteArchivedIssueModal } from "components/issues"; // helpers @@ -37,7 +37,7 @@ export const ArchivedIssueQuickActions: React.FC = (props) => const activeLayout = `${issuesFilter.issueFilters?.displayFilters?.layout} layout`; const handleCopyIssueLink = () => { - copyUrlToClipboard(`${workspaceSlug}/projects/${issue.project}/archived-issues/${issue.id}`).then(() => + copyUrlToClipboard(`${workspaceSlug}/projects/${issue.project_id}/archived-issues/${issue.id}`).then(() => setToastAlert({ type: "success", title: "Link copied", @@ -75,7 +75,7 @@ export const ArchivedIssueQuickActions: React.FC = (props) => { setTrackElement(activeLayout); - setDeleteIssueModal(true); + setDeleteIssueModal(true); }} >
diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx index 8c3beb3d2c1..4699b1c81e3 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx @@ -2,9 +2,10 @@ import { useState } from "react"; import { useRouter } from "next/router"; import { CustomMenu } from "@plane/ui"; import { Copy, Link, Pencil, Trash2, XCircle } from "lucide-react"; +import omit from "lodash/omit"; // hooks import useToast from "hooks/use-toast"; -import { useEventTracker, useIssues,useUser } from "hooks/store"; +import { useEventTracker, useIssues, useUser } from "hooks/store"; // components import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; // helpers @@ -49,7 +50,7 @@ export const CycleIssueQuickActions: React.FC = (props) => { const activeLayout = `${issuesFilter.issueFilters?.displayFilters?.layout} layout`; const handleCopyIssueLink = () => { - copyUrlToClipboard(`${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`).then(() => + copyUrlToClipboard(`${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`).then(() => setToastAlert({ type: "success", title: "Link copied", @@ -58,11 +59,13 @@ export const CycleIssueQuickActions: React.FC = (props) => { ); }; - const duplicateIssuePayload = { - ...issue, - name: `${issue.name} (copy)`, - }; - delete duplicateIssuePayload.id; + const duplicateIssuePayload = omit( + { + ...issue, + name: `${issue.name} (copy)`, + }, + ["id"] + ); return ( <> @@ -107,10 +110,10 @@ export const CycleIssueQuickActions: React.FC = (props) => { onClick={() => { setIssueToEdit({ ...issue, - cycle: cycleId?.toString() ?? null, + cycle_id: cycleId?.toString() ?? null, }); setTrackElement(activeLayout); - setCreateUpdateIssueModal(true); + setCreateUpdateIssueModal(true); }} >
@@ -131,7 +134,7 @@ export const CycleIssueQuickActions: React.FC = (props) => { { setTrackElement(activeLayout); - setCreateUpdateIssueModal(true); + setCreateUpdateIssueModal(true); }} >
@@ -142,7 +145,7 @@ export const CycleIssueQuickActions: React.FC = (props) => { { setTrackElement(activeLayout); - setDeleteIssueModal(true); + setDeleteIssueModal(true); }} >
diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx index a3ed73ec0c9..6eabfda59fd 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx @@ -2,9 +2,10 @@ import { useState } from "react"; import { useRouter } from "next/router"; import { CustomMenu } from "@plane/ui"; import { Copy, Link, Pencil, Trash2, XCircle } from "lucide-react"; +import omit from "lodash/omit"; // hooks import useToast from "hooks/use-toast"; -import { useIssues, useEventTracker ,useUser } from "hooks/store"; +import { useIssues, useEventTracker, useUser } from "hooks/store"; // components import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; // helpers @@ -49,7 +50,7 @@ export const ModuleIssueQuickActions: React.FC = (props) => { const activeLayout = `${issuesFilter.issueFilters?.displayFilters?.layout} layout`; const handleCopyIssueLink = () => { - copyUrlToClipboard(`${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`).then(() => + copyUrlToClipboard(`${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`).then(() => setToastAlert({ type: "success", title: "Link copied", @@ -58,11 +59,13 @@ export const ModuleIssueQuickActions: React.FC = (props) => { ); }; - const duplicateIssuePayload = { - ...issue, - name: `${issue.name} (copy)`, - }; - delete duplicateIssuePayload.id; + const duplicateIssuePayload = omit( + { + ...issue, + name: `${issue.name} (copy)`, + }, + ["id"] + ); return ( <> @@ -105,9 +108,9 @@ export const ModuleIssueQuickActions: React.FC = (props) => { <> { - setIssueToEdit({ ...issue, module: moduleId?.toString() ?? null }); + setIssueToEdit({ ...issue, module_ids: moduleId ? [moduleId.toString()] : [] }); setTrackElement(activeLayout); - setCreateUpdateIssueModal(true); + setCreateUpdateIssueModal(true); }} >
@@ -128,7 +131,7 @@ export const ModuleIssueQuickActions: React.FC = (props) => { { setTrackElement(activeLayout); - setCreateUpdateIssueModal(true); + setCreateUpdateIssueModal(true); }} >
@@ -141,7 +144,7 @@ export const ModuleIssueQuickActions: React.FC = (props) => { e.preventDefault(); e.stopPropagation(); setTrackElement(activeLayout); - setDeleteIssueModal(true); + setDeleteIssueModal(true); }} >
diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx index 65adc85427c..1d6d88f2575 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { useRouter } from "next/router"; import { CustomMenu } from "@plane/ui"; import { Copy, Link, Pencil, Trash2 } from "lucide-react"; +import omit from "lodash/omit"; // hooks import { useEventTracker, useIssues, useUser } from "hooks/store"; import useToast from "hooks/use-toast"; @@ -39,7 +40,7 @@ export const ProjectIssueQuickActions: React.FC = (props) => const { setToastAlert } = useToast(); const handleCopyIssueLink = () => { - copyUrlToClipboard(`${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`).then(() => + copyUrlToClipboard(`${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`).then(() => setToastAlert({ type: "success", title: "Link copied", @@ -48,11 +49,13 @@ export const ProjectIssueQuickActions: React.FC = (props) => ); }; - const duplicateIssuePayload = { - ...issue, - name: `${issue.name} (copy)`, - }; - delete duplicateIssuePayload.id; + const duplicateIssuePayload = omit( + { + ...issue, + name: `${issue.name} (copy)`, + }, + ["id"] + ); const isDraftIssue = router?.asPath?.includes("draft-issues") || false; diff --git a/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx b/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx index 59cf5b9af11..1a77ed5faa8 100644 --- a/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx @@ -159,7 +159,7 @@ export const AllIssueLayoutRoot: React.FC = observer(() => { globalViewId.toString() ); }, - [updateFilters, workspaceSlug] + [updateFilters, workspaceSlug, globalViewId] ); const renderQuickActions = useCallback( diff --git a/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx b/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx index e4efc513746..a94455a0b03 100644 --- a/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx @@ -88,7 +88,7 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => { viewId ); }, - [issueFiltersStore, projectId, workspaceSlug, viewId] + [issueFiltersStore?.updateFilters, projectId, workspaceSlug, viewId] ); const renderQuickActions = useCallback( diff --git a/web/components/issues/issue-layouts/spreadsheet/issue-column.tsx b/web/components/issues/issue-layouts/spreadsheet/issue-column.tsx index 5d2e62fa55e..20dd946dfc6 100644 --- a/web/components/issues/issue-layouts/spreadsheet/issue-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/issue-column.tsx @@ -38,7 +38,7 @@ export const IssueColumn = observer((props: Props) => { > { shouldRenderProperty={shouldRenderProperty} > diff --git a/web/components/issues/peek-overview/header.tsx b/web/components/issues/peek-overview/header.tsx new file mode 100644 index 00000000000..8b51c977e53 --- /dev/null +++ b/web/components/issues/peek-overview/header.tsx @@ -0,0 +1,153 @@ +import { FC } from "react"; +import { useRouter } from "next/router"; +import { observer } from "mobx-react"; +import { MoveRight, MoveDiagonal, Link2, Trash2 } from "lucide-react"; +// ui +import { CenterPanelIcon, CustomSelect, FullScreenPanelIcon, SidePanelIcon } from "@plane/ui"; +// helpers +import { copyUrlToClipboard } from "helpers/string.helper"; +// hooks +import useToast from "hooks/use-toast"; +// store hooks +import { useUser } from "hooks/store"; +// components +import { IssueSubscription, IssueUpdateStatus } from "components/issues"; + +export 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 type PeekOverviewHeaderProps = { + peekMode: TPeekModes; + setPeekMode: (value: TPeekModes) => void; + removeRoutePeekId: () => void; + workspaceSlug: string; + projectId: string; + issueId: string; + isArchived: boolean; + disabled: boolean; + toggleDeleteIssueModal: (value: boolean) => void; + isSubmitting: "submitting" | "submitted" | "saved"; +}; + +export const IssuePeekOverviewHeader: FC = observer((props) => { + const { + peekMode, + setPeekMode, + workspaceSlug, + projectId, + issueId, + isArchived, + disabled, + removeRoutePeekId, + toggleDeleteIssueModal, + isSubmitting, + } = props; + // router + const router = useRouter(); + // store hooks + const { currentUser } = useUser(); + // hooks + const { setToastAlert } = useToast(); + // derived values + const currentMode = PEEK_OPTIONS.find((m) => m.key === peekMode); + + const handleCopyText = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + copyUrlToClipboard( + `${workspaceSlug}/projects/${projectId}/${isArchived ? "archived-issues" : "issues"}/${issueId}` + ).then(() => { + setToastAlert({ + type: "success", + title: "Link Copied!", + message: "Issue link copied to clipboard.", + }); + }); + }; + + const redirectToIssueDetail = () => { + router.push({ + pathname: `/${workspaceSlug}/projects/${projectId}/${isArchived ? "archived-issues" : "issues"}/${issueId}`, + }); + removeRoutePeekId(); + }; + + return ( +
+
+ + + + {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 6d602e45b80..aa341b939f6 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 8c510193843..7f540874c76 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 { 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 { IssueDescriptionForm, TIssueOperations } from "components/issues"; +import { TIssueOperations } from "components/issues"; import { IssueReaction } from "../issue-detail/reactions"; -import { observer } from "mobx-react"; +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 b491ebe3638..c49c0a50330 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 82bda41d5f5..4e80c493859 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 00000000000..55dd80b87bb --- /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 ( +
+