mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
Merge branch 'develop' of github.com:makeplane/plane into fix/playwright-automation-tests
This commit is contained in:
commit
d7c37560db
@ -1,6 +1,7 @@
|
|||||||
# Django imports
|
# Django imports
|
||||||
from django.db.models import Count, Sum, F, Q
|
from django.db.models import Count, Sum, F, Q
|
||||||
from django.db.models.functions import ExtractMonth
|
from django.db.models.functions import ExtractMonth
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
@ -331,8 +332,9 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
|
|||||||
.order_by("state_group")
|
.order_by("state_group")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
current_year = timezone.now().year
|
||||||
issue_completed_month_wise = (
|
issue_completed_month_wise = (
|
||||||
base_issues.filter(completed_at__isnull=False)
|
base_issues.filter(completed_at__year=current_year)
|
||||||
.annotate(month=ExtractMonth("completed_at"))
|
.annotate(month=ExtractMonth("completed_at"))
|
||||||
.values("month")
|
.values("month")
|
||||||
.annotate(count=Count("*"))
|
.annotate(count=Count("*"))
|
||||||
|
@ -1209,13 +1209,13 @@ class IssueArchiveViewSet(BaseViewSet):
|
|||||||
return Response(issues, status=status.HTTP_200_OK)
|
return Response(issues, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def retrieve(self, request, slug, project_id, pk=None):
|
def retrieve(self, request, slug, project_id, pk=None):
|
||||||
issue = Issue.objects.get(
|
issue = self.get_queryset().filter(pk=pk).first()
|
||||||
workspace__slug=slug,
|
return Response(
|
||||||
project_id=project_id,
|
IssueDetailSerializer(
|
||||||
archived_at__isnull=False,
|
issue, fields=self.fields, expand=self.expand
|
||||||
pk=pk,
|
).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):
|
def unarchive(self, request, slug, project_id, pk=None):
|
||||||
issue = Issue.objects.get(
|
issue = Issue.objects.get(
|
||||||
|
@ -15,6 +15,7 @@ import { EditorBubbleMenu } from "src/ui/menus/bubble-menu";
|
|||||||
|
|
||||||
export type IRichTextEditor = {
|
export type IRichTextEditor = {
|
||||||
value: string;
|
value: string;
|
||||||
|
initialValue?: string;
|
||||||
dragDropEnabled?: boolean;
|
dragDropEnabled?: boolean;
|
||||||
uploadFile: UploadImage;
|
uploadFile: UploadImage;
|
||||||
restoreFile: RestoreImage;
|
restoreFile: RestoreImage;
|
||||||
@ -54,6 +55,7 @@ const RichTextEditor = ({
|
|||||||
setShouldShowAlert,
|
setShouldShowAlert,
|
||||||
editorContentCustomClassNames,
|
editorContentCustomClassNames,
|
||||||
value,
|
value,
|
||||||
|
initialValue,
|
||||||
uploadFile,
|
uploadFile,
|
||||||
deleteFile,
|
deleteFile,
|
||||||
noBorder,
|
noBorder,
|
||||||
@ -97,6 +99,10 @@ const RichTextEditor = ({
|
|||||||
customClassName,
|
customClassName,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (editor && initialValue && editor.getHTML() != initialValue) editor.commands.setContent(initialValue);
|
||||||
|
}, [editor, initialValue]);
|
||||||
|
|
||||||
if (!editor) return null;
|
if (!editor) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -4,3 +4,4 @@ export * from "./sidebar";
|
|||||||
export * from "./theme";
|
export * from "./theme";
|
||||||
export * from "./activity";
|
export * from "./activity";
|
||||||
export * from "./image-picker-popover";
|
export * from "./image-picker-popover";
|
||||||
|
export * from "./page-title";
|
||||||
|
18
web/components/core/page-title.tsx
Normal file
18
web/components/core/page-title.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import Head from "next/head";
|
||||||
|
|
||||||
|
type PageHeadTitleProps = {
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PageHead: React.FC<PageHeadTitleProps> = (props) => {
|
||||||
|
const { title } = props;
|
||||||
|
|
||||||
|
if (!title) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Head>
|
||||||
|
<title>{title}</title>
|
||||||
|
</Head>
|
||||||
|
);
|
||||||
|
};
|
@ -66,7 +66,6 @@ export const CustomThemeSelector: React.FC = observer(() => {
|
|||||||
|
|
||||||
const handleValueChange = (val: string | undefined, onChange: any) => {
|
const handleValueChange = (val: string | undefined, onChange: any) => {
|
||||||
let hex = val;
|
let hex = val;
|
||||||
|
|
||||||
// prepend a hashtag if it doesn't exist
|
// prepend a hashtag if it doesn't exist
|
||||||
if (val && val[0] !== "#") hex = `#${val}`;
|
if (val && val[0] !== "#") hex = `#${val}`;
|
||||||
|
|
||||||
@ -94,7 +93,7 @@ export const CustomThemeSelector: React.FC = observer(() => {
|
|||||||
placeholder="#0d101b"
|
placeholder="#0d101b"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: value,
|
backgroundColor: watch("background"),
|
||||||
color: watch("text"),
|
color: watch("text"),
|
||||||
}}
|
}}
|
||||||
hasError={Boolean(errors?.background)}
|
hasError={Boolean(errors?.background)}
|
||||||
@ -120,8 +119,8 @@ export const CustomThemeSelector: React.FC = observer(() => {
|
|||||||
placeholder="#c5c5c5"
|
placeholder="#c5c5c5"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: watch("background"),
|
backgroundColor: watch("text"),
|
||||||
color: value,
|
color: watch("background"),
|
||||||
}}
|
}}
|
||||||
hasError={Boolean(errors?.text)}
|
hasError={Boolean(errors?.text)}
|
||||||
/>
|
/>
|
||||||
@ -146,7 +145,7 @@ export const CustomThemeSelector: React.FC = observer(() => {
|
|||||||
placeholder="#3f76ff"
|
placeholder="#3f76ff"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: value,
|
backgroundColor: watch("primary"),
|
||||||
color: watch("text"),
|
color: watch("text"),
|
||||||
}}
|
}}
|
||||||
hasError={Boolean(errors?.primary)}
|
hasError={Boolean(errors?.primary)}
|
||||||
@ -172,7 +171,7 @@ export const CustomThemeSelector: React.FC = observer(() => {
|
|||||||
placeholder="#0d101b"
|
placeholder="#0d101b"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: value,
|
backgroundColor: watch("sidebarBackground"),
|
||||||
color: watch("sidebarText"),
|
color: watch("sidebarText"),
|
||||||
}}
|
}}
|
||||||
hasError={Boolean(errors?.sidebarBackground)}
|
hasError={Boolean(errors?.sidebarBackground)}
|
||||||
@ -200,8 +199,8 @@ export const CustomThemeSelector: React.FC = observer(() => {
|
|||||||
placeholder="#c5c5c5"
|
placeholder="#c5c5c5"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: watch("sidebarBackground"),
|
backgroundColor: watch("sidebarText"),
|
||||||
color: value,
|
color: watch("sidebarBackground"),
|
||||||
}}
|
}}
|
||||||
hasError={Boolean(errors?.sidebarText)}
|
hasError={Boolean(errors?.sidebarText)}
|
||||||
/>
|
/>
|
||||||
|
@ -96,7 +96,7 @@ export const RecentProjectsWidget: React.FC<WidgetProps> = observer((props) => {
|
|||||||
href={`/${workspaceSlug}/projects`}
|
href={`/${workspaceSlug}/projects`}
|
||||||
className="text-lg font-semibold text-custom-text-300 mx-7 hover:underline"
|
className="text-lg font-semibold text-custom-text-300 mx-7 hover:underline"
|
||||||
>
|
>
|
||||||
Your projects
|
Recent projects
|
||||||
</Link>
|
</Link>
|
||||||
<div className="space-y-8 mt-4 mx-7">
|
<div className="space-y-8 mt-4 mx-7">
|
||||||
{canCreateProject && (
|
{canCreateProject && (
|
||||||
|
@ -61,6 +61,7 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
|||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
// refs
|
// refs
|
||||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||||
// popper-js refs
|
// popper-js refs
|
||||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||||
@ -111,23 +112,15 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
|||||||
const filteredOptions =
|
const filteredOptions =
|
||||||
query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase()));
|
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 selectedCycle = value ? getCycleById(value) : null;
|
||||||
|
|
||||||
const onOpen = () => {
|
const onOpen = () => {
|
||||||
if (referenceElement) referenceElement.focus();
|
if (workspaceSlug && !cycleIds) fetchAllCycles(workspaceSlug, projectId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
if (!isOpen) return;
|
if (!isOpen) return;
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
if (referenceElement) referenceElement.blur();
|
|
||||||
onClose && onClose();
|
onClose && onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -151,6 +144,12 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
useOutsideClickDetector(dropdownRef, handleClose);
|
useOutsideClickDetector(dropdownRef, handleClose);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen && inputRef.current) {
|
||||||
|
inputRef.current.focus();
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Combobox
|
<Combobox
|
||||||
as="div"
|
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">
|
<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} />
|
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
|
||||||
<Combobox.Input
|
<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"
|
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||||
value={query}
|
value={query}
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
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 { observer } from "mobx-react-lite";
|
||||||
import { Combobox } from "@headlessui/react";
|
import { Combobox } from "@headlessui/react";
|
||||||
import { usePopper } from "react-popper";
|
import { usePopper } from "react-popper";
|
||||||
@ -60,6 +60,7 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
|||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
// refs
|
// refs
|
||||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||||
// popper-js refs
|
// popper-js refs
|
||||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||||
@ -110,13 +111,11 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
const onOpen = () => {
|
const onOpen = () => {
|
||||||
if (!activeEstimate && workspaceSlug) fetchProjectEstimates(workspaceSlug, projectId);
|
if (!activeEstimate && workspaceSlug) fetchProjectEstimates(workspaceSlug, projectId);
|
||||||
if (referenceElement) referenceElement.focus();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
if (!isOpen) return;
|
if (!isOpen) return;
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
if (referenceElement) referenceElement.blur();
|
|
||||||
onClose && onClose();
|
onClose && onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -140,6 +139,12 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
useOutsideClickDetector(dropdownRef, handleClose);
|
useOutsideClickDetector(dropdownRef, handleClose);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen && inputRef.current) {
|
||||||
|
inputRef.current.focus();
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Combobox
|
<Combobox
|
||||||
as="div"
|
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">
|
<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} />
|
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
|
||||||
<Combobox.Input
|
<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"
|
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||||
value={query}
|
value={query}
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
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 { observer } from "mobx-react-lite";
|
||||||
import { Combobox } from "@headlessui/react";
|
import { Combobox } from "@headlessui/react";
|
||||||
import { usePopper } from "react-popper";
|
import { usePopper } from "react-popper";
|
||||||
@ -50,6 +50,7 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
|
|||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
// refs
|
// refs
|
||||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||||
// popper-js refs
|
// popper-js refs
|
||||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||||
@ -103,13 +104,11 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
const onOpen = () => {
|
const onOpen = () => {
|
||||||
if (!projectMemberIds && workspaceSlug) fetchProjectMembers(workspaceSlug, projectId);
|
if (!projectMemberIds && workspaceSlug) fetchProjectMembers(workspaceSlug, projectId);
|
||||||
if (referenceElement) referenceElement.focus();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
if (!isOpen) return;
|
if (!isOpen) return;
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
if (referenceElement) referenceElement.blur();
|
|
||||||
onClose && onClose();
|
onClose && onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -133,6 +132,12 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
useOutsideClickDetector(dropdownRef, handleClose);
|
useOutsideClickDetector(dropdownRef, handleClose);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen && inputRef.current) {
|
||||||
|
inputRef.current.focus();
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Combobox
|
<Combobox
|
||||||
as="div"
|
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">
|
<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} />
|
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
|
||||||
<Combobox.Input
|
<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"
|
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||||
value={query}
|
value={query}
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
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 { observer } from "mobx-react-lite";
|
||||||
import { Combobox } from "@headlessui/react";
|
import { Combobox } from "@headlessui/react";
|
||||||
import { usePopper } from "react-popper";
|
import { usePopper } from "react-popper";
|
||||||
@ -44,6 +44,7 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
|
|||||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||||
// refs
|
// refs
|
||||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||||
// popper-js refs
|
// popper-js refs
|
||||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | 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;
|
if (multiple) comboboxProps.multiple = true;
|
||||||
|
|
||||||
const onOpen = () => {
|
|
||||||
if (referenceElement) referenceElement.focus();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
if (!isOpen) return;
|
if (!isOpen) return;
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
if (referenceElement) referenceElement.blur();
|
|
||||||
onClose && onClose();
|
onClose && onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleDropdown = () => {
|
const toggleDropdown = () => {
|
||||||
if (!isOpen) onOpen();
|
|
||||||
setIsOpen((prevIsOpen) => !prevIsOpen);
|
setIsOpen((prevIsOpen) => !prevIsOpen);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -122,6 +117,12 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
|
|||||||
|
|
||||||
useOutsideClickDetector(dropdownRef, handleClose);
|
useOutsideClickDetector(dropdownRef, handleClose);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen && inputRef.current) {
|
||||||
|
inputRef.current.focus();
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Combobox
|
<Combobox
|
||||||
as="div"
|
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">
|
<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} />
|
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
|
||||||
<Combobox.Input
|
<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"
|
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||||
value={query}
|
value={query}
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
|
@ -166,6 +166,7 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
|||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
// refs
|
// refs
|
||||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||||
// popper-js refs
|
// popper-js refs
|
||||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||||
@ -216,21 +217,13 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
|||||||
const filteredOptions =
|
const filteredOptions =
|
||||||
query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase()));
|
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 = () => {
|
const onOpen = () => {
|
||||||
if (referenceElement) referenceElement.focus();
|
if (!moduleIds && workspaceSlug) fetchModules(workspaceSlug, projectId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
if (!isOpen) return;
|
if (!isOpen) return;
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
if (referenceElement) referenceElement.blur();
|
|
||||||
onClose && onClose();
|
onClose && onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -261,6 +254,12 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
|||||||
};
|
};
|
||||||
if (multiple) comboboxProps.multiple = true;
|
if (multiple) comboboxProps.multiple = true;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen && inputRef.current) {
|
||||||
|
inputRef.current.focus();
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Combobox
|
<Combobox
|
||||||
as="div"
|
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">
|
<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} />
|
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
|
||||||
<Combobox.Input
|
<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"
|
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||||
value={query}
|
value={query}
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
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 { Combobox } from "@headlessui/react";
|
||||||
import { usePopper } from "react-popper";
|
import { usePopper } from "react-popper";
|
||||||
import { Check, ChevronDown, Search } from "lucide-react";
|
import { Check, ChevronDown, Search } from "lucide-react";
|
||||||
@ -272,6 +272,7 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
|||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
// refs
|
// refs
|
||||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||||
// popper-js refs
|
// popper-js refs
|
||||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||||
@ -305,19 +306,13 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
|||||||
const filteredOptions =
|
const filteredOptions =
|
||||||
query === "" ? options : options.filter((o) => o.query.toLowerCase().includes(query.toLowerCase()));
|
query === "" ? options : options.filter((o) => o.query.toLowerCase().includes(query.toLowerCase()));
|
||||||
|
|
||||||
const onOpen = () => {
|
|
||||||
if (referenceElement) referenceElement.focus();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
if (!isOpen) return;
|
if (!isOpen) return;
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
if (referenceElement) referenceElement.blur();
|
|
||||||
onClose && onClose();
|
onClose && onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleDropdown = () => {
|
const toggleDropdown = () => {
|
||||||
if (!isOpen) onOpen();
|
|
||||||
setIsOpen((prevIsOpen) => !prevIsOpen);
|
setIsOpen((prevIsOpen) => !prevIsOpen);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -342,6 +337,12 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
|||||||
? BackgroundButton
|
? BackgroundButton
|
||||||
: TransparentButton;
|
: TransparentButton;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen && inputRef.current) {
|
||||||
|
inputRef.current.focus();
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Combobox
|
<Combobox
|
||||||
as="div"
|
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">
|
<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} />
|
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
|
||||||
<Combobox.Input
|
<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"
|
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||||
value={query}
|
value={query}
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
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 { observer } from "mobx-react-lite";
|
||||||
import { Combobox } from "@headlessui/react";
|
import { Combobox } from "@headlessui/react";
|
||||||
import { usePopper } from "react-popper";
|
import { usePopper } from "react-popper";
|
||||||
@ -50,6 +50,7 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
|||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
// refs
|
// refs
|
||||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||||
// popper-js refs
|
// popper-js refs
|
||||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | 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 selectedProject = value ? getProjectById(value) : null;
|
||||||
|
|
||||||
const onOpen = () => {
|
|
||||||
if (referenceElement) referenceElement.focus();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
if (!isOpen) return;
|
if (!isOpen) return;
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
onClose && onClose();
|
onClose && onClose();
|
||||||
if (referenceElement) referenceElement.blur();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleDropdown = () => {
|
const toggleDropdown = () => {
|
||||||
if (!isOpen) onOpen();
|
|
||||||
setIsOpen((prevIsOpen) => !prevIsOpen);
|
setIsOpen((prevIsOpen) => !prevIsOpen);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -125,6 +120,12 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
useOutsideClickDetector(dropdownRef, handleClose);
|
useOutsideClickDetector(dropdownRef, handleClose);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen && inputRef.current) {
|
||||||
|
inputRef.current.focus();
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Combobox
|
<Combobox
|
||||||
as="div"
|
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">
|
<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} />
|
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
|
||||||
<Combobox.Input
|
<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"
|
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||||
value={query}
|
value={query}
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
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 { observer } from "mobx-react-lite";
|
||||||
import { Combobox } from "@headlessui/react";
|
import { Combobox } from "@headlessui/react";
|
||||||
import { usePopper } from "react-popper";
|
import { usePopper } from "react-popper";
|
||||||
@ -52,6 +52,7 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
|||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
// refs
|
// refs
|
||||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||||
// popper-js refs
|
// popper-js refs
|
||||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||||
@ -92,14 +93,12 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
const onOpen = () => {
|
const onOpen = () => {
|
||||||
if (!statesList && workspaceSlug) fetchProjectStates(workspaceSlug, projectId);
|
if (!statesList && workspaceSlug) fetchProjectStates(workspaceSlug, projectId);
|
||||||
if (referenceElement) referenceElement.focus();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
if (!isOpen) return;
|
if (!isOpen) return;
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
onClose && onClose();
|
onClose && onClose();
|
||||||
if (referenceElement) referenceElement.blur();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleDropdown = () => {
|
const toggleDropdown = () => {
|
||||||
@ -122,6 +121,12 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
useOutsideClickDetector(dropdownRef, handleClose);
|
useOutsideClickDetector(dropdownRef, handleClose);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen && inputRef.current) {
|
||||||
|
inputRef.current.focus();
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Combobox
|
<Combobox
|
||||||
as="div"
|
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">
|
<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} />
|
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
|
||||||
<Combobox.Input
|
<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"
|
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||||
value={query}
|
value={query}
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Briefcase, Circle, ExternalLink, Plus, Inbox } from "lucide-react";
|
import { Briefcase, Circle, ExternalLink, Plus } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
import {
|
import {
|
||||||
useApplication,
|
useApplication,
|
||||||
@ -11,7 +10,6 @@ import {
|
|||||||
useProject,
|
useProject,
|
||||||
useProjectState,
|
useProjectState,
|
||||||
useUser,
|
useUser,
|
||||||
useInbox,
|
|
||||||
useMember,
|
useMember,
|
||||||
} from "hooks/store";
|
} from "hooks/store";
|
||||||
// components
|
// components
|
||||||
@ -54,7 +52,6 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
|||||||
const { currentProjectDetails } = useProject();
|
const { currentProjectDetails } = useProject();
|
||||||
const { projectStates } = useProjectState();
|
const { projectStates } = useProjectState();
|
||||||
const { projectLabels } = useLabel();
|
const { projectLabels } = useLabel();
|
||||||
const { getInboxesByProjectId, getInboxById } = useInbox();
|
|
||||||
|
|
||||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||||
|
|
||||||
@ -101,9 +98,6 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
|||||||
[workspaceSlug, projectId, updateFilters]
|
[workspaceSlug, projectId, updateFilters]
|
||||||
);
|
);
|
||||||
|
|
||||||
const inboxesMap = currentProjectDetails?.inbox_view ? getInboxesByProjectId(currentProjectDetails.id) : undefined;
|
|
||||||
const inboxDetails = inboxesMap && inboxesMap.length > 0 ? getInboxById(inboxesMap[0]) : undefined;
|
|
||||||
|
|
||||||
const deployUrl = process.env.NEXT_PUBLIC_DEPLOY_URL;
|
const deployUrl = process.env.NEXT_PUBLIC_DEPLOY_URL;
|
||||||
const canUserCreateIssue =
|
const canUserCreateIssue =
|
||||||
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
||||||
@ -154,7 +148,9 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
|||||||
|
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
type="text"
|
type="text"
|
||||||
link={<BreadcrumbLink label="Issues" icon={<LayersIcon className="h-4 w-4 text-custom-text-300" />} />}
|
link={
|
||||||
|
<BreadcrumbLink label="Issues" icon={<LayersIcon className="h-4 w-4 text-custom-text-300" />} />
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
</div>
|
</div>
|
||||||
@ -201,24 +197,15 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
|||||||
/>
|
/>
|
||||||
</FiltersDropdown>
|
</FiltersDropdown>
|
||||||
</div>
|
</div>
|
||||||
{currentProjectDetails?.inbox_view && inboxDetails && (
|
|
||||||
<Link href={`/${workspaceSlug}/projects/${projectId}/inbox/${inboxDetails?.id}`}>
|
|
||||||
<span className="hidden md:block" >
|
|
||||||
<Button variant="neutral-primary" size="sm" className="relative">
|
|
||||||
Inbox
|
|
||||||
{inboxDetails?.pending_issue_count > 0 && (
|
|
||||||
<span className="absolute -right-1.5 -top-1.5 h-4 w-4 rounded-full border border-custom-sidebar-border-200 bg-custom-sidebar-background-80 text-custom-text-100">
|
|
||||||
{inboxDetails?.pending_issue_count}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</span>
|
|
||||||
<Inbox className="w-4 h-4 mr-2 text-custom-text-200 block md:hidden" />
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
{canUserCreateIssue && (
|
{canUserCreateIssue && (
|
||||||
<>
|
<>
|
||||||
<Button className="hidden md:block" onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
|
<Button
|
||||||
|
className="hidden md:block"
|
||||||
|
onClick={() => setAnalyticsModal(true)}
|
||||||
|
variant="neutral-primary"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
Analytics
|
Analytics
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
@ -4,7 +4,7 @@ import { observer } from "mobx-react";
|
|||||||
// hooks
|
// hooks
|
||||||
import { useInboxIssues } from "hooks/store";
|
import { useInboxIssues } from "hooks/store";
|
||||||
// ui
|
// ui
|
||||||
import { Loader } from "@plane/ui";
|
import { InboxSidebarLoader } from "components/ui";
|
||||||
// components
|
// components
|
||||||
import { InboxIssueList, InboxIssueFilterSelection, InboxIssueAppliedFilter } from "../";
|
import { InboxIssueList, InboxIssueFilterSelection, InboxIssueAppliedFilter } from "../";
|
||||||
|
|
||||||
@ -21,6 +21,10 @@ export const InboxSidebarRoot: FC<TInboxSidebarRoot> = observer((props) => {
|
|||||||
issues: { loader },
|
issues: { loader },
|
||||||
} = useInboxIssues();
|
} = useInboxIssues();
|
||||||
|
|
||||||
|
if (loader === "init-loader") {
|
||||||
|
return <InboxSidebarLoader />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex flex-col w-full h-full">
|
<div className="relative flex flex-col w-full h-full">
|
||||||
<div className="flex-shrink-0 w-full h-[50px] relative flex justify-between items-center gap-2 p-2 px-3 border-b border-custom-border-300">
|
<div className="flex-shrink-0 w-full h-[50px] relative flex justify-between items-center gap-2 p-2 px-3 border-b border-custom-border-300">
|
||||||
@ -28,7 +32,6 @@ export const InboxSidebarRoot: FC<TInboxSidebarRoot> = observer((props) => {
|
|||||||
<div className="relative w-6 h-6 flex justify-center items-center rounded bg-custom-background-80">
|
<div className="relative w-6 h-6 flex justify-center items-center rounded bg-custom-background-80">
|
||||||
<Inbox className="w-4 h-4" />
|
<Inbox className="w-4 h-4" />
|
||||||
</div>
|
</div>
|
||||||
<div className="font-medium">Inbox</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="z-20">
|
<div className="z-20">
|
||||||
<InboxIssueFilterSelection workspaceSlug={workspaceSlug} projectId={projectId} inboxId={inboxId} />
|
<InboxIssueFilterSelection workspaceSlug={workspaceSlug} projectId={projectId} inboxId={inboxId} />
|
||||||
@ -39,18 +42,9 @@ export const InboxSidebarRoot: FC<TInboxSidebarRoot> = observer((props) => {
|
|||||||
<InboxIssueAppliedFilter workspaceSlug={workspaceSlug} projectId={projectId} inboxId={inboxId} />
|
<InboxIssueAppliedFilter workspaceSlug={workspaceSlug} projectId={projectId} inboxId={inboxId} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{loader && ["init-loader", "mutation"].includes(loader) ? (
|
<div className="w-full h-full overflow-hidden">
|
||||||
<Loader className="flex flex-col h-full gap-5 p-5">
|
<InboxIssueList workspaceSlug={workspaceSlug} projectId={projectId} inboxId={inboxId} />
|
||||||
<Loader.Item height="30px" />
|
</div>
|
||||||
<Loader.Item height="30px" />
|
|
||||||
<Loader.Item height="30px" />
|
|
||||||
<Loader.Item height="30px" />
|
|
||||||
</Loader>
|
|
||||||
) : (
|
|
||||||
<div className="w-full h-full overflow-hidden">
|
|
||||||
<InboxIssueList workspaceSlug={workspaceSlug} projectId={projectId} inboxId={inboxId} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
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";
|
import { observer } from "mobx-react-lite";
|
||||||
// hooks
|
// hooks
|
||||||
import { useIssueDetail, useProjectState, useUser } from "hooks/store";
|
import { useIssueDetail, useProjectState, useUser } from "hooks/store";
|
||||||
|
import useReloadConfirmations from "hooks/use-reload-confirmation";
|
||||||
// components
|
// 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 { IssueReaction } from "../reactions";
|
||||||
import { IssueActivity } from "../issue-activity";
|
import { IssueActivity } from "../issue-activity";
|
||||||
import { InboxIssueStatus } from "../../../inbox/inbox-issue-status";
|
import { InboxIssueStatus } from "../../../inbox/inbox-issue-status";
|
||||||
@ -29,12 +32,31 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
|
|||||||
const {
|
const {
|
||||||
issue: { getIssueById },
|
issue: { getIssueById },
|
||||||
} = useIssueDetail();
|
} = 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 <></>;
|
if (!issue) return <></>;
|
||||||
|
|
||||||
const currentIssueState = projectStates?.find((s) => s.id === issue.state_id);
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="rounded-lg space-y-4">
|
<div className="rounded-lg space-y-4">
|
||||||
@ -57,15 +79,26 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
|
|||||||
<IssueUpdateStatus isSubmitting={isSubmitting} issueDetail={issue} />
|
<IssueUpdateStatus isSubmitting={isSubmitting} issueDetail={issue} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<IssueDescriptionForm
|
<IssueTitleInput
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
projectId={projectId}
|
projectId={issue.project_id}
|
||||||
issueId={issueId}
|
issueId={issue.id}
|
||||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
|
||||||
isSubmitting={isSubmitting}
|
isSubmitting={isSubmitting}
|
||||||
issue={issue}
|
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||||
issueOperations={issueOperations}
|
issueOperations={issueOperations}
|
||||||
disabled={!is_editable}
|
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 && (
|
{currentUser && (
|
||||||
|
@ -81,7 +81,6 @@ export const IssueCommentCreate: FC<TIssueCommentCreate> = (props) => {
|
|||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<LiteTextEditorWithRef
|
<LiteTextEditorWithRef
|
||||||
onEnterKeyPress={(e) => {
|
onEnterKeyPress={(e) => {
|
||||||
console.log("yo");
|
|
||||||
handleSubmit(onSubmit)(e);
|
handleSubmit(onSubmit)(e);
|
||||||
}}
|
}}
|
||||||
cancelUploadImage={fileService.cancelUpload}
|
cancelUploadImage={fileService.cancelUpload}
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// hooks
|
// hooks
|
||||||
import { useIssueDetail, useProjectState, useUser } from "hooks/store";
|
import { useIssueDetail, useProjectState, useUser } from "hooks/store";
|
||||||
|
import useReloadConfirmations from "hooks/use-reload-confirmation";
|
||||||
// components
|
// 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 { IssueParentDetail } from "./parent";
|
||||||
import { IssueReaction } from "./reactions";
|
import { IssueReaction } from "./reactions";
|
||||||
import { SubIssuesRoot } from "../sub-issues";
|
import { SubIssuesRoot } from "../sub-issues";
|
||||||
@ -31,12 +34,31 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
|
|||||||
const {
|
const {
|
||||||
issue: { getIssueById },
|
issue: { getIssueById },
|
||||||
} = useIssueDetail();
|
} = 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 <></>;
|
if (!issue) return <></>;
|
||||||
|
|
||||||
const currentIssueState = projectStates?.find((s) => s.id === issue.state_id);
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="rounded-lg space-y-4">
|
<div className="rounded-lg space-y-4">
|
||||||
@ -61,15 +83,26 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
|
|||||||
<IssueUpdateStatus isSubmitting={isSubmitting} issueDetail={issue} />
|
<IssueUpdateStatus isSubmitting={isSubmitting} issueDetail={issue} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<IssueDescriptionForm
|
<IssueTitleInput
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
projectId={projectId}
|
projectId={issue.project_id}
|
||||||
issueId={issueId}
|
issueId={issue.id}
|
||||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
|
||||||
isSubmitting={isSubmitting}
|
isSubmitting={isSubmitting}
|
||||||
issue={issue}
|
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||||
issueOperations={issueOperations}
|
issueOperations={issueOperations}
|
||||||
disabled={!is_editable}
|
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 && (
|
{currentUser && (
|
||||||
|
@ -76,7 +76,7 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
|
|||||||
<CalendarWeekHeader isLoading={!issues} showWeekends={showWeekends} />
|
<CalendarWeekHeader isLoading={!issues} showWeekends={showWeekends} />
|
||||||
<div className="h-full w-full overflow-y-auto">
|
<div className="h-full w-full overflow-y-auto">
|
||||||
{layout === "month" && (
|
{layout === "month" && (
|
||||||
<div className="grid h-full w-full grid-cols-1 divide-y-[0.5px] divide-custom-border-200">
|
<div className="grid h-full w-full grid-cols-1 divide-y-[0.5px] divide-custom-border-400">
|
||||||
{allWeeksOfActiveMonth &&
|
{allWeeksOfActiveMonth &&
|
||||||
Object.values(allWeeksOfActiveMonth).map((week: ICalendarWeek, weekIndex) => (
|
Object.values(allWeeksOfActiveMonth).map((week: ICalendarWeek, weekIndex) => (
|
||||||
<CalendarWeekDays
|
<CalendarWeekDays
|
||||||
|
@ -52,12 +52,15 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
|
|||||||
const issueIdList = groupedIssueIds ? groupedIssueIds[formattedDatePayload] : null;
|
const issueIdList = groupedIssueIds ? groupedIssueIds[formattedDatePayload] : null;
|
||||||
|
|
||||||
const totalIssues = issueIdList?.length ?? 0;
|
const totalIssues = issueIdList?.length ?? 0;
|
||||||
|
|
||||||
|
const isToday = date.date.toDateString() === new Date().toDateString();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="group relative flex h-full w-full flex-col bg-custom-background-90">
|
<div className="group relative flex h-full w-full flex-col bg-custom-background-90">
|
||||||
{/* header */}
|
{/* header */}
|
||||||
<div
|
<div
|
||||||
className={`flex-shrink-0 px-2 py-1 text-right text-xs ${
|
className={`flex items-center justify-end flex-shrink-0 px-2 py-1.5 text-right text-xs ${
|
||||||
calendarLayout === "month" // if month layout, highlight current month days
|
calendarLayout === "month" // if month layout, highlight current month days
|
||||||
? date.is_current_month
|
? date.is_current_month
|
||||||
? "font-medium"
|
? "font-medium"
|
||||||
@ -67,10 +70,16 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
|
|||||||
date.date.getDay() === 0 || date.date.getDay() === 6
|
date.date.getDay() === 0 || date.date.getDay() === 6
|
||||||
? "bg-custom-background-90"
|
? "bg-custom-background-90"
|
||||||
: "bg-custom-background-100"
|
: "bg-custom-background-100"
|
||||||
}`}
|
} `}
|
||||||
>
|
>
|
||||||
{date.date.getDate() === 1 && MONTHS_LIST[date.date.getMonth() + 1].shortTitle + " "}
|
{date.date.getDate() === 1 && MONTHS_LIST[date.date.getMonth() + 1].shortTitle + " "}
|
||||||
{date.date.getDate()}
|
{isToday ? (
|
||||||
|
<span className="flex items-center justify-center h-5 w-5 rounded-full bg-custom-primary-100 text-white">
|
||||||
|
{date.date.getDate()}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<>{date.date.getDate()}</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* content */}
|
{/* content */}
|
||||||
|
@ -50,7 +50,7 @@ export const CalendarWeekDays: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`grid divide-x-[0.5px] divide-custom-border-200 ${showWeekends ? "grid-cols-7" : "grid-cols-5"} ${
|
className={`grid divide-x-[0.5px] divide-custom-border-400 ${showWeekends ? "grid-cols-7" : "grid-cols-5"} ${
|
||||||
calendarLayout === "month" ? "" : "h-full"
|
calendarLayout === "month" ? "" : "h-full"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
@ -13,7 +13,7 @@ export const CalendarWeekHeader: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`relative grid divide-x-[0.5px] divide-custom-border-200 text-sm font-medium ${
|
className={`relative grid divide-x-[0.5px] divide-custom-border-400 text-sm font-medium ${
|
||||||
showWeekends ? "grid-cols-7" : "grid-cols-5"
|
showWeekends ? "grid-cols-7" : "grid-cols-5"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@ -24,7 +24,7 @@ export const CalendarWeekHeader: React.FC<Props> = observer((props) => {
|
|||||||
if (!showWeekends && (day.shortTitle === "Sat" || day.shortTitle === "Sun")) return null;
|
if (!showWeekends && (day.shortTitle === "Sat" || day.shortTitle === "Sun")) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={day.shortTitle} className="flex h-11 items-center bg-custom-background-90 px-4">
|
<div key={day.shortTitle} className="flex h-11 items-center justify-end bg-custom-background-90 px-4">
|
||||||
{day.shortTitle}
|
{day.shortTitle}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -42,7 +42,7 @@ export const ProjectDraftEmptyState: React.FC = observer(() => {
|
|||||||
|
|
||||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||||
const currentLayoutEmptyStateImagePath = getEmptyStateImagePath("empty-filters", activeLayout ?? "list", isLightMode);
|
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(
|
const issueFilterCount = size(
|
||||||
Object.fromEntries(
|
Object.fromEntries(
|
||||||
|
@ -288,7 +288,7 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
|||||||
handleKanbanFilters={handleKanbanFilters}
|
handleKanbanFilters={handleKanbanFilters}
|
||||||
kanbanFilters={kanbanFilters}
|
kanbanFilters={kanbanFilters}
|
||||||
enableQuickIssueCreate={enableQuickAdd}
|
enableQuickIssueCreate={enableQuickAdd}
|
||||||
showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
|
showEmptyGroup={userDisplayFilters?.show_empty_groups ?? true}
|
||||||
quickAddCallback={issues?.quickAddIssue}
|
quickAddCallback={issues?.quickAddIssue}
|
||||||
viewId={viewId}
|
viewId={viewId}
|
||||||
disableIssueCreation={!enableIssueCreation || !isEditingAllowed || isCompletedCycle}
|
disableIssueCreation={!enableIssueCreation || !isEditingAllowed || isCompletedCycle}
|
||||||
|
@ -48,6 +48,7 @@ export interface IGroupByKanBan {
|
|||||||
canEditProperties: (projectId: string | undefined) => boolean;
|
canEditProperties: (projectId: string | undefined) => boolean;
|
||||||
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
||||||
isDragStarted?: boolean;
|
isDragStarted?: boolean;
|
||||||
|
showEmptyGroup?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
||||||
@ -72,6 +73,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
|||||||
canEditProperties,
|
canEditProperties,
|
||||||
scrollableContainerRef,
|
scrollableContainerRef,
|
||||||
isDragStarted,
|
isDragStarted,
|
||||||
|
showEmptyGroup = true,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const member = useMember();
|
const member = useMember();
|
||||||
@ -84,6 +86,10 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
|||||||
|
|
||||||
if (!list) return null;
|
if (!list) return null;
|
||||||
|
|
||||||
|
const groupWithIssues = list.filter((_list) => (issueIds as TGroupedIssues)[_list.id]?.length > 0);
|
||||||
|
|
||||||
|
const groupList = showEmptyGroup ? list : groupWithIssues;
|
||||||
|
|
||||||
const visibilityGroupBy = (_list: IGroupByColumn) =>
|
const visibilityGroupBy = (_list: IGroupByColumn) =>
|
||||||
sub_group_by ? false : kanbanFilters?.group_by.includes(_list.id) ? true : false;
|
sub_group_by ? false : kanbanFilters?.group_by.includes(_list.id) ? true : false;
|
||||||
|
|
||||||
@ -91,9 +97,9 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`relative w-full flex gap-3 ${sub_group_by ? "h-full" : "h-full"}`}>
|
<div className={`relative w-full flex gap-3 ${sub_group_by ? "h-full" : "h-full"}`}>
|
||||||
{list &&
|
{groupList &&
|
||||||
list.length > 0 &&
|
groupList.length > 0 &&
|
||||||
list.map((_list: IGroupByColumn) => {
|
groupList.map((_list: IGroupByColumn) => {
|
||||||
const groupByVisibilityToggle = visibilityGroupBy(_list);
|
const groupByVisibilityToggle = visibilityGroupBy(_list);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -196,6 +202,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
|||||||
canEditProperties,
|
canEditProperties,
|
||||||
scrollableContainerRef,
|
scrollableContainerRef,
|
||||||
isDragStarted,
|
isDragStarted,
|
||||||
|
showEmptyGroup,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const issueKanBanView = useKanbanView();
|
const issueKanBanView = useKanbanView();
|
||||||
@ -222,6 +229,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
|||||||
canEditProperties={canEditProperties}
|
canEditProperties={canEditProperties}
|
||||||
scrollableContainerRef={scrollableContainerRef}
|
scrollableContainerRef={scrollableContainerRef}
|
||||||
isDragStarted={isDragStarted}
|
isDragStarted={isDragStarted}
|
||||||
|
showEmptyGroup={showEmptyGroup}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -50,9 +50,8 @@ export const IssueBlock: React.FC<IssueBlockProps> = observer((props: IssueBlock
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn("min-h-12 relative flex items-center gap-3 bg-custom-background-100 p-3 text-sm", {
|
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":
|
"border border-custom-primary-70 hover:border-custom-primary-70": peekIssue && peekIssue.issueId === issue.id,
|
||||||
peekIssue && peekIssue.issueId === issue.id,
|
"last:border-b-transparent": peekIssue?.issueId !== issue.id,
|
||||||
"last:border-b-transparent": peekIssue?.issueId !== issue.id
|
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{displayProperties && displayProperties?.key && (
|
{displayProperties && displayProperties?.key && (
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { TIssue } from "@plane/types";
|
||||||
|
|
||||||
export interface IQuickActionProps {
|
export interface IQuickActionProps {
|
||||||
issue: TIssue;
|
issue: TIssue;
|
||||||
handleDelete: () => Promise<void>;
|
handleDelete: () => Promise<void>;
|
||||||
|
@ -2,6 +2,7 @@ import { useState } from "react";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { CustomMenu } from "@plane/ui";
|
import { CustomMenu } from "@plane/ui";
|
||||||
import { Copy, Link, Pencil, Trash2 } from "lucide-react";
|
import { Copy, Link, Pencil, Trash2 } from "lucide-react";
|
||||||
|
import omit from "lodash/omit";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
import { useEventTracker } from "hooks/store";
|
import { useEventTracker } from "hooks/store";
|
||||||
@ -30,7 +31,7 @@ export const AllIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
|||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const handleCopyIssueLink = () => {
|
const handleCopyIssueLink = () => {
|
||||||
copyUrlToClipboard(`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`).then(() =>
|
copyUrlToClipboard(`/${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`).then(() =>
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Link copied",
|
title: "Link copied",
|
||||||
@ -39,11 +40,13 @@ export const AllIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const duplicateIssuePayload = {
|
const duplicateIssuePayload = omit(
|
||||||
...issue,
|
{
|
||||||
name: `${issue.name} (copy)`,
|
...issue,
|
||||||
};
|
name: `${issue.name} (copy)`,
|
||||||
delete duplicateIssuePayload.id;
|
},
|
||||||
|
["id"]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -87,7 +90,7 @@ export const AllIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
|||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setTrackElement("Global issues");
|
setTrackElement("Global issues");
|
||||||
setIssueToEdit(issue);
|
setIssueToEdit(issue);
|
||||||
setCreateUpdateIssueModal(true);
|
setCreateUpdateIssueModal(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -99,7 +102,7 @@ export const AllIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
|||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setTrackElement("Global issues");
|
setTrackElement("Global issues");
|
||||||
setCreateUpdateIssueModal(true);
|
setCreateUpdateIssueModal(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@ -110,7 +113,7 @@ export const AllIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
|||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setTrackElement("Global issues");
|
setTrackElement("Global issues");
|
||||||
setDeleteIssueModal(true);
|
setDeleteIssueModal(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
@ -4,7 +4,7 @@ import { CustomMenu } from "@plane/ui";
|
|||||||
import { Link, Trash2 } from "lucide-react";
|
import { Link, Trash2 } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
import { useEventTracker, useIssues ,useUser} from "hooks/store";
|
import { useEventTracker, useIssues, useUser } from "hooks/store";
|
||||||
// components
|
// components
|
||||||
import { DeleteArchivedIssueModal } from "components/issues";
|
import { DeleteArchivedIssueModal } from "components/issues";
|
||||||
// helpers
|
// helpers
|
||||||
@ -37,7 +37,7 @@ export const ArchivedIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
|
|||||||
const activeLayout = `${issuesFilter.issueFilters?.displayFilters?.layout} layout`;
|
const activeLayout = `${issuesFilter.issueFilters?.displayFilters?.layout} layout`;
|
||||||
|
|
||||||
const handleCopyIssueLink = () => {
|
const handleCopyIssueLink = () => {
|
||||||
copyUrlToClipboard(`${workspaceSlug}/projects/${issue.project}/archived-issues/${issue.id}`).then(() =>
|
copyUrlToClipboard(`${workspaceSlug}/projects/${issue.project_id}/archived-issues/${issue.id}`).then(() =>
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Link copied",
|
title: "Link copied",
|
||||||
@ -75,7 +75,7 @@ export const ArchivedIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
|
|||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setTrackElement(activeLayout);
|
setTrackElement(activeLayout);
|
||||||
setDeleteIssueModal(true);
|
setDeleteIssueModal(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
@ -2,9 +2,10 @@ import { useState } from "react";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { CustomMenu } from "@plane/ui";
|
import { CustomMenu } from "@plane/ui";
|
||||||
import { Copy, Link, Pencil, Trash2, XCircle } from "lucide-react";
|
import { Copy, Link, Pencil, Trash2, XCircle } from "lucide-react";
|
||||||
|
import omit from "lodash/omit";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
import { useEventTracker, useIssues,useUser } from "hooks/store";
|
import { useEventTracker, useIssues, useUser } from "hooks/store";
|
||||||
// components
|
// components
|
||||||
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
||||||
// helpers
|
// helpers
|
||||||
@ -49,7 +50,7 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
|||||||
const activeLayout = `${issuesFilter.issueFilters?.displayFilters?.layout} layout`;
|
const activeLayout = `${issuesFilter.issueFilters?.displayFilters?.layout} layout`;
|
||||||
|
|
||||||
const handleCopyIssueLink = () => {
|
const handleCopyIssueLink = () => {
|
||||||
copyUrlToClipboard(`${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`).then(() =>
|
copyUrlToClipboard(`${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`).then(() =>
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Link copied",
|
title: "Link copied",
|
||||||
@ -58,11 +59,13 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const duplicateIssuePayload = {
|
const duplicateIssuePayload = omit(
|
||||||
...issue,
|
{
|
||||||
name: `${issue.name} (copy)`,
|
...issue,
|
||||||
};
|
name: `${issue.name} (copy)`,
|
||||||
delete duplicateIssuePayload.id;
|
},
|
||||||
|
["id"]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -107,10 +110,10 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIssueToEdit({
|
setIssueToEdit({
|
||||||
...issue,
|
...issue,
|
||||||
cycle: cycleId?.toString() ?? null,
|
cycle_id: cycleId?.toString() ?? null,
|
||||||
});
|
});
|
||||||
setTrackElement(activeLayout);
|
setTrackElement(activeLayout);
|
||||||
setCreateUpdateIssueModal(true);
|
setCreateUpdateIssueModal(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@ -131,7 +134,7 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
|||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setTrackElement(activeLayout);
|
setTrackElement(activeLayout);
|
||||||
setCreateUpdateIssueModal(true);
|
setCreateUpdateIssueModal(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@ -142,7 +145,7 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
|||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setTrackElement(activeLayout);
|
setTrackElement(activeLayout);
|
||||||
setDeleteIssueModal(true);
|
setDeleteIssueModal(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
@ -2,9 +2,10 @@ import { useState } from "react";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { CustomMenu } from "@plane/ui";
|
import { CustomMenu } from "@plane/ui";
|
||||||
import { Copy, Link, Pencil, Trash2, XCircle } from "lucide-react";
|
import { Copy, Link, Pencil, Trash2, XCircle } from "lucide-react";
|
||||||
|
import omit from "lodash/omit";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
import { useIssues, useEventTracker ,useUser } from "hooks/store";
|
import { useIssues, useEventTracker, useUser } from "hooks/store";
|
||||||
// components
|
// components
|
||||||
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
||||||
// helpers
|
// helpers
|
||||||
@ -49,7 +50,7 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
|||||||
const activeLayout = `${issuesFilter.issueFilters?.displayFilters?.layout} layout`;
|
const activeLayout = `${issuesFilter.issueFilters?.displayFilters?.layout} layout`;
|
||||||
|
|
||||||
const handleCopyIssueLink = () => {
|
const handleCopyIssueLink = () => {
|
||||||
copyUrlToClipboard(`${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`).then(() =>
|
copyUrlToClipboard(`${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`).then(() =>
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Link copied",
|
title: "Link copied",
|
||||||
@ -58,11 +59,13 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const duplicateIssuePayload = {
|
const duplicateIssuePayload = omit(
|
||||||
...issue,
|
{
|
||||||
name: `${issue.name} (copy)`,
|
...issue,
|
||||||
};
|
name: `${issue.name} (copy)`,
|
||||||
delete duplicateIssuePayload.id;
|
},
|
||||||
|
["id"]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -105,9 +108,9 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
|||||||
<>
|
<>
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIssueToEdit({ ...issue, module: moduleId?.toString() ?? null });
|
setIssueToEdit({ ...issue, module_ids: moduleId ? [moduleId.toString()] : [] });
|
||||||
setTrackElement(activeLayout);
|
setTrackElement(activeLayout);
|
||||||
setCreateUpdateIssueModal(true);
|
setCreateUpdateIssueModal(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@ -128,7 +131,7 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
|||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setTrackElement(activeLayout);
|
setTrackElement(activeLayout);
|
||||||
setCreateUpdateIssueModal(true);
|
setCreateUpdateIssueModal(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@ -141,7 +144,7 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setTrackElement(activeLayout);
|
setTrackElement(activeLayout);
|
||||||
setDeleteIssueModal(true);
|
setDeleteIssueModal(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
@ -2,6 +2,7 @@ import { useState } from "react";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { CustomMenu } from "@plane/ui";
|
import { CustomMenu } from "@plane/ui";
|
||||||
import { Copy, Link, Pencil, Trash2 } from "lucide-react";
|
import { Copy, Link, Pencil, Trash2 } from "lucide-react";
|
||||||
|
import omit from "lodash/omit";
|
||||||
// hooks
|
// hooks
|
||||||
import { useEventTracker, useIssues, useUser } from "hooks/store";
|
import { useEventTracker, useIssues, useUser } from "hooks/store";
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
@ -39,7 +40,7 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
|
|||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const handleCopyIssueLink = () => {
|
const handleCopyIssueLink = () => {
|
||||||
copyUrlToClipboard(`${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`).then(() =>
|
copyUrlToClipboard(`${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`).then(() =>
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Link copied",
|
title: "Link copied",
|
||||||
@ -48,11 +49,13 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const duplicateIssuePayload = {
|
const duplicateIssuePayload = omit(
|
||||||
...issue,
|
{
|
||||||
name: `${issue.name} (copy)`,
|
...issue,
|
||||||
};
|
name: `${issue.name} (copy)`,
|
||||||
delete duplicateIssuePayload.id;
|
},
|
||||||
|
["id"]
|
||||||
|
);
|
||||||
|
|
||||||
const isDraftIssue = router?.asPath?.includes("draft-issues") || false;
|
const isDraftIssue = router?.asPath?.includes("draft-issues") || false;
|
||||||
|
|
||||||
|
@ -159,7 +159,7 @@ export const AllIssueLayoutRoot: React.FC = observer(() => {
|
|||||||
globalViewId.toString()
|
globalViewId.toString()
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[updateFilters, workspaceSlug]
|
[updateFilters, workspaceSlug, globalViewId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderQuickActions = useCallback(
|
const renderQuickActions = useCallback(
|
||||||
|
@ -88,7 +88,7 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => {
|
|||||||
viewId
|
viewId
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[issueFiltersStore, projectId, workspaceSlug, viewId]
|
[issueFiltersStore?.updateFilters, projectId, workspaceSlug, viewId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderQuickActions = useCallback(
|
const renderQuickActions = useCallback(
|
||||||
|
@ -38,7 +38,7 @@ export const IssueColumn = observer((props: Props) => {
|
|||||||
>
|
>
|
||||||
<td
|
<td
|
||||||
tabIndex={0}
|
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}
|
ref={tableCellRef}
|
||||||
>
|
>
|
||||||
<Column
|
<Column
|
||||||
|
@ -28,7 +28,7 @@ export const SpreadsheetHeaderColumn = observer((props: Props) => {
|
|||||||
shouldRenderProperty={shouldRenderProperty}
|
shouldRenderProperty={shouldRenderProperty}
|
||||||
>
|
>
|
||||||
<th
|
<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}
|
ref={tableHeaderCellRef}
|
||||||
tabIndex={0}
|
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 "./properties";
|
||||||
export * from "./root";
|
export * from "./root";
|
||||||
export * from "./view";
|
export * from "./view";
|
||||||
|
export * from "./header";
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
import { FC } from "react";
|
import { FC, useEffect } from "react";
|
||||||
// hooks
|
|
||||||
import { useIssueDetail, useProject, useUser } from "hooks/store";
|
|
||||||
// components
|
|
||||||
import { IssueDescriptionForm, TIssueOperations } from "components/issues";
|
|
||||||
import { IssueReaction } from "../issue-detail/reactions";
|
|
||||||
import { observer } from "mobx-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 {
|
interface IPeekOverviewIssueDetails {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
@ -17,38 +21,70 @@ interface IPeekOverviewIssueDetails {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = observer((props) => {
|
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
|
// store hooks
|
||||||
const { getProjectById } = useProject();
|
const { getProjectById } = useProject();
|
||||||
const { currentUser } = useUser();
|
const { currentUser } = useUser();
|
||||||
const {
|
const {
|
||||||
issue: { getIssueById },
|
issue: { getIssueById },
|
||||||
} = useIssueDetail();
|
} = useIssueDetail();
|
||||||
|
// hooks
|
||||||
|
const { setShowAlert } = useReloadConfirmations(isSubmitting === "submitting");
|
||||||
|
|
||||||
// derived values
|
useEffect(() => {
|
||||||
const issue = getIssueById(issueId);
|
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 <></>;
|
if (!issue) return <></>;
|
||||||
|
|
||||||
const projectDetails = getProjectById(issue?.project_id);
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<span className="text-base font-medium text-custom-text-400">
|
<span className="text-base font-medium text-custom-text-400">
|
||||||
{projectDetails?.identifier}-{issue?.sequence_id}
|
{projectDetails?.identifier}-{issue?.sequence_id}
|
||||||
</span>
|
</span>
|
||||||
<IssueDescriptionForm
|
<IssueTitleInput
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
projectId={projectId}
|
projectId={issue.project_id}
|
||||||
issueId={issueId}
|
issueId={issue.id}
|
||||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
|
||||||
isSubmitting={isSubmitting}
|
isSubmitting={isSubmitting}
|
||||||
issue={issue}
|
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||||
issueOperations={issueOperations}
|
issueOperations={issueOperations}
|
||||||
disabled={disabled}
|
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 && (
|
{currentUser && (
|
||||||
<IssueReaction
|
<IssueReaction
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
projectId={projectId}
|
projectId={issue.project_id}
|
||||||
issueId={issueId}
|
issueId={issueId}
|
||||||
currentUser={currentUser}
|
currentUser={currentUser}
|
||||||
/>
|
/>
|
||||||
|
@ -69,20 +69,11 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
// state
|
// state
|
||||||
const [loader, setLoader] = useState(false);
|
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(
|
const issueOperations: TIssuePeekOperations = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
fetch: async (workspaceSlug: string, projectId: string, issueId: string) => {
|
fetch: async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||||
try {
|
try {
|
||||||
await fetchIssue(workspaceSlug, projectId, issueId);
|
await fetchIssue(workspaceSlug, projectId, issueId, is_archived);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching the parent issue");
|
console.error("Error fetching the parent issue");
|
||||||
}
|
}
|
||||||
@ -324,9 +315,20 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
removeModulesFromIssue,
|
removeModulesFromIssue,
|
||||||
setToastAlert,
|
setToastAlert,
|
||||||
onIssueUpdate,
|
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 <></>;
|
if (!peekIssue?.workspaceSlug || !peekIssue?.projectId || !peekIssue?.issueId) return <></>;
|
||||||
|
|
||||||
const issue = getIssueById(peekIssue.issueId) || undefined;
|
const issue = getIssueById(peekIssue.issueId) || undefined;
|
||||||
|
@ -1,28 +1,25 @@
|
|||||||
import { FC, useRef, useState } from "react";
|
import { FC, useRef, useState } from "react";
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { MoveRight, MoveDiagonal, Link2, Trash2 } from "lucide-react";
|
|
||||||
// hooks
|
// hooks
|
||||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
import useKeypress from "hooks/use-keypress";
|
import useKeypress from "hooks/use-keypress";
|
||||||
// store hooks
|
// store hooks
|
||||||
import { useIssueDetail, useUser } from "hooks/store";
|
import { useIssueDetail } from "hooks/store";
|
||||||
import useToast from "hooks/use-toast";
|
|
||||||
// components
|
// components
|
||||||
import {
|
import {
|
||||||
DeleteArchivedIssueModal,
|
DeleteArchivedIssueModal,
|
||||||
DeleteIssueModal,
|
DeleteIssueModal,
|
||||||
IssueSubscription,
|
IssuePeekOverviewHeader,
|
||||||
IssueUpdateStatus,
|
TPeekModes,
|
||||||
PeekOverviewIssueDetails,
|
PeekOverviewIssueDetails,
|
||||||
PeekOverviewProperties,
|
PeekOverviewProperties,
|
||||||
TIssueOperations,
|
TIssueOperations,
|
||||||
} from "components/issues";
|
} from "components/issues";
|
||||||
import { IssueActivity } from "../issue-detail/issue-activity";
|
import { IssueActivity } from "../issue-detail/issue-activity";
|
||||||
// ui
|
// ui
|
||||||
import { CenterPanelIcon, CustomSelect, FullScreenPanelIcon, SidePanelIcon, Spinner } from "@plane/ui";
|
import { Spinner } from "@plane/ui";
|
||||||
// helpers
|
|
||||||
import { copyUrlToClipboard } from "helpers/string.helper";
|
|
||||||
|
|
||||||
interface IIssueView {
|
interface IIssueView {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
@ -34,72 +31,28 @@ interface IIssueView {
|
|||||||
issueOperations: TIssueOperations;
|
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) => {
|
export const IssueView: FC<IIssueView> = observer((props) => {
|
||||||
const { workspaceSlug, projectId, issueId, isLoading, is_archived, disabled = false, issueOperations } = props;
|
const { workspaceSlug, projectId, issueId, isLoading, is_archived, disabled = false, issueOperations } = props;
|
||||||
// router
|
|
||||||
const router = useRouter();
|
|
||||||
// states
|
// states
|
||||||
const [peekMode, setPeekMode] = useState<TPeekModes>("side-peek");
|
const [peekMode, setPeekMode] = useState<TPeekModes>("side-peek");
|
||||||
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
|
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
|
||||||
// ref
|
// ref
|
||||||
const issuePeekOverviewRef = useRef<HTMLDivElement>(null);
|
const issuePeekOverviewRef = useRef<HTMLDivElement>(null);
|
||||||
// store hooks
|
// store hooks
|
||||||
const { setPeekIssue, isAnyModalOpen, isDeleteIssueModalOpen, toggleDeleteIssueModal } = useIssueDetail();
|
|
||||||
const { currentUser } = useUser();
|
|
||||||
const {
|
const {
|
||||||
|
setPeekIssue,
|
||||||
|
isAnyModalOpen,
|
||||||
|
isDeleteIssueModalOpen,
|
||||||
|
toggleDeleteIssueModal,
|
||||||
issue: { getIssueById },
|
issue: { getIssueById },
|
||||||
} = useIssueDetail();
|
} = useIssueDetail();
|
||||||
const { setToastAlert } = useToast();
|
|
||||||
// derived values
|
|
||||||
const currentMode = PEEK_OPTIONS.find((m) => m.key === peekMode);
|
|
||||||
const issue = getIssueById(issueId);
|
const issue = getIssueById(issueId);
|
||||||
|
// remove peek id
|
||||||
const removeRoutePeekId = () => {
|
const removeRoutePeekId = () => {
|
||||||
setPeekIssue(undefined);
|
setPeekIssue(undefined);
|
||||||
};
|
};
|
||||||
|
// hooks
|
||||||
useOutsideClickDetector(issuePeekOverviewRef, () => !isAnyModalOpen && removeRoutePeekId());
|
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();
|
const handleKeyDown = () => !isAnyModalOpen && removeRoutePeekId();
|
||||||
useKeypress("Escape", handleKeyDown);
|
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 && (
|
{issueId && (
|
||||||
<div
|
<div
|
||||||
ref={issuePeekOverviewRef}
|
ref={issuePeekOverviewRef}
|
||||||
@ -141,66 +94,20 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* header */}
|
{/* header */}
|
||||||
<div
|
<IssuePeekOverviewHeader
|
||||||
className={`relative flex items-center justify-between p-4 ${
|
peekMode={peekMode}
|
||||||
currentMode?.key === "full-screen" ? "border-b border-custom-border-200" : ""
|
setPeekMode={(value: TPeekModes) => {
|
||||||
}`}
|
setPeekMode(value);
|
||||||
>
|
}}
|
||||||
<div className="flex items-center gap-4">
|
removeRoutePeekId={removeRoutePeekId}
|
||||||
<button onClick={removeRoutePeekId}>
|
toggleDeleteIssueModal={toggleDeleteIssueModal}
|
||||||
<MoveRight className="h-4 w-4 text-custom-text-400 hover:text-custom-text-200" />
|
isArchived={is_archived}
|
||||||
</button>
|
issueId={issueId}
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
<button onClick={redirectToIssueDetail}>
|
projectId={projectId}
|
||||||
<MoveDiagonal className="h-4 w-4 text-custom-text-400 hover:text-custom-text-200" />
|
isSubmitting={isSubmitting}
|
||||||
</button>
|
disabled={disabled}
|
||||||
{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>
|
|
||||||
|
|
||||||
{/* content */}
|
{/* content */}
|
||||||
<div className="relative h-full w-full overflow-hidden overflow-y-auto">
|
<div className="relative h-full w-full overflow-hidden overflow-y-auto">
|
||||||
{isLoading && !issue ? (
|
{isLoading && !issue ? (
|
||||||
@ -230,11 +137,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<IssueActivity
|
<IssueActivity workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} />
|
||||||
workspaceSlug={workspaceSlug}
|
|
||||||
projectId={projectId}
|
|
||||||
issueId={issueId}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className={`flex h-full w-full overflow-auto`}>
|
<div className={`flex h-full w-full overflow-auto`}>
|
||||||
@ -250,11 +153,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
|||||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<IssueActivity
|
<IssueActivity workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} />
|
||||||
workspaceSlug={workspaceSlug}
|
|
||||||
projectId={projectId}
|
|
||||||
issueId={issueId}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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 { useRouter } from "next/router";
|
||||||
import { Combobox } from "@headlessui/react";
|
import { Combobox } from "@headlessui/react";
|
||||||
import { usePopper } from "react-popper";
|
import { usePopper } from "react-popper";
|
||||||
@ -36,6 +36,7 @@ export const IssueLabelSelect: React.FC<Props> = observer((props) => {
|
|||||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||||
// refs
|
// refs
|
||||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||||
// popper
|
// popper
|
||||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||||
placement: "bottom-start",
|
placement: "bottom-start",
|
||||||
@ -76,6 +77,12 @@ export const IssueLabelSelect: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
useOutsideClickDetector(dropdownRef, handleClose);
|
useOutsideClickDetector(dropdownRef, handleClose);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isDropdownOpen && inputRef.current) {
|
||||||
|
inputRef.current.focus();
|
||||||
|
}
|
||||||
|
}, [isDropdownOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Combobox
|
<Combobox
|
||||||
as="div"
|
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">
|
<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} />
|
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
|
||||||
<Combobox.Input
|
<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"
|
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)}
|
onChange={(event) => setQuery(event.target.value)}
|
||||||
placeholder="Search"
|
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>
|
||||||
|
);
|
||||||
|
});
|
@ -6,6 +6,7 @@ import { useApplication, useUser } from "hooks/store";
|
|||||||
import useSignInRedirection from "hooks/use-sign-in-redirection";
|
import useSignInRedirection from "hooks/use-sign-in-redirection";
|
||||||
// components
|
// components
|
||||||
import { SignInRoot } from "components/account";
|
import { SignInRoot } from "components/account";
|
||||||
|
import { PageHead } from "components/core";
|
||||||
// ui
|
// ui
|
||||||
import { Spinner } from "@plane/ui";
|
import { Spinner } from "@plane/ui";
|
||||||
// images
|
// images
|
||||||
@ -34,19 +35,22 @@ export const SignInView = observer(() => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full bg-onboarding-gradient-100">
|
<>
|
||||||
<div className="flex items-center justify-between px-8 pb-4 sm:px-16 sm:py-5 lg:px-28">
|
<PageHead title="Sign In" />
|
||||||
<div className="flex items-center gap-x-2 py-10">
|
<div className="h-full w-full bg-onboarding-gradient-100">
|
||||||
<Image src={BluePlaneLogoWithoutText} height={30} width={30} alt="Plane Logo" className="mr-2" />
|
<div className="flex items-center justify-between px-8 pb-4 sm:px-16 sm:py-5 lg:px-28">
|
||||||
<span className="text-2xl font-semibold sm:text-3xl">Plane</span>
|
<div className="flex items-center gap-x-2 py-10">
|
||||||
|
<Image src={BluePlaneLogoWithoutText} height={30} width={30} alt="Plane Logo" className="mr-2" />
|
||||||
|
<span className="text-2xl font-semibold sm:text-3xl">Plane</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mx-auto h-full rounded-t-md border-x border-t border-custom-border-200 bg-onboarding-gradient-100 px-4 pt-4 shadow-sm sm:w-4/5 md:w-2/3">
|
<div className="mx-auto h-full rounded-t-md border-x border-t border-custom-border-200 bg-onboarding-gradient-100 px-4 pt-4 shadow-sm sm:w-4/5 md:w-2/3">
|
||||||
<div className="h-full overflow-auto rounded-t-md bg-onboarding-gradient-200 px-7 pb-56 pt-24 sm:px-0">
|
<div className="h-full overflow-auto rounded-t-md bg-onboarding-gradient-200 px-7 pb-56 pt-24 sm:px-0">
|
||||||
<SignInRoot />
|
<SignInRoot />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -68,7 +68,6 @@ export const CreateUpdatePageModal: FC<Props> = (props) => {
|
|||||||
state: "SUCCESS",
|
state: "SUCCESS",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
console.log("Page updated successfully", pageStore);
|
|
||||||
} else {
|
} else {
|
||||||
await createProjectPage(formData);
|
await createProjectPage(formData);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { FC, useEffect, useState } from "react";
|
import { FC, useEffect, useState } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
// hooks
|
// hooks
|
||||||
import { useEventTracker, useProject, useWorkspace } from "hooks/store";
|
import { useEventTracker, useProject } from "hooks/store";
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// components
|
// components
|
||||||
import EmojiIconPicker from "components/emoji-icon-picker";
|
import EmojiIconPicker from "components/emoji-icon-picker";
|
||||||
@ -19,22 +19,19 @@ import { NETWORK_CHOICES } from "constants/project";
|
|||||||
// services
|
// services
|
||||||
import { ProjectService } from "services/project";
|
import { ProjectService } from "services/project";
|
||||||
import { PROJECT_UPDATED } from "constants/event-tracker";
|
import { PROJECT_UPDATED } from "constants/event-tracker";
|
||||||
|
|
||||||
export interface IProjectDetailsForm {
|
export interface IProjectDetailsForm {
|
||||||
project: IProject;
|
project: IProject;
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
|
projectId: string;
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const projectService = new ProjectService();
|
const projectService = new ProjectService();
|
||||||
|
|
||||||
export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
|
export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
|
||||||
const { project, workspaceSlug, isAdmin } = props;
|
const { project, workspaceSlug, projectId, isAdmin } = props;
|
||||||
// states
|
// states
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
// store hooks
|
// store hooks
|
||||||
const { captureProjectEvent } = useEventTracker();
|
const { captureProjectEvent } = useEventTracker();
|
||||||
const { currentWorkspace } = useWorkspace();
|
|
||||||
const { updateProject } = useProject();
|
const { updateProject } = useProject();
|
||||||
// toast alert
|
// toast alert
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
@ -47,6 +44,7 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
|
|||||||
setError,
|
setError,
|
||||||
reset,
|
reset,
|
||||||
formState: { errors, dirtyFields },
|
formState: { errors, dirtyFields },
|
||||||
|
getValues,
|
||||||
} = useForm<IProject>({
|
} = useForm<IProject>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
...project,
|
...project,
|
||||||
@ -56,30 +54,27 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!project) return;
|
if (project && projectId !== getValues("id")) {
|
||||||
reset({
|
reset({
|
||||||
...project,
|
...project,
|
||||||
emoji_and_icon: project.emoji ?? project.icon_prop,
|
emoji_and_icon: project.emoji ?? project.icon_prop,
|
||||||
workspace: (project.workspace as IWorkspace).id,
|
workspace: (project.workspace as IWorkspace).id,
|
||||||
});
|
});
|
||||||
}, [project, reset]);
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [project, projectId]);
|
||||||
const handleIdentifierChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleIdentifierChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const { value } = event.target;
|
const { value } = event.target;
|
||||||
|
|
||||||
const alphanumericValue = value.replace(/[^a-zA-Z0-9]/g, "");
|
const alphanumericValue = value.replace(/[^a-zA-Z0-9]/g, "");
|
||||||
const formattedValue = alphanumericValue.toUpperCase();
|
const formattedValue = alphanumericValue.toUpperCase();
|
||||||
|
|
||||||
setValue("identifier", formattedValue);
|
setValue("identifier", formattedValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdateChange = async (payload: Partial<IProject>) => {
|
const handleUpdateChange = async (payload: Partial<IProject>) => {
|
||||||
if (!workspaceSlug || !project) return;
|
if (!workspaceSlug || !project) return;
|
||||||
|
|
||||||
return updateProject(workspaceSlug.toString(), project.id, payload)
|
return updateProject(workspaceSlug.toString(), project.id, payload)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
const changed_properties = Object.keys(dirtyFields);
|
const changed_properties = Object.keys(dirtyFields);
|
||||||
console.log(dirtyFields);
|
|
||||||
captureProjectEvent({
|
captureProjectEvent({
|
||||||
eventName: PROJECT_UPDATED,
|
eventName: PROJECT_UPDATED,
|
||||||
payload: {
|
payload: {
|
||||||
@ -107,11 +102,9 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = async (formData: IProject) => {
|
const onSubmit = async (formData: IProject) => {
|
||||||
if (!workspaceSlug) return;
|
if (!workspaceSlug) return;
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
const payload: Partial<IProject> = {
|
const payload: Partial<IProject> = {
|
||||||
name: formData.name,
|
name: formData.name,
|
||||||
network: formData.network,
|
network: formData.network,
|
||||||
@ -119,7 +112,6 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
|
|||||||
description: formData.description,
|
description: formData.description,
|
||||||
cover_image: formData.cover_image,
|
cover_image: formData.cover_image,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (typeof formData.emoji_and_icon === "object") {
|
if (typeof formData.emoji_and_icon === "object") {
|
||||||
payload.emoji = null;
|
payload.emoji = null;
|
||||||
payload.icon_prop = formData.emoji_and_icon;
|
payload.icon_prop = formData.emoji_and_icon;
|
||||||
@ -127,7 +119,6 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
|
|||||||
payload.emoji = formData.emoji_and_icon;
|
payload.emoji = formData.emoji_and_icon;
|
||||||
payload.icon_prop = null;
|
payload.icon_prop = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (project.identifier !== formData.identifier)
|
if (project.identifier !== formData.identifier)
|
||||||
await projectService
|
await projectService
|
||||||
.checkProjectIdentifierAvailability(workspaceSlug as string, payload.identifier ?? "")
|
.checkProjectIdentifierAvailability(workspaceSlug as string, payload.identifier ?? "")
|
||||||
@ -136,20 +127,16 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
|
|||||||
else await handleUpdateChange(payload);
|
else await handleUpdateChange(payload);
|
||||||
});
|
});
|
||||||
else await handleUpdateChange(payload);
|
else await handleUpdateChange(payload);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}, 300);
|
}, 300);
|
||||||
};
|
};
|
||||||
|
|
||||||
const currentNetwork = NETWORK_CHOICES.find((n) => n.key === project?.network);
|
const currentNetwork = NETWORK_CHOICES.find((n) => n.key === project?.network);
|
||||||
const selectedNetwork = NETWORK_CHOICES.find((n) => n.key === watch("network"));
|
const selectedNetwork = NETWORK_CHOICES.find((n) => n.key === watch("network"));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<div className="relative mt-6 h-44 w-full">
|
<div className="relative mt-6 h-44 w-full">
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent" />
|
<div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent" />
|
||||||
|
|
||||||
<img src={watch("cover_image")!} alt={watch("cover_image")!} className="h-44 w-full rounded-md object-cover" />
|
<img src={watch("cover_image")!} alt={watch("cover_image")!} className="h-44 w-full rounded-md object-cover" />
|
||||||
<div className="z-5 absolute bottom-4 flex w-full items-end justify-between gap-3 px-4">
|
<div className="z-5 absolute bottom-4 flex w-full items-end justify-between gap-3 px-4">
|
||||||
<div className="flex flex-grow gap-3 truncate">
|
<div className="flex flex-grow gap-3 truncate">
|
||||||
@ -180,7 +167,6 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-shrink-0 justify-center">
|
<div className="flex flex-shrink-0 justify-center">
|
||||||
<div>
|
<div>
|
||||||
<Controller
|
<Controller
|
||||||
@ -225,7 +211,6 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<h4 className="text-sm">Description</h4>
|
<h4 className="text-sm">Description</h4>
|
||||||
<Controller
|
<Controller
|
||||||
@ -245,7 +230,6 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex w-full items-center justify-between gap-10">
|
<div className="flex w-full items-center justify-between gap-10">
|
||||||
<div className="flex w-1/2 flex-col gap-1">
|
<div className="flex w-1/2 flex-col gap-1">
|
||||||
<h4 className="text-sm">Identifier</h4>
|
<h4 className="text-sm">Identifier</h4>
|
||||||
@ -280,7 +264,6 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex w-1/2 flex-col gap-1">
|
<div className="flex w-1/2 flex-col gap-1">
|
||||||
<h4 className="text-sm">Network</h4>
|
<h4 className="text-sm">Network</h4>
|
||||||
<Controller
|
<Controller
|
||||||
@ -306,7 +289,6 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between py-2">
|
<div className="flex items-center justify-between py-2">
|
||||||
<>
|
<>
|
||||||
<Button variant="primary" type="submit" loading={isLoading} disabled={!isAdmin}>
|
<Button variant="primary" type="submit" loading={isLoading} disabled={!isAdmin}>
|
||||||
|
@ -16,12 +16,15 @@ import {
|
|||||||
LogOut,
|
LogOut,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
MoreHorizontal,
|
MoreHorizontal,
|
||||||
|
Inbox,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
import { useApplication, useEventTracker, useProject } from "hooks/store";
|
import { useApplication, useEventTracker, useInbox, useProject } from "hooks/store";
|
||||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// helpers
|
// helpers
|
||||||
|
import { cn } from "helpers/common.helper";
|
||||||
|
import { getNumberCount } from "helpers/string.helper";
|
||||||
import { renderEmoji } from "helpers/emoji.helper";
|
import { renderEmoji } from "helpers/emoji.helper";
|
||||||
// components
|
// components
|
||||||
import { CustomMenu, Tooltip, ArchiveIcon, PhotoFilterIcon, DiceIcon, ContrastIcon, LayersIcon } from "@plane/ui";
|
import { CustomMenu, Tooltip, ArchiveIcon, PhotoFilterIcon, DiceIcon, ContrastIcon, LayersIcon } from "@plane/ui";
|
||||||
@ -62,6 +65,11 @@ const navigation = (workspaceSlug: string, projectId: string) => [
|
|||||||
href: `/${workspaceSlug}/projects/${projectId}/pages`,
|
href: `/${workspaceSlug}/projects/${projectId}/pages`,
|
||||||
Icon: FileText,
|
Icon: FileText,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Inbox",
|
||||||
|
href: `/${workspaceSlug}/projects/${projectId}/inbox`,
|
||||||
|
Icon: Inbox,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "Settings",
|
name: "Settings",
|
||||||
href: `/${workspaceSlug}/projects/${projectId}/settings`,
|
href: `/${workspaceSlug}/projects/${projectId}/settings`,
|
||||||
@ -75,7 +83,8 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
|
|||||||
// store hooks
|
// store hooks
|
||||||
const { theme: themeStore } = useApplication();
|
const { theme: themeStore } = useApplication();
|
||||||
const { setTrackElement } = useEventTracker();
|
const { setTrackElement } = useEventTracker();
|
||||||
const { addProjectToFavorites, removeProjectFromFavorites, getProjectById } = useProject();
|
const { currentProjectDetails, addProjectToFavorites, removeProjectFromFavorites, getProjectById } = useProject();
|
||||||
|
const { getInboxesByProjectId, getInboxById } = useInbox();
|
||||||
// states
|
// states
|
||||||
const [leaveProjectModalOpen, setLeaveProjectModal] = useState(false);
|
const [leaveProjectModalOpen, setLeaveProjectModal] = useState(false);
|
||||||
const [publishModalOpen, setPublishModal] = useState(false);
|
const [publishModalOpen, setPublishModal] = useState(false);
|
||||||
@ -96,6 +105,9 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
const actionSectionRef = useRef<HTMLDivElement | null>(null);
|
const actionSectionRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
const inboxesMap = currentProjectDetails?.inbox_view ? getInboxesByProjectId(currentProjectDetails.id) : undefined;
|
||||||
|
const inboxDetails = inboxesMap && inboxesMap.length > 0 ? getInboxById(inboxesMap[0]) : undefined;
|
||||||
|
|
||||||
const handleAddToFavorites = () => {
|
const handleAddToFavorites = () => {
|
||||||
if (!workspaceSlug || !project) return;
|
if (!workspaceSlug || !project) return;
|
||||||
|
|
||||||
@ -147,8 +159,9 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
|
|||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className={`group relative flex w-full items-center rounded-md px-2 py-1 text-custom-sidebar-text-10 hover:bg-custom-sidebar-background-80 ${snapshot?.isDragging ? "opacity-60" : ""
|
className={`group relative flex w-full items-center rounded-md px-2 py-1 text-custom-sidebar-text-10 hover:bg-custom-sidebar-background-80 ${
|
||||||
} ${isMenuActive ? "!bg-custom-sidebar-background-80" : ""}`}
|
snapshot?.isDragging ? "opacity-60" : ""
|
||||||
|
} ${isMenuActive ? "!bg-custom-sidebar-background-80" : ""}`}
|
||||||
>
|
>
|
||||||
{provided && (
|
{provided && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
@ -157,9 +170,11 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`absolute -left-2.5 top-1/2 hidden -translate-y-1/2 rounded p-0.5 text-custom-sidebar-text-400 ${isCollapsed ? "" : "group-hover:!flex"
|
className={`absolute -left-2.5 top-1/2 hidden -translate-y-1/2 rounded p-0.5 text-custom-sidebar-text-400 ${
|
||||||
} ${project.sort_order === null ? "cursor-not-allowed opacity-60" : ""} ${isMenuActive ? "!flex" : ""
|
isCollapsed ? "" : "group-hover:!flex"
|
||||||
}`}
|
} ${project.sort_order === null ? "cursor-not-allowed opacity-60" : ""} ${
|
||||||
|
isMenuActive ? "!flex" : ""
|
||||||
|
}`}
|
||||||
{...provided?.dragHandleProps}
|
{...provided?.dragHandleProps}
|
||||||
>
|
>
|
||||||
<MoreVertical className="h-3.5" />
|
<MoreVertical className="h-3.5" />
|
||||||
@ -170,12 +185,14 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
|
|||||||
<Tooltip tooltipContent={`${project.name}`} position="right" className="ml-2" disabled={!isCollapsed}>
|
<Tooltip tooltipContent={`${project.name}`} position="right" className="ml-2" disabled={!isCollapsed}>
|
||||||
<Disclosure.Button
|
<Disclosure.Button
|
||||||
as="div"
|
as="div"
|
||||||
className={`flex flex-grow cursor-pointer select-none items-center truncate text-left text-sm font-medium ${isCollapsed ? "justify-center" : `justify-between`
|
className={`flex flex-grow cursor-pointer select-none items-center truncate text-left text-sm font-medium ${
|
||||||
}`}
|
isCollapsed ? "justify-center" : `justify-between`
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`flex w-full flex-grow items-center gap-x-2 truncate ${isCollapsed ? "justify-center" : ""
|
className={`flex w-full flex-grow items-center gap-x-2 truncate ${
|
||||||
}`}
|
isCollapsed ? "justify-center" : ""
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{project.emoji ? (
|
{project.emoji ? (
|
||||||
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded uppercase">
|
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded uppercase">
|
||||||
@ -195,8 +212,9 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
|
|||||||
</div>
|
</div>
|
||||||
{!isCollapsed && (
|
{!isCollapsed && (
|
||||||
<ChevronDown
|
<ChevronDown
|
||||||
className={`hidden h-4 w-4 flex-shrink-0 ${open ? "rotate-180" : ""} ${isMenuActive ? "!block" : ""
|
className={`hidden h-4 w-4 flex-shrink-0 ${open ? "rotate-180" : ""} ${
|
||||||
} mb-0.5 text-custom-sidebar-text-400 duration-300 group-hover:!block`}
|
isMenuActive ? "!block" : ""
|
||||||
|
} mb-0.5 text-custom-sidebar-text-400 duration-300 group-hover:!block`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Disclosure.Button>
|
</Disclosure.Button>
|
||||||
@ -306,7 +324,8 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
|
|||||||
(item.name === "Cycles" && !project.cycle_view) ||
|
(item.name === "Cycles" && !project.cycle_view) ||
|
||||||
(item.name === "Modules" && !project.module_view) ||
|
(item.name === "Modules" && !project.module_view) ||
|
||||||
(item.name === "Views" && !project.issue_views_view) ||
|
(item.name === "Views" && !project.issue_views_view) ||
|
||||||
(item.name === "Pages" && !project.page_view)
|
(item.name === "Pages" && !project.page_view) ||
|
||||||
|
(item.name === "Inbox" && !project.inbox_view)
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@ -320,13 +339,42 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
|
|||||||
disabled={!isCollapsed}
|
disabled={!isCollapsed}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`group flex items-center gap-2.5 rounded-md px-2 py-1.5 text-xs font-medium outline-none ${router.asPath.includes(item.href)
|
className={`group flex items-center gap-2.5 rounded-md px-2 py-1.5 text-xs font-medium outline-none ${
|
||||||
? "bg-custom-primary-100/10 text-custom-primary-100"
|
router.asPath.includes(item.href)
|
||||||
: "text-custom-sidebar-text-300 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
|
? "bg-custom-primary-100/10 text-custom-primary-100"
|
||||||
} ${isCollapsed ? "justify-center" : ""}`}
|
: "text-custom-sidebar-text-300 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
|
||||||
|
} ${isCollapsed ? "justify-center" : ""}`}
|
||||||
>
|
>
|
||||||
<item.Icon className="h-4 w-4 stroke-[1.5]" />
|
{item.name === "Inbox" && inboxDetails ? (
|
||||||
{!isCollapsed && item.name}
|
<>
|
||||||
|
<div className="flex items-center justify-center relative">
|
||||||
|
{inboxDetails?.pending_issue_count > 0 && (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"absolute -right-1.5 -top-1 px-0.5 h-3.5 w-3.5 flex items-center tracking-tight justify-center rounded-full text-[0.5rem] border-[0.5px] border-custom-sidebar-border-200 bg-custom-background-80 text-custom-text-100",
|
||||||
|
{
|
||||||
|
"text-[0.375rem] leading-5": inboxDetails?.pending_issue_count >= 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"border-none bg-custom-primary-300 text-white": router.asPath.includes(
|
||||||
|
item.href
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{getNumberCount(inboxDetails?.pending_issue_count)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<item.Icon className="h-4 w-4 stroke-[1.5]" />
|
||||||
|
</div>
|
||||||
|
{!isCollapsed && item.name}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<item.Icon className="h-4 w-4 stroke-[1.5]" />
|
||||||
|
{!isCollapsed && item.name}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</span>
|
</span>
|
||||||
|
@ -3,3 +3,4 @@ export * from "./kanban-layout-loader";
|
|||||||
export * from "./calendar-layout-loader";
|
export * from "./calendar-layout-loader";
|
||||||
export * from "./spreadsheet-layout-loader";
|
export * from "./spreadsheet-layout-loader";
|
||||||
export * from "./gantt-layout-loader";
|
export * from "./gantt-layout-loader";
|
||||||
|
export * from "./project-inbox";
|
||||||
|
@ -0,0 +1,19 @@
|
|||||||
|
import React from "react";
|
||||||
|
// ui
|
||||||
|
import { InboxSidebarLoader } from "./inbox-sidebar-loader";
|
||||||
|
|
||||||
|
export const InboxLayoutLoader = () => (
|
||||||
|
<div className="relative flex h-full overflow-hidden">
|
||||||
|
<InboxSidebarLoader />
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="grid h-full place-items-center p-4 text-custom-text-200">
|
||||||
|
<div className="grid h-full place-items-center">
|
||||||
|
<div className="my-5 flex flex-col items-center gap-4">
|
||||||
|
<span className="h-[60px] w-[60px] bg-custom-background-80 rounded" />
|
||||||
|
<span className="h-6 w-96 bg-custom-background-80 rounded" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
@ -0,0 +1,24 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export const InboxSidebarLoader = () => (
|
||||||
|
<div className="h-full w-[340px] border-r border-custom-border-300">
|
||||||
|
<div className="flex-shrink-0 w-full h-[50px] relative flex justify-between items-center gap-2 p-2 px-3 border-b border-custom-border-300">
|
||||||
|
<span className="h-6 w-16 bg-custom-background-80 rounded" />
|
||||||
|
<span className="h-6 w-16 bg-custom-background-80 rounded" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
{[...Array(6)].map(() => (
|
||||||
|
<div className="flex flex-col gap-3 h-[5rem]space-y-3 border-b border-custom-border-200 px-4 py-2">
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
|
<span className="h-5 w-20 bg-custom-background-80 rounded" />
|
||||||
|
<span className="h-5 w-16 bg-custom-background-80 rounded" />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="h-5 w-5 bg-custom-background-80 rounded" />
|
||||||
|
<span className="h-5 w-16 bg-custom-background-80 rounded" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
2
web/components/ui/loader/layouts/project-inbox/index.ts
Normal file
2
web/components/ui/loader/layouts/project-inbox/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./inbox-layout-loader";
|
||||||
|
export * from "./inbox-sidebar-loader";
|
@ -2,7 +2,7 @@ import { useRef, useState } from "react";
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { ChevronUp, PenSquare, Search } from "lucide-react";
|
import { ChevronUp, PenSquare, Search } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
import { useApplication, useEventTracker, useUser } from "hooks/store";
|
import { useApplication, useEventTracker, useProject, useUser } from "hooks/store";
|
||||||
import useLocalStorage from "hooks/use-local-storage";
|
import useLocalStorage from "hooks/use-local-storage";
|
||||||
// components
|
// components
|
||||||
import { CreateUpdateDraftIssueModal } from "components/issues";
|
import { CreateUpdateDraftIssueModal } from "components/issues";
|
||||||
@ -16,6 +16,7 @@ export const WorkspaceSidebarQuickAction = observer(() => {
|
|||||||
|
|
||||||
const { theme: themeStore, commandPalette: commandPaletteStore } = useApplication();
|
const { theme: themeStore, commandPalette: commandPaletteStore } = useApplication();
|
||||||
const { setTrackElement } = useEventTracker();
|
const { setTrackElement } = useEventTracker();
|
||||||
|
const { joinedProjectIds } = useProject();
|
||||||
const {
|
const {
|
||||||
membership: { currentWorkspaceRole },
|
membership: { currentWorkspaceRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
@ -31,6 +32,8 @@ export const WorkspaceSidebarQuickAction = observer(() => {
|
|||||||
|
|
||||||
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
||||||
|
|
||||||
|
const disabled = joinedProjectIds.length === 0;
|
||||||
|
|
||||||
const onMouseEnter = () => {
|
const onMouseEnter = () => {
|
||||||
//if renet before timout clear the timeout
|
//if renet before timout clear the timeout
|
||||||
timeoutRef?.current && clearTimeout(timeoutRef.current);
|
timeoutRef?.current && clearTimeout(timeoutRef.current);
|
||||||
@ -73,17 +76,18 @@ export const WorkspaceSidebarQuickAction = observer(() => {
|
|||||||
type="button"
|
type="button"
|
||||||
className={`relative flex flex-shrink-0 flex-grow items-center gap-2 rounded py-1.5 outline-none ${
|
className={`relative flex flex-shrink-0 flex-grow items-center gap-2 rounded py-1.5 outline-none ${
|
||||||
isSidebarCollapsed ? "justify-center" : ""
|
isSidebarCollapsed ? "justify-center" : ""
|
||||||
}`}
|
} ${disabled ? "cursor-not-allowed opacity-50" : ""}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setTrackElement("APP_SIDEBAR_QUICK_ACTIONS");
|
setTrackElement("APP_SIDEBAR_QUICK_ACTIONS");
|
||||||
commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);
|
commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);
|
||||||
}}
|
}}
|
||||||
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<PenSquare className="h-4 w-4 text-custom-sidebar-text-300" />
|
<PenSquare className="h-4 w-4 text-custom-sidebar-text-300" />
|
||||||
{!isSidebarCollapsed && <span className="text-sm font-medium">New Issue</span>}
|
{!isSidebarCollapsed && <span className="text-sm font-medium">New Issue</span>}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{storedValue && Object.keys(JSON.parse(storedValue)).length > 0 && (
|
{!disabled && storedValue && Object.keys(JSON.parse(storedValue)).length > 0 && (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className={`h-8 w-0.5 bg-custom-sidebar-background-80 ${isSidebarCollapsed ? "hidden" : "block"}`}
|
className={`h-8 w-0.5 bg-custom-sidebar-background-80 ${isSidebarCollapsed ? "hidden" : "block"}`}
|
||||||
|
@ -20,54 +20,55 @@ export const ProfilePreferenceSettingsLayout: FC<IProfilePreferenceSettingsLayou
|
|||||||
const { theme: themeStore } = useApplication();
|
const { theme: themeStore } = useApplication();
|
||||||
|
|
||||||
const showMenuItem = () => {
|
const showMenuItem = () => {
|
||||||
const item = router.asPath.split('/');
|
const item = router.asPath.split("/");
|
||||||
let splittedItem = item[item.length - 1];
|
let splittedItem = item[item.length - 1];
|
||||||
splittedItem = splittedItem.replace(splittedItem[0], splittedItem[0].toUpperCase());
|
splittedItem = splittedItem.replace(splittedItem[0], splittedItem[0].toUpperCase());
|
||||||
console.log(splittedItem);
|
|
||||||
return splittedItem;
|
return splittedItem;
|
||||||
}
|
};
|
||||||
|
|
||||||
const profilePreferenceLinks: Array<{
|
const profilePreferenceLinks: Array<{
|
||||||
label: string;
|
label: string;
|
||||||
href: string;
|
href: string;
|
||||||
}> = [
|
}> = [
|
||||||
{
|
{
|
||||||
label: "Theme",
|
label: "Theme",
|
||||||
href: `/profile/preferences/theme`,
|
href: `/profile/preferences/theme`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Email",
|
label: "Email",
|
||||||
href: `/profile/preferences/email`,
|
href: `/profile/preferences/email`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProfileSettingsLayout header={
|
<ProfileSettingsLayout
|
||||||
<div className="md:hidden flex flex-shrink-0 gap-4 items-center justify-start border-b border-custom-border-200 p-4">
|
header={
|
||||||
<SidebarHamburgerToggle onClick={() => themeStore.toggleSidebar()} />
|
<div className="md:hidden flex flex-shrink-0 gap-4 items-center justify-start border-b border-custom-border-200 p-4">
|
||||||
<CustomMenu
|
<SidebarHamburgerToggle onClick={() => themeStore.toggleSidebar()} />
|
||||||
maxHeight={"md"}
|
<CustomMenu
|
||||||
className="flex flex-grow justify-center text-custom-text-200 text-sm"
|
maxHeight={"md"}
|
||||||
placement="bottom-start"
|
className="flex flex-grow justify-center text-custom-text-200 text-sm"
|
||||||
customButton={
|
placement="bottom-start"
|
||||||
<div className="flex gap-2 items-center px-2 py-1.5 border rounded-md border-custom-border-400">
|
customButton={
|
||||||
<span className="flex flex-grow justify-center text-custom-text-200 text-sm">{showMenuItem()}</span>
|
<div className="flex gap-2 items-center px-2 py-1.5 border rounded-md border-custom-border-400">
|
||||||
<ChevronDown className="w-4 h-4 text-custom-text-400" />
|
<span className="flex flex-grow justify-center text-custom-text-200 text-sm">{showMenuItem()}</span>
|
||||||
</div>
|
<ChevronDown className="w-4 h-4 text-custom-text-400" />
|
||||||
}
|
</div>
|
||||||
customButtonClassName="flex flex-grow justify-start text-custom-text-200 text-sm"
|
}
|
||||||
>
|
customButtonClassName="flex flex-grow justify-start text-custom-text-200 text-sm"
|
||||||
<></>
|
>
|
||||||
{profilePreferenceLinks.map((link) => (
|
<></>
|
||||||
<CustomMenu.MenuItem
|
{profilePreferenceLinks.map((link) => (
|
||||||
className="flex items-center gap-2"
|
<CustomMenu.MenuItem className="flex items-center gap-2">
|
||||||
>
|
<Link key={link.href} href={link.href} className="text-custom-text-300 w-full">
|
||||||
<Link key={link.href} href={link.href} className="text-custom-text-300 w-full">{link.label}</Link>
|
{link.label}
|
||||||
</CustomMenu.MenuItem>
|
</Link>
|
||||||
))}
|
</CustomMenu.MenuItem>
|
||||||
</CustomMenu>
|
))}
|
||||||
</div>
|
</CustomMenu>
|
||||||
}>
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className="relative flex h-screen w-full overflow-hidden">
|
<div className="relative flex h-screen w-full overflow-hidden">
|
||||||
<ProfilePreferenceSettingsSidebar />
|
<ProfilePreferenceSettingsSidebar />
|
||||||
<main className="relative flex h-full w-full flex-col overflow-hidden bg-custom-background-100">
|
<main className="relative flex h-full w-full flex-col overflow-hidden bg-custom-background-100">
|
||||||
|
@ -41,13 +41,11 @@ export const ProjectSettingLayout: FC<IProjectSettingLayout> = observer((props)
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="inset-y-0 z-20 flex flex-grow-0 h-full w-full gap-2 overflow-x-hidden overflow-y-scroll">
|
<div className="inset-y-0 z-20 flex flex-grow-0 h-full w-full">
|
||||||
<div className="w-80 flex-shrink-0 overflow-y-hidden pt-8 sm:hidden hidden md:block lg:block">
|
<div className="w-80 flex-shrink-0 overflow-y-hidden pt-8 sm:hidden hidden md:block lg:block">
|
||||||
<ProjectSettingsSidebar />
|
<ProjectSettingsSidebar />
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full pl-10 sm:pl-10 md:pl-0 lg:pl-0">
|
<div className="w-full pl-10 sm:pl-10 md:pl-0 lg:pl-0 overflow-x-hidden overflow-y-scroll">{children}</div>
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
// ui
|
||||||
|
import { Loader } from "@plane/ui";
|
||||||
// hooks
|
// hooks
|
||||||
import { useUser } from "hooks/store";
|
import { useUser } from "hooks/store";
|
||||||
// constants
|
// constants
|
||||||
@ -16,6 +18,21 @@ export const ProjectSettingsSidebar = () => {
|
|||||||
|
|
||||||
const projectMemberInfo = currentProjectRole || EUserProjectRoles.GUEST;
|
const projectMemberInfo = currentProjectRole || EUserProjectRoles.GUEST;
|
||||||
|
|
||||||
|
if (!currentProjectRole) {
|
||||||
|
return (
|
||||||
|
<div className="flex w-80 flex-col gap-6 px-5">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<span className="text-xs font-semibold text-custom-sidebar-text-400">SETTINGS</span>
|
||||||
|
<Loader className="flex w-full flex-col gap-2">
|
||||||
|
{[...Array(8)].map(() => (
|
||||||
|
<Loader.Item height="34px" />
|
||||||
|
))}
|
||||||
|
</Loader>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-80 flex-col gap-6 px-5">
|
<div className="flex w-80 flex-col gap-6 px-5">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
|
@ -2,7 +2,8 @@ import React from "react";
|
|||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
// components
|
||||||
|
import { PageHead } from "components/core";
|
||||||
// layouts
|
// layouts
|
||||||
import DefaultLayout from "layouts/default-layout";
|
import DefaultLayout from "layouts/default-layout";
|
||||||
// ui
|
// ui
|
||||||
@ -14,6 +15,7 @@ import type { NextPage } from "next";
|
|||||||
|
|
||||||
const PageNotFound: NextPage = () => (
|
const PageNotFound: NextPage = () => (
|
||||||
<DefaultLayout>
|
<DefaultLayout>
|
||||||
|
<PageHead title="404 - Page Not Found" />
|
||||||
<div className="grid h-full place-items-center p-4">
|
<div className="grid h-full place-items-center p-4">
|
||||||
<div className="space-y-8 text-center">
|
<div className="space-y-8 text-center">
|
||||||
<div className="relative mx-auto h-60 w-60 lg:h-80 lg:w-80">
|
<div className="relative mx-auto h-60 w-60 lg:h-80 lg:w-80">
|
||||||
|
@ -1,13 +1,28 @@
|
|||||||
import { ReactElement } from "react";
|
import { ReactElement } from "react";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
// components
|
// components
|
||||||
|
import { PageHead } from "components/core";
|
||||||
import { WorkspaceActiveCycleHeader } from "components/headers";
|
import { WorkspaceActiveCycleHeader } from "components/headers";
|
||||||
import { WorkspaceActiveCyclesUpgrade } from "components/workspace";
|
import { WorkspaceActiveCyclesUpgrade } from "components/workspace";
|
||||||
// layouts
|
// layouts
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
// types
|
// types
|
||||||
import { NextPageWithLayout } from "lib/types";
|
import { NextPageWithLayout } from "lib/types";
|
||||||
|
// hooks
|
||||||
|
import { useWorkspace } from "hooks/store";
|
||||||
|
|
||||||
const WorkspaceActiveCyclesPage: NextPageWithLayout = () => <WorkspaceActiveCyclesUpgrade />;
|
const WorkspaceActiveCyclesPage: NextPageWithLayout = observer(() => {
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
// derived values
|
||||||
|
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Active Cycles` : undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHead title={pageTitle} />
|
||||||
|
<WorkspaceActiveCyclesUpgrade />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
WorkspaceActiveCyclesPage.getLayout = function getLayout(page: ReactElement) {
|
WorkspaceActiveCyclesPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return <AppLayout header={<WorkspaceActiveCycleHeader />}>{page}</AppLayout>;
|
return <AppLayout header={<WorkspaceActiveCycleHeader />}>{page}</AppLayout>;
|
||||||
|
@ -1,22 +1,23 @@
|
|||||||
import React, { Fragment, ReactElement } from "react";
|
import React, { Fragment, ReactElement } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Tab } from "@headlessui/react";
|
import { Tab } from "@headlessui/react";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
// hooks
|
// hooks
|
||||||
import { useApplication, useEventTracker, useProject, useUser } from "hooks/store";
|
import { useApplication, useEventTracker, useProject, useUser, useWorkspace } from "hooks/store";
|
||||||
// layouts
|
// layouts
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
// components
|
// components
|
||||||
|
import { PageHead } from "components/core";
|
||||||
import { CustomAnalytics, ScopeAndDemand } from "components/analytics";
|
import { CustomAnalytics, ScopeAndDemand } from "components/analytics";
|
||||||
import { WorkspaceAnalyticsHeader } from "components/headers";
|
import { WorkspaceAnalyticsHeader } from "components/headers";
|
||||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||||
// constants
|
// constants
|
||||||
import { ANALYTICS_TABS } from "constants/analytics";
|
import { ANALYTICS_TABS } from "constants/analytics";
|
||||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
import { WORKSPACE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||||
// type
|
// type
|
||||||
import { NextPageWithLayout } from "lib/types";
|
import { NextPageWithLayout } from "lib/types";
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { WORKSPACE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
|
||||||
|
|
||||||
const AnalyticsPage: NextPageWithLayout = observer(() => {
|
const AnalyticsPage: NextPageWithLayout = observer(() => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -33,13 +34,16 @@ const AnalyticsPage: NextPageWithLayout = observer(() => {
|
|||||||
currentUser,
|
currentUser,
|
||||||
} = useUser();
|
} = useUser();
|
||||||
const { workspaceProjectIds } = useProject();
|
const { workspaceProjectIds } = useProject();
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
// derived values
|
||||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||||
const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "analytics", isLightMode);
|
const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "analytics", isLightMode);
|
||||||
const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
||||||
|
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Analytics` : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<PageHead title={pageTitle} />
|
||||||
{workspaceProjectIds && workspaceProjectIds.length > 0 ? (
|
{workspaceProjectIds && workspaceProjectIds.length > 0 ? (
|
||||||
<div className="flex h-full flex-col overflow-hidden bg-custom-background-100">
|
<div className="flex h-full flex-col overflow-hidden bg-custom-background-100">
|
||||||
<Tab.Group as={Fragment} defaultIndex={analytics_tab === "custom" ? 1 : 0}>
|
<Tab.Group as={Fragment} defaultIndex={analytics_tab === "custom" ? 1 : 0}>
|
||||||
|
@ -1,13 +1,28 @@
|
|||||||
import { ReactElement } from "react";
|
import { ReactElement } from "react";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
// layouts
|
// layouts
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
// components
|
// components
|
||||||
|
import { PageHead } from "components/core";
|
||||||
import { WorkspaceDashboardView } from "components/page-views";
|
import { WorkspaceDashboardView } from "components/page-views";
|
||||||
import { WorkspaceDashboardHeader } from "components/headers/workspace-dashboard";
|
import { WorkspaceDashboardHeader } from "components/headers/workspace-dashboard";
|
||||||
// types
|
// types
|
||||||
import { NextPageWithLayout } from "lib/types";
|
import { NextPageWithLayout } from "lib/types";
|
||||||
|
// hooks
|
||||||
|
import { useWorkspace } from "hooks/store";
|
||||||
|
|
||||||
const WorkspacePage: NextPageWithLayout = () => <WorkspaceDashboardView />;
|
const WorkspacePage: NextPageWithLayout = observer(() => {
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
// derived values
|
||||||
|
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Dashboard` : undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHead title={pageTitle} />
|
||||||
|
<WorkspaceDashboardView />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
WorkspacePage.getLayout = function getLayout(page: ReactElement) {
|
WorkspacePage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return <AppLayout header={<WorkspaceDashboardHeader />}>{page}</AppLayout>;
|
return <AppLayout header={<WorkspaceDashboardHeader />}>{page}</AppLayout>;
|
||||||
|
@ -4,11 +4,17 @@ import { AppLayout } from "layouts/app-layout";
|
|||||||
import { ProfileAuthWrapper } from "layouts/user-profile-layout";
|
import { ProfileAuthWrapper } from "layouts/user-profile-layout";
|
||||||
// components
|
// components
|
||||||
import { UserProfileHeader } from "components/headers";
|
import { UserProfileHeader } from "components/headers";
|
||||||
|
import { PageHead } from "components/core";
|
||||||
// types
|
// types
|
||||||
import { NextPageWithLayout } from "lib/types";
|
import { NextPageWithLayout } from "lib/types";
|
||||||
import { ProfileIssuesPage } from "components/profile/profile-issues";
|
import { ProfileIssuesPage } from "components/profile/profile-issues";
|
||||||
|
|
||||||
const ProfileAssignedIssuesPage: NextPageWithLayout = () => <ProfileIssuesPage type="assigned" />;
|
const ProfileAssignedIssuesPage: NextPageWithLayout = () => (
|
||||||
|
<>
|
||||||
|
<PageHead title="Profile - Assigned" />
|
||||||
|
<ProfileIssuesPage type="assigned" />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
ProfileAssignedIssuesPage.getLayout = function getLayout(page: ReactElement) {
|
ProfileAssignedIssuesPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
|
@ -6,11 +6,17 @@ import { AppLayout } from "layouts/app-layout";
|
|||||||
import { ProfileAuthWrapper } from "layouts/user-profile-layout";
|
import { ProfileAuthWrapper } from "layouts/user-profile-layout";
|
||||||
// components
|
// components
|
||||||
import { UserProfileHeader } from "components/headers";
|
import { UserProfileHeader } from "components/headers";
|
||||||
|
import { PageHead } from "components/core";
|
||||||
// types
|
// types
|
||||||
import { NextPageWithLayout } from "lib/types";
|
import { NextPageWithLayout } from "lib/types";
|
||||||
import { ProfileIssuesPage } from "components/profile/profile-issues";
|
import { ProfileIssuesPage } from "components/profile/profile-issues";
|
||||||
|
|
||||||
const ProfileCreatedIssuesPage: NextPageWithLayout = () => <ProfileIssuesPage type="created" />;
|
const ProfileCreatedIssuesPage: NextPageWithLayout = () => (
|
||||||
|
<>
|
||||||
|
<PageHead title="Profile - Created" />
|
||||||
|
<ProfileIssuesPage type="created" />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
ProfileCreatedIssuesPage.getLayout = function getLayout(page: ReactElement) {
|
ProfileCreatedIssuesPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
|
@ -8,6 +8,7 @@ import { AppLayout } from "layouts/app-layout";
|
|||||||
import { ProfileAuthWrapper } from "layouts/user-profile-layout";
|
import { ProfileAuthWrapper } from "layouts/user-profile-layout";
|
||||||
// components
|
// components
|
||||||
import { UserProfileHeader } from "components/headers";
|
import { UserProfileHeader } from "components/headers";
|
||||||
|
import { PageHead } from "components/core";
|
||||||
import {
|
import {
|
||||||
ProfileActivity,
|
ProfileActivity,
|
||||||
ProfilePriorityDistribution,
|
ProfilePriorityDistribution,
|
||||||
@ -42,21 +43,24 @@ const ProfileOverviewPage: NextPageWithLayout = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full space-y-7 overflow-y-auto px-5 py-5 md:px-9">
|
<>
|
||||||
<ProfileStats userProfile={userProfile} />
|
<PageHead title="Profile - Summary" />
|
||||||
<ProfileWorkload stateDistribution={stateDistribution} />
|
<div className="h-full w-full space-y-7 overflow-y-auto px-5 py-5 md:px-9">
|
||||||
<div className="grid grid-cols-1 items-stretch gap-5 xl:grid-cols-2">
|
<ProfileStats userProfile={userProfile} />
|
||||||
<ProfilePriorityDistribution userProfile={userProfile} />
|
<ProfileWorkload stateDistribution={stateDistribution} />
|
||||||
<ProfileStateDistribution stateDistribution={stateDistribution} userProfile={userProfile} />
|
<div className="grid grid-cols-1 items-stretch gap-5 xl:grid-cols-2">
|
||||||
|
<ProfilePriorityDistribution userProfile={userProfile} />
|
||||||
|
<ProfileStateDistribution stateDistribution={stateDistribution} userProfile={userProfile} />
|
||||||
|
</div>
|
||||||
|
<ProfileActivity />
|
||||||
</div>
|
</div>
|
||||||
<ProfileActivity />
|
</>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
ProfileOverviewPage.getLayout = function getLayout(page: ReactElement) {
|
ProfileOverviewPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
<AppLayout header={<UserProfileHeader type='Summary' />}>
|
<AppLayout header={<UserProfileHeader type="Summary" />}>
|
||||||
<ProfileAuthWrapper>{page}</ProfileAuthWrapper>
|
<ProfileAuthWrapper>{page}</ProfileAuthWrapper>
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
);
|
);
|
||||||
|
@ -6,11 +6,17 @@ import { AppLayout } from "layouts/app-layout";
|
|||||||
import { ProfileAuthWrapper } from "layouts/user-profile-layout";
|
import { ProfileAuthWrapper } from "layouts/user-profile-layout";
|
||||||
// components
|
// components
|
||||||
import { UserProfileHeader } from "components/headers";
|
import { UserProfileHeader } from "components/headers";
|
||||||
|
import { PageHead } from "components/core";
|
||||||
// types
|
// types
|
||||||
import { NextPageWithLayout } from "lib/types";
|
import { NextPageWithLayout } from "lib/types";
|
||||||
import { ProfileIssuesPage } from "components/profile/profile-issues";
|
import { ProfileIssuesPage } from "components/profile/profile-issues";
|
||||||
|
|
||||||
const ProfileSubscribedIssuesPage: NextPageWithLayout = () => <ProfileIssuesPage type="subscribed" />;
|
const ProfileSubscribedIssuesPage: NextPageWithLayout = () => (
|
||||||
|
<>
|
||||||
|
<PageHead title="Profile - Subscribed" />
|
||||||
|
<ProfileIssuesPage type="subscribed" />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
ProfileSubscribedIssuesPage.getLayout = function getLayout(page: ReactElement) {
|
ProfileSubscribedIssuesPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { useState, ReactElement } from "react";
|
import { useState, ReactElement } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
@ -9,6 +10,7 @@ import { AppLayout } from "layouts/app-layout";
|
|||||||
// components
|
// components
|
||||||
import { IssueDetailRoot } from "components/issues";
|
import { IssueDetailRoot } from "components/issues";
|
||||||
import { ProjectArchivedIssueDetailsHeader } from "components/headers";
|
import { ProjectArchivedIssueDetailsHeader } from "components/headers";
|
||||||
|
import { PageHead } from "components/core";
|
||||||
// ui
|
// ui
|
||||||
import { ArchiveIcon, Loader } from "@plane/ui";
|
import { ArchiveIcon, Loader } from "@plane/ui";
|
||||||
// icons
|
// icons
|
||||||
@ -18,7 +20,7 @@ import { NextPageWithLayout } from "lib/types";
|
|||||||
// constants
|
// constants
|
||||||
import { EIssuesStoreType } from "constants/issue";
|
import { EIssuesStoreType } from "constants/issue";
|
||||||
|
|
||||||
const ArchivedIssueDetailsPage: NextPageWithLayout = () => {
|
const ArchivedIssueDetailsPage: NextPageWithLayout = observer(() => {
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, archivedIssueId } = router.query;
|
const { workspaceSlug, projectId, archivedIssueId } = router.query;
|
||||||
@ -45,6 +47,9 @@ const ArchivedIssueDetailsPage: NextPageWithLayout = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const issue = getIssueById(archivedIssueId?.toString() || "") || undefined;
|
const issue = getIssueById(archivedIssueId?.toString() || "") || undefined;
|
||||||
|
const project = (issue?.project_id && getProjectById(issue?.project_id)) || undefined;
|
||||||
|
const pageTitle = project && issue ? `${project?.identifier}-${issue?.sequence_id} ${issue?.name}` : undefined;
|
||||||
|
|
||||||
if (!issue) return <></>;
|
if (!issue) return <></>;
|
||||||
|
|
||||||
const handleUnArchive = async () => {
|
const handleUnArchive = async () => {
|
||||||
@ -79,6 +84,7 @@ const ArchivedIssueDetailsPage: NextPageWithLayout = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<PageHead title={pageTitle} />
|
||||||
{issueLoader ? (
|
{issueLoader ? (
|
||||||
<Loader className="flex h-full gap-5 p-5">
|
<Loader className="flex h-full gap-5 p-5">
|
||||||
<div className="basis-2/3 space-y-2">
|
<div className="basis-2/3 space-y-2">
|
||||||
@ -126,7 +132,7 @@ const ArchivedIssueDetailsPage: NextPageWithLayout = () => {
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
ArchivedIssueDetailsPage.getLayout = function getLayout(page: ReactElement) {
|
ArchivedIssueDetailsPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
|
@ -1,38 +1,51 @@
|
|||||||
import { ReactElement } from "react";
|
import { ReactElement } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
// layouts
|
// layouts
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
// contexts
|
// contexts
|
||||||
import { ArchivedIssueLayoutRoot } from "components/issues";
|
import { ArchivedIssueLayoutRoot } from "components/issues";
|
||||||
// ui
|
// ui
|
||||||
import { ArchiveIcon } from "@plane/ui";
|
import { ArchiveIcon } from "@plane/ui";
|
||||||
|
// components
|
||||||
import { ProjectArchivedIssuesHeader } from "components/headers";
|
import { ProjectArchivedIssuesHeader } from "components/headers";
|
||||||
|
import { PageHead } from "components/core";
|
||||||
// icons
|
// icons
|
||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
// types
|
// types
|
||||||
import { NextPageWithLayout } from "lib/types";
|
import { NextPageWithLayout } from "lib/types";
|
||||||
|
// hooks
|
||||||
|
import { useProject } from "hooks/store";
|
||||||
|
|
||||||
const ProjectArchivedIssuesPage: NextPageWithLayout = () => {
|
const ProjectArchivedIssuesPage: NextPageWithLayout = observer(() => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
// store hooks
|
||||||
|
const { getProjectById } = useProject();
|
||||||
|
// derived values
|
||||||
|
const project = projectId ? getProjectById(projectId.toString()) : undefined;
|
||||||
|
const pageTitle = project?.name && `${project?.name} - Archived Issues`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col">
|
<>
|
||||||
<div className="ga-1 flex items-center border-b border-custom-border-200 px-4 py-2.5 shadow-sm">
|
<PageHead title={pageTitle} />
|
||||||
<button
|
<div className="flex h-full w-full flex-col">
|
||||||
type="button"
|
<div className="ga-1 flex items-center border-b border-custom-border-200 px-4 py-2.5 shadow-sm">
|
||||||
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/issues/`)}
|
<button
|
||||||
className="flex items-center gap-1.5 rounded-full border border-custom-border-200 px-3 py-1.5 text-xs"
|
type="button"
|
||||||
>
|
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/issues/`)}
|
||||||
<ArchiveIcon className="h-4 w-4" />
|
className="flex items-center gap-1.5 rounded-full border border-custom-border-200 px-3 py-1.5 text-xs"
|
||||||
<span>Archived Issues</span>
|
>
|
||||||
<X className="h-3 w-3" />
|
<ArchiveIcon className="h-4 w-4" />
|
||||||
</button>
|
<span>Archived Issues</span>
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<ArchivedIssueLayoutRoot />
|
||||||
</div>
|
</div>
|
||||||
<ArchivedIssueLayoutRoot />
|
</>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
ProjectArchivedIssuesPage.getLayout = function getLayout(page: ReactElement) {
|
ProjectArchivedIssuesPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
import { ReactElement } from "react";
|
import { ReactElement } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
// hooks
|
// hooks
|
||||||
import { useCycle } from "hooks/store";
|
import { useCycle, useProject } from "hooks/store";
|
||||||
import useLocalStorage from "hooks/use-local-storage";
|
import useLocalStorage from "hooks/use-local-storage";
|
||||||
// layouts
|
// layouts
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
// components
|
// components
|
||||||
|
import { PageHead } from "components/core";
|
||||||
import { CycleIssuesHeader } from "components/headers";
|
import { CycleIssuesHeader } from "components/headers";
|
||||||
import { CycleDetailsSidebar } from "components/cycles";
|
import { CycleDetailsSidebar } from "components/cycles";
|
||||||
import { CycleLayoutRoot } from "components/issues/issue-layouts";
|
import { CycleLayoutRoot } from "components/issues/issue-layouts";
|
||||||
@ -17,27 +19,36 @@ import emptyCycle from "public/empty-state/cycle.svg";
|
|||||||
// types
|
// types
|
||||||
import { NextPageWithLayout } from "lib/types";
|
import { NextPageWithLayout } from "lib/types";
|
||||||
|
|
||||||
const CycleDetailPage: NextPageWithLayout = () => {
|
const CycleDetailPage: NextPageWithLayout = observer(() => {
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId } = router.query;
|
const { workspaceSlug, projectId, cycleId } = router.query;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { fetchCycleDetails } = useCycle();
|
const { fetchCycleDetails, getCycleById } = useCycle();
|
||||||
|
const { getProjectById } = useProject();
|
||||||
|
// hooks
|
||||||
const { setValue, storedValue } = useLocalStorage("cycle_sidebar_collapsed", "false");
|
const { setValue, storedValue } = useLocalStorage("cycle_sidebar_collapsed", "false");
|
||||||
const isSidebarCollapsed = storedValue ? (storedValue === "true" ? true : false) : false;
|
// fetching cycle details
|
||||||
|
|
||||||
const { error } = useSWR(
|
const { error } = useSWR(
|
||||||
workspaceSlug && projectId && cycleId ? `CYCLE_DETAILS_${cycleId.toString()}` : null,
|
workspaceSlug && projectId && cycleId ? `CYCLE_DETAILS_${cycleId.toString()}` : null,
|
||||||
workspaceSlug && projectId && cycleId
|
workspaceSlug && projectId && cycleId
|
||||||
? () => fetchCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleId.toString())
|
? () => fetchCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleId.toString())
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
// derived values
|
||||||
|
const isSidebarCollapsed = storedValue ? (storedValue === "true" ? true : false) : false;
|
||||||
|
const cycle = cycleId ? getCycleById(cycleId.toString()) : undefined;
|
||||||
|
const project = projectId ? getProjectById(projectId.toString()) : undefined;
|
||||||
|
const pageTitle = project?.name && cycle?.name ? `${project?.name} - ${cycle?.name}` : undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the sidebar
|
||||||
|
*/
|
||||||
const toggleSidebar = () => setValue(`${!isSidebarCollapsed}`);
|
const toggleSidebar = () => setValue(`${!isSidebarCollapsed}`);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<PageHead title={pageTitle} />
|
||||||
{error ? (
|
{error ? (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
image={emptyCycle}
|
image={emptyCycle}
|
||||||
@ -70,7 +81,7 @@ const CycleDetailPage: NextPageWithLayout = () => {
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
CycleDetailPage.getLayout = function getLayout(page: ReactElement) {
|
CycleDetailPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
|
@ -4,11 +4,12 @@ import { observer } from "mobx-react-lite";
|
|||||||
import { Tab } from "@headlessui/react";
|
import { Tab } from "@headlessui/react";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
// hooks
|
// hooks
|
||||||
import { useEventTracker, useCycle, useUser } from "hooks/store";
|
import { useEventTracker, useCycle, useUser, useProject } from "hooks/store";
|
||||||
import useLocalStorage from "hooks/use-local-storage";
|
import useLocalStorage from "hooks/use-local-storage";
|
||||||
// layouts
|
// layouts
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
// components
|
// components
|
||||||
|
import { PageHead } from "components/core";
|
||||||
import { CyclesHeader } from "components/headers";
|
import { CyclesHeader } from "components/headers";
|
||||||
import { CyclesView, ActiveCycleDetails, CycleCreateUpdateModal } from "components/cycles";
|
import { CyclesView, ActiveCycleDetails, CycleCreateUpdateModal } from "components/cycles";
|
||||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||||
@ -34,12 +35,20 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
|
|||||||
currentUser,
|
currentUser,
|
||||||
} = useUser();
|
} = useUser();
|
||||||
const { currentProjectCycleIds, loader } = useCycle();
|
const { currentProjectCycleIds, loader } = useCycle();
|
||||||
|
const { getProjectById } = useProject();
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, peekCycle } = router.query;
|
const { workspaceSlug, projectId, peekCycle } = router.query;
|
||||||
// local storage
|
// local storage
|
||||||
const { storedValue: cycleTab, setValue: setCycleTab } = useLocalStorage<TCycleView>("cycle_tab", "active");
|
const { storedValue: cycleTab, setValue: setCycleTab } = useLocalStorage<TCycleView>("cycle_tab", "active");
|
||||||
const { storedValue: cycleLayout, setValue: setCycleLayout } = useLocalStorage<TCycleLayout>("cycle_layout", "list");
|
const { storedValue: cycleLayout, setValue: setCycleLayout } = useLocalStorage<TCycleLayout>("cycle_layout", "list");
|
||||||
|
// derived values
|
||||||
|
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||||
|
const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "cycles", isLightMode);
|
||||||
|
const totalCycles = currentProjectCycleIds?.length ?? 0;
|
||||||
|
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
||||||
|
const project = projectId ? getProjectById(projectId?.toString()) : undefined;
|
||||||
|
const pageTitle = project?.name ? `${project?.name} - Cycles` : undefined;
|
||||||
|
|
||||||
const handleCurrentLayout = useCallback(
|
const handleCurrentLayout = useCallback(
|
||||||
(_layout: TCycleLayout) => {
|
(_layout: TCycleLayout) => {
|
||||||
@ -56,13 +65,6 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
|
|||||||
[handleCurrentLayout, setCycleTab]
|
[handleCurrentLayout, setCycleTab]
|
||||||
);
|
);
|
||||||
|
|
||||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
|
||||||
const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "cycles", isLightMode);
|
|
||||||
|
|
||||||
const totalCycles = currentProjectCycleIds?.length ?? 0;
|
|
||||||
|
|
||||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
|
||||||
|
|
||||||
if (!workspaceSlug || !projectId) return null;
|
if (!workspaceSlug || !projectId) return null;
|
||||||
|
|
||||||
if (loader)
|
if (loader)
|
||||||
@ -75,143 +77,146 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full">
|
<>
|
||||||
<CycleCreateUpdateModal
|
<PageHead title={pageTitle} />
|
||||||
workspaceSlug={workspaceSlug.toString()}
|
<div className="w-full h-full">
|
||||||
projectId={projectId.toString()}
|
<CycleCreateUpdateModal
|
||||||
isOpen={createModal}
|
workspaceSlug={workspaceSlug.toString()}
|
||||||
handleClose={() => setCreateModal(false)}
|
projectId={projectId.toString()}
|
||||||
/>
|
isOpen={createModal}
|
||||||
{totalCycles === 0 ? (
|
handleClose={() => setCreateModal(false)}
|
||||||
<div className="h-full place-items-center">
|
/>
|
||||||
<EmptyState
|
{totalCycles === 0 ? (
|
||||||
title={CYCLE_EMPTY_STATE_DETAILS["cycles"].title}
|
<div className="h-full place-items-center">
|
||||||
description={CYCLE_EMPTY_STATE_DETAILS["cycles"].description}
|
<EmptyState
|
||||||
image={EmptyStateImagePath}
|
title={CYCLE_EMPTY_STATE_DETAILS["cycles"].title}
|
||||||
comicBox={{
|
description={CYCLE_EMPTY_STATE_DETAILS["cycles"].description}
|
||||||
title: CYCLE_EMPTY_STATE_DETAILS["cycles"].comicBox.title,
|
image={EmptyStateImagePath}
|
||||||
description: CYCLE_EMPTY_STATE_DETAILS["cycles"].comicBox.description,
|
comicBox={{
|
||||||
}}
|
title: CYCLE_EMPTY_STATE_DETAILS["cycles"].comicBox.title,
|
||||||
primaryButton={{
|
description: CYCLE_EMPTY_STATE_DETAILS["cycles"].comicBox.description,
|
||||||
text: CYCLE_EMPTY_STATE_DETAILS["cycles"].primaryButton.text,
|
}}
|
||||||
onClick: () => {
|
primaryButton={{
|
||||||
setTrackElement("Cycle empty state");
|
text: CYCLE_EMPTY_STATE_DETAILS["cycles"].primaryButton.text,
|
||||||
setCreateModal(true);
|
onClick: () => {
|
||||||
},
|
setTrackElement("Cycle empty state");
|
||||||
}}
|
setCreateModal(true);
|
||||||
size="lg"
|
},
|
||||||
disabled={!isEditingAllowed}
|
}}
|
||||||
/>
|
size="lg"
|
||||||
</div>
|
disabled={!isEditingAllowed}
|
||||||
) : (
|
/>
|
||||||
<Tab.Group
|
|
||||||
as="div"
|
|
||||||
className="flex h-full flex-col overflow-hidden"
|
|
||||||
defaultIndex={CYCLE_TAB_LIST.findIndex((i) => i.key == cycleTab)}
|
|
||||||
selectedIndex={CYCLE_TAB_LIST.findIndex((i) => i.key == cycleTab)}
|
|
||||||
onChange={(i) => handleCurrentView(CYCLE_TAB_LIST[i]?.key ?? "active")}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col items-start justify-between gap-4 border-b border-custom-border-200 px-4 sm:flex-row sm:items-center sm:px-5 sm:pb-0">
|
|
||||||
<Tab.List as="div" className="flex items-center overflow-x-scroll">
|
|
||||||
{CYCLE_TAB_LIST.map((tab) => (
|
|
||||||
<Tab
|
|
||||||
key={tab.key}
|
|
||||||
className={({ selected }) =>
|
|
||||||
`border-b-2 p-4 text-sm font-medium outline-none ${
|
|
||||||
selected ? "border-custom-primary-100 text-custom-primary-100" : "border-transparent"
|
|
||||||
}`
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{tab.name}
|
|
||||||
</Tab>
|
|
||||||
))}
|
|
||||||
</Tab.List>
|
|
||||||
<div className="hidden sm:block">
|
|
||||||
{cycleTab !== "active" && (
|
|
||||||
<div className="flex items-center self-end sm:self-center md:self-center lg:self-center gap-1 rounded bg-custom-background-80 p-1">
|
|
||||||
{CYCLE_VIEW_LAYOUTS.map((layout) => {
|
|
||||||
if (layout.key === "gantt" && cycleTab === "draft") return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip key={layout.key} tooltipContent={layout.title}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100 ${
|
|
||||||
cycleLayout == layout.key ? "bg-custom-background-100 shadow-custom-shadow-2xs" : ""
|
|
||||||
}`}
|
|
||||||
onClick={() => handleCurrentLayout(layout.key as TCycleLayout)}
|
|
||||||
>
|
|
||||||
<layout.icon
|
|
||||||
strokeWidth={2}
|
|
||||||
className={`h-3.5 w-3.5 ${
|
|
||||||
cycleLayout == layout.key ? "text-custom-text-100" : "text-custom-text-200"
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<Tab.Group
|
||||||
|
as="div"
|
||||||
|
className="flex h-full flex-col overflow-hidden"
|
||||||
|
defaultIndex={CYCLE_TAB_LIST.findIndex((i) => i.key == cycleTab)}
|
||||||
|
selectedIndex={CYCLE_TAB_LIST.findIndex((i) => i.key == cycleTab)}
|
||||||
|
onChange={(i) => handleCurrentView(CYCLE_TAB_LIST[i]?.key ?? "active")}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col items-start justify-between gap-4 border-b border-custom-border-200 px-4 sm:flex-row sm:items-center sm:px-5 sm:pb-0">
|
||||||
|
<Tab.List as="div" className="flex items-center overflow-x-scroll">
|
||||||
|
{CYCLE_TAB_LIST.map((tab) => (
|
||||||
|
<Tab
|
||||||
|
key={tab.key}
|
||||||
|
className={({ selected }) =>
|
||||||
|
`border-b-2 p-4 text-sm font-medium outline-none ${
|
||||||
|
selected ? "border-custom-primary-100 text-custom-primary-100" : "border-transparent"
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{tab.name}
|
||||||
|
</Tab>
|
||||||
|
))}
|
||||||
|
</Tab.List>
|
||||||
|
<div className="hidden sm:block">
|
||||||
|
{cycleTab !== "active" && (
|
||||||
|
<div className="flex items-center self-end sm:self-center md:self-center lg:self-center gap-1 rounded bg-custom-background-80 p-1">
|
||||||
|
{CYCLE_VIEW_LAYOUTS.map((layout) => {
|
||||||
|
if (layout.key === "gantt" && cycleTab === "draft") return null;
|
||||||
|
|
||||||
<Tab.Panels as={Fragment}>
|
return (
|
||||||
<Tab.Panel as="div" className="h-full overflow-y-auto">
|
<Tooltip key={layout.key} tooltipContent={layout.title}>
|
||||||
{cycleTab && cycleLayout && (
|
<button
|
||||||
<CyclesView
|
type="button"
|
||||||
filter="all"
|
className={`group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100 ${
|
||||||
layout={cycleLayout}
|
cycleLayout == layout.key ? "bg-custom-background-100 shadow-custom-shadow-2xs" : ""
|
||||||
workspaceSlug={workspaceSlug.toString()}
|
}`}
|
||||||
projectId={projectId.toString()}
|
onClick={() => handleCurrentLayout(layout.key as TCycleLayout)}
|
||||||
peekCycle={peekCycle?.toString()}
|
>
|
||||||
/>
|
<layout.icon
|
||||||
)}
|
strokeWidth={2}
|
||||||
</Tab.Panel>
|
className={`h-3.5 w-3.5 ${
|
||||||
|
cycleLayout == layout.key ? "text-custom-text-100" : "text-custom-text-200"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Tab.Panel as="div" className="h-full space-y-5 overflow-y-auto p-4 sm:p-5">
|
<Tab.Panels as={Fragment}>
|
||||||
<ActiveCycleDetails workspaceSlug={workspaceSlug.toString()} projectId={projectId.toString()} />
|
<Tab.Panel as="div" className="h-full overflow-y-auto">
|
||||||
</Tab.Panel>
|
{cycleTab && cycleLayout && (
|
||||||
|
<CyclesView
|
||||||
|
filter="all"
|
||||||
|
layout={cycleLayout}
|
||||||
|
workspaceSlug={workspaceSlug.toString()}
|
||||||
|
projectId={projectId.toString()}
|
||||||
|
peekCycle={peekCycle?.toString()}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Tab.Panel>
|
||||||
|
|
||||||
<Tab.Panel as="div" className="h-full overflow-y-auto">
|
<Tab.Panel as="div" className="h-full space-y-5 overflow-y-auto p-4 sm:p-5">
|
||||||
{cycleTab && cycleLayout && (
|
<ActiveCycleDetails workspaceSlug={workspaceSlug.toString()} projectId={projectId.toString()} />
|
||||||
<CyclesView
|
</Tab.Panel>
|
||||||
filter="upcoming"
|
|
||||||
layout={cycleLayout as TCycleLayout}
|
|
||||||
workspaceSlug={workspaceSlug.toString()}
|
|
||||||
projectId={projectId.toString()}
|
|
||||||
peekCycle={peekCycle?.toString()}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Tab.Panel>
|
|
||||||
|
|
||||||
<Tab.Panel as="div" className="h-full overflow-y-auto">
|
<Tab.Panel as="div" className="h-full overflow-y-auto">
|
||||||
{cycleTab && cycleLayout && workspaceSlug && projectId && (
|
{cycleTab && cycleLayout && (
|
||||||
<CyclesView
|
<CyclesView
|
||||||
filter="completed"
|
filter="upcoming"
|
||||||
layout={cycleLayout as TCycleLayout}
|
layout={cycleLayout as TCycleLayout}
|
||||||
workspaceSlug={workspaceSlug.toString()}
|
workspaceSlug={workspaceSlug.toString()}
|
||||||
projectId={projectId.toString()}
|
projectId={projectId.toString()}
|
||||||
peekCycle={peekCycle?.toString()}
|
peekCycle={peekCycle?.toString()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
|
|
||||||
<Tab.Panel as="div" className="h-full overflow-y-auto">
|
<Tab.Panel as="div" className="h-full overflow-y-auto">
|
||||||
{cycleTab && cycleLayout && workspaceSlug && projectId && (
|
{cycleTab && cycleLayout && workspaceSlug && projectId && (
|
||||||
<CyclesView
|
<CyclesView
|
||||||
filter="draft"
|
filter="completed"
|
||||||
layout={cycleLayout as TCycleLayout}
|
layout={cycleLayout as TCycleLayout}
|
||||||
workspaceSlug={workspaceSlug.toString()}
|
workspaceSlug={workspaceSlug.toString()}
|
||||||
projectId={projectId.toString()}
|
projectId={projectId.toString()}
|
||||||
peekCycle={peekCycle?.toString()}
|
peekCycle={peekCycle?.toString()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
</Tab.Panels>
|
|
||||||
</Tab.Group>
|
<Tab.Panel as="div" className="h-full overflow-y-auto">
|
||||||
)}
|
{cycleTab && cycleLayout && workspaceSlug && projectId && (
|
||||||
</div>
|
<CyclesView
|
||||||
|
filter="draft"
|
||||||
|
layout={cycleLayout as TCycleLayout}
|
||||||
|
workspaceSlug={workspaceSlug.toString()}
|
||||||
|
projectId={projectId.toString()}
|
||||||
|
peekCycle={peekCycle?.toString()}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Tab.Panel>
|
||||||
|
</Tab.Panels>
|
||||||
|
</Tab.Group>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -5,31 +5,43 @@ import { X, PenSquare } from "lucide-react";
|
|||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
// components
|
// components
|
||||||
import { DraftIssueLayoutRoot } from "components/issues/issue-layouts/roots/draft-issue-layout-root";
|
import { DraftIssueLayoutRoot } from "components/issues/issue-layouts/roots/draft-issue-layout-root";
|
||||||
|
import { PageHead } from "components/core";
|
||||||
import { ProjectDraftIssueHeader } from "components/headers";
|
import { ProjectDraftIssueHeader } from "components/headers";
|
||||||
// types
|
// types
|
||||||
import { NextPageWithLayout } from "lib/types";
|
import { NextPageWithLayout } from "lib/types";
|
||||||
|
// hooks
|
||||||
|
import { useProject } from "hooks/store";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
|
||||||
const ProjectDraftIssuesPage: NextPageWithLayout = () => {
|
const ProjectDraftIssuesPage: NextPageWithLayout = observer(() => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
// store
|
||||||
|
const { getProjectById } = useProject();
|
||||||
|
// derived values
|
||||||
|
const project = projectId ? getProjectById(projectId.toString()) : undefined;
|
||||||
|
const pageTitle = project?.name ? `${project?.name} - Draft Issues` : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col">
|
<>
|
||||||
<div className="ga-1 flex items-center border-b border-custom-border-200 px-4 py-2.5 shadow-sm">
|
<PageHead title={pageTitle} />
|
||||||
<button
|
<div className="flex h-full w-full flex-col">
|
||||||
type="button"
|
<div className="ga-1 flex items-center border-b border-custom-border-200 px-4 py-2.5 shadow-sm">
|
||||||
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/issues/`)}
|
<button
|
||||||
className="flex items-center gap-1.5 rounded-full border border-custom-border-200 px-3 py-1.5 text-xs"
|
type="button"
|
||||||
>
|
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/issues/`)}
|
||||||
<PenSquare className="h-4 w-4" />
|
className="flex items-center gap-1.5 rounded-full border border-custom-border-200 px-3 py-1.5 text-xs"
|
||||||
<span>Draft Issues</span>
|
>
|
||||||
|
<PenSquare className="h-4 w-4" />
|
||||||
|
<span>Draft Issues</span>
|
||||||
|
</button>
|
||||||
<X className="h-3 w-3" />
|
<X className="h-3 w-3" />
|
||||||
</button>
|
</div>
|
||||||
|
<DraftIssueLayoutRoot />
|
||||||
</div>
|
</div>
|
||||||
<DraftIssueLayoutRoot />
|
</>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
ProjectDraftIssuesPage.getLayout = function getLayout(page: ReactElement) {
|
ProjectDraftIssuesPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
|
@ -7,9 +7,9 @@ import { useProject, useInboxIssues } from "hooks/store";
|
|||||||
// layouts
|
// layouts
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
// components
|
// components
|
||||||
|
import { PageHead } from "components/core";
|
||||||
import { ProjectInboxHeader } from "components/headers";
|
import { ProjectInboxHeader } from "components/headers";
|
||||||
import { InboxSidebarRoot, InboxContentRoot } from "components/inbox";
|
import { InboxSidebarRoot, InboxContentRoot } from "components/inbox";
|
||||||
|
|
||||||
// types
|
// types
|
||||||
import { NextPageWithLayout } from "lib/types";
|
import { NextPageWithLayout } from "lib/types";
|
||||||
|
|
||||||
@ -22,7 +22,7 @@ const ProjectInboxPage: NextPageWithLayout = observer(() => {
|
|||||||
filters: { fetchInboxFilters },
|
filters: { fetchInboxFilters },
|
||||||
issues: { fetchInboxIssues },
|
issues: { fetchInboxIssues },
|
||||||
} = useInboxIssues();
|
} = useInboxIssues();
|
||||||
|
// fetching the Inbox filters and issues
|
||||||
useSWR(
|
useSWR(
|
||||||
workspaceSlug && projectId && currentProjectDetails && currentProjectDetails?.inbox_view
|
workspaceSlug && projectId && currentProjectDetails && currentProjectDetails?.inbox_view
|
||||||
? `INBOX_ISSUES_${workspaceSlug.toString()}_${projectId.toString()}`
|
? `INBOX_ISSUES_${workspaceSlug.toString()}_${projectId.toString()}`
|
||||||
@ -34,26 +34,32 @@ const ProjectInboxPage: NextPageWithLayout = observer(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
// derived values
|
||||||
|
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Inbox` : undefined;
|
||||||
|
|
||||||
if (!workspaceSlug || !projectId || !inboxId || !currentProjectDetails?.inbox_view) return <></>;
|
if (!workspaceSlug || !projectId || !inboxId || !currentProjectDetails?.inbox_view) return <></>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex h-full overflow-hidden">
|
<>
|
||||||
<div className="flex-shrink-0 w-[340px] h-full border-r border-custom-border-300">
|
<PageHead title={pageTitle} />
|
||||||
<InboxSidebarRoot
|
<div className="relative flex h-full overflow-hidden">
|
||||||
workspaceSlug={workspaceSlug.toString()}
|
<div className="flex-shrink-0 w-[340px] h-full border-r border-custom-border-300">
|
||||||
projectId={projectId.toString()}
|
<InboxSidebarRoot
|
||||||
inboxId={inboxId.toString()}
|
workspaceSlug={workspaceSlug.toString()}
|
||||||
/>
|
projectId={projectId.toString()}
|
||||||
|
inboxId={inboxId.toString()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-full">
|
||||||
|
<InboxContentRoot
|
||||||
|
workspaceSlug={workspaceSlug.toString()}
|
||||||
|
projectId={projectId.toString()}
|
||||||
|
inboxId={inboxId.toString()}
|
||||||
|
inboxIssueId={inboxIssueId?.toString() || undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full">
|
</>
|
||||||
<InboxContentRoot
|
|
||||||
workspaceSlug={workspaceSlug.toString()}
|
|
||||||
projectId={projectId.toString()}
|
|
||||||
inboxId={inboxId.toString()}
|
|
||||||
inboxIssueId={inboxIssueId?.toString() || undefined}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -6,6 +6,8 @@ import { observer } from "mobx-react";
|
|||||||
import { useInbox, useProject } from "hooks/store";
|
import { useInbox, useProject } from "hooks/store";
|
||||||
// layouts
|
// layouts
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
|
// ui
|
||||||
|
import { InboxLayoutLoader } from "components/ui";
|
||||||
// components
|
// components
|
||||||
import { ProjectInboxHeader } from "components/headers";
|
import { ProjectInboxHeader } from "components/headers";
|
||||||
// types
|
// types
|
||||||
@ -33,7 +35,7 @@ const ProjectInboxPage: NextPageWithLayout = observer(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col">
|
<div className="flex h-full flex-col">
|
||||||
{currentProjectDetails?.inbox_view ? <div>Loading...</div> : <div>You don{"'"}t have access to inbox</div>}
|
{currentProjectDetails?.inbox_view ? <InboxLayoutLoader /> : <div>You don{"'"}t have access to inbox</div>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -5,14 +5,15 @@ import { observer } from "mobx-react-lite";
|
|||||||
// layouts
|
// layouts
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
// components
|
// components
|
||||||
|
import { PageHead } from "components/core";
|
||||||
import { ProjectIssueDetailsHeader } from "components/headers";
|
import { ProjectIssueDetailsHeader } from "components/headers";
|
||||||
import { IssueDetailRoot } from "components/issues";
|
import { IssueDetailRoot } from "components/issues";
|
||||||
// ui
|
// ui
|
||||||
import { Loader } from "@plane/ui";
|
import { Loader } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
import { NextPageWithLayout } from "lib/types";
|
import { NextPageWithLayout } from "lib/types";
|
||||||
// fetch-keys
|
// store hooks
|
||||||
import { useApplication, useIssueDetail } from "hooks/store";
|
import { useApplication, useIssueDetail, useProject } from "hooks/store";
|
||||||
|
|
||||||
const IssueDetailsPage: NextPageWithLayout = observer(() => {
|
const IssueDetailsPage: NextPageWithLayout = observer(() => {
|
||||||
// router
|
// router
|
||||||
@ -23,17 +24,20 @@ const IssueDetailsPage: NextPageWithLayout = observer(() => {
|
|||||||
fetchIssue,
|
fetchIssue,
|
||||||
issue: { getIssueById },
|
issue: { getIssueById },
|
||||||
} = useIssueDetail();
|
} = useIssueDetail();
|
||||||
|
const { getProjectById } = useProject();
|
||||||
const { theme: themeStore } = useApplication();
|
const { theme: themeStore } = useApplication();
|
||||||
|
// fetching issue details
|
||||||
const { isLoading } = useSWR(
|
const { isLoading } = useSWR(
|
||||||
workspaceSlug && projectId && issueId ? `ISSUE_DETAIL_${workspaceSlug}_${projectId}_${issueId}` : null,
|
workspaceSlug && projectId && issueId ? `ISSUE_DETAIL_${workspaceSlug}_${projectId}_${issueId}` : null,
|
||||||
workspaceSlug && projectId && issueId
|
workspaceSlug && projectId && issueId
|
||||||
? () => fetchIssue(workspaceSlug.toString(), projectId.toString(), issueId.toString())
|
? () => fetchIssue(workspaceSlug.toString(), projectId.toString(), issueId.toString())
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
// derived values
|
||||||
const issue = getIssueById(issueId?.toString() || "") || undefined;
|
const issue = getIssueById(issueId?.toString() || "") || undefined;
|
||||||
|
const project = (issue?.project_id && getProjectById(issue?.project_id)) || undefined;
|
||||||
const issueLoader = !issue || isLoading ? true : false;
|
const issueLoader = !issue || isLoading ? true : false;
|
||||||
|
const pageTitle = project && issue ? `${project?.identifier}-${issue?.sequence_id} ${issue?.name}` : undefined;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleToggleIssueDetailSidebar = () => {
|
const handleToggleIssueDetailSidebar = () => {
|
||||||
@ -52,6 +56,7 @@ const IssueDetailsPage: NextPageWithLayout = observer(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<PageHead title={pageTitle} />
|
||||||
{issueLoader ? (
|
{issueLoader ? (
|
||||||
<Loader className="flex h-full gap-5 p-5">
|
<Loader className="flex h-full gap-5 p-5">
|
||||||
<div className="basis-2/3 space-y-2">
|
<div className="basis-2/3 space-y-2">
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
import { ReactElement } from "react";
|
import { ReactElement } from "react";
|
||||||
|
import Head from "next/head";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
// components
|
// components
|
||||||
import { ProjectLayoutRoot } from "components/issues";
|
import { ProjectLayoutRoot } from "components/issues";
|
||||||
import { ProjectIssuesHeader } from "components/headers";
|
import { ProjectIssuesHeader } from "components/headers";
|
||||||
@ -6,12 +9,36 @@ import { ProjectIssuesHeader } from "components/headers";
|
|||||||
import { NextPageWithLayout } from "lib/types";
|
import { NextPageWithLayout } from "lib/types";
|
||||||
// layouts
|
// layouts
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
|
// hooks
|
||||||
|
import { useProject } from "hooks/store";
|
||||||
|
import { PageHead } from "components/core";
|
||||||
|
|
||||||
const ProjectIssuesPage: NextPageWithLayout = () => (
|
const ProjectIssuesPage: NextPageWithLayout = observer(() => {
|
||||||
<div className="h-full w-full">
|
const router = useRouter();
|
||||||
<ProjectLayoutRoot />
|
const { projectId } = router.query;
|
||||||
</div>
|
// store
|
||||||
);
|
const { getProjectById } = useProject();
|
||||||
|
|
||||||
|
if (!projectId) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// derived values
|
||||||
|
const project = getProjectById(projectId.toString());
|
||||||
|
const pageTitle = project?.name ? `${project?.name} - Issues` : undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHead title={pageTitle} />
|
||||||
|
<Head>
|
||||||
|
<title>{project?.name} - Issues</title>
|
||||||
|
</Head>
|
||||||
|
<div className="h-full w-full">
|
||||||
|
<ProjectLayoutRoot />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
ProjectIssuesPage.getLayout = function getLayout(page: ReactElement) {
|
ProjectIssuesPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { ReactElement } from "react";
|
import { ReactElement } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
// hooks
|
// hooks
|
||||||
import { useModule } from "hooks/store";
|
import { useModule, useProject } from "hooks/store";
|
||||||
import useLocalStorage from "hooks/use-local-storage";
|
import useLocalStorage from "hooks/use-local-storage";
|
||||||
// layouts
|
// layouts
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
@ -10,37 +11,44 @@ import { AppLayout } from "layouts/app-layout";
|
|||||||
import { ModuleDetailsSidebar } from "components/modules";
|
import { ModuleDetailsSidebar } from "components/modules";
|
||||||
import { ModuleLayoutRoot } from "components/issues";
|
import { ModuleLayoutRoot } from "components/issues";
|
||||||
import { ModuleIssuesHeader } from "components/headers";
|
import { ModuleIssuesHeader } from "components/headers";
|
||||||
// ui
|
import { PageHead } from "components/core";
|
||||||
import { EmptyState } from "components/common";
|
import { EmptyState } from "components/common";
|
||||||
// assets
|
// assets
|
||||||
import emptyModule from "public/empty-state/module.svg";
|
import emptyModule from "public/empty-state/module.svg";
|
||||||
// types
|
// types
|
||||||
import { NextPageWithLayout } from "lib/types";
|
import { NextPageWithLayout } from "lib/types";
|
||||||
|
|
||||||
const ModuleIssuesPage: NextPageWithLayout = () => {
|
const ModuleIssuesPage: NextPageWithLayout = observer(() => {
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, moduleId } = router.query;
|
const { workspaceSlug, projectId, moduleId } = router.query;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { fetchModuleDetails } = useModule();
|
const { fetchModuleDetails, getModuleById } = useModule();
|
||||||
|
const { getProjectById } = useProject();
|
||||||
// local storage
|
// local storage
|
||||||
const { setValue, storedValue } = useLocalStorage("module_sidebar_collapsed", "false");
|
const { setValue, storedValue } = useLocalStorage("module_sidebar_collapsed", "false");
|
||||||
const isSidebarCollapsed = storedValue ? (storedValue === "true" ? true : false) : false;
|
const isSidebarCollapsed = storedValue ? (storedValue === "true" ? true : false) : false;
|
||||||
|
// fetching module details
|
||||||
const { error } = useSWR(
|
const { error } = useSWR(
|
||||||
workspaceSlug && projectId && moduleId ? `CURRENT_MODULE_DETAILS_${moduleId.toString()}` : null,
|
workspaceSlug && projectId && moduleId ? `CURRENT_MODULE_DETAILS_${moduleId.toString()}` : null,
|
||||||
workspaceSlug && projectId && moduleId
|
workspaceSlug && projectId && moduleId
|
||||||
? () => fetchModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId.toString())
|
? () => fetchModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId.toString())
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
// derived values
|
||||||
|
const projectModule = moduleId ? getModuleById(moduleId.toString()) : undefined;
|
||||||
|
const project = projectId ? getProjectById(projectId.toString()) : undefined;
|
||||||
|
const pageTitle = project?.name && projectModule?.name ? `${project?.name} - ${projectModule?.name}` : undefined;
|
||||||
|
|
||||||
const toggleSidebar = () => {
|
const toggleSidebar = () => {
|
||||||
setValue(`${!isSidebarCollapsed}`);
|
setValue(`${!isSidebarCollapsed}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!workspaceSlug || !projectId || !moduleId) return <></>;
|
if (!workspaceSlug || !projectId || !moduleId) return <></>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<PageHead title={pageTitle} />
|
||||||
{error ? (
|
{error ? (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
image={emptyModule}
|
image={emptyModule}
|
||||||
@ -71,7 +79,7 @@ const ModuleIssuesPage: NextPageWithLayout = () => {
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
ModuleIssuesPage.getLayout = function getLayout(page: ReactElement) {
|
ModuleIssuesPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
|
@ -1,13 +1,33 @@
|
|||||||
import { ReactElement } from "react";
|
import { ReactElement } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
// layouts
|
// layouts
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
// components
|
// components
|
||||||
|
import { PageHead } from "components/core";
|
||||||
import { ModulesListView } from "components/modules";
|
import { ModulesListView } from "components/modules";
|
||||||
import { ModulesListHeader } from "components/headers";
|
import { ModulesListHeader } from "components/headers";
|
||||||
// types
|
// types
|
||||||
import { NextPageWithLayout } from "lib/types";
|
import { NextPageWithLayout } from "lib/types";
|
||||||
|
// hooks
|
||||||
|
import { useProject } from "hooks/store";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
|
||||||
const ProjectModulesPage: NextPageWithLayout = () => <ModulesListView />;
|
const ProjectModulesPage: NextPageWithLayout = observer(() => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { projectId } = router.query;
|
||||||
|
// store
|
||||||
|
const { getProjectById } = useProject();
|
||||||
|
// derived values
|
||||||
|
const project = projectId ? getProjectById(projectId.toString()) : undefined;
|
||||||
|
const pageTitle = project?.name ? `${project?.name} - Modules` : undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHead title={pageTitle} />
|
||||||
|
<ModulesListView />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
ProjectModulesPage.getLayout = function getLayout(page: ReactElement) {
|
ProjectModulesPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
|
@ -14,7 +14,7 @@ import { FileService } from "services/file.service";
|
|||||||
// layouts
|
// layouts
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
// components
|
// components
|
||||||
import { GptAssistantPopover } from "components/core";
|
import { GptAssistantPopover, PageHead } from "components/core";
|
||||||
import { PageDetailsHeader } from "components/headers/page-details";
|
import { PageDetailsHeader } from "components/headers/page-details";
|
||||||
// ui
|
// ui
|
||||||
import { DocumentEditorWithRef, DocumentReadOnlyEditorWithRef } from "@plane/document-editor";
|
import { DocumentEditorWithRef, DocumentReadOnlyEditorWithRef } from "@plane/document-editor";
|
||||||
@ -256,113 +256,116 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
|
|||||||
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
||||||
|
|
||||||
return pageIdMobx ? (
|
return pageIdMobx ? (
|
||||||
<div className="flex h-full flex-col justify-between">
|
<>
|
||||||
<div className="h-full w-full overflow-hidden">
|
<PageHead title={pageTitle} />
|
||||||
{isPageReadOnly ? (
|
<div className="flex h-full flex-col justify-between">
|
||||||
<DocumentReadOnlyEditorWithRef
|
<div className="h-full w-full overflow-hidden">
|
||||||
onActionCompleteHandler={actionCompleteAlert}
|
{isPageReadOnly ? (
|
||||||
ref={editorRef}
|
<DocumentReadOnlyEditorWithRef
|
||||||
value={pageDescription}
|
onActionCompleteHandler={actionCompleteAlert}
|
||||||
customClassName={"tracking-tight w-full px-0"}
|
ref={editorRef}
|
||||||
borderOnFocus={false}
|
value={pageDescription}
|
||||||
noBorder
|
customClassName={"tracking-tight w-full px-0"}
|
||||||
documentDetails={{
|
borderOnFocus={false}
|
||||||
title: pageTitle,
|
noBorder
|
||||||
created_by: created_by,
|
documentDetails={{
|
||||||
created_on: created_at,
|
title: pageTitle,
|
||||||
last_updated_at: updated_at,
|
created_by: created_by,
|
||||||
last_updated_by: updated_by,
|
created_on: created_at,
|
||||||
}}
|
last_updated_at: updated_at,
|
||||||
pageLockConfig={userCanLock && !archived_at ? { action: unlockPage, is_locked: is_locked } : undefined}
|
last_updated_by: updated_by,
|
||||||
pageDuplicationConfig={userCanDuplicate && !archived_at ? { action: duplicate_page } : undefined}
|
}}
|
||||||
pageArchiveConfig={
|
pageLockConfig={userCanLock && !archived_at ? { action: unlockPage, is_locked: is_locked } : undefined}
|
||||||
userCanArchive
|
pageDuplicationConfig={userCanDuplicate && !archived_at ? { action: duplicate_page } : undefined}
|
||||||
? {
|
pageArchiveConfig={
|
||||||
action: archived_at ? unArchivePage : archivePage,
|
userCanArchive
|
||||||
is_archived: archived_at ? true : false,
|
? {
|
||||||
archived_at: archived_at ? new Date(archived_at) : undefined,
|
action: archived_at ? unArchivePage : archivePage,
|
||||||
}
|
is_archived: archived_at ? true : false,
|
||||||
: undefined
|
archived_at: archived_at ? new Date(archived_at) : undefined,
|
||||||
}
|
}
|
||||||
/>
|
: undefined
|
||||||
) : (
|
}
|
||||||
<div className="relative h-full w-full overflow-hidden">
|
|
||||||
<Controller
|
|
||||||
name="description_html"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { onChange } }) => (
|
|
||||||
<DocumentEditorWithRef
|
|
||||||
isSubmitting={isSubmitting}
|
|
||||||
documentDetails={{
|
|
||||||
title: pageTitle,
|
|
||||||
created_by: created_by,
|
|
||||||
created_on: created_at,
|
|
||||||
last_updated_at: updated_at,
|
|
||||||
last_updated_by: updated_by,
|
|
||||||
}}
|
|
||||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
|
||||||
deleteFile={fileService.getDeleteImageFunction(workspaceId)}
|
|
||||||
restoreFile={fileService.getRestoreImageFunction(workspaceId)}
|
|
||||||
value={pageDescription}
|
|
||||||
setShouldShowAlert={setShowAlert}
|
|
||||||
cancelUploadImage={fileService.cancelUpload}
|
|
||||||
ref={editorRef}
|
|
||||||
debouncedUpdatesEnabled={false}
|
|
||||||
setIsSubmitting={setIsSubmitting}
|
|
||||||
updatePageTitle={updatePageTitle}
|
|
||||||
onActionCompleteHandler={actionCompleteAlert}
|
|
||||||
customClassName="tracking-tight self-center h-full w-full right-[0.675rem]"
|
|
||||||
onChange={(_description_json: Object, description_html: string) => {
|
|
||||||
setShowAlert(true);
|
|
||||||
onChange(description_html);
|
|
||||||
handleSubmit(updatePage)();
|
|
||||||
}}
|
|
||||||
duplicationConfig={userCanDuplicate ? { action: duplicate_page } : undefined}
|
|
||||||
pageArchiveConfig={
|
|
||||||
userCanArchive
|
|
||||||
? {
|
|
||||||
is_archived: archived_at ? true : false,
|
|
||||||
action: archived_at ? unArchivePage : archivePage,
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
pageLockConfig={userCanLock ? { is_locked: false, action: lockPage } : undefined}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
{projectId && envConfig?.has_openai_configured && (
|
) : (
|
||||||
<div className="absolute right-[68px] top-2.5">
|
<div className="relative h-full w-full overflow-hidden">
|
||||||
<GptAssistantPopover
|
<Controller
|
||||||
isOpen={gptModalOpen}
|
name="description_html"
|
||||||
projectId={projectId.toString()}
|
control={control}
|
||||||
handleClose={() => {
|
render={({ field: { onChange } }) => (
|
||||||
setGptModal((prevData) => !prevData);
|
<DocumentEditorWithRef
|
||||||
// this is done so that the title do not reset after gpt popover closed
|
isSubmitting={isSubmitting}
|
||||||
reset(getValues());
|
documentDetails={{
|
||||||
}}
|
title: pageTitle,
|
||||||
onResponse={(response) => {
|
created_by: created_by,
|
||||||
handleAiAssistance(response);
|
created_on: created_at,
|
||||||
}}
|
last_updated_at: updated_at,
|
||||||
placement="top-end"
|
last_updated_by: updated_by,
|
||||||
button={
|
}}
|
||||||
<button
|
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||||
type="button"
|
deleteFile={fileService.getDeleteImageFunction(workspaceId)}
|
||||||
className="flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-90"
|
restoreFile={fileService.getRestoreImageFunction(workspaceId)}
|
||||||
onClick={() => setGptModal((prevData) => !prevData)}
|
value={pageDescription}
|
||||||
>
|
setShouldShowAlert={setShowAlert}
|
||||||
<Sparkle className="h-4 w-4" />
|
cancelUploadImage={fileService.cancelUpload}
|
||||||
AI
|
ref={editorRef}
|
||||||
</button>
|
debouncedUpdatesEnabled={false}
|
||||||
}
|
setIsSubmitting={setIsSubmitting}
|
||||||
className="!min-w-[38rem]"
|
updatePageTitle={updatePageTitle}
|
||||||
/>
|
onActionCompleteHandler={actionCompleteAlert}
|
||||||
</div>
|
customClassName="tracking-tight self-center h-full w-full right-[0.675rem]"
|
||||||
)}
|
onChange={(_description_json: Object, description_html: string) => {
|
||||||
</div>
|
setShowAlert(true);
|
||||||
)}
|
onChange(description_html);
|
||||||
<IssuePeekOverview />
|
handleSubmit(updatePage)();
|
||||||
|
}}
|
||||||
|
duplicationConfig={userCanDuplicate ? { action: duplicate_page } : undefined}
|
||||||
|
pageArchiveConfig={
|
||||||
|
userCanArchive
|
||||||
|
? {
|
||||||
|
is_archived: archived_at ? true : false,
|
||||||
|
action: archived_at ? unArchivePage : archivePage,
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
pageLockConfig={userCanLock ? { is_locked: false, action: lockPage } : undefined}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{projectId && envConfig?.has_openai_configured && (
|
||||||
|
<div className="absolute right-[68px] top-2.5">
|
||||||
|
<GptAssistantPopover
|
||||||
|
isOpen={gptModalOpen}
|
||||||
|
projectId={projectId.toString()}
|
||||||
|
handleClose={() => {
|
||||||
|
setGptModal((prevData) => !prevData);
|
||||||
|
// this is done so that the title do not reset after gpt popover closed
|
||||||
|
reset(getValues());
|
||||||
|
}}
|
||||||
|
onResponse={(response) => {
|
||||||
|
handleAiAssistance(response);
|
||||||
|
}}
|
||||||
|
placement="top-end"
|
||||||
|
button={
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-90"
|
||||||
|
onClick={() => setGptModal((prevData) => !prevData)}
|
||||||
|
>
|
||||||
|
<Sparkle className="h-4 w-4" />
|
||||||
|
AI
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
className="!min-w-[38rem]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<IssuePeekOverview />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid h-full w-full place-items-center">
|
<div className="grid h-full w-full place-items-center">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
|
@ -6,7 +6,7 @@ import useSWR from "swr";
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
// hooks
|
// hooks
|
||||||
import { useApplication, useEventTracker, useUser } from "hooks/store";
|
import { useApplication, useEventTracker, useUser, useProject } from "hooks/store";
|
||||||
import useLocalStorage from "hooks/use-local-storage";
|
import useLocalStorage from "hooks/use-local-storage";
|
||||||
import useUserAuth from "hooks/use-user-auth";
|
import useUserAuth from "hooks/use-user-auth";
|
||||||
import useSize from "hooks/use-window-size";
|
import useSize from "hooks/use-window-size";
|
||||||
@ -24,6 +24,7 @@ import { PAGE_TABS_LIST } from "constants/page";
|
|||||||
import { useProjectPages } from "hooks/store/use-project-page";
|
import { useProjectPages } from "hooks/store/use-project-page";
|
||||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
import { PAGE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
import { PAGE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||||
|
import { PageHead } from "components/core";
|
||||||
|
|
||||||
const AllPagesList = dynamic<any>(() => import("components/pages").then((a) => a.AllPagesList), {
|
const AllPagesList = dynamic<any>(() => import("components/pages").then((a) => a.AllPagesList), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
@ -63,7 +64,7 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => {
|
|||||||
commandPalette: { toggleCreatePageModal },
|
commandPalette: { toggleCreatePageModal },
|
||||||
} = useApplication();
|
} = useApplication();
|
||||||
const { setTrackElement } = useEventTracker();
|
const { setTrackElement } = useEventTracker();
|
||||||
|
const { getProjectById } = useProject();
|
||||||
const { fetchProjectPages, fetchArchivedProjectPages, loader, archivedPageLoader, projectPageIds, archivedPageIds } =
|
const { fetchProjectPages, fetchArchivedProjectPages, loader, archivedPageLoader, projectPageIds, archivedPageIds } =
|
||||||
useProjectPages();
|
useProjectPages();
|
||||||
// hooks
|
// hooks
|
||||||
@ -101,10 +102,12 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// derived values
|
||||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||||
const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "pages", isLightMode);
|
const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "pages", isLightMode);
|
||||||
|
|
||||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
||||||
|
const project = projectId ? getProjectById(projectId.toString()) : undefined;
|
||||||
|
const pageTitle = project?.name ? `${project?.name} - Pages` : undefined;
|
||||||
|
|
||||||
const MobileTabList = () => (
|
const MobileTabList = () => (
|
||||||
<Tab.List as="div" className="flex items-center justify-between border-b border-custom-border-200 px-3 pt-3 mb-4">
|
<Tab.List as="div" className="flex items-center justify-between border-b border-custom-border-200 px-3 pt-3 mb-4">
|
||||||
@ -129,6 +132,7 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<PageHead title={pageTitle} />
|
||||||
{projectPageIds && archivedPageIds && projectPageIds.length + archivedPageIds.length > 0 ? (
|
{projectPageIds && archivedPageIds && projectPageIds.length + archivedPageIds.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
{workspaceSlug && projectId && (
|
{workspaceSlug && projectId && (
|
||||||
|
@ -10,6 +10,7 @@ import { ProjectSettingLayout } from "layouts/settings-layout";
|
|||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// components
|
// components
|
||||||
import { AutoArchiveAutomation, AutoCloseAutomation } from "components/automation";
|
import { AutoArchiveAutomation, AutoCloseAutomation } from "components/automation";
|
||||||
|
import { PageHead } from "components/core";
|
||||||
import { ProjectSettingHeader } from "components/headers";
|
import { ProjectSettingHeader } from "components/headers";
|
||||||
// types
|
// types
|
||||||
import { NextPageWithLayout } from "lib/types";
|
import { NextPageWithLayout } from "lib/types";
|
||||||
@ -41,16 +42,21 @@ const AutomationSettingsPage: NextPageWithLayout = observer(() => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// derived values
|
||||||
const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN;
|
const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN;
|
||||||
|
const pageTitle = projectDetails?.name ? `${projectDetails?.name} - Automations` : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={`w-full overflow-y-auto py-8 pr-9 ${isAdmin ? "" : "opacity-60"}`}>
|
<>
|
||||||
<div className="flex items-center border-b border-custom-border-100 py-3.5">
|
<PageHead title={pageTitle} />
|
||||||
<h3 className="text-xl font-medium">Automations</h3>
|
<section className={`w-full overflow-y-auto py-8 pr-9 ${isAdmin ? "" : "opacity-60"}`}>
|
||||||
</div>
|
<div className="flex items-center border-b border-custom-border-100 py-3.5">
|
||||||
<AutoArchiveAutomation handleChange={handleChange} />
|
<h3 className="text-xl font-medium">Automations</h3>
|
||||||
<AutoCloseAutomation handleChange={handleChange} />
|
</div>
|
||||||
</section>
|
<AutoArchiveAutomation handleChange={handleChange} />
|
||||||
|
<AutoCloseAutomation handleChange={handleChange} />
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { ReactElement } from "react";
|
import { ReactElement } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// hooks
|
// hooks
|
||||||
import { useUser } from "hooks/store";
|
import { useUser, useProject } from "hooks/store";
|
||||||
// layouts
|
// layouts
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
import { ProjectSettingLayout } from "layouts/settings-layout";
|
import { ProjectSettingLayout } from "layouts/settings-layout";
|
||||||
// components
|
// components
|
||||||
|
import { PageHead } from "components/core";
|
||||||
import { ProjectSettingHeader } from "components/headers";
|
import { ProjectSettingHeader } from "components/headers";
|
||||||
import { EstimatesList } from "components/estimates";
|
import { EstimatesList } from "components/estimates";
|
||||||
// types
|
// types
|
||||||
@ -17,13 +18,18 @@ const EstimatesSettingsPage: NextPageWithLayout = observer(() => {
|
|||||||
const {
|
const {
|
||||||
membership: { currentProjectRole },
|
membership: { currentProjectRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
|
const { currentProjectDetails } = useProject();
|
||||||
|
// derived values
|
||||||
const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN;
|
const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN;
|
||||||
|
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Estimates` : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`h-full w-full overflow-y-auto py-8 pr-9 ${isAdmin ? "" : "pointer-events-none opacity-60"}`}>
|
<>
|
||||||
<EstimatesList />
|
<PageHead title={pageTitle} />
|
||||||
</div>
|
<div className={`h-full w-full overflow-y-auto py-8 pr-9 ${isAdmin ? "" : "pointer-events-none opacity-60"}`}>
|
||||||
|
<EstimatesList />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,41 +1,48 @@
|
|||||||
import { ReactElement } from "react";
|
import { ReactElement } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
// hooks
|
// hooks
|
||||||
import { useUser } from "hooks/store";
|
import { useProject, useUser } from "hooks/store";
|
||||||
// layouts
|
// layouts
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
import { ProjectSettingLayout } from "layouts/settings-layout";
|
import { ProjectSettingLayout } from "layouts/settings-layout";
|
||||||
// components
|
// components
|
||||||
|
import { PageHead } from "components/core";
|
||||||
import { ProjectSettingHeader } from "components/headers";
|
import { ProjectSettingHeader } from "components/headers";
|
||||||
import { ProjectFeaturesList } from "components/project";
|
import { ProjectFeaturesList } from "components/project";
|
||||||
// types
|
// types
|
||||||
import { NextPageWithLayout } from "lib/types";
|
import { NextPageWithLayout } from "lib/types";
|
||||||
|
|
||||||
const FeaturesSettingsPage: NextPageWithLayout = () => {
|
const FeaturesSettingsPage: NextPageWithLayout = observer(() => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
// store
|
// store
|
||||||
const {
|
const {
|
||||||
membership: { fetchUserProjectInfo },
|
membership: { fetchUserProjectInfo },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
|
const { currentProjectDetails } = useProject();
|
||||||
|
// fetch the project details
|
||||||
const { data: memberDetails } = useSWR(
|
const { data: memberDetails } = useSWR(
|
||||||
workspaceSlug && projectId ? `PROJECT_MEMBERS_ME_${workspaceSlug}_${projectId}` : null,
|
workspaceSlug && projectId ? `PROJECT_MEMBERS_ME_${workspaceSlug}_${projectId}` : null,
|
||||||
workspaceSlug && projectId ? () => fetchUserProjectInfo(workspaceSlug.toString(), projectId.toString()) : null
|
workspaceSlug && projectId ? () => fetchUserProjectInfo(workspaceSlug.toString(), projectId.toString()) : null
|
||||||
);
|
);
|
||||||
|
// derived values
|
||||||
const isAdmin = memberDetails?.role === 20;
|
const isAdmin = memberDetails?.role === 20;
|
||||||
|
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Features` : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={`w-full overflow-y-auto py-8 pr-9 ${isAdmin ? "" : "opacity-60"}`}>
|
<>
|
||||||
<div className="flex items-center border-b border-custom-border-100 py-3.5">
|
<PageHead title={pageTitle} />
|
||||||
<h3 className="text-xl font-medium">Features</h3>
|
<section className={`w-full overflow-y-auto py-8 pr-9 ${isAdmin ? "" : "opacity-60"}`}>
|
||||||
</div>
|
<div className="flex items-center border-b border-custom-border-100 py-3.5">
|
||||||
<ProjectFeaturesList />
|
<h3 className="text-xl font-medium">Features</h3>
|
||||||
</section>
|
</div>
|
||||||
|
<ProjectFeaturesList />
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
FeaturesSettingsPage.getLayout = function getLayout(page: ReactElement) {
|
FeaturesSettingsPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
|
@ -8,6 +8,7 @@ import { useProject } from "hooks/store";
|
|||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
import { ProjectSettingLayout } from "layouts/settings-layout";
|
import { ProjectSettingLayout } from "layouts/settings-layout";
|
||||||
// components
|
// components
|
||||||
|
import { PageHead } from "components/core";
|
||||||
import { ProjectSettingHeader } from "components/headers";
|
import { ProjectSettingHeader } from "components/headers";
|
||||||
import {
|
import {
|
||||||
DeleteProjectModal,
|
DeleteProjectModal,
|
||||||
@ -28,18 +29,19 @@ const GeneralSettingsPage: NextPageWithLayout = observer(() => {
|
|||||||
const { currentProjectDetails, fetchProjectDetails } = useProject();
|
const { currentProjectDetails, fetchProjectDetails } = useProject();
|
||||||
// api call to fetch project details
|
// api call to fetch project details
|
||||||
// TODO: removed this API if not necessary
|
// TODO: removed this API if not necessary
|
||||||
useSWR(
|
const { isLoading } = useSWR(
|
||||||
workspaceSlug && projectId ? `PROJECT_DETAILS_${projectId}` : null,
|
workspaceSlug && projectId ? `PROJECT_DETAILS_${projectId}` : null,
|
||||||
workspaceSlug && projectId ? () => fetchProjectDetails(workspaceSlug.toString(), projectId.toString()) : null
|
workspaceSlug && projectId ? () => fetchProjectDetails(workspaceSlug.toString(), projectId.toString()) : null
|
||||||
);
|
);
|
||||||
|
// derived values
|
||||||
|
const isAdmin = currentProjectDetails?.member_role === 20;
|
||||||
|
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - General Settings` : undefined;
|
||||||
// const currentNetwork = NETWORK_CHOICES.find((n) => n.key === projectDetails?.network);
|
// const currentNetwork = NETWORK_CHOICES.find((n) => n.key === projectDetails?.network);
|
||||||
// const selectedNetwork = NETWORK_CHOICES.find((n) => n.key === watch("network"));
|
// const selectedNetwork = NETWORK_CHOICES.find((n) => n.key === watch("network"));
|
||||||
|
|
||||||
const isAdmin = currentProjectDetails?.member_role === 20;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<PageHead title={pageTitle} />
|
||||||
{currentProjectDetails && (
|
{currentProjectDetails && (
|
||||||
<DeleteProjectModal
|
<DeleteProjectModal
|
||||||
project={currentProjectDetails}
|
project={currentProjectDetails}
|
||||||
@ -49,10 +51,11 @@ const GeneralSettingsPage: NextPageWithLayout = observer(() => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={`w-full overflow-y-auto py-8 pr-9 ${isAdmin ? "" : "opacity-60"}`}>
|
<div className={`w-full overflow-y-auto py-8 pr-9 ${isAdmin ? "" : "opacity-60"}`}>
|
||||||
{currentProjectDetails && workspaceSlug ? (
|
{currentProjectDetails && workspaceSlug && projectId && !isLoading ? (
|
||||||
<ProjectDetailsForm
|
<ProjectDetailsForm
|
||||||
project={currentProjectDetails}
|
project={currentProjectDetails}
|
||||||
workspaceSlug={workspaceSlug.toString()}
|
workspaceSlug={workspaceSlug.toString()}
|
||||||
|
projectId={projectId.toString()}
|
||||||
isAdmin={isAdmin}
|
isAdmin={isAdmin}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
@ -2,6 +2,7 @@ import { ReactElement } from "react";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
// hooks
|
// hooks
|
||||||
import { useUser } from "hooks/store";
|
import { useUser } from "hooks/store";
|
||||||
// layouts
|
// layouts
|
||||||
@ -11,6 +12,7 @@ import { ProjectSettingLayout } from "layouts/settings-layout";
|
|||||||
import { IntegrationService } from "services/integrations";
|
import { IntegrationService } from "services/integrations";
|
||||||
import { ProjectService } from "services/project";
|
import { ProjectService } from "services/project";
|
||||||
// components
|
// components
|
||||||
|
import { PageHead } from "components/core";
|
||||||
import { IntegrationCard } from "components/project";
|
import { IntegrationCard } from "components/project";
|
||||||
import { ProjectSettingHeader } from "components/headers";
|
import { ProjectSettingHeader } from "components/headers";
|
||||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||||
@ -27,63 +29,66 @@ import { PROJECT_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
|||||||
const integrationService = new IntegrationService();
|
const integrationService = new IntegrationService();
|
||||||
const projectService = new ProjectService();
|
const projectService = new ProjectService();
|
||||||
|
|
||||||
const ProjectIntegrationsPage: NextPageWithLayout = () => {
|
const ProjectIntegrationsPage: NextPageWithLayout = observer(() => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
// theme
|
// theme
|
||||||
const { resolvedTheme } = useTheme();
|
const { resolvedTheme } = useTheme();
|
||||||
// store hooks
|
// store hooks
|
||||||
const { currentUser } = useUser();
|
const { currentUser } = useUser();
|
||||||
|
// fetch project details
|
||||||
const { data: projectDetails } = useSWR<IProject>(
|
const { data: projectDetails } = useSWR<IProject>(
|
||||||
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
|
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
|
||||||
workspaceSlug && projectId ? () => projectService.getProject(workspaceSlug as string, projectId as string) : null
|
workspaceSlug && projectId ? () => projectService.getProject(workspaceSlug as string, projectId as string) : null
|
||||||
);
|
);
|
||||||
|
// fetch Integrations list
|
||||||
const { data: workspaceIntegrations } = useSWR(
|
const { data: workspaceIntegrations } = useSWR(
|
||||||
workspaceSlug ? WORKSPACE_INTEGRATIONS(workspaceSlug as string) : null,
|
workspaceSlug ? WORKSPACE_INTEGRATIONS(workspaceSlug as string) : null,
|
||||||
() => (workspaceSlug ? integrationService.getWorkspaceIntegrationsList(workspaceSlug as string) : null)
|
() => (workspaceSlug ? integrationService.getWorkspaceIntegrationsList(workspaceSlug as string) : null)
|
||||||
);
|
);
|
||||||
|
// derived values
|
||||||
const emptyStateDetail = PROJECT_SETTINGS_EMPTY_STATE_DETAILS["integrations"];
|
const emptyStateDetail = PROJECT_SETTINGS_EMPTY_STATE_DETAILS["integrations"];
|
||||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||||
const emptyStateImage = getEmptyStateImagePath("project-settings", "integrations", isLightMode);
|
const emptyStateImage = getEmptyStateImagePath("project-settings", "integrations", isLightMode);
|
||||||
|
|
||||||
const isAdmin = projectDetails?.member_role === 20;
|
const isAdmin = projectDetails?.member_role === 20;
|
||||||
|
const pageTitle = projectDetails?.name ? `${projectDetails?.name} - Integrations` : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`h-full w-full gap-10 overflow-y-auto py-8 pr-9 ${isAdmin ? "" : "opacity-60"}`}>
|
<>
|
||||||
<div className="flex items-center border-b border-custom-border-100 py-3.5">
|
<PageHead title={pageTitle} />
|
||||||
<h3 className="text-xl font-medium">Integrations</h3>
|
<div className={`h-full w-full gap-10 overflow-y-auto py-8 pr-9 ${isAdmin ? "" : "opacity-60"}`}>
|
||||||
</div>
|
<div className="flex items-center border-b border-custom-border-100 py-3.5">
|
||||||
{workspaceIntegrations ? (
|
<h3 className="text-xl font-medium">Integrations</h3>
|
||||||
workspaceIntegrations.length > 0 ? (
|
</div>
|
||||||
<div>
|
{workspaceIntegrations ? (
|
||||||
{workspaceIntegrations.map((integration) => (
|
workspaceIntegrations.length > 0 ? (
|
||||||
<IntegrationCard key={integration.integration_detail.id} integration={integration} />
|
<div>
|
||||||
))}
|
{workspaceIntegrations.map((integration) => (
|
||||||
</div>
|
<IntegrationCard key={integration.integration_detail.id} integration={integration} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="h-full w-full py-8">
|
||||||
|
<EmptyState
|
||||||
|
title={emptyStateDetail.title}
|
||||||
|
description={emptyStateDetail.description}
|
||||||
|
image={emptyStateImage}
|
||||||
|
primaryButton={{
|
||||||
|
text: "Configure now",
|
||||||
|
onClick: () => router.push(`/${workspaceSlug}/settings/integrations`),
|
||||||
|
}}
|
||||||
|
size="lg"
|
||||||
|
disabled={!isAdmin}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<div className="h-full w-full py-8">
|
<IntegrationsSettingsLoader />
|
||||||
<EmptyState
|
)}
|
||||||
title={emptyStateDetail.title}
|
</div>
|
||||||
description={emptyStateDetail.description}
|
</>
|
||||||
image={emptyStateImage}
|
|
||||||
primaryButton={{
|
|
||||||
text: "Configure now",
|
|
||||||
onClick: () => router.push(`/${workspaceSlug}/settings/integrations`),
|
|
||||||
}}
|
|
||||||
size="lg"
|
|
||||||
disabled={!isAdmin}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<IntegrationsSettingsLoader />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
ProjectIntegrationsPage.getLayout = function getLayout(page: ReactElement) {
|
ProjectIntegrationsPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
|
@ -1,18 +1,30 @@
|
|||||||
import { ReactElement } from "react";
|
import { ReactElement } from "react";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
// layouts
|
// layouts
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
import { ProjectSettingLayout } from "layouts/settings-layout";
|
import { ProjectSettingLayout } from "layouts/settings-layout";
|
||||||
// components
|
// components
|
||||||
|
import { PageHead } from "components/core";
|
||||||
import { ProjectSettingsLabelList } from "components/labels";
|
import { ProjectSettingsLabelList } from "components/labels";
|
||||||
import { ProjectSettingHeader } from "components/headers";
|
import { ProjectSettingHeader } from "components/headers";
|
||||||
// types
|
// types
|
||||||
import { NextPageWithLayout } from "lib/types";
|
import { NextPageWithLayout } from "lib/types";
|
||||||
|
// hooks
|
||||||
|
import { useProject } from "hooks/store";
|
||||||
|
|
||||||
const LabelsSettingsPage: NextPageWithLayout = () => (
|
const LabelsSettingsPage: NextPageWithLayout = observer(() => {
|
||||||
<div className="h-full w-full gap-10 overflow-y-auto py-8 pr-9">
|
const { currentProjectDetails } = useProject();
|
||||||
<ProjectSettingsLabelList />
|
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Labels` : undefined;
|
||||||
</div>
|
|
||||||
);
|
return (
|
||||||
|
<>
|
||||||
|
<PageHead title={pageTitle} />
|
||||||
|
<div className="h-full w-full gap-10 overflow-y-auto py-8 pr-9">
|
||||||
|
<ProjectSettingsLabelList />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
LabelsSettingsPage.getLayout = function getLayout(page: ReactElement) {
|
LabelsSettingsPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
|
@ -1,19 +1,33 @@
|
|||||||
import { ReactElement } from "react";
|
import { ReactElement } from "react";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
// layouts
|
// layouts
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
import { ProjectSettingLayout } from "layouts/settings-layout";
|
import { ProjectSettingLayout } from "layouts/settings-layout";
|
||||||
// components
|
// components
|
||||||
|
import { PageHead } from "components/core";
|
||||||
import { ProjectSettingHeader } from "components/headers";
|
import { ProjectSettingHeader } from "components/headers";
|
||||||
import { ProjectMemberList, ProjectSettingsMemberDefaults } from "components/project";
|
import { ProjectMemberList, ProjectSettingsMemberDefaults } from "components/project";
|
||||||
// types
|
// types
|
||||||
import { NextPageWithLayout } from "lib/types";
|
import { NextPageWithLayout } from "lib/types";
|
||||||
|
// hooks
|
||||||
|
import { useProject } from "hooks/store";
|
||||||
|
|
||||||
const MembersSettingsPage: NextPageWithLayout = () => (
|
const MembersSettingsPage: NextPageWithLayout = observer(() => {
|
||||||
<section className={`w-full overflow-y-auto py-8 pr-9`}>
|
// store
|
||||||
<ProjectSettingsMemberDefaults />
|
const { currentProjectDetails } = useProject();
|
||||||
<ProjectMemberList />
|
// derived values
|
||||||
</section>
|
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Members` : undefined;
|
||||||
);
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHead title={pageTitle} />
|
||||||
|
<section className={`w-full overflow-y-auto py-8 pr-9`}>
|
||||||
|
<ProjectSettingsMemberDefaults />
|
||||||
|
<ProjectMemberList />
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
MembersSettingsPage.getLayout = function getLayout(page: ReactElement) {
|
MembersSettingsPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
import { ReactElement } from "react";
|
import { ReactElement } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
// hooks
|
// hooks
|
||||||
import { useProjectView } from "hooks/store";
|
import { useProject, useProjectView } from "hooks/store";
|
||||||
// layouts
|
// layouts
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
// components
|
// components
|
||||||
import { ProjectViewLayoutRoot } from "components/issues";
|
import { ProjectViewLayoutRoot } from "components/issues";
|
||||||
import { ProjectViewIssuesHeader } from "components/headers";
|
import { ProjectViewIssuesHeader } from "components/headers";
|
||||||
|
import { PageHead } from "components/core";
|
||||||
// ui
|
// ui
|
||||||
import { EmptyState } from "components/common";
|
import { EmptyState } from "components/common";
|
||||||
// assets
|
// assets
|
||||||
@ -15,12 +17,17 @@ import emptyView from "public/empty-state/view.svg";
|
|||||||
// types
|
// types
|
||||||
import { NextPageWithLayout } from "lib/types";
|
import { NextPageWithLayout } from "lib/types";
|
||||||
|
|
||||||
const ProjectViewIssuesPage: NextPageWithLayout = () => {
|
const ProjectViewIssuesPage: NextPageWithLayout = observer(() => {
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, viewId } = router.query;
|
const { workspaceSlug, projectId, viewId } = router.query;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { fetchViewDetails } = useProjectView();
|
const { fetchViewDetails, getViewById } = useProjectView();
|
||||||
|
const { getProjectById } = useProject();
|
||||||
|
// derived values
|
||||||
|
const projectView = viewId ? getViewById(viewId.toString()) : undefined;
|
||||||
|
const project = projectId ? getProjectById(projectId.toString()) : undefined;
|
||||||
|
const pageTitle = project?.name && projectView?.name ? `${project?.name} - ${projectView?.name}` : undefined;
|
||||||
|
|
||||||
const { error } = useSWR(
|
const { error } = useSWR(
|
||||||
workspaceSlug && projectId && viewId ? `VIEW_DETAILS_${viewId.toString()}` : null,
|
workspaceSlug && projectId && viewId ? `VIEW_DETAILS_${viewId.toString()}` : null,
|
||||||
@ -42,11 +49,14 @@ const ProjectViewIssuesPage: NextPageWithLayout = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ProjectViewLayoutRoot />
|
<>
|
||||||
|
<PageHead title={pageTitle} />
|
||||||
|
<ProjectViewLayoutRoot />
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
ProjectViewIssuesPage.getLayout = function getLayout(page: ReactElement) {
|
ProjectViewIssuesPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
|
@ -1,13 +1,34 @@
|
|||||||
import { ReactElement } from "react";
|
import { ReactElement } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
// components
|
// components
|
||||||
import { ProjectViewsHeader } from "components/headers";
|
import { ProjectViewsHeader } from "components/headers";
|
||||||
import { ProjectViewsList } from "components/views";
|
import { ProjectViewsList } from "components/views";
|
||||||
|
import { PageHead } from "components/core";
|
||||||
|
// hooks
|
||||||
|
import { useProject } from "hooks/store";
|
||||||
// layouts
|
// layouts
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
// types
|
// types
|
||||||
import { NextPageWithLayout } from "lib/types";
|
import { NextPageWithLayout } from "lib/types";
|
||||||
|
|
||||||
const ProjectViewsPage: NextPageWithLayout = () => <ProjectViewsList />;
|
const ProjectViewsPage: NextPageWithLayout = observer(() => {
|
||||||
|
// router
|
||||||
|
const router = useRouter();
|
||||||
|
const { projectId } = router.query;
|
||||||
|
// store
|
||||||
|
const { getProjectById } = useProject();
|
||||||
|
// derived values
|
||||||
|
const project = projectId ? getProjectById(projectId.toString()) : undefined;
|
||||||
|
const pageTitle = project?.name ? `${project?.name} - Views` : undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHead title={pageTitle} />
|
||||||
|
<ProjectViewsList />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
ProjectViewsPage.getLayout = function getLayout(page: ReactElement) {
|
ProjectViewsPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
|
@ -1,13 +1,28 @@
|
|||||||
import { ReactElement } from "react";
|
import { ReactElement } from "react";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
// components
|
// components
|
||||||
|
import { PageHead } from "components/core";
|
||||||
import { ProjectCardList } from "components/project";
|
import { ProjectCardList } from "components/project";
|
||||||
import { ProjectsHeader } from "components/headers";
|
import { ProjectsHeader } from "components/headers";
|
||||||
// layouts
|
// layouts
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
// type
|
// type
|
||||||
import { NextPageWithLayout } from "lib/types";
|
import { NextPageWithLayout } from "lib/types";
|
||||||
|
import { useWorkspace } from "hooks/store";
|
||||||
|
|
||||||
const ProjectsPage: NextPageWithLayout = () => <ProjectCardList />;
|
const ProjectsPage: NextPageWithLayout = observer(() => {
|
||||||
|
// store
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
// derived values
|
||||||
|
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Projects` : undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHead title={pageTitle} />
|
||||||
|
<ProjectCardList />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
ProjectsPage.getLayout = function getLayout(page: ReactElement) {
|
ProjectsPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return <AppLayout header={<ProjectsHeader />}>{page}</AppLayout>;
|
return <AppLayout header={<ProjectsHeader />}>{page}</AppLayout>;
|
||||||
|
@ -4,7 +4,7 @@ import { observer } from "mobx-react-lite";
|
|||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
// store hooks
|
// store hooks
|
||||||
import { useUser } from "hooks/store";
|
import { useUser, useWorkspace } from "hooks/store";
|
||||||
// layouts
|
// layouts
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
||||||
@ -23,6 +23,7 @@ import { NextPageWithLayout } from "lib/types";
|
|||||||
import { API_TOKENS_LIST } from "constants/fetch-keys";
|
import { API_TOKENS_LIST } from "constants/fetch-keys";
|
||||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||||
|
import { PageHead } from "components/core";
|
||||||
|
|
||||||
const apiTokenService = new APITokenService();
|
const apiTokenService = new APITokenService();
|
||||||
|
|
||||||
@ -39,6 +40,7 @@ const ApiTokensPage: NextPageWithLayout = observer(() => {
|
|||||||
membership: { currentWorkspaceRole },
|
membership: { currentWorkspaceRole },
|
||||||
currentUser,
|
currentUser,
|
||||||
} = useUser();
|
} = useUser();
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN;
|
const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN;
|
||||||
|
|
||||||
@ -49,12 +51,16 @@ const ApiTokensPage: NextPageWithLayout = observer(() => {
|
|||||||
const emptyStateDetail = WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS["api-tokens"];
|
const emptyStateDetail = WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS["api-tokens"];
|
||||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||||
const emptyStateImage = getEmptyStateImagePath("workspace-settings", "api-tokens", isLightMode);
|
const emptyStateImage = getEmptyStateImagePath("workspace-settings", "api-tokens", isLightMode);
|
||||||
|
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - API Tokens` : undefined;
|
||||||
|
|
||||||
if (!isAdmin)
|
if (!isAdmin)
|
||||||
return (
|
return (
|
||||||
<div className="mt-10 flex h-full w-full justify-center p-4">
|
<>
|
||||||
<p className="text-sm text-custom-text-300">You are not authorized to access this page.</p>
|
<PageHead title={pageTitle} />
|
||||||
</div>
|
<div className="mt-10 flex h-full w-full justify-center p-4">
|
||||||
|
<p className="text-sm text-custom-text-300">You are not authorized to access this page.</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!tokens) {
|
if (!tokens) {
|
||||||
@ -63,6 +69,7 @@ const ApiTokensPage: NextPageWithLayout = observer(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<PageHead title={pageTitle} />
|
||||||
<CreateApiTokenModal isOpen={isCreateTokenModalOpen} onClose={() => setIsCreateTokenModalOpen(false)} />
|
<CreateApiTokenModal isOpen={isCreateTokenModalOpen} onClose={() => setIsCreateTokenModalOpen(false)} />
|
||||||
<section className="h-full w-full overflow-y-auto py-8 pr-9">
|
<section className="h-full w-full overflow-y-auto py-8 pr-9">
|
||||||
{tokens.length > 0 ? (
|
{tokens.length > 0 ? (
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// hooks
|
// hooks
|
||||||
import { useUser } from "hooks/store";
|
import { useUser, useWorkspace } from "hooks/store";
|
||||||
// layouts
|
// layouts
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
||||||
// component
|
// component
|
||||||
import { WorkspaceSettingHeader } from "components/headers";
|
import { WorkspaceSettingHeader } from "components/headers";
|
||||||
|
import { PageHead } from "components/core";
|
||||||
// ui
|
// ui
|
||||||
import { Button } from "@plane/ui";
|
import { Button } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
@ -18,33 +19,41 @@ const BillingSettingsPage: NextPageWithLayout = observer(() => {
|
|||||||
const {
|
const {
|
||||||
membership: { currentWorkspaceRole },
|
membership: { currentWorkspaceRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
// derived values
|
||||||
const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN;
|
const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN;
|
||||||
|
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Billing & Plans` : undefined;
|
||||||
|
|
||||||
if (!isAdmin)
|
if (!isAdmin)
|
||||||
return (
|
return (
|
||||||
<div className="mt-10 flex h-full w-full justify-center p-4">
|
<>
|
||||||
<p className="text-sm text-custom-text-300">You are not authorized to access this page.</p>
|
<PageHead title={pageTitle} />
|
||||||
</div>
|
<div className="mt-10 flex h-full w-full justify-center p-4">
|
||||||
|
<p className="text-sm text-custom-text-300">You are not authorized to access this page.</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="w-full overflow-y-auto py-8 pr-9">
|
<>
|
||||||
<div>
|
<PageHead title={pageTitle} />
|
||||||
<div className="flex items-center border-b border-custom-border-100 py-3.5">
|
<section className="w-full overflow-y-auto py-8 pr-9">
|
||||||
<h3 className="text-xl font-medium">Billing & Plans</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="px-4 py-6">
|
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-md mb-1 leading-6">Current plan</h4>
|
<div className="flex items-center border-b border-custom-border-100 py-3.5">
|
||||||
<p className="mb-3 text-sm text-custom-text-200">You are currently using the free plan</p>
|
<h3 className="text-xl font-medium">Billing & Plans</h3>
|
||||||
<a href="https://plane.so/pricing" target="_blank" rel="noreferrer">
|
</div>
|
||||||
<Button variant="neutral-primary">View Plans</Button>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="px-4 py-6">
|
||||||
</section>
|
<div>
|
||||||
|
<h4 className="text-md mb-1 leading-6">Current plan</h4>
|
||||||
|
<p className="mb-3 text-sm text-custom-text-200">You are currently using the free plan</p>
|
||||||
|
<a href="https://plane.so/pricing" target="_blank" rel="noreferrer">
|
||||||
|
<Button variant="neutral-primary">View Plans</Button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// hooks
|
// hooks
|
||||||
import { useUser } from "hooks/store";
|
import { useUser, useWorkspace } from "hooks/store";
|
||||||
// layout
|
// layout
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
||||||
// components
|
// components
|
||||||
import { WorkspaceSettingHeader } from "components/headers";
|
import { WorkspaceSettingHeader } from "components/headers";
|
||||||
import ExportGuide from "components/exporter/guide";
|
import ExportGuide from "components/exporter/guide";
|
||||||
|
import { PageHead } from "components/core";
|
||||||
// types
|
// types
|
||||||
import { NextPageWithLayout } from "lib/types";
|
import { NextPageWithLayout } from "lib/types";
|
||||||
// constants
|
// constants
|
||||||
@ -17,24 +18,33 @@ const ExportsPage: NextPageWithLayout = observer(() => {
|
|||||||
const {
|
const {
|
||||||
membership: { currentWorkspaceRole },
|
membership: { currentWorkspaceRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
|
// derived values
|
||||||
const hasPageAccess =
|
const hasPageAccess =
|
||||||
currentWorkspaceRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentWorkspaceRole);
|
currentWorkspaceRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentWorkspaceRole);
|
||||||
|
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Exports` : undefined;
|
||||||
|
|
||||||
if (!hasPageAccess)
|
if (!hasPageAccess)
|
||||||
return (
|
return (
|
||||||
<div className="mt-10 flex h-full w-full justify-center p-4">
|
<>
|
||||||
<p className="text-sm text-custom-text-300">You are not authorized to access this page.</p>
|
<PageHead title={pageTitle} />
|
||||||
</div>
|
<div className="mt-10 flex h-full w-full justify-center p-4">
|
||||||
|
<p className="text-sm text-custom-text-300">You are not authorized to access this page.</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full overflow-y-auto py-8 pr-9">
|
<>
|
||||||
<div className="flex items-center border-b border-custom-border-100 py-3.5">
|
<PageHead title={pageTitle} />
|
||||||
<h3 className="text-xl font-medium">Exports</h3>
|
<div className="w-full overflow-y-auto py-8 pr-9">
|
||||||
|
<div className="flex items-center border-b border-custom-border-100 py-3.5">
|
||||||
|
<h3 className="text-xl font-medium">Exports</h3>
|
||||||
|
</div>
|
||||||
|
<ExportGuide />
|
||||||
</div>
|
</div>
|
||||||
<ExportGuide />
|
</>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// hooks
|
// hooks
|
||||||
import { useUser } from "hooks/store";
|
import { useUser, useWorkspace } from "hooks/store";
|
||||||
// layouts
|
// layouts
|
||||||
import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
// components
|
// components
|
||||||
import IntegrationGuide from "components/integration/guide";
|
import IntegrationGuide from "components/integration/guide";
|
||||||
import { WorkspaceSettingHeader } from "components/headers";
|
import { WorkspaceSettingHeader } from "components/headers";
|
||||||
|
import { PageHead } from "components/core";
|
||||||
// types
|
// types
|
||||||
import { NextPageWithLayout } from "lib/types";
|
import { NextPageWithLayout } from "lib/types";
|
||||||
// constants
|
// constants
|
||||||
@ -17,23 +18,32 @@ const ImportsPage: NextPageWithLayout = observer(() => {
|
|||||||
const {
|
const {
|
||||||
membership: { currentWorkspaceRole },
|
membership: { currentWorkspaceRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
|
// derived values
|
||||||
const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN;
|
const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN;
|
||||||
|
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Imports` : undefined;
|
||||||
|
|
||||||
if (!isAdmin)
|
if (!isAdmin)
|
||||||
return (
|
return (
|
||||||
<div className="mt-10 flex h-full w-full justify-center p-4">
|
<>
|
||||||
<p className="text-sm text-custom-text-300">You are not authorized to access this page.</p>
|
<PageHead title={pageTitle} />
|
||||||
</div>
|
<div className="mt-10 flex h-full w-full justify-center p-4">
|
||||||
|
<p className="text-sm text-custom-text-300">You are not authorized to access this page.</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="w-full overflow-y-auto py-8 pr-9">
|
<>
|
||||||
<div className="flex items-center border-b border-custom-border-100 py-3.5">
|
<PageHead title={pageTitle} />
|
||||||
<h3 className="text-xl font-medium">Imports</h3>
|
<section className="w-full overflow-y-auto py-8 pr-9">
|
||||||
</div>
|
<div className="flex items-center border-b border-custom-border-100 py-3.5">
|
||||||
<IntegrationGuide />
|
<h3 className="text-xl font-medium">Imports</h3>
|
||||||
</section>
|
</div>
|
||||||
|
<IntegrationGuide />
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,14 +1,30 @@
|
|||||||
import { ReactElement } from "react";
|
import { ReactElement } from "react";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
// layouts
|
// layouts
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
||||||
|
// hooks
|
||||||
|
import { useWorkspace } from "hooks/store";
|
||||||
// components
|
// components
|
||||||
import { WorkspaceSettingHeader } from "components/headers";
|
import { WorkspaceSettingHeader } from "components/headers";
|
||||||
import { WorkspaceDetails } from "components/workspace";
|
import { WorkspaceDetails } from "components/workspace";
|
||||||
|
import { PageHead } from "components/core";
|
||||||
// types
|
// types
|
||||||
import { NextPageWithLayout } from "lib/types";
|
import { NextPageWithLayout } from "lib/types";
|
||||||
|
|
||||||
const WorkspaceSettingsPage: NextPageWithLayout = () => <WorkspaceDetails />;
|
const WorkspaceSettingsPage: NextPageWithLayout = observer(() => {
|
||||||
|
// store hooks
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
// derived values
|
||||||
|
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - General Settings` : undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHead title={pageTitle} />
|
||||||
|
<WorkspaceDetails />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
WorkspaceSettingsPage.getLayout = function getLayout(page: ReactElement) {
|
WorkspaceSettingsPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
|
@ -3,7 +3,7 @@ import { useRouter } from "next/router";
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
// hooks
|
// hooks
|
||||||
import { useUser } from "hooks/store";
|
import { useUser, useWorkspace } from "hooks/store";
|
||||||
// services
|
// services
|
||||||
import { IntegrationService } from "services/integrations";
|
import { IntegrationService } from "services/integrations";
|
||||||
// layouts
|
// layouts
|
||||||
@ -12,6 +12,7 @@ import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
|||||||
// components
|
// components
|
||||||
import { SingleIntegrationCard } from "components/integration";
|
import { SingleIntegrationCard } from "components/integration";
|
||||||
import { WorkspaceSettingHeader } from "components/headers";
|
import { WorkspaceSettingHeader } from "components/headers";
|
||||||
|
import { PageHead } from "components/core";
|
||||||
// ui
|
// ui
|
||||||
import { IntegrationAndImportExportBanner, IntegrationsSettingsLoader } from "components/ui";
|
import { IntegrationAndImportExportBanner, IntegrationsSettingsLoader } from "components/ui";
|
||||||
// types
|
// types
|
||||||
@ -31,14 +32,20 @@ const WorkspaceIntegrationsPage: NextPageWithLayout = observer(() => {
|
|||||||
const {
|
const {
|
||||||
membership: { currentWorkspaceRole },
|
membership: { currentWorkspaceRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
|
// derived values
|
||||||
const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN;
|
const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN;
|
||||||
|
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Integrations` : undefined;
|
||||||
|
|
||||||
if (!isAdmin)
|
if (!isAdmin)
|
||||||
return (
|
return (
|
||||||
<div className="mt-10 flex h-full w-full justify-center p-4">
|
<>
|
||||||
<p className="text-sm text-custom-text-300">You are not authorized to access this page.</p>
|
<PageHead title={pageTitle} />
|
||||||
</div>
|
<div className="mt-10 flex h-full w-full justify-center p-4">
|
||||||
|
<p className="text-sm text-custom-text-300">You are not authorized to access this page.</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: appIntegrations } = useSWR(workspaceSlug && isAdmin ? APP_INTEGRATIONS : null, () =>
|
const { data: appIntegrations } = useSWR(workspaceSlug && isAdmin ? APP_INTEGRATIONS : null, () =>
|
||||||
@ -46,16 +53,21 @@ const WorkspaceIntegrationsPage: NextPageWithLayout = observer(() => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="w-full overflow-y-auto py-8 pr-9">
|
<>
|
||||||
<IntegrationAndImportExportBanner bannerName="Integrations" />
|
<PageHead title={pageTitle} />
|
||||||
<div>
|
<section className="w-full overflow-y-auto py-8 pr-9">
|
||||||
{appIntegrations ? (
|
<IntegrationAndImportExportBanner bannerName="Integrations" />
|
||||||
appIntegrations.map((integration) => <SingleIntegrationCard key={integration.id} integration={integration} />)
|
<div>
|
||||||
) : (
|
{appIntegrations ? (
|
||||||
<IntegrationsSettingsLoader />
|
appIntegrations.map((integration) => (
|
||||||
)}
|
<SingleIntegrationCard key={integration.id} integration={integration} />
|
||||||
</div>
|
))
|
||||||
</section>
|
) : (
|
||||||
|
<IntegrationsSettingsLoader />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import { useRouter } from "next/router";
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Search } from "lucide-react";
|
import { Search } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
import { useEventTracker, useMember, useUser } from "hooks/store";
|
import { useEventTracker, useMember, useUser, useWorkspace } from "hooks/store";
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// layouts
|
// layouts
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
@ -11,6 +11,7 @@ import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
|||||||
// components
|
// components
|
||||||
import { WorkspaceSettingHeader } from "components/headers";
|
import { WorkspaceSettingHeader } from "components/headers";
|
||||||
import { SendWorkspaceInvitationModal, WorkspaceMembersList } from "components/workspace";
|
import { SendWorkspaceInvitationModal, WorkspaceMembersList } from "components/workspace";
|
||||||
|
import { PageHead } from "components/core";
|
||||||
// ui
|
// ui
|
||||||
import { Button } from "@plane/ui";
|
import { Button } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
@ -37,6 +38,7 @@ const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => {
|
|||||||
const {
|
const {
|
||||||
workspace: { inviteMembersToWorkspace },
|
workspace: { inviteMembersToWorkspace },
|
||||||
} = useMember();
|
} = useMember();
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
// toast alert
|
// toast alert
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
@ -83,11 +85,14 @@ const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// derived values
|
||||||
const hasAddMemberPermission =
|
const hasAddMemberPermission =
|
||||||
currentWorkspaceRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentWorkspaceRole);
|
currentWorkspaceRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentWorkspaceRole);
|
||||||
|
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Members` : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<PageHead title={pageTitle} />
|
||||||
<SendWorkspaceInvitationModal
|
<SendWorkspaceInvitationModal
|
||||||
isOpen={inviteModal}
|
isOpen={inviteModal}
|
||||||
onClose={() => setInviteModal(false)}
|
onClose={() => setInviteModal(false)}
|
||||||
|
@ -3,7 +3,7 @@ import { useRouter } from "next/router";
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
// hooks
|
// hooks
|
||||||
import { useUser, useWebhook } from "hooks/store";
|
import { useUser, useWebhook, useWorkspace } from "hooks/store";
|
||||||
// layouts
|
// layouts
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
||||||
@ -12,6 +12,7 @@ import useToast from "hooks/use-toast";
|
|||||||
// components
|
// components
|
||||||
import { WorkspaceSettingHeader } from "components/headers";
|
import { WorkspaceSettingHeader } from "components/headers";
|
||||||
import { DeleteWebhookModal, WebhookDeleteSection, WebhookForm } from "components/web-hooks";
|
import { DeleteWebhookModal, WebhookDeleteSection, WebhookForm } from "components/web-hooks";
|
||||||
|
import { PageHead } from "components/core";
|
||||||
// ui
|
// ui
|
||||||
import { Spinner } from "@plane/ui";
|
import { Spinner } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
@ -29,6 +30,7 @@ const WebhookDetailsPage: NextPageWithLayout = observer(() => {
|
|||||||
membership: { currentWorkspaceRole },
|
membership: { currentWorkspaceRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
const { currentWebhook, fetchWebhookById, updateWebhook } = useWebhook();
|
const { currentWebhook, fetchWebhookById, updateWebhook } = useWebhook();
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
// toast
|
// toast
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
@ -38,6 +40,7 @@ const WebhookDetailsPage: NextPageWithLayout = observer(() => {
|
|||||||
// }, [clearSecretKey, isCreated]);
|
// }, [clearSecretKey, isCreated]);
|
||||||
|
|
||||||
const isAdmin = currentWorkspaceRole === 20;
|
const isAdmin = currentWorkspaceRole === 20;
|
||||||
|
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Webhook` : undefined;
|
||||||
|
|
||||||
useSWR(
|
useSWR(
|
||||||
workspaceSlug && webhookId && isAdmin ? `WEBHOOK_DETAILS_${workspaceSlug}_${webhookId}` : null,
|
workspaceSlug && webhookId && isAdmin ? `WEBHOOK_DETAILS_${workspaceSlug}_${webhookId}` : null,
|
||||||
@ -76,9 +79,12 @@ const WebhookDetailsPage: NextPageWithLayout = observer(() => {
|
|||||||
|
|
||||||
if (!isAdmin)
|
if (!isAdmin)
|
||||||
return (
|
return (
|
||||||
<div className="mt-10 flex h-full w-full justify-center p-4">
|
<>
|
||||||
<p className="text-sm text-custom-text-300">You are not authorized to access this page.</p>
|
<PageHead title={pageTitle} />
|
||||||
</div>
|
<div className="mt-10 flex h-full w-full justify-center p-4">
|
||||||
|
<p className="text-sm text-custom-text-300">You are not authorized to access this page.</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!currentWebhook)
|
if (!currentWebhook)
|
||||||
@ -90,6 +96,7 @@ const WebhookDetailsPage: NextPageWithLayout = observer(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<PageHead title={pageTitle} />
|
||||||
<DeleteWebhookModal isOpen={deleteWebhookModal} onClose={() => setDeleteWebhookModal(false)} />
|
<DeleteWebhookModal isOpen={deleteWebhookModal} onClose={() => setDeleteWebhookModal(false)} />
|
||||||
<div className="w-full space-y-8 overflow-y-auto py-8 pr-9">
|
<div className="w-full space-y-8 overflow-y-auto py-8 pr-9">
|
||||||
<WebhookForm onSubmit={async (data) => await handleUpdateWebhook(data)} data={currentWebhook} />
|
<WebhookForm onSubmit={async (data) => await handleUpdateWebhook(data)} data={currentWebhook} />
|
||||||
|
@ -19,6 +19,7 @@ import { WebhookSettingsLoader } from "components/ui";
|
|||||||
import { NextPageWithLayout } from "lib/types";
|
import { NextPageWithLayout } from "lib/types";
|
||||||
// constants
|
// constants
|
||||||
import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||||
|
import { PageHead } from "components/core";
|
||||||
|
|
||||||
const WebhooksListPage: NextPageWithLayout = observer(() => {
|
const WebhooksListPage: NextPageWithLayout = observer(() => {
|
||||||
// states
|
// states
|
||||||
@ -47,6 +48,7 @@ const WebhooksListPage: NextPageWithLayout = observer(() => {
|
|||||||
|
|
||||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||||
const emptyStateImage = getEmptyStateImagePath("workspace-settings", "webhooks", isLightMode);
|
const emptyStateImage = getEmptyStateImagePath("workspace-settings", "webhooks", isLightMode);
|
||||||
|
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Webhooks` : undefined;
|
||||||
|
|
||||||
// clear secret key when modal is closed.
|
// clear secret key when modal is closed.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -55,53 +57,59 @@ const WebhooksListPage: NextPageWithLayout = observer(() => {
|
|||||||
|
|
||||||
if (!isAdmin)
|
if (!isAdmin)
|
||||||
return (
|
return (
|
||||||
<div className="mt-10 flex h-full w-full justify-center p-4">
|
<>
|
||||||
<p className="text-sm text-custom-text-300">You are not authorized to access this page.</p>
|
<PageHead title={pageTitle} />
|
||||||
</div>
|
<div className="mt-10 flex h-full w-full justify-center p-4">
|
||||||
|
<p className="text-sm text-custom-text-300">You are not authorized to access this page.</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!webhooks) return <WebhookSettingsLoader />;
|
if (!webhooks) return <WebhookSettingsLoader />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full overflow-hidden py-8 pr-9">
|
<>
|
||||||
<CreateWebhookModal
|
<PageHead title={pageTitle} />
|
||||||
createWebhook={createWebhook}
|
<div className="h-full w-full overflow-hidden py-8 pr-9">
|
||||||
clearSecretKey={clearSecretKey}
|
<CreateWebhookModal
|
||||||
currentWorkspace={currentWorkspace}
|
createWebhook={createWebhook}
|
||||||
isOpen={showCreateWebhookModal}
|
clearSecretKey={clearSecretKey}
|
||||||
onClose={() => {
|
currentWorkspace={currentWorkspace}
|
||||||
setShowCreateWebhookModal(false);
|
isOpen={showCreateWebhookModal}
|
||||||
}}
|
onClose={() => {
|
||||||
/>
|
setShowCreateWebhookModal(false);
|
||||||
{Object.keys(webhooks).length > 0 ? (
|
}}
|
||||||
<div className="flex h-full w-full flex-col">
|
/>
|
||||||
<div className="flex items-center justify-between gap-4 border-b border-custom-border-200 pb-3.5">
|
{Object.keys(webhooks).length > 0 ? (
|
||||||
<div className="text-xl font-medium">Webhooks</div>
|
<div className="flex h-full w-full flex-col">
|
||||||
<Button variant="primary" size="sm" onClick={() => setShowCreateWebhookModal(true)}>
|
<div className="flex items-center justify-between gap-4 border-b border-custom-border-200 pb-3.5">
|
||||||
Add webhook
|
<div className="text-xl font-medium">Webhooks</div>
|
||||||
</Button>
|
<Button variant="primary" size="sm" onClick={() => setShowCreateWebhookModal(true)}>
|
||||||
|
Add webhook
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<WebhooksList />
|
||||||
</div>
|
</div>
|
||||||
<WebhooksList />
|
) : (
|
||||||
</div>
|
<div className="flex h-full w-full flex-col">
|
||||||
) : (
|
<div className="flex items-center justify-between gap-4 border-b border-custom-border-200 pb-3.5">
|
||||||
<div className="flex h-full w-full flex-col">
|
<div className="text-xl font-medium">Webhooks</div>
|
||||||
<div className="flex items-center justify-between gap-4 border-b border-custom-border-200 pb-3.5">
|
<Button variant="primary" size="sm" onClick={() => setShowCreateWebhookModal(true)}>
|
||||||
<div className="text-xl font-medium">Webhooks</div>
|
Add webhook
|
||||||
<Button variant="primary" size="sm" onClick={() => setShowCreateWebhookModal(true)}>
|
</Button>
|
||||||
Add webhook
|
</div>
|
||||||
</Button>
|
<div className="h-full w-full flex items-center justify-center">
|
||||||
|
<EmptyState
|
||||||
|
title={emptyStateDetail.title}
|
||||||
|
description={emptyStateDetail.description}
|
||||||
|
image={emptyStateImage}
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-full w-full flex items-center justify-center">
|
)}
|
||||||
<EmptyState
|
</div>
|
||||||
title={emptyStateDetail.title}
|
</>
|
||||||
description={emptyStateDetail.description}
|
|
||||||
image={emptyStateImage}
|
|
||||||
size="lg"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -7,15 +7,26 @@ import { AllIssueLayoutRoot } from "components/issues";
|
|||||||
import { GlobalIssuesHeader } from "components/headers";
|
import { GlobalIssuesHeader } from "components/headers";
|
||||||
// types
|
// types
|
||||||
import { NextPageWithLayout } from "lib/types";
|
import { NextPageWithLayout } from "lib/types";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { useWorkspace } from "hooks/store";
|
||||||
|
import { PageHead } from "components/core";
|
||||||
|
|
||||||
const GlobalViewIssuesPage: NextPageWithLayout = () => (
|
const GlobalViewIssuesPage: NextPageWithLayout = observer(() => {
|
||||||
<div className="h-full overflow-hidden bg-custom-background-100">
|
const { currentWorkspace } = useWorkspace();
|
||||||
<div className="flex h-full w-full flex-col border-b border-custom-border-300">
|
// derived values
|
||||||
<GlobalViewsHeader />
|
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Views` : undefined;
|
||||||
<AllIssueLayoutRoot />
|
return (
|
||||||
</div>
|
<>
|
||||||
</div>
|
<PageHead title={pageTitle} />
|
||||||
);
|
<div className="h-full overflow-hidden bg-custom-background-100">
|
||||||
|
<div className="flex h-full w-full flex-col border-b border-custom-border-300">
|
||||||
|
<GlobalViewsHeader />
|
||||||
|
<AllIssueLayoutRoot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
GlobalViewIssuesPage.getLayout = function getLayout(page: ReactElement) {
|
GlobalViewIssuesPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return <AppLayout header={<GlobalIssuesHeader activeLayout="spreadsheet" />}>{page}</AppLayout>;
|
return <AppLayout header={<GlobalIssuesHeader activeLayout="spreadsheet" />}>{page}</AppLayout>;
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import React, { useState, ReactElement } from "react";
|
import React, { useState, ReactElement } from "react";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
// layouts
|
// layouts
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
// components
|
// components
|
||||||
|
import { PageHead } from "components/core";
|
||||||
import { GlobalDefaultViewListItem, GlobalViewsList } from "components/workspace";
|
import { GlobalDefaultViewListItem, GlobalViewsList } from "components/workspace";
|
||||||
import { GlobalIssuesHeader } from "components/headers";
|
import { GlobalIssuesHeader } from "components/headers";
|
||||||
// ui
|
// ui
|
||||||
@ -12,31 +14,40 @@ import { Search } from "lucide-react";
|
|||||||
import { NextPageWithLayout } from "lib/types";
|
import { NextPageWithLayout } from "lib/types";
|
||||||
// constants
|
// constants
|
||||||
import { DEFAULT_GLOBAL_VIEWS_LIST } from "constants/workspace";
|
import { DEFAULT_GLOBAL_VIEWS_LIST } from "constants/workspace";
|
||||||
|
// hooks
|
||||||
|
import { useWorkspace } from "hooks/store";
|
||||||
|
|
||||||
const WorkspaceViewsPage: NextPageWithLayout = () => {
|
const WorkspaceViewsPage: NextPageWithLayout = observer(() => {
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
|
// store
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
// derived values
|
||||||
|
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - All Views` : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col">
|
<>
|
||||||
<div className="flex h-full w-full flex-col overflow-hidden">
|
<PageHead title={pageTitle} />
|
||||||
<div className="flex w-full items-center gap-2.5 border-b border-custom-border-200 px-5 py-3">
|
<div className="flex flex-col">
|
||||||
<Search className="text-custom-text-200" size={14} strokeWidth={2} />
|
<div className="flex h-full w-full flex-col overflow-hidden">
|
||||||
<Input
|
<div className="flex w-full items-center gap-2.5 border-b border-custom-border-200 px-5 py-3">
|
||||||
className="w-full bg-transparent !p-0 text-xs leading-5 text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
<Search className="text-custom-text-200" size={14} strokeWidth={2} />
|
||||||
value={query}
|
<Input
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
className="w-full bg-transparent !p-0 text-xs leading-5 text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||||
placeholder="Search"
|
value={query}
|
||||||
mode="true-transparent"
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
/>
|
placeholder="Search"
|
||||||
|
mode="true-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{DEFAULT_GLOBAL_VIEWS_LIST.filter((v) => v.label.toLowerCase().includes(query.toLowerCase())).map((option) => (
|
||||||
|
<GlobalDefaultViewListItem key={option.key} view={option} />
|
||||||
|
))}
|
||||||
|
<GlobalViewsList searchQuery={query} />
|
||||||
</div>
|
</div>
|
||||||
{DEFAULT_GLOBAL_VIEWS_LIST.filter((v) => v.label.toLowerCase().includes(query.toLowerCase())).map((option) => (
|
</>
|
||||||
<GlobalDefaultViewListItem key={option.key} view={option} />
|
|
||||||
))}
|
|
||||||
<GlobalViewsList searchQuery={query} />
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
WorkspaceViewsPage.getLayout = function getLayout(page: ReactElement) {
|
WorkspaceViewsPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return <AppLayout header={<GlobalIssuesHeader activeLayout="list" />}>{page}</AppLayout>;
|
return <AppLayout header={<GlobalIssuesHeader activeLayout="list" />}>{page}</AppLayout>;
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user