Merge branch 'develop' of github.com:makeplane/plane into fix/playwright-automation-tests

This commit is contained in:
sriram veeraghanta 2024-02-20 17:48:04 +05:30
commit d7c37560db
117 changed files with 2593 additions and 1651 deletions

View File

@ -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("*"))

View File

@ -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(

View File

@ -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 (

View File

@ -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";

View 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>
);
};

View File

@ -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)}
/> />

View File

@ -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 && (

View File

@ -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)}

View File

@ -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)}

View File

@ -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)}

View File

@ -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)}

View File

@ -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)}

View File

@ -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)}

View File

@ -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)}

View File

@ -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)}

View File

@ -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

View File

@ -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>
); );
}); });

View 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}
/>
);
};

View File

@ -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 && (

View File

@ -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}

View File

@ -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 && (

View File

@ -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

View File

@ -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 */}

View File

@ -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"
}`} }`}
> >

View File

@ -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>
); );

View File

@ -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(

View File

@ -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}

View File

@ -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}
/> />
); );
}); });

View File

@ -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 && (

View File

@ -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>;

View File

@ -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">

View File

@ -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">

View File

@ -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">

View File

@ -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">

View File

@ -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;

View File

@ -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(

View File

@ -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(

View File

@ -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

View File

@ -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}
> >

View 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>
);
});

View File

@ -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";

View File

@ -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}
/> />

View File

@ -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;

View File

@ -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

View File

@ -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"

View 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>
);
});

View File

@ -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> </>
); );
}); });

View File

@ -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);
} }

View File

@ -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}>

View File

@ -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>

View File

@ -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";

View File

@ -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>
);

View File

@ -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>
);

View File

@ -0,0 +1,2 @@
export * from "./inbox-layout-loader";
export * from "./inbox-sidebar-loader";

View File

@ -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"}`}

View File

@ -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">

View File

@ -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>
); );
}); });

View File

@ -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">

View File

@ -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">

View File

@ -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>;

View File

@ -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}>

View File

@ -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>;

View File

@ -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 (

View File

@ -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 (

View File

@ -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>
); );

View File

@ -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 (

View File

@ -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 (

View File

@ -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 (

View File

@ -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 (

View File

@ -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>
</>
); );
}); });

View File

@ -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 (

View File

@ -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>
); );
}); });

View File

@ -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>
); );
}); });

View File

@ -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">

View File

@ -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 (

View File

@ -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 (

View File

@ -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 (

View File

@ -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 />

View File

@ -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 && (

View File

@ -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>
</>
); );
}); });

View File

@ -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>
</>
); );
}); });

View File

@ -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 (

View File

@ -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}
/> />
) : ( ) : (

View File

@ -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 (

View File

@ -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 (

View File

@ -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 (

View File

@ -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 (

View File

@ -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 (

View File

@ -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>;

View File

@ -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 ? (

View File

@ -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>
</>
); );
}); });

View File

@ -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>
); );
}); });

View File

@ -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>
</>
); );
}); });

View File

@ -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 (

View File

@ -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>
</>
); );
}); });

View File

@ -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)}

View File

@ -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} />

View File

@ -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>
); );
}); });

View File

@ -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>;

View File

@ -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