forked from github/plane
commit
4566d6e80c
2
.github/workflows/build-branch.yml
vendored
2
.github/workflows/build-branch.yml
vendored
@ -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
|
||||
|
@ -27,7 +27,7 @@ from plane.app.serializers import (
|
||||
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 @@ class InboxIssueViewSet(BaseViewSet):
|
||||
|
||||
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):
|
||||
|
@ -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(
|
||||
|
@ -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 (
|
||||
|
@ -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)}
|
||||
/>
|
||||
|
@ -61,6 +61,7 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
// refs
|
||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
// popper-js refs
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
@ -111,23 +112,15 @@ export const CycleDropdown: React.FC<Props> = 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<Props> = observer((props) => {
|
||||
|
||||
useOutsideClickDetector(dropdownRef, handleClose);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as="div"
|
||||
@ -216,6 +215,8 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
||||
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
|
||||
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
|
||||
<Combobox.Input
|
||||
as="input"
|
||||
ref={inputRef}
|
||||
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
|
@ -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<Props> = observer((props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
// refs
|
||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
// popper-js refs
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
@ -110,13 +111,11 @@ export const EstimateDropdown: React.FC<Props> = 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<Props> = observer((props) => {
|
||||
|
||||
useOutsideClickDetector(dropdownRef, handleClose);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as="div"
|
||||
@ -205,6 +210,8 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
||||
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
|
||||
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
|
||||
<Combobox.Input
|
||||
as="input"
|
||||
ref={inputRef}
|
||||
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
|
@ -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<Props> = observer((props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
// refs
|
||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
// popper-js refs
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
@ -103,13 +104,11 @@ export const ProjectMemberDropdown: React.FC<Props> = 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<Props> = observer((props) => {
|
||||
|
||||
useOutsideClickDetector(dropdownRef, handleClose);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as="div"
|
||||
@ -203,6 +208,8 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
|
||||
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
|
||||
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
|
||||
<Combobox.Input
|
||||
as="input"
|
||||
ref={inputRef}
|
||||
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
|
@ -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<MemberDropdownProps> = observer((
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
// refs
|
||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
// popper-js refs
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
@ -91,19 +92,13 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = 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<MemberDropdownProps> = observer((
|
||||
|
||||
useOutsideClickDetector(dropdownRef, handleClose);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as="div"
|
||||
@ -192,6 +193,8 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
|
||||
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
|
||||
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
|
||||
<Combobox.Input
|
||||
as="input"
|
||||
ref={inputRef}
|
||||
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
|
@ -166,6 +166,7 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
// refs
|
||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
// popper-js refs
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
@ -216,21 +217,13 @@ export const ModuleDropdown: React.FC<Props> = 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<Props> = observer((props) => {
|
||||
};
|
||||
if (multiple) comboboxProps.multiple = true;
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as="div"
|
||||
@ -331,6 +330,8 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
||||
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
|
||||
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
|
||||
<Combobox.Input
|
||||
as="input"
|
||||
ref={inputRef}
|
||||
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
|
@ -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> = (props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
// refs
|
||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
// popper-js refs
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
@ -305,19 +306,13 @@ export const PriorityDropdown: React.FC<Props> = (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> = (props) => {
|
||||
? BackgroundButton
|
||||
: TransparentButton;
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as="div"
|
||||
@ -409,6 +410,8 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
||||
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
|
||||
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
|
||||
<Combobox.Input
|
||||
as="input"
|
||||
ref={inputRef}
|
||||
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
|
@ -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<Props> = observer((props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
// refs
|
||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
// popper-js refs
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
@ -94,19 +95,13 @@ export const ProjectDropdown: React.FC<Props> = 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<Props> = observer((props) => {
|
||||
|
||||
useOutsideClickDetector(dropdownRef, handleClose);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as="div"
|
||||
@ -198,6 +199,8 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
||||
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
|
||||
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
|
||||
<Combobox.Input
|
||||
as="input"
|
||||
ref={inputRef}
|
||||
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
|
@ -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<Props> = observer((props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
// refs
|
||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
// popper-js refs
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
@ -92,14 +93,12 @@ export const StateDropdown: React.FC<Props> = 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<Props> = observer((props) => {
|
||||
|
||||
useOutsideClickDetector(dropdownRef, handleClose);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as="div"
|
||||
@ -193,6 +198,8 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
||||
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
|
||||
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
|
||||
<Combobox.Input
|
||||
as="input"
|
||||
ref={inputRef}
|
||||
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
|
@ -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> = (props) => {
|
||||
export const InboxIssueStatus: React.FC<Props> = observer((props) => {
|
||||
const { workspaceSlug, projectId, inboxId, issueId, iconSize = 18, showDescription = false } = props;
|
||||
// hooks
|
||||
const {
|
||||
@ -52,4 +53,4 @@ export const InboxIssueStatus: React.FC<Props> = (props) => {
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -108,7 +108,7 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = observer((props) => {
|
||||
description_html: issue.description_html === "" ? "<p></p>" : 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
|
||||
|
91
web/components/issues/description-input.tsx
Normal file
91
web/components/issues/description-input.tsx
Normal file
@ -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<IssueDescriptionInputProps> = (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 (
|
||||
<Loader>
|
||||
<Loader.Item height="150px" />
|
||||
</Loader>
|
||||
);
|
||||
}
|
||||
|
||||
if (disabled) {
|
||||
return (
|
||||
<RichReadOnlyEditor
|
||||
value={descriptionHTML}
|
||||
customClassName="!p-0 !pt-2 text-custom-text-200"
|
||||
noBorder={disabled}
|
||||
mentionHighlights={mentionHighlights}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<RichTextEditor
|
||||
cancelUploadImage={fileService.cancelUpload}
|
||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug)}
|
||||
deleteFile={fileService.getDeleteImageFunction(workspaceId)}
|
||||
restoreFile={fileService.getRestoreImageFunction(workspaceId)}
|
||||
value={descriptionHTML}
|
||||
initialValue={initialValue}
|
||||
dragDropEnabled
|
||||
customClassName="min-h-[150px] shadow-sm"
|
||||
onChange={(description: Object, description_html: string) => {
|
||||
setIsSubmitting("submitting");
|
||||
setDescriptionHTML(description_html === "" ? "<p></p>" : description_html);
|
||||
}}
|
||||
mentionSuggestions={mentionSuggestions}
|
||||
mentionHighlights={mentionHighlights}
|
||||
/>
|
||||
);
|
||||
};
|
@ -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<Props> = 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
|
||||
: "<p></p>"
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="rounded-lg space-y-4">
|
||||
@ -57,15 +79,26 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
|
||||
<IssueUpdateStatus isSubmitting={isSubmitting} issueDetail={issue} />
|
||||
</div>
|
||||
|
||||
<IssueDescriptionForm
|
||||
<IssueTitleInput
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
setIsSubmitting={(value) => 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}
|
||||
/>
|
||||
|
||||
<IssueDescriptionInput
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={issue.project_id}
|
||||
issueId={issue.id}
|
||||
value={issueDescription}
|
||||
initialValue={issueDescription}
|
||||
disabled={!is_editable}
|
||||
issueOperations={issueOperations}
|
||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||
/>
|
||||
|
||||
{currentUser && (
|
||||
|
@ -81,7 +81,6 @@ export const IssueCommentCreate: FC<TIssueCommentCreate> = (props) => {
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<LiteTextEditorWithRef
|
||||
onEnterKeyPress={(e) => {
|
||||
console.log("yo");
|
||||
handleSubmit(onSubmit)(e);
|
||||
}}
|
||||
cancelUploadImage={fileService.cancelUpload}
|
||||
|
@ -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<Props> = 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
|
||||
: "<p></p>"
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="rounded-lg space-y-4">
|
||||
@ -61,15 +83,26 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
|
||||
<IssueUpdateStatus isSubmitting={isSubmitting} issueDetail={issue} />
|
||||
</div>
|
||||
|
||||
<IssueDescriptionForm
|
||||
<IssueTitleInput
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
setIsSubmitting={(value) => 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}
|
||||
/>
|
||||
|
||||
<IssueDescriptionInput
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={issue.project_id}
|
||||
issueId={issue.id}
|
||||
value={issueDescription}
|
||||
initialValue={issueDescription}
|
||||
disabled={!is_editable}
|
||||
issueOperations={issueOperations}
|
||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||
/>
|
||||
|
||||
{currentUser && (
|
||||
|
@ -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(
|
||||
|
@ -50,9 +50,8 @@ export const IssueBlock: React.FC<IssueBlockProps> = observer((props: IssueBlock
|
||||
return (
|
||||
<div
|
||||
className={cn("min-h-12 relative flex items-center gap-3 bg-custom-background-100 p-3 text-sm", {
|
||||
"border border-custom-primary-70 hover:border-custom-primary-70":
|
||||
peekIssue && peekIssue.issueId === issue.id,
|
||||
"last:border-b-transparent": peekIssue?.issueId !== issue.id
|
||||
"border border-custom-primary-70 hover:border-custom-primary-70": peekIssue && peekIssue.issueId === issue.id,
|
||||
"last:border-b-transparent": peekIssue?.issueId !== issue.id,
|
||||
})}
|
||||
>
|
||||
{displayProperties && displayProperties?.key && (
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { TIssue } from "@plane/types";
|
||||
|
||||
export interface IQuickActionProps {
|
||||
issue: TIssue;
|
||||
handleDelete: () => Promise<void>;
|
||||
|
@ -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<IQuickActionProps> = (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<IQuickActionProps> = (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<IQuickActionProps> = (props) => {
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setTrackElement("Global issues");
|
||||
setIssueToEdit(issue);
|
||||
setIssueToEdit(issue);
|
||||
setCreateUpdateIssueModal(true);
|
||||
}}
|
||||
>
|
||||
@ -99,7 +102,7 @@ export const AllIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setTrackElement("Global issues");
|
||||
setCreateUpdateIssueModal(true);
|
||||
setCreateUpdateIssueModal(true);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
@ -110,7 +113,7 @@ export const AllIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setTrackElement("Global issues");
|
||||
setDeleteIssueModal(true);
|
||||
setDeleteIssueModal(true);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
|
@ -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<IQuickActionProps> = (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<IQuickActionProps> = (props) =>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setTrackElement(activeLayout);
|
||||
setDeleteIssueModal(true);
|
||||
setDeleteIssueModal(true);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
|
@ -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<IQuickActionProps> = (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<IQuickActionProps> = (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<IQuickActionProps> = (props) => {
|
||||
onClick={() => {
|
||||
setIssueToEdit({
|
||||
...issue,
|
||||
cycle: cycleId?.toString() ?? null,
|
||||
cycle_id: cycleId?.toString() ?? null,
|
||||
});
|
||||
setTrackElement(activeLayout);
|
||||
setCreateUpdateIssueModal(true);
|
||||
setCreateUpdateIssueModal(true);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
@ -131,7 +134,7 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setTrackElement(activeLayout);
|
||||
setCreateUpdateIssueModal(true);
|
||||
setCreateUpdateIssueModal(true);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
@ -142,7 +145,7 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setTrackElement(activeLayout);
|
||||
setDeleteIssueModal(true);
|
||||
setDeleteIssueModal(true);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
|
@ -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<IQuickActionProps> = (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<IQuickActionProps> = (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<IQuickActionProps> = (props) => {
|
||||
<>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setIssueToEdit({ ...issue, module: moduleId?.toString() ?? null });
|
||||
setIssueToEdit({ ...issue, module_ids: moduleId ? [moduleId.toString()] : [] });
|
||||
setTrackElement(activeLayout);
|
||||
setCreateUpdateIssueModal(true);
|
||||
setCreateUpdateIssueModal(true);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
@ -128,7 +131,7 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setTrackElement(activeLayout);
|
||||
setCreateUpdateIssueModal(true);
|
||||
setCreateUpdateIssueModal(true);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
@ -141,7 +144,7 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setTrackElement(activeLayout);
|
||||
setDeleteIssueModal(true);
|
||||
setDeleteIssueModal(true);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
|
@ -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<IQuickActionProps> = (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<IQuickActionProps> = (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;
|
||||
|
||||
|
@ -159,7 +159,7 @@ export const AllIssueLayoutRoot: React.FC = observer(() => {
|
||||
globalViewId.toString()
|
||||
);
|
||||
},
|
||||
[updateFilters, workspaceSlug]
|
||||
[updateFilters, workspaceSlug, globalViewId]
|
||||
);
|
||||
|
||||
const renderQuickActions = useCallback(
|
||||
|
@ -88,7 +88,7 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => {
|
||||
viewId
|
||||
);
|
||||
},
|
||||
[issueFiltersStore, projectId, workspaceSlug, viewId]
|
||||
[issueFiltersStore?.updateFilters, projectId, workspaceSlug, viewId]
|
||||
);
|
||||
|
||||
const renderQuickActions = useCallback(
|
||||
|
@ -38,7 +38,7 @@ export const IssueColumn = observer((props: Props) => {
|
||||
>
|
||||
<td
|
||||
tabIndex={0}
|
||||
className="h-11 w-full min-w-[8rem] bg-custom-background-100 text-sm after:absolute after:w-full after:bottom-[-1px] after:border after:border-custom-border-100 border-r-[1px] border-custom-border-100 focus:border-custom-primary-70"
|
||||
className="h-11 w-full min-w-[8rem] bg-custom-background-100 text-sm after:absolute after:w-full after:bottom-[-1px] after:border after:border-custom-border-100 border-r-[1px] border-custom-border-100"
|
||||
ref={tableCellRef}
|
||||
>
|
||||
<Column
|
||||
|
@ -28,7 +28,7 @@ export const SpreadsheetHeaderColumn = observer((props: Props) => {
|
||||
shouldRenderProperty={shouldRenderProperty}
|
||||
>
|
||||
<th
|
||||
className="h-11 w-full min-w-[8rem] items-center bg-custom-background-90 text-sm font-medium px-4 py-1 border border-b-0 border-t-0 border-custom-border-100 focus:border-custom-primary-70"
|
||||
className="h-11 w-full min-w-[8rem] items-center bg-custom-background-90 text-sm font-medium px-4 py-1 border border-b-0 border-t-0 border-custom-border-100"
|
||||
ref={tableHeaderCellRef}
|
||||
tabIndex={0}
|
||||
>
|
||||
|
153
web/components/issues/peek-overview/header.tsx
Normal file
153
web/components/issues/peek-overview/header.tsx
Normal file
@ -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<PeekOverviewHeaderProps> = 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<HTMLButtonElement>) => {
|
||||
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 (
|
||||
<div
|
||||
className={`relative flex items-center justify-between p-4 ${
|
||||
currentMode?.key === "full-screen" ? "border-b border-custom-border-200" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<button onClick={removeRoutePeekId}>
|
||||
<MoveRight className="h-4 w-4 text-custom-text-400 hover:text-custom-text-200" />
|
||||
</button>
|
||||
|
||||
<button onClick={redirectToIssueDetail}>
|
||||
<MoveDiagonal className="h-4 w-4 text-custom-text-400 hover:text-custom-text-200" />
|
||||
</button>
|
||||
{currentMode && (
|
||||
<div className="flex flex-shrink-0 items-center gap-2">
|
||||
<CustomSelect
|
||||
value={currentMode}
|
||||
onChange={(val: any) => setPeekMode(val)}
|
||||
customButton={
|
||||
<button type="button" className="">
|
||||
<currentMode.icon className="h-4 w-4 text-custom-text-400 hover:text-custom-text-200" />
|
||||
</button>
|
||||
}
|
||||
>
|
||||
{PEEK_OPTIONS.map((mode) => (
|
||||
<CustomSelect.Option key={mode.key} value={mode.key}>
|
||||
<div
|
||||
className={`flex items-center gap-1.5 ${
|
||||
currentMode.key === mode.key
|
||||
? "text-custom-text-200"
|
||||
: "text-custom-text-400 hover:text-custom-text-200"
|
||||
}`}
|
||||
>
|
||||
<mode.icon className="-my-1 h-4 w-4 flex-shrink-0" />
|
||||
{mode.title}
|
||||
</div>
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
</CustomSelect>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-x-4">
|
||||
<IssueUpdateStatus isSubmitting={isSubmitting} />
|
||||
<div className="flex items-center gap-4">
|
||||
{currentUser && !isArchived && (
|
||||
<IssueSubscription workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} />
|
||||
)}
|
||||
<button onClick={handleCopyText}>
|
||||
<Link2 className="h-4 w-4 -rotate-45 text-custom-text-300 hover:text-custom-text-200" />
|
||||
</button>
|
||||
{!disabled && (
|
||||
<button onClick={() => toggleDeleteIssueModal(true)}>
|
||||
<Trash2 className="h-4 w-4 text-custom-text-300 hover:text-custom-text-200" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
@ -2,3 +2,4 @@ export * from "./issue-detail";
|
||||
export * from "./properties";
|
||||
export * from "./root";
|
||||
export * from "./view";
|
||||
export * from "./header";
|
||||
|
@ -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<IPeekOverviewIssueDetails> = 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
|
||||
: "<p></p>"
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className="text-base font-medium text-custom-text-400">
|
||||
{projectDetails?.identifier}-{issue?.sequence_id}
|
||||
</span>
|
||||
<IssueDescriptionForm
|
||||
<IssueTitleInput
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||
projectId={issue.project_id}
|
||||
issueId={issue.id}
|
||||
isSubmitting={isSubmitting}
|
||||
issue={issue}
|
||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||
issueOperations={issueOperations}
|
||||
disabled={disabled}
|
||||
value={issue.name}
|
||||
/>
|
||||
|
||||
<IssueDescriptionInput
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={issue.project_id}
|
||||
issueId={issue.id}
|
||||
value={issueDescription}
|
||||
initialValue={issueDescription}
|
||||
disabled={disabled}
|
||||
issueOperations={issueOperations}
|
||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||
/>
|
||||
|
||||
{currentUser && (
|
||||
<IssueReaction
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
projectId={issue.project_id}
|
||||
issueId={issueId}
|
||||
currentUser={currentUser}
|
||||
/>
|
||||
|
@ -69,20 +69,11 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = 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<IIssuePeekOverview> = 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;
|
||||
|
@ -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<IIssueView> = observer((props) => {
|
||||
const { workspaceSlug, projectId, issueId, isLoading, is_archived, disabled = false, issueOperations } = props;
|
||||
// router
|
||||
const router = useRouter();
|
||||
// states
|
||||
const [peekMode, setPeekMode] = useState<TPeekModes>("side-peek");
|
||||
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
|
||||
// ref
|
||||
const issuePeekOverviewRef = useRef<HTMLDivElement>(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<HTMLButtonElement>) => {
|
||||
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<IIssueView> = observer((props) => {
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="w-full truncate !text-base">
|
||||
<div className="w-full !text-base">
|
||||
{issueId && (
|
||||
<div
|
||||
ref={issuePeekOverviewRef}
|
||||
@ -141,66 +94,20 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
||||
}}
|
||||
>
|
||||
{/* header */}
|
||||
<div
|
||||
className={`relative flex items-center justify-between p-4 ${
|
||||
currentMode?.key === "full-screen" ? "border-b border-custom-border-200" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<button onClick={removeRoutePeekId}>
|
||||
<MoveRight className="h-4 w-4 text-custom-text-400 hover:text-custom-text-200" />
|
||||
</button>
|
||||
|
||||
<button onClick={redirectToIssueDetail}>
|
||||
<MoveDiagonal className="h-4 w-4 text-custom-text-400 hover:text-custom-text-200" />
|
||||
</button>
|
||||
{currentMode && (
|
||||
<div className="flex flex-shrink-0 items-center gap-2">
|
||||
<CustomSelect
|
||||
value={currentMode}
|
||||
onChange={(val: any) => setPeekMode(val)}
|
||||
customButton={
|
||||
<button type="button" className="">
|
||||
<currentMode.icon className="h-4 w-4 text-custom-text-400 hover:text-custom-text-200" />
|
||||
</button>
|
||||
}
|
||||
>
|
||||
{PEEK_OPTIONS.map((mode) => (
|
||||
<CustomSelect.Option key={mode.key} value={mode.key}>
|
||||
<div
|
||||
className={`flex items-center gap-1.5 ${
|
||||
currentMode.key === mode.key
|
||||
? "text-custom-text-200"
|
||||
: "text-custom-text-400 hover:text-custom-text-200"
|
||||
}`}
|
||||
>
|
||||
<mode.icon className="-my-1 h-4 w-4 flex-shrink-0" />
|
||||
{mode.title}
|
||||
</div>
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
</CustomSelect>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-x-4">
|
||||
<IssueUpdateStatus isSubmitting={isSubmitting} />
|
||||
<div className="flex items-center gap-4">
|
||||
{currentUser && !is_archived && (
|
||||
<IssueSubscription workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} />
|
||||
)}
|
||||
<button onClick={handleCopyText}>
|
||||
<Link2 className="h-4 w-4 -rotate-45 text-custom-text-300 hover:text-custom-text-200" />
|
||||
</button>
|
||||
{!disabled && (
|
||||
<button onClick={() => toggleDeleteIssueModal(true)}>
|
||||
<Trash2 className="h-4 w-4 text-custom-text-300 hover:text-custom-text-200" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<IssuePeekOverviewHeader
|
||||
peekMode={peekMode}
|
||||
setPeekMode={(value: TPeekModes) => {
|
||||
setPeekMode(value);
|
||||
}}
|
||||
removeRoutePeekId={removeRoutePeekId}
|
||||
toggleDeleteIssueModal={toggleDeleteIssueModal}
|
||||
isArchived={is_archived}
|
||||
issueId={issueId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
isSubmitting={isSubmitting}
|
||||
disabled={disabled}
|
||||
/>
|
||||
{/* content */}
|
||||
<div className="relative h-full w-full overflow-hidden overflow-y-auto">
|
||||
{isLoading && !issue ? (
|
||||
@ -230,11 +137,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
<IssueActivity
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
/>
|
||||
<IssueActivity workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} />
|
||||
</div>
|
||||
) : (
|
||||
<div className={`flex h-full w-full overflow-auto`}>
|
||||
@ -250,11 +153,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||
/>
|
||||
|
||||
<IssueActivity
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
/>
|
||||
<IssueActivity workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { Fragment, useRef, useState } from "react";
|
||||
import React, { Fragment, useEffect, useRef, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
import { usePopper } from "react-popper";
|
||||
@ -36,6 +36,7 @@ export const IssueLabelSelect: React.FC<Props> = observer((props) => {
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
// refs
|
||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
// popper
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: "bottom-start",
|
||||
@ -76,6 +77,12 @@ export const IssueLabelSelect: React.FC<Props> = observer((props) => {
|
||||
|
||||
useOutsideClickDetector(dropdownRef, handleClose);
|
||||
|
||||
useEffect(() => {
|
||||
if (isDropdownOpen && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [isDropdownOpen]);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as="div"
|
||||
@ -125,6 +132,8 @@ export const IssueLabelSelect: React.FC<Props> = observer((props) => {
|
||||
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
|
||||
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
|
||||
<Combobox.Input
|
||||
as="input"
|
||||
ref={inputRef}
|
||||
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
placeholder="Search"
|
||||
|
69
web/components/issues/title-input.tsx
Normal file
69
web/components/issues/title-input.tsx
Normal file
@ -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<IssueTitleInputProps> = 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<HTMLTextAreaElement>) => {
|
||||
setIsSubmitting("submitting");
|
||||
setTitle(e.target.value);
|
||||
},
|
||||
[setIsSubmitting]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<TextArea
|
||||
className={`min-h-min block w-full resize-none overflow-hidden rounded border-none bg-transparent px-3 py-2 text-2xl font-medium outline-none ring-0 focus:ring-1 focus:ring-custom-primary ${
|
||||
title?.length === 0 ? "!ring-red-400" : ""
|
||||
}`}
|
||||
disabled={disabled}
|
||||
value={title}
|
||||
onChange={handleTitleChange}
|
||||
maxLength={255}
|
||||
placeholder="Issue title"
|
||||
/>
|
||||
<div className="pointer-events-none absolute bottom-1 right-1 z-[2] rounded bg-custom-background-100 p-0.5 text-xs text-custom-text-200">
|
||||
<span className={`${title.length === 0 || title.length > 255 ? "text-red-500" : ""}`}>{title.length}</span>
|
||||
/255
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
@ -68,7 +68,6 @@ export const CreateUpdatePageModal: FC<Props> = (props) => {
|
||||
state: "SUCCESS",
|
||||
},
|
||||
});
|
||||
console.log("Page updated successfully", pageStore);
|
||||
} else {
|
||||
await createProjectPage(formData);
|
||||
}
|
||||
|
@ -79,7 +79,7 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
|
||||
return updateProject(workspaceSlug.toString(), project.id, payload)
|
||||
.then((res) => {
|
||||
const changed_properties = Object.keys(dirtyFields);
|
||||
console.log(dirtyFields);
|
||||
|
||||
captureProjectEvent({
|
||||
eventName: PROJECT_UPDATED,
|
||||
payload: {
|
||||
|
@ -20,54 +20,55 @@ export const ProfilePreferenceSettingsLayout: FC<IProfilePreferenceSettingsLayou
|
||||
const { theme: themeStore } = useApplication();
|
||||
|
||||
const showMenuItem = () => {
|
||||
const item = router.asPath.split('/');
|
||||
const item = router.asPath.split("/");
|
||||
let splittedItem = item[item.length - 1];
|
||||
splittedItem = splittedItem.replace(splittedItem[0], splittedItem[0].toUpperCase());
|
||||
console.log(splittedItem);
|
||||
return splittedItem;
|
||||
}
|
||||
};
|
||||
|
||||
const profilePreferenceLinks: Array<{
|
||||
label: string;
|
||||
href: string;
|
||||
}> = [
|
||||
{
|
||||
label: "Theme",
|
||||
href: `/profile/preferences/theme`,
|
||||
},
|
||||
{
|
||||
label: "Email",
|
||||
href: `/profile/preferences/email`,
|
||||
},
|
||||
];
|
||||
{
|
||||
label: "Theme",
|
||||
href: `/profile/preferences/theme`,
|
||||
},
|
||||
{
|
||||
label: "Email",
|
||||
href: `/profile/preferences/email`,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<ProfileSettingsLayout header={
|
||||
<div className="md:hidden flex flex-shrink-0 gap-4 items-center justify-start border-b border-custom-border-200 p-4">
|
||||
<SidebarHamburgerToggle onClick={() => themeStore.toggleSidebar()} />
|
||||
<CustomMenu
|
||||
maxHeight={"md"}
|
||||
className="flex flex-grow justify-center text-custom-text-200 text-sm"
|
||||
placement="bottom-start"
|
||||
customButton={
|
||||
<div className="flex gap-2 items-center px-2 py-1.5 border rounded-md border-custom-border-400">
|
||||
<span className="flex flex-grow justify-center text-custom-text-200 text-sm">{showMenuItem()}</span>
|
||||
<ChevronDown className="w-4 h-4 text-custom-text-400" />
|
||||
</div>
|
||||
}
|
||||
customButtonClassName="flex flex-grow justify-start text-custom-text-200 text-sm"
|
||||
>
|
||||
<></>
|
||||
{profilePreferenceLinks.map((link) => (
|
||||
<CustomMenu.MenuItem
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Link key={link.href} href={link.href} className="text-custom-text-300 w-full">{link.label}</Link>
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
}>
|
||||
<ProfileSettingsLayout
|
||||
header={
|
||||
<div className="md:hidden flex flex-shrink-0 gap-4 items-center justify-start border-b border-custom-border-200 p-4">
|
||||
<SidebarHamburgerToggle onClick={() => themeStore.toggleSidebar()} />
|
||||
<CustomMenu
|
||||
maxHeight={"md"}
|
||||
className="flex flex-grow justify-center text-custom-text-200 text-sm"
|
||||
placement="bottom-start"
|
||||
customButton={
|
||||
<div className="flex gap-2 items-center px-2 py-1.5 border rounded-md border-custom-border-400">
|
||||
<span className="flex flex-grow justify-center text-custom-text-200 text-sm">{showMenuItem()}</span>
|
||||
<ChevronDown className="w-4 h-4 text-custom-text-400" />
|
||||
</div>
|
||||
}
|
||||
customButtonClassName="flex flex-grow justify-start text-custom-text-200 text-sm"
|
||||
>
|
||||
<></>
|
||||
{profilePreferenceLinks.map((link) => (
|
||||
<CustomMenu.MenuItem className="flex items-center gap-2">
|
||||
<Link key={link.href} href={link.href} className="text-custom-text-300 w-full">
|
||||
{link.label}
|
||||
</Link>
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="relative flex h-screen w-full overflow-hidden">
|
||||
<ProfilePreferenceSettingsSidebar />
|
||||
<main className="relative flex h-full w-full flex-col overflow-hidden bg-custom-background-100">
|
||||
|
@ -202,7 +202,7 @@ export class IssueHelperStore implements TIssueHelperStore {
|
||||
if (!memberMap) break;
|
||||
for (const dataId of dataIdsArray) {
|
||||
const member = memberMap[dataId];
|
||||
if (memberMap && member.first_name) dataValues.push(member.first_name.toLocaleLowerCase());
|
||||
if (member && member.first_name) dataValues.push(member.first_name.toLocaleLowerCase());
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user