Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 41e812a811 | |||
| a94c607031 | |||
| 665a07f15a | |||
| 85a8af5125 | |||
| 7628419a26 | |||
| 2cd16c61ba | |||
| 8f7b05b73f | |||
| 98d545cfb9 | |||
| 2548a9f062 | |||
| d901277a20 | |||
| 489555f788 | |||
| b827a1af27 | |||
| 1d2e331cec | |||
| e51e4761b9 | |||
| 9299478539 | |||
| fb4cffdd1c | |||
| 83bf28bb83 | |||
| 25a2816a76 | |||
| 571b89632c | |||
| 5b5698ad97 | |||
| b1989bae1b | |||
| 83139989c2 | |||
| 1bf06821bb | |||
| eea3b4fa54 | |||
| f64284f6a0 | |||
| 06496ff0f0 | |||
| 247937d93a | |||
| e0a18578f5 | |||
| e93bbec4cd |
@@ -28,7 +28,7 @@ jobs:
|
||||
- id: set_env_variables
|
||||
name: Set Environment Variables
|
||||
run: |
|
||||
if [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
|
||||
if [ "${{ env.TARGET_BRANCH }}" == "master" ] || [ "${{ github.event_name }}" == "release" ]; then
|
||||
echo "BUILDX_DRIVER=cloud" >> $GITHUB_OUTPUT
|
||||
echo "BUILDX_VERSION=lab:latest" >> $GITHUB_OUTPUT
|
||||
echo "BUILDX_PLATFORMS=linux/amd64,linux/arm64" >> $GITHUB_OUTPUT
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
steps:
|
||||
- name: Set Frontend Docker Tag
|
||||
run: |
|
||||
if [ "${{ env.TARGET_BRANCH }}" == "master" ] && [ "${{ github.event_name }}" == "release" ]; then
|
||||
if [ "${{ github.event_name }}" == "release" ]; then
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend:${{ github.event.release.tag_name }}
|
||||
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend:stable
|
||||
@@ -105,7 +105,7 @@ jobs:
|
||||
steps:
|
||||
- name: Set Space Docker Tag
|
||||
run: |
|
||||
if [ "${{ env.TARGET_BRANCH }}" == "master" ] && [ "${{ github.event_name }}" == "release" ]; then
|
||||
if [ "${{ github.event_name }}" == "release" ]; then
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-space:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-space:${{ github.event.release.tag_name }}
|
||||
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-space:stable
|
||||
@@ -156,7 +156,7 @@ jobs:
|
||||
steps:
|
||||
- name: Set Backend Docker Tag
|
||||
run: |
|
||||
if [ "${{ env.TARGET_BRANCH }}" == "master" ] && [ "${{ github.event_name }}" == "release" ]; then
|
||||
if [ "${{ github.event_name }}" == "release" ]; then
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-backend:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-backend:${{ github.event.release.tag_name }}
|
||||
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-backend:stable
|
||||
@@ -208,7 +208,7 @@ jobs:
|
||||
steps:
|
||||
- name: Set Proxy Docker Tag
|
||||
run: |
|
||||
if [ "${{ env.TARGET_BRANCH }}" == "master" ] && [ "${{ github.event_name }}" == "release" ]; then
|
||||
if [ "${{ github.event_name }}" == "release" ]; then
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy:${{ github.event.release.tag_name }}
|
||||
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy:stable
|
||||
|
||||
@@ -27,7 +27,7 @@ from plane.app.serializers import (
|
||||
InboxSerializer,
|
||||
InboxIssueSerializer,
|
||||
IssueCreateSerializer,
|
||||
IssueStateInboxSerializer,
|
||||
IssueDetailSerializer,
|
||||
)
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
from plane.bgtasks.issue_activites_task import issue_activity
|
||||
@@ -333,7 +333,7 @@ class InboxIssueViewSet(BaseViewSet):
|
||||
|
||||
def retrieve(self, request, slug, project_id, inbox_id, issue_id):
|
||||
issue = self.get_queryset().filter(pk=issue_id).first()
|
||||
serializer = IssueSerializer(issue, expand=self.expand,)
|
||||
serializer = IssueDetailSerializer(issue, expand=self.expand,)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
def destroy(self, request, slug, project_id, inbox_id, issue_id):
|
||||
|
||||
@@ -1209,13 +1209,13 @@ class IssueArchiveViewSet(BaseViewSet):
|
||||
return Response(issues, status=status.HTTP_200_OK)
|
||||
|
||||
def retrieve(self, request, slug, project_id, pk=None):
|
||||
issue = Issue.objects.get(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
archived_at__isnull=False,
|
||||
pk=pk,
|
||||
issue = self.get_queryset().filter(pk=pk).first()
|
||||
return Response(
|
||||
IssueDetailSerializer(
|
||||
issue, fields=self.fields, expand=self.expand
|
||||
).data,
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK)
|
||||
|
||||
def unarchive(self, request, slug, project_id, pk=None):
|
||||
issue = Issue.objects.get(
|
||||
|
||||
@@ -247,12 +247,7 @@ class IssueSearchEndpoint(BaseAPIView):
|
||||
if parent == "true" and issue_id:
|
||||
issue = Issue.issue_objects.get(pk=issue_id)
|
||||
issues = issues.filter(
|
||||
~Q(pk=issue_id), ~Q(pk=issue.parent_id), parent__isnull=True
|
||||
).exclude(
|
||||
pk__in=Issue.issue_objects.filter(
|
||||
parent__isnull=False
|
||||
).values_list("parent_id", flat=True)
|
||||
)
|
||||
~Q(pk=issue_id), ~Q(pk=issue.parent_id), ~Q(parent_id=issue_id))
|
||||
if issue_relation == "true" and issue_id:
|
||||
issue = Issue.issue_objects.get(pk=issue_id)
|
||||
issues = issues.filter(
|
||||
|
||||
@@ -137,7 +137,7 @@ services:
|
||||
dockerfile: Dockerfile.dev
|
||||
args:
|
||||
DOCKER_BUILDKIT: 1
|
||||
restart: no
|
||||
restart: "no"
|
||||
networks:
|
||||
- dev_env
|
||||
volumes:
|
||||
|
||||
Vendored
+8
-2
@@ -1,5 +1,11 @@
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
import type { IUser, IUserLite, IWorkspace, IWorkspaceLite, TStateGroups } from ".";
|
||||
import type {
|
||||
IUser,
|
||||
IUserLite,
|
||||
IWorkspace,
|
||||
IWorkspaceLite,
|
||||
TStateGroups,
|
||||
} from ".";
|
||||
|
||||
export interface IProject {
|
||||
archive_in: number;
|
||||
@@ -117,7 +123,7 @@ export type TProjectIssuesSearchParams = {
|
||||
parent?: boolean;
|
||||
issue_relation?: boolean;
|
||||
cycle?: boolean;
|
||||
module?: string[];
|
||||
module?: string;
|
||||
sub_issue?: boolean;
|
||||
issue_id?: string;
|
||||
workspace_search: boolean;
|
||||
|
||||
@@ -78,7 +78,6 @@ export const ExistingIssuesListModal: React.FC<Props> = (props) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen || !workspaceSlug || !projectId) return;
|
||||
if (issues.length <= 0) setIsSearching(true);
|
||||
|
||||
projectService
|
||||
.projectIssuesSearch(workspaceSlug as string, projectId as string, {
|
||||
@@ -88,16 +87,7 @@ export const ExistingIssuesListModal: React.FC<Props> = (props) => {
|
||||
})
|
||||
.then((res) => setIssues(res))
|
||||
.finally(() => setIsSearching(false));
|
||||
}, [issues, debouncedSearchTerm, isOpen, isWorkspaceLevel, projectId, searchParams, workspaceSlug]);
|
||||
|
||||
useEffect(() => {
|
||||
setSearchTerm("");
|
||||
setIssues([]);
|
||||
setSelectedIssues([]);
|
||||
setIsSearching(false);
|
||||
setIsSubmitting(false);
|
||||
setIsWorkspaceLevel(false);
|
||||
}, [isOpen]);
|
||||
}, [debouncedSearchTerm, isOpen, isWorkspaceLevel, projectId, searchParams, workspaceSlug]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -3,12 +3,20 @@ import { Menu } from "lucide-react";
|
||||
import { useApplication } from "hooks/store";
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
export const SidebarHamburgerToggle: FC = observer(() => {
|
||||
const { theme: themStore } = useApplication();
|
||||
type Props = {
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export const SidebarHamburgerToggle: FC<Props> = observer((props) => {
|
||||
const { onClick } = props
|
||||
const { theme: themeStore } = useApplication();
|
||||
return (
|
||||
<div
|
||||
className="w-7 h-7 flex-shrink-0 rounded flex justify-center items-center bg-custom-background-80 transition-all hover:bg-custom-background-90 cursor-pointer group md:hidden"
|
||||
onClick={() => themStore.toggleSidebar()}
|
||||
onClick={() => {
|
||||
if (onClick) onClick()
|
||||
else themeStore.toggleMobileSidebar()
|
||||
}}
|
||||
>
|
||||
<Menu size={14} className="text-custom-text-200 group-hover:text-custom-text-100 transition-all" />
|
||||
</div>
|
||||
|
||||
@@ -34,7 +34,8 @@ import { ICycle, TCycleGroups } from "@plane/types";
|
||||
// constants
|
||||
import { EIssuesStoreType } from "constants/issue";
|
||||
import { CYCLE_ISSUES_WITH_PARAMS } from "constants/fetch-keys";
|
||||
import { CYCLE_EMPTY_STATE_DETAILS, CYCLE_STATE_GROUPS_DETAILS } from "constants/cycle";
|
||||
import { CYCLE_STATE_GROUPS_DETAILS } from "constants/cycle";
|
||||
import { CYCLE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
|
||||
interface IActiveCycleDetails {
|
||||
workspaceSlug: string;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { FC, MouseEvent, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import Link from "next/link";
|
||||
import { observer } from "mobx-react";
|
||||
// hooks
|
||||
import { useEventTracker, useCycle, useUser } from "hooks/store";
|
||||
import useToast from "hooks/use-toast";
|
||||
@@ -26,7 +27,7 @@ export interface ICyclesBoardCard {
|
||||
cycleId: string;
|
||||
}
|
||||
|
||||
export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
|
||||
export const CyclesBoardCard: FC<ICyclesBoardCard> = observer((props) => {
|
||||
const { cycleId, workspaceSlug, projectId } = props;
|
||||
// states
|
||||
const [updateModal, setUpdateModal] = useState(false);
|
||||
@@ -69,8 +70,8 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
|
||||
? cycleTotalIssues === 0
|
||||
? "0 Issue"
|
||||
: cycleTotalIssues === cycleDetails.completed_issues
|
||||
? `${cycleTotalIssues} Issue${cycleTotalIssues > 1 ? "s" : ""}`
|
||||
: `${cycleDetails.completed_issues}/${cycleTotalIssues} Issues`
|
||||
? `${cycleTotalIssues} Issue${cycleTotalIssues > 1 ? "s" : ""}`
|
||||
: `${cycleDetails.completed_issues}/${cycleTotalIssues} Issues`
|
||||
: "0 Issue";
|
||||
|
||||
const handleCopyText = (e: MouseEvent<HTMLButtonElement>) => {
|
||||
@@ -295,4 +296,4 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useUser } from "hooks/store";
|
||||
import { CyclePeekOverview, CyclesBoardCard } from "components/cycles";
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
// constants
|
||||
import { CYCLE_EMPTY_STATE_DETAILS } from "constants/cycle";
|
||||
import { CYCLE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
|
||||
export interface ICyclesBoard {
|
||||
cycleIds: string[];
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { FC, MouseEvent, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react";
|
||||
// hooks
|
||||
import { useEventTracker, useCycle, useUser } from "hooks/store";
|
||||
import useToast from "hooks/use-toast";
|
||||
@@ -30,7 +31,7 @@ type TCyclesListItem = {
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export const CyclesListItem: FC<TCyclesListItem> = (props) => {
|
||||
export const CyclesListItem: FC<TCyclesListItem> = observer((props) => {
|
||||
const { cycleId, workspaceSlug, projectId } = props;
|
||||
// states
|
||||
const [updateModal, setUpdateModal] = useState(false);
|
||||
@@ -289,4 +290,4 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
|
||||
</Link>
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
// ui
|
||||
import { Loader } from "@plane/ui";
|
||||
// constants
|
||||
import { CYCLE_EMPTY_STATE_DETAILS } from "constants/cycle";
|
||||
import { CYCLE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
|
||||
export interface ICyclesList {
|
||||
cycleIds: string[];
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useCycle } from "hooks/store";
|
||||
// components
|
||||
import { CyclesBoard, CyclesList, CyclesListGanttChartView } from "components/cycles";
|
||||
// ui components
|
||||
import { Loader } from "@plane/ui";
|
||||
import { CycleModuleBoardLayout, CycleModuleListLayout, GanttLayoutLoader } from "components/ui";
|
||||
// types
|
||||
import { TCycleLayout, TCycleView } from "@plane/types";
|
||||
|
||||
@@ -25,6 +25,7 @@ export const CyclesView: FC<ICyclesView> = observer((props) => {
|
||||
currentProjectDraftCycleIds,
|
||||
currentProjectUpcomingCycleIds,
|
||||
currentProjectCycleIds,
|
||||
loader,
|
||||
} = useCycle();
|
||||
|
||||
const cyclesList =
|
||||
@@ -36,55 +37,32 @@ export const CyclesView: FC<ICyclesView> = observer((props) => {
|
||||
? currentProjectUpcomingCycleIds
|
||||
: currentProjectCycleIds;
|
||||
|
||||
if (loader || !cyclesList)
|
||||
return (
|
||||
<>
|
||||
{layout === "list" && <CycleModuleListLayout />}
|
||||
{layout === "board" && <CycleModuleBoardLayout />}
|
||||
{layout === "gantt" && <GanttLayoutLoader />}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{layout === "list" && (
|
||||
<>
|
||||
{cyclesList ? (
|
||||
<CyclesList cycleIds={cyclesList} filter={filter} workspaceSlug={workspaceSlug} projectId={projectId} />
|
||||
) : (
|
||||
<Loader className="space-y-4 p-8">
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
</Loader>
|
||||
)}
|
||||
</>
|
||||
<CyclesList cycleIds={cyclesList} filter={filter} workspaceSlug={workspaceSlug} projectId={projectId} />
|
||||
)}
|
||||
|
||||
{layout === "board" && (
|
||||
<>
|
||||
{cyclesList ? (
|
||||
<CyclesBoard
|
||||
cycleIds={cyclesList}
|
||||
filter={filter}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
peekCycle={peekCycle}
|
||||
/>
|
||||
) : (
|
||||
<Loader className="grid grid-cols-1 gap-9 p-8 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Loader.Item height="200px" />
|
||||
<Loader.Item height="200px" />
|
||||
<Loader.Item height="200px" />
|
||||
</Loader>
|
||||
)}
|
||||
</>
|
||||
<CyclesBoard
|
||||
cycleIds={cyclesList}
|
||||
filter={filter}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
peekCycle={peekCycle}
|
||||
/>
|
||||
)}
|
||||
|
||||
{layout === "gantt" && (
|
||||
<>
|
||||
{cyclesList ? (
|
||||
<CyclesListGanttChartView cycleIds={cyclesList} workspaceSlug={workspaceSlug} />
|
||||
) : (
|
||||
<Loader className="space-y-4">
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
</Loader>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{layout === "gantt" && <CyclesListGanttChartView cycleIds={cyclesList} workspaceSlug={workspaceSlug} />}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -61,6 +61,7 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
// refs
|
||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
// popper-js refs
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
@@ -111,23 +112,15 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
||||
const filteredOptions =
|
||||
query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase()));
|
||||
|
||||
// fetch cycles of the project if not already present in the store
|
||||
useEffect(() => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
if (!cycleIds) fetchAllCycles(workspaceSlug, projectId);
|
||||
}, [cycleIds, fetchAllCycles, projectId, workspaceSlug]);
|
||||
|
||||
const selectedCycle = value ? getCycleById(value) : null;
|
||||
|
||||
const onOpen = () => {
|
||||
if (referenceElement) referenceElement.focus();
|
||||
if (workspaceSlug && !cycleIds) fetchAllCycles(workspaceSlug, projectId);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
if (!isOpen) return;
|
||||
setIsOpen(false);
|
||||
if (referenceElement) referenceElement.blur();
|
||||
onClose && onClose();
|
||||
};
|
||||
|
||||
@@ -151,6 +144,12 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
||||
|
||||
useOutsideClickDetector(dropdownRef, handleClose);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as="div"
|
||||
@@ -216,6 +215,8 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
||||
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
|
||||
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
|
||||
<Combobox.Input
|
||||
as="input"
|
||||
ref={inputRef}
|
||||
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Fragment, ReactNode, useRef, useState } from "react";
|
||||
import { Fragment, ReactNode, useEffect, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
import { usePopper } from "react-popper";
|
||||
@@ -60,6 +60,7 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
// refs
|
||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
// popper-js refs
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
@@ -110,13 +111,11 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
||||
|
||||
const onOpen = () => {
|
||||
if (!activeEstimate && workspaceSlug) fetchProjectEstimates(workspaceSlug, projectId);
|
||||
if (referenceElement) referenceElement.focus();
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
if (!isOpen) return;
|
||||
setIsOpen(false);
|
||||
if (referenceElement) referenceElement.blur();
|
||||
onClose && onClose();
|
||||
};
|
||||
|
||||
@@ -140,6 +139,12 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
||||
|
||||
useOutsideClickDetector(dropdownRef, handleClose);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as="div"
|
||||
@@ -205,6 +210,8 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
||||
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
|
||||
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
|
||||
<Combobox.Input
|
||||
as="input"
|
||||
ref={inputRef}
|
||||
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Fragment, useRef, useState } from "react";
|
||||
import { Fragment, useEffect, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
import { usePopper } from "react-popper";
|
||||
@@ -50,6 +50,7 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
// refs
|
||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
// popper-js refs
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
@@ -103,13 +104,11 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
|
||||
|
||||
const onOpen = () => {
|
||||
if (!projectMemberIds && workspaceSlug) fetchProjectMembers(workspaceSlug, projectId);
|
||||
if (referenceElement) referenceElement.focus();
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
if (!isOpen) return;
|
||||
setIsOpen(false);
|
||||
if (referenceElement) referenceElement.blur();
|
||||
onClose && onClose();
|
||||
};
|
||||
|
||||
@@ -133,6 +132,12 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
|
||||
|
||||
useOutsideClickDetector(dropdownRef, handleClose);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as="div"
|
||||
@@ -203,6 +208,8 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
|
||||
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
|
||||
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
|
||||
<Combobox.Input
|
||||
as="input"
|
||||
ref={inputRef}
|
||||
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Fragment, useRef, useState } from "react";
|
||||
import { Fragment, useEffect, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
import { usePopper } from "react-popper";
|
||||
@@ -44,6 +44,7 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
// refs
|
||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
// popper-js refs
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
@@ -91,19 +92,13 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
|
||||
};
|
||||
if (multiple) comboboxProps.multiple = true;
|
||||
|
||||
const onOpen = () => {
|
||||
if (referenceElement) referenceElement.focus();
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
if (!isOpen) return;
|
||||
setIsOpen(false);
|
||||
if (referenceElement) referenceElement.blur();
|
||||
onClose && onClose();
|
||||
};
|
||||
|
||||
const toggleDropdown = () => {
|
||||
if (!isOpen) onOpen();
|
||||
setIsOpen((prevIsOpen) => !prevIsOpen);
|
||||
};
|
||||
|
||||
@@ -122,6 +117,12 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
|
||||
|
||||
useOutsideClickDetector(dropdownRef, handleClose);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as="div"
|
||||
@@ -192,6 +193,8 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
|
||||
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
|
||||
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
|
||||
<Combobox.Input
|
||||
as="input"
|
||||
ref={inputRef}
|
||||
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
|
||||
@@ -166,6 +166,7 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
// refs
|
||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
// popper-js refs
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
@@ -216,21 +217,13 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
||||
const filteredOptions =
|
||||
query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase()));
|
||||
|
||||
// fetch modules of the project if not already present in the store
|
||||
useEffect(() => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
if (!moduleIds) fetchModules(workspaceSlug, projectId);
|
||||
}, [moduleIds, fetchModules, projectId, workspaceSlug]);
|
||||
|
||||
const onOpen = () => {
|
||||
if (referenceElement) referenceElement.focus();
|
||||
if (!moduleIds && workspaceSlug) fetchModules(workspaceSlug, projectId);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
if (!isOpen) return;
|
||||
setIsOpen(false);
|
||||
if (referenceElement) referenceElement.blur();
|
||||
onClose && onClose();
|
||||
};
|
||||
|
||||
@@ -261,6 +254,12 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
||||
};
|
||||
if (multiple) comboboxProps.multiple = true;
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as="div"
|
||||
@@ -331,6 +330,8 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
||||
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
|
||||
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
|
||||
<Combobox.Input
|
||||
as="input"
|
||||
ref={inputRef}
|
||||
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Fragment, ReactNode, useRef, useState } from "react";
|
||||
import { Fragment, ReactNode, useEffect, useRef, useState } from "react";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
import { usePopper } from "react-popper";
|
||||
import { Check, ChevronDown, Search } from "lucide-react";
|
||||
@@ -272,6 +272,7 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
// refs
|
||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
// popper-js refs
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
@@ -305,19 +306,13 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
||||
const filteredOptions =
|
||||
query === "" ? options : options.filter((o) => o.query.toLowerCase().includes(query.toLowerCase()));
|
||||
|
||||
const onOpen = () => {
|
||||
if (referenceElement) referenceElement.focus();
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
if (!isOpen) return;
|
||||
setIsOpen(false);
|
||||
if (referenceElement) referenceElement.blur();
|
||||
onClose && onClose();
|
||||
};
|
||||
|
||||
const toggleDropdown = () => {
|
||||
if (!isOpen) onOpen();
|
||||
setIsOpen((prevIsOpen) => !prevIsOpen);
|
||||
};
|
||||
|
||||
@@ -342,6 +337,12 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
||||
? BackgroundButton
|
||||
: TransparentButton;
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as="div"
|
||||
@@ -409,6 +410,8 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
||||
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
|
||||
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
|
||||
<Combobox.Input
|
||||
as="input"
|
||||
ref={inputRef}
|
||||
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Fragment, ReactNode, useRef, useState } from "react";
|
||||
import { Fragment, ReactNode, useEffect, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
import { usePopper } from "react-popper";
|
||||
@@ -50,6 +50,7 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
// refs
|
||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
// popper-js refs
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
@@ -94,19 +95,13 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
||||
|
||||
const selectedProject = value ? getProjectById(value) : null;
|
||||
|
||||
const onOpen = () => {
|
||||
if (referenceElement) referenceElement.focus();
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
if (!isOpen) return;
|
||||
setIsOpen(false);
|
||||
onClose && onClose();
|
||||
if (referenceElement) referenceElement.blur();
|
||||
};
|
||||
|
||||
const toggleDropdown = () => {
|
||||
if (!isOpen) onOpen();
|
||||
setIsOpen((prevIsOpen) => !prevIsOpen);
|
||||
};
|
||||
|
||||
@@ -125,6 +120,12 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
||||
|
||||
useOutsideClickDetector(dropdownRef, handleClose);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as="div"
|
||||
@@ -198,6 +199,8 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
||||
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
|
||||
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
|
||||
<Combobox.Input
|
||||
as="input"
|
||||
ref={inputRef}
|
||||
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Fragment, ReactNode, useRef, useState } from "react";
|
||||
import { Fragment, ReactNode, useEffect, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
import { usePopper } from "react-popper";
|
||||
@@ -52,6 +52,7 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
// refs
|
||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
// popper-js refs
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
@@ -92,14 +93,12 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
||||
|
||||
const onOpen = () => {
|
||||
if (!statesList && workspaceSlug) fetchProjectStates(workspaceSlug, projectId);
|
||||
if (referenceElement) referenceElement.focus();
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
if (!isOpen) return;
|
||||
setIsOpen(false);
|
||||
onClose && onClose();
|
||||
if (referenceElement) referenceElement.blur();
|
||||
};
|
||||
|
||||
const toggleDropdown = () => {
|
||||
@@ -122,6 +121,12 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
||||
|
||||
useOutsideClickDetector(dropdownRef, handleClose);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as="div"
|
||||
@@ -193,6 +198,8 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
||||
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
|
||||
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
|
||||
<Combobox.Input
|
||||
as="input"
|
||||
ref={inputRef}
|
||||
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import React, { useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Plus } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
// store hooks
|
||||
import { useEstimate, useProject } from "hooks/store";
|
||||
import { useEstimate, useProject, useUser } from "hooks/store";
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { CreateUpdateEstimateModal, DeleteEstimateModal, EstimateListItem } from "components/estimates";
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
// ui
|
||||
import { Button, Loader } from "@plane/ui";
|
||||
import { EmptyState } from "components/common";
|
||||
// images
|
||||
import emptyEstimate from "public/empty-state/estimate.svg";
|
||||
// types
|
||||
import { IEstimate } from "@plane/types";
|
||||
// helpers
|
||||
import { orderArrayBy } from "helpers/array.helper";
|
||||
// constants
|
||||
import { PROJECT_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
|
||||
export const EstimatesList: React.FC = observer(() => {
|
||||
// states
|
||||
@@ -25,9 +25,12 @@ export const EstimatesList: React.FC = observer(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store hooks
|
||||
const { updateProject, currentProjectDetails } = useProject();
|
||||
const { projectEstimates, getProjectEstimateById } = useEstimate();
|
||||
const { currentUser } = useUser();
|
||||
// toast alert
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
@@ -55,6 +58,10 @@ export const EstimatesList: React.FC = observer(() => {
|
||||
});
|
||||
};
|
||||
|
||||
const emptyStateDetail = PROJECT_SETTINGS_EMPTY_STATE_DETAILS["estimate"];
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const emptyStateImage = getEmptyStateImagePath("project-settings", "estimates", isLightMode);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateUpdateEstimateModal
|
||||
@@ -108,19 +115,12 @@ export const EstimatesList: React.FC = observer(() => {
|
||||
))}
|
||||
</section>
|
||||
) : (
|
||||
<div className="w-full py-8">
|
||||
<div className="h-full w-full py-8">
|
||||
<EmptyState
|
||||
title="No estimates yet"
|
||||
description="Estimates help you communicate the complexity of an issue."
|
||||
image={emptyEstimate}
|
||||
primaryButton={{
|
||||
icon: <Plus className="h-4 w-4" />,
|
||||
text: "Add Estimate",
|
||||
onClick: () => {
|
||||
setEstimateFormOpen(true);
|
||||
setEstimateToUpdate(undefined);
|
||||
},
|
||||
}}
|
||||
title={emptyStateDetail.title}
|
||||
description={emptyStateDetail.description}
|
||||
image={emptyStateImage}
|
||||
size="lg"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -3,25 +3,28 @@ import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
import useSWR, { mutate } from "swr";
|
||||
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useUser } from "hooks/store";
|
||||
import useUserAuth from "hooks/use-user-auth";
|
||||
// services
|
||||
import { IntegrationService } from "services/integrations";
|
||||
// components
|
||||
import { Exporter, SingleExport } from "components/exporter";
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
// ui
|
||||
import { Button, Loader } from "@plane/ui";
|
||||
import { Button } from "@plane/ui";
|
||||
import { ImportExportSettingsLoader } from "components/ui";
|
||||
// icons
|
||||
import { MoveLeft, MoveRight, RefreshCw } from "lucide-react";
|
||||
// fetch-keys
|
||||
import { EXPORT_SERVICES_LIST } from "constants/fetch-keys";
|
||||
// constants
|
||||
import { EXPORTERS_LIST } from "constants/workspace";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useUser } from "hooks/store";
|
||||
|
||||
import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
|
||||
// services
|
||||
const integrationService = new IntegrationService();
|
||||
@@ -34,6 +37,8 @@ const IntegrationGuide = observer(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, provider } = router.query;
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store hooks
|
||||
const { currentUser, currentUserLoader } = useUser();
|
||||
// custom hooks
|
||||
@@ -46,6 +51,10 @@ const IntegrationGuide = observer(() => {
|
||||
: null
|
||||
);
|
||||
|
||||
const emptyStateDetail = WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS["export"];
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const emptyStateImage = getEmptyStateImagePath("workspace-settings", "exports", isLightMode);
|
||||
|
||||
const handleCsvClose = () => {
|
||||
router.replace(`/${workspaceSlug?.toString()}/settings/exports`);
|
||||
};
|
||||
@@ -140,15 +149,17 @@ const IntegrationGuide = observer(() => {
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="px-4 py-6 text-sm text-custom-text-200">No previous export available.</p>
|
||||
<div className="h-full w-full flex items-center justify-center">
|
||||
<EmptyState
|
||||
title={emptyStateDetail.title}
|
||||
description={emptyStateDetail.description}
|
||||
image={emptyStateImage}
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<Loader className="mt-6 grid grid-cols-1 gap-3">
|
||||
<Loader.Item height="40px" width="100%" />
|
||||
<Loader.Item height="40px" width="100%" />
|
||||
<Loader.Item height="40px" width="100%" />
|
||||
<Loader.Item height="40px" width="100%" />
|
||||
</Loader>
|
||||
<ImportExportSettingsLoader />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -86,7 +86,7 @@ export const GanttChartBlocksList: FC<GanttChartBlocksProps> = observer((props)
|
||||
className="h-full"
|
||||
style={{
|
||||
width: `${itemsContainerWidth}px`,
|
||||
marginTop: `${HEADER_HEIGHT}px`,
|
||||
transform: `translateY(${HEADER_HEIGHT}px)`,
|
||||
}}
|
||||
>
|
||||
{blocks?.map((block) => {
|
||||
|
||||
@@ -33,6 +33,7 @@ type Props = {
|
||||
sidebarToRender: (props: any) => React.ReactNode;
|
||||
title: string;
|
||||
updateCurrentViewRenderPayload: (direction: "left" | "right", currentView: TGanttViews) => void;
|
||||
quickAdd?: React.JSX.Element | undefined;
|
||||
};
|
||||
|
||||
export const GanttChartMainContent: React.FC<Props> = (props) => {
|
||||
@@ -52,6 +53,7 @@ export const GanttChartMainContent: React.FC<Props> = (props) => {
|
||||
sidebarToRender,
|
||||
title,
|
||||
updateCurrentViewRenderPayload,
|
||||
quickAdd,
|
||||
} = props;
|
||||
// chart hook
|
||||
const { currentView, currentViewData, updateScrollLeft } = useChart();
|
||||
@@ -101,6 +103,7 @@ export const GanttChartMainContent: React.FC<Props> = (props) => {
|
||||
enableReorder={enableReorder}
|
||||
sidebarToRender={sidebarToRender}
|
||||
title={title}
|
||||
quickAdd={quickAdd}
|
||||
/>
|
||||
<div className="relative min-h-full h-max flex-shrink-0 flex-grow">
|
||||
<ActiveChartView />
|
||||
|
||||
@@ -31,6 +31,7 @@ type ChartViewRootProps = {
|
||||
enableAddBlock: boolean;
|
||||
bottomSpacing: boolean;
|
||||
showAllBlocks: boolean;
|
||||
quickAdd?: React.JSX.Element | undefined;
|
||||
};
|
||||
|
||||
export const ChartViewRoot: FC<ChartViewRootProps> = (props) => {
|
||||
@@ -49,6 +50,7 @@ export const ChartViewRoot: FC<ChartViewRootProps> = (props) => {
|
||||
enableAddBlock,
|
||||
bottomSpacing,
|
||||
showAllBlocks,
|
||||
quickAdd,
|
||||
} = props;
|
||||
// states
|
||||
const [itemsContainerWidth, setItemsContainerWidth] = useState(0);
|
||||
@@ -200,6 +202,7 @@ export const ChartViewRoot: FC<ChartViewRootProps> = (props) => {
|
||||
sidebarToRender={sidebarToRender}
|
||||
title={title}
|
||||
updateCurrentViewRenderPayload={updateCurrentViewRenderPayload}
|
||||
quickAdd={quickAdd}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -12,6 +12,7 @@ type GanttChartRootProps = {
|
||||
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
||||
blockToRender: (data: any) => React.ReactNode;
|
||||
sidebarToRender: (props: any) => React.ReactNode;
|
||||
quickAdd?: React.JSX.Element | undefined;
|
||||
enableBlockLeftResize?: boolean;
|
||||
enableBlockRightResize?: boolean;
|
||||
enableBlockMove?: boolean;
|
||||
@@ -37,6 +38,7 @@ export const GanttChartRoot: FC<GanttChartRootProps> = (props) => {
|
||||
enableAddBlock = false,
|
||||
bottomSpacing = false,
|
||||
showAllBlocks = false,
|
||||
quickAdd,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
@@ -56,6 +58,7 @@ export const GanttChartRoot: FC<GanttChartRootProps> = (props) => {
|
||||
enableAddBlock={enableAddBlock}
|
||||
bottomSpacing={bottomSpacing}
|
||||
showAllBlocks={showAllBlocks}
|
||||
quickAdd={quickAdd}
|
||||
/>
|
||||
</ChartContextProvider>
|
||||
);
|
||||
|
||||
@@ -7,42 +7,23 @@ import { useIssueDetail } from "hooks/store";
|
||||
// ui
|
||||
import { Loader } from "@plane/ui";
|
||||
// components
|
||||
import { GanttQuickAddIssueForm, IssueGanttSidebarBlock } from "components/issues";
|
||||
import { IssueGanttSidebarBlock } from "components/issues";
|
||||
// helpers
|
||||
import { findTotalDaysInRange } from "helpers/date-time.helper";
|
||||
import { cn } from "helpers/common.helper";
|
||||
// types
|
||||
import { IGanttBlock, IBlockUpdateData } from "components/gantt-chart/types";
|
||||
import { TIssue } from "@plane/types";
|
||||
import { BLOCK_HEIGHT } from "../constants";
|
||||
|
||||
type Props = {
|
||||
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
||||
blocks: IGanttBlock[] | null;
|
||||
enableReorder: boolean;
|
||||
enableQuickIssueCreate?: boolean;
|
||||
quickAddCallback?: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
data: TIssue,
|
||||
viewId?: string
|
||||
) => Promise<TIssue | undefined>;
|
||||
viewId?: string;
|
||||
disableIssueCreation?: boolean;
|
||||
showAllBlocks?: boolean;
|
||||
};
|
||||
|
||||
export const IssueGanttSidebar: React.FC<Props> = observer((props) => {
|
||||
const {
|
||||
blockUpdateHandler,
|
||||
blocks,
|
||||
enableReorder,
|
||||
enableQuickIssueCreate,
|
||||
quickAddCallback,
|
||||
viewId,
|
||||
disableIssueCreation,
|
||||
showAllBlocks = false,
|
||||
} = props;
|
||||
export const IssueGanttSidebar: React.FC<Props> = observer((props: Props) => {
|
||||
const { blockUpdateHandler, blocks, enableReorder, showAllBlocks = false } = props;
|
||||
|
||||
const { activeBlock, dispatch } = useChart();
|
||||
const { peekIssue } = useIssueDetail();
|
||||
@@ -187,9 +168,6 @@ export const IssueGanttSidebar: React.FC<Props> = observer((props) => {
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
{enableQuickIssueCreate && !disableIssueCreation && (
|
||||
<GanttQuickAddIssueForm quickAddCallback={quickAddCallback} viewId={viewId} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -9,16 +9,17 @@ type Props = {
|
||||
enableReorder: boolean;
|
||||
sidebarToRender: (props: any) => React.ReactNode;
|
||||
title: string;
|
||||
quickAdd?: React.JSX.Element | undefined;
|
||||
};
|
||||
|
||||
export const GanttChartSidebar: React.FC<Props> = (props) => {
|
||||
const { blocks, blockUpdateHandler, enableReorder, sidebarToRender, title } = props;
|
||||
const { blocks, blockUpdateHandler, enableReorder, sidebarToRender, title, quickAdd } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
// DO NOT REMOVE THE ID
|
||||
id="gantt-sidebar"
|
||||
className="sticky top-0 left-0 z-10 min-h-full h-max flex-shrink-0 border-r-[0.5px] border-custom-border-200 bg-custom-background-100"
|
||||
className="sticky left-0 z-10 min-h-full h-max flex-shrink-0 border-r-[0.5px] border-custom-border-200 bg-custom-background-100"
|
||||
style={{
|
||||
width: `${SIDEBAR_WIDTH}px`,
|
||||
}}
|
||||
@@ -33,9 +34,10 @@ export const GanttChartSidebar: React.FC<Props> = (props) => {
|
||||
<h6>Duration</h6>
|
||||
</div>
|
||||
|
||||
<div className="min-h-full h-max bg-custom-background-100">
|
||||
<div className="min-h-full h-max bg-custom-background-100 overflow-x-hidden overflow-y-auto">
|
||||
{sidebarToRender && sidebarToRender({ title, blockUpdateHandler, blocks, enableReorder })}
|
||||
</div>
|
||||
{quickAdd ? quickAdd : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// hooks
|
||||
import { useInboxIssues } from "hooks/store";
|
||||
// constants
|
||||
@@ -13,7 +14,7 @@ type Props = {
|
||||
showDescription?: boolean;
|
||||
};
|
||||
|
||||
export const InboxIssueStatus: React.FC<Props> = (props) => {
|
||||
export const InboxIssueStatus: React.FC<Props> = observer((props) => {
|
||||
const { workspaceSlug, projectId, inboxId, issueId, iconSize = 18, showDescription = false } = props;
|
||||
// hooks
|
||||
const {
|
||||
@@ -52,4 +53,4 @@ export const InboxIssueStatus: React.FC<Props> = (props) => {
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
import useSWR, { mutate } from "swr";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useTheme } from "next-themes";
|
||||
// hooks
|
||||
import { useUser } from "hooks/store";
|
||||
import useUserAuth from "hooks/use-user-auth";
|
||||
@@ -11,8 +12,10 @@ import useUserAuth from "hooks/use-user-auth";
|
||||
import { IntegrationService } from "services/integrations";
|
||||
// components
|
||||
import { DeleteImportModal, GithubImporterRoot, JiraImporterRoot, SingleImport } from "components/integration";
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
// ui
|
||||
import { Button, Loader } from "@plane/ui";
|
||||
import { Button } from "@plane/ui";
|
||||
import { ImportExportSettingsLoader } from "components/ui";
|
||||
// icons
|
||||
import { RefreshCw } from "lucide-react";
|
||||
// types
|
||||
@@ -21,6 +24,7 @@ import { IImporterService } from "@plane/types";
|
||||
import { IMPORTER_SERVICES_LIST } from "constants/fetch-keys";
|
||||
// constants
|
||||
import { IMPORTERS_LIST } from "constants/workspace";
|
||||
import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
|
||||
// services
|
||||
const integrationService = new IntegrationService();
|
||||
@@ -33,6 +37,8 @@ const IntegrationGuide = observer(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, provider } = router.query;
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store hooks
|
||||
const { currentUser, currentUserLoader } = useUser();
|
||||
// custom hooks
|
||||
@@ -43,6 +49,10 @@ const IntegrationGuide = observer(() => {
|
||||
workspaceSlug ? () => integrationService.getImporterServicesList(workspaceSlug as string) : null
|
||||
);
|
||||
|
||||
const emptyStateDetail = WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS["import"];
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const emptyStateImage = getEmptyStateImagePath("workspace-settings", "imports", isLightMode);
|
||||
|
||||
const handleDeleteImport = (importService: IImporterService) => {
|
||||
setImportToDelete(importService);
|
||||
setDeleteImportModal(true);
|
||||
@@ -134,15 +144,17 @@ const IntegrationGuide = observer(() => {
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="px-4 py-6 text-sm text-custom-text-200">No previous imports available.</p>
|
||||
<div className="h-full w-full flex items-center justify-center">
|
||||
<EmptyState
|
||||
title={emptyStateDetail.title}
|
||||
description={emptyStateDetail.description}
|
||||
image={emptyStateImage}
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<Loader className="mt-6 grid grid-cols-1 gap-3">
|
||||
<Loader.Item height="40px" width="100%" />
|
||||
<Loader.Item height="40px" width="100%" />
|
||||
<Loader.Item height="40px" width="100%" />
|
||||
<Loader.Item height="40px" width="100%" />
|
||||
</Loader>
|
||||
<ImportExportSettingsLoader />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -168,7 +168,7 @@ export const SingleIntegrationCard: React.FC<Props> = observer(({ integration })
|
||||
)
|
||||
) : (
|
||||
<Loader>
|
||||
<Loader.Item height="35px" width="150px" />
|
||||
<Loader.Item height="32px" width="64px" />
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,6 @@ import { TIssueOperations } from "./issue-detail";
|
||||
import { FileService } from "services/file.service";
|
||||
import { useMention, useWorkspace } from "hooks/store";
|
||||
import { observer } from "mobx-react";
|
||||
import { isNil } from "lodash";
|
||||
|
||||
export interface IssueDescriptionFormValues {
|
||||
name: string;
|
||||
@@ -42,10 +41,9 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = observer((props) => {
|
||||
const { workspaceSlug, projectId, issueId, issue, issueOperations, disabled, isSubmitting, setIsSubmitting } = props;
|
||||
const workspaceStore = useWorkspace();
|
||||
const workspaceId = workspaceStore.getWorkspaceBySlug(workspaceSlug)?.id as string;
|
||||
|
||||
// states
|
||||
const [characterLimit, setCharacterLimit] = useState(false);
|
||||
|
||||
// hooks
|
||||
const { setShowAlert } = useReloadConfirmations();
|
||||
// store hooks
|
||||
const { mentionHighlights, mentionSuggestions } = useMention();
|
||||
@@ -58,8 +56,8 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = observer((props) => {
|
||||
formState: { errors },
|
||||
} = useForm<TIssue>({
|
||||
defaultValues: {
|
||||
name: "",
|
||||
description_html: "",
|
||||
name: issue?.name,
|
||||
description_html: issue?.description_html,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -69,24 +67,6 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = observer((props) => {
|
||||
description_html: issue.description_html,
|
||||
});
|
||||
|
||||
// adding issue.description_html or issue.name to dependency array causes
|
||||
// editor rerendering on every save
|
||||
useEffect(() => {
|
||||
if (issue.id) {
|
||||
setLocalTitleValue(issue.name);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [issue.id]); // TODO: verify the exhaustive-deps warning
|
||||
|
||||
useEffect(() => {
|
||||
if (issue.description_html) {
|
||||
setLocalIssueDescription((state) => {
|
||||
if (!isNil(state.description_html)) return state;
|
||||
return { id: issue.id, description_html: issue.description_html };
|
||||
});
|
||||
}
|
||||
}, [issue.description_html]);
|
||||
|
||||
const handleDescriptionFormSubmit = useCallback(
|
||||
async (formData: Partial<TIssue>) => {
|
||||
if (!formData?.name || formData?.name.length === 0 || formData?.name.length > 255) return;
|
||||
@@ -123,7 +103,12 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = observer((props) => {
|
||||
reset({
|
||||
...issue,
|
||||
});
|
||||
}, [issue, reset]);
|
||||
setLocalIssueDescription({
|
||||
id: issue.id,
|
||||
description_html: issue.description_html === "" ? "<p></p>" : issue.description_html,
|
||||
});
|
||||
setLocalTitleValue(issue.name);
|
||||
}, [issue, issue.description_html, reset]);
|
||||
|
||||
// ADDING handleDescriptionFormSubmit TO DEPENDENCY ARRAY PRODUCES ADVERSE EFFECTS
|
||||
// TODO: Verify the exhaustive-deps warning
|
||||
@@ -177,7 +162,7 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = observer((props) => {
|
||||
</div>
|
||||
<span>{errors.name ? errors.name.message : null}</span>
|
||||
<div className="relative">
|
||||
{issue.description_html ? (
|
||||
{localIssueDescription.description_html ? (
|
||||
<Controller
|
||||
name="description_html"
|
||||
control={control}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { FC, useState, useEffect } from "react";
|
||||
import { observer } from "mobx-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";
|
||||
import useReloadConfirmations from "hooks/use-reload-confirmation";
|
||||
|
||||
export type IssueDescriptionInputProps = {
|
||||
disabled?: boolean;
|
||||
value: string | undefined | null;
|
||||
workspaceSlug: string;
|
||||
setIsSubmitting: (value: "submitting" | "submitted" | "saved") => void;
|
||||
issueOperations: TIssueOperations;
|
||||
projectId: string;
|
||||
issueId: string;
|
||||
};
|
||||
|
||||
export const IssueDescriptionInput: FC<IssueDescriptionInputProps> = observer((props) => {
|
||||
const { disabled, value, workspaceSlug, setIsSubmitting, issueId, issueOperations, projectId } = props;
|
||||
// states
|
||||
const [descriptionHTML, setDescriptionHTML] = useState(value);
|
||||
// store hooks
|
||||
const { mentionHighlights, mentionSuggestions } = useMention();
|
||||
const workspaceStore = useWorkspace();
|
||||
// hooks
|
||||
const { setShowAlert } = useReloadConfirmations();
|
||||
const debouncedValue = useDebounce(descriptionHTML, 1500);
|
||||
// computed values
|
||||
const workspaceId = workspaceStore.getWorkspaceBySlug(workspaceSlug)?.id as string;
|
||||
|
||||
useEffect(() => {
|
||||
setDescriptionHTML(value);
|
||||
}, [value]);
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedValue || debouncedValue === "") {
|
||||
issueOperations
|
||||
.update(workspaceSlug, projectId, issueId, { description_html: 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]);
|
||||
|
||||
if (!descriptionHTML && 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}
|
||||
setShouldShowAlert={setShowAlert}
|
||||
setIsSubmitting={setIsSubmitting}
|
||||
dragDropEnabled
|
||||
customClassName="min-h-[150px] shadow-sm"
|
||||
onChange={(description: Object, description_html: string) => {
|
||||
setShowAlert(true);
|
||||
setIsSubmitting("submitting");
|
||||
setDescriptionHTML(description_html);
|
||||
}}
|
||||
mentionSuggestions={mentionSuggestions}
|
||||
mentionHighlights={mentionHighlights}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@@ -3,7 +3,9 @@ import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useIssueDetail, useProjectState, useUser } from "hooks/store";
|
||||
// components
|
||||
import { IssueDescriptionForm, IssueUpdateStatus, TIssueOperations } from "components/issues";
|
||||
import { IssueUpdateStatus, TIssueOperations } from "components/issues";
|
||||
import { IssueTitleInput } from "../../title-input";
|
||||
import { IssueDescriptionInput } from "../../description-input";
|
||||
import { IssueReaction } from "../reactions";
|
||||
import { IssueActivity } from "../issue-activity";
|
||||
import { InboxIssueStatus } from "../../../inbox/inbox-issue-status";
|
||||
@@ -57,15 +59,24 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
|
||||
<IssueUpdateStatus isSubmitting={isSubmitting} issueDetail={issue} />
|
||||
</div>
|
||||
|
||||
<IssueDescriptionForm
|
||||
<IssueTitleInput
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
projectId={issue.project_id}
|
||||
issueId={issue.id}
|
||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||
isSubmitting={isSubmitting}
|
||||
issue={issue}
|
||||
issueOperations={issueOperations}
|
||||
disabled={!is_editable}
|
||||
value={issue.name}
|
||||
/>
|
||||
|
||||
<IssueDescriptionInput
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={issue.project_id}
|
||||
issueId={issue.id}
|
||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||
issueOperations={issueOperations}
|
||||
disabled={!is_editable}
|
||||
value={issue.description_html}
|
||||
/>
|
||||
|
||||
{currentUser && (
|
||||
|
||||
@@ -81,7 +81,6 @@ export const IssueCommentCreate: FC<TIssueCommentCreate> = (props) => {
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<LiteTextEditorWithRef
|
||||
onEnterKeyPress={(e) => {
|
||||
console.log("yo");
|
||||
handleSubmit(onSubmit)(e);
|
||||
}}
|
||||
cancelUploadImage={fileService.cancelUpload}
|
||||
|
||||
@@ -3,7 +3,9 @@ import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useIssueDetail, useProjectState, useUser } from "hooks/store";
|
||||
// components
|
||||
import { IssueDescriptionForm, IssueAttachmentRoot, IssueUpdateStatus } from "components/issues";
|
||||
import { IssueAttachmentRoot, IssueUpdateStatus } from "components/issues";
|
||||
import { IssueTitleInput } from "../title-input";
|
||||
import { IssueDescriptionInput } from "../description-input";
|
||||
import { IssueParentDetail } from "./parent";
|
||||
import { IssueReaction } from "./reactions";
|
||||
import { SubIssuesRoot } from "../sub-issues";
|
||||
@@ -61,15 +63,24 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
|
||||
<IssueUpdateStatus isSubmitting={isSubmitting} issueDetail={issue} />
|
||||
</div>
|
||||
|
||||
<IssueDescriptionForm
|
||||
<IssueTitleInput
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
projectId={issue.project_id}
|
||||
issueId={issue.id}
|
||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||
isSubmitting={isSubmitting}
|
||||
issue={issue}
|
||||
issueOperations={issueOperations}
|
||||
disabled={!is_editable}
|
||||
value={issue.name}
|
||||
/>
|
||||
|
||||
<IssueDescriptionInput
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={issue.project_id}
|
||||
issueId={issue.id}
|
||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||
issueOperations={issueOperations}
|
||||
disabled={!is_editable}
|
||||
value={issue.description_html}
|
||||
/>
|
||||
|
||||
{currentUser && (
|
||||
|
||||
@@ -6,7 +6,7 @@ import { IssueParentSiblings } from "./siblings";
|
||||
// ui
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
// hooks
|
||||
import { useIssueDetail, useIssues, useProject, useProjectState } from "hooks/store";
|
||||
import { useIssues, useProject, useProjectState } from "hooks/store";
|
||||
// types
|
||||
import { TIssueOperations } from "../root";
|
||||
import { TIssue } from "@plane/types";
|
||||
@@ -23,7 +23,6 @@ export const IssueParentDetail: FC<TIssueParentDetail> = (props) => {
|
||||
const { workspaceSlug, projectId, issueId, issue, issueOperations } = props;
|
||||
// hooks
|
||||
const { issueMap } = useIssues();
|
||||
const { peekIssue } = useIssueDetail();
|
||||
const { getProjectById } = useProject();
|
||||
const { getProjectStates } = useProjectState();
|
||||
|
||||
@@ -39,7 +38,7 @@ export const IssueParentDetail: FC<TIssueParentDetail> = (props) => {
|
||||
return (
|
||||
<>
|
||||
<div className="mb-5 flex w-min items-center gap-3 whitespace-nowrap rounded-md border border-custom-border-300 bg-custom-background-80 px-2.5 py-1 text-xs">
|
||||
<Link href={`/${peekIssue?.workspaceSlug}/projects/${parentIssue?.project_id}/issues/${parentIssue.id}`}>
|
||||
<Link href={`/${workspaceSlug}/projects/${parentIssue?.project_id}/issues/${parentIssue.id}`}>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<span className="block h-2 w-2 rounded-full" style={{ backgroundColor: stateColor }} />
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
CalendarCheck2,
|
||||
} from "lucide-react";
|
||||
// hooks
|
||||
import { useEstimate, useIssueDetail, useProject, useProjectState, useUser } from "hooks/store";
|
||||
import { useEstimate, useIssueDetail, useProject, useUser } from "hooks/store";
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import {
|
||||
@@ -56,11 +56,9 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
const { workspaceSlug, projectId, issueId, issueOperations, is_archived, is_editable } = props;
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { inboxIssueId } = router.query;
|
||||
// store hooks
|
||||
const { getProjectById } = useProject();
|
||||
const { currentUser } = useUser();
|
||||
const { projectStates } = useProjectState();
|
||||
const { areEstimatesEnabledForCurrentProject } = useEstimate();
|
||||
const { setToastAlert } = useToast();
|
||||
const {
|
||||
@@ -91,8 +89,6 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
const maxDate = issue.target_date ? new Date(issue.target_date) : null;
|
||||
maxDate?.setDate(maxDate.getDate());
|
||||
|
||||
const currentIssueState = projectStates?.find((s) => s.id === issue.state_id);
|
||||
|
||||
return (
|
||||
<>
|
||||
{workspaceSlug && projectId && issue && (
|
||||
@@ -108,22 +104,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
)}
|
||||
|
||||
<div className="flex h-full w-full flex-col divide-y-2 divide-custom-border-200 overflow-hidden">
|
||||
<div className="flex items-center justify-between px-5 pb-3">
|
||||
<div className="flex items-center gap-x-2">
|
||||
{currentIssueState ? (
|
||||
<StateGroupIcon
|
||||
className="h-4 w-4"
|
||||
stateGroup={currentIssueState.group}
|
||||
color={currentIssueState.color}
|
||||
/>
|
||||
) : inboxIssueId ? (
|
||||
<StateGroupIcon className="h-4 w-4" stateGroup="backlog" color="#ff7700" />
|
||||
) : null}
|
||||
<h4 className="text-lg font-medium text-custom-text-300">
|
||||
{projectDetails?.identifier}-{issue?.sequence_id}
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end px-5 pb-3">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{currentUser && !is_archived && (
|
||||
<IssueSubscription workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} />
|
||||
@@ -187,8 +168,9 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
buttonVariant={issue?.assignee_ids?.length > 1 ? "transparent-without-text" : "transparent-with-text"}
|
||||
className="w-3/5 flex-grow group"
|
||||
buttonContainerClassName="w-full text-left"
|
||||
buttonClassName={`text-sm justify-between ${issue?.assignee_ids.length > 0 ? "" : "text-custom-text-400"
|
||||
}`}
|
||||
buttonClassName={`text-sm justify-between ${
|
||||
issue?.assignee_ids.length > 0 ? "" : "text-custom-text-400"
|
||||
}`}
|
||||
hideIcon={issue.assignee_ids?.length === 0}
|
||||
dropdownArrow
|
||||
dropdownArrowClassName="h-3.5 w-3.5 hidden group-hover:inline"
|
||||
@@ -232,8 +214,8 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
buttonClassName={`text-sm ${issue?.start_date ? "" : "text-custom-text-400"}`}
|
||||
hideIcon
|
||||
clearIconClassName="h-3 w-3 hidden group-hover:inline"
|
||||
// TODO: add this logic
|
||||
// showPlaceholderIcon
|
||||
// TODO: add this logic
|
||||
// showPlaceholderIcon
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -258,8 +240,8 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
buttonClassName={`text-sm ${issue?.target_date ? "" : "text-custom-text-400"}`}
|
||||
hideIcon
|
||||
clearIconClassName="h-3 w-3 hidden group-hover:inline"
|
||||
// TODO: add this logic
|
||||
// showPlaceholderIcon
|
||||
// TODO: add this logic
|
||||
// showPlaceholderIcon
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
// constants
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
|
||||
import { EMPTY_FILTER_STATE_DETAILS, EMPTY_ISSUE_STATE_DETAILS } from "constants/empty-state";
|
||||
// types
|
||||
import { IIssueFilterOptions } from "@plane/types";
|
||||
|
||||
@@ -65,20 +66,19 @@ export const ProjectArchivedEmptyState: React.FC = observer(() => {
|
||||
const emptyStateProps: EmptyStateProps =
|
||||
issueFilterCount > 0
|
||||
? {
|
||||
title: "No issues found matching the filters applied",
|
||||
title: EMPTY_FILTER_STATE_DETAILS["archived"].title,
|
||||
image: currentLayoutEmptyStateImagePath,
|
||||
secondaryButton: {
|
||||
text: "Clear all filters",
|
||||
text: EMPTY_FILTER_STATE_DETAILS["archived"].secondaryButton?.text,
|
||||
onClick: handleClearAllFilters,
|
||||
},
|
||||
}
|
||||
: {
|
||||
title: "No archived issues yet",
|
||||
description:
|
||||
"Archived issues help you remove issues you completed or cancelled from focus. You can set automation to auto archive issues and find them here.",
|
||||
title: EMPTY_ISSUE_STATE_DETAILS["archived"].title,
|
||||
description: EMPTY_ISSUE_STATE_DETAILS["archived"].description,
|
||||
image: EmptyStateImagePath,
|
||||
primaryButton: {
|
||||
text: "Set Automation",
|
||||
text: EMPTY_ISSUE_STATE_DETAILS["archived"].primaryButton.text,
|
||||
onClick: () => router.push(`/${workspaceSlug}/projects/${projectId}/settings/automations`),
|
||||
},
|
||||
size: "sm",
|
||||
|
||||
@@ -1,32 +1,46 @@
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
// hooks
|
||||
import { useApplication, useEventTracker, useIssueDetail, useIssues, useUser } from "hooks/store";
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { EmptyState } from "components/common";
|
||||
import { ExistingIssuesListModal } from "components/core";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// assets
|
||||
import emptyIssue from "public/empty-state/issue.svg";
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
// types
|
||||
import { ISearchIssueResponse } from "@plane/types";
|
||||
import { ISearchIssueResponse, TIssueLayouts } from "@plane/types";
|
||||
// constants
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
import { EIssuesStoreType } from "constants/issue";
|
||||
import { CYCLE_EMPTY_STATE_DETAILS, EMPTY_FILTER_STATE_DETAILS } from "constants/empty-state";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string | undefined;
|
||||
projectId: string | undefined;
|
||||
cycleId: string | undefined;
|
||||
activeLayout: TIssueLayouts | undefined;
|
||||
handleClearAllFilters: () => void;
|
||||
isEmptyFilters?: boolean;
|
||||
};
|
||||
|
||||
interface EmptyStateProps {
|
||||
title: string;
|
||||
image: string;
|
||||
description?: string;
|
||||
comicBox?: { title: string; description: string };
|
||||
primaryButton?: { text: string; icon?: React.ReactNode; onClick: () => void };
|
||||
secondaryButton?: { text: string; icon?: React.ReactNode; onClick: () => void };
|
||||
size?: "lg" | "sm" | undefined;
|
||||
disabled?: boolean | undefined;
|
||||
}
|
||||
|
||||
export const CycleEmptyState: React.FC<Props> = observer((props) => {
|
||||
const { workspaceSlug, projectId, cycleId } = props;
|
||||
const { workspaceSlug, projectId, cycleId, activeLayout, handleClearAllFilters, isEmptyFilters = false } = props;
|
||||
// states
|
||||
const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false);
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store hooks
|
||||
const { issues } = useIssues(EIssuesStoreType.CYCLE);
|
||||
const { updateIssue, fetchIssue } = useIssueDetail();
|
||||
@@ -36,6 +50,7 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
|
||||
const { setTrackElement } = useEventTracker();
|
||||
const {
|
||||
membership: { currentProjectRole: userRole },
|
||||
currentUser,
|
||||
} = useUser();
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
@@ -60,8 +75,44 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
|
||||
});
|
||||
};
|
||||
|
||||
const emptyStateDetail = CYCLE_EMPTY_STATE_DETAILS["no-issues"];
|
||||
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const currentLayoutEmptyStateImagePath = getEmptyStateImagePath("empty-filters", activeLayout ?? "list", isLightMode);
|
||||
const emptyStateImage = getEmptyStateImagePath("cycle-issues", activeLayout ?? "list", isLightMode);
|
||||
|
||||
const isEditingAllowed = !!userRole && userRole >= EUserProjectRoles.MEMBER;
|
||||
|
||||
const emptyStateProps: EmptyStateProps = isEmptyFilters
|
||||
? {
|
||||
title: EMPTY_FILTER_STATE_DETAILS["project"].title,
|
||||
image: currentLayoutEmptyStateImagePath,
|
||||
secondaryButton: {
|
||||
text: EMPTY_FILTER_STATE_DETAILS["project"].secondaryButton.text,
|
||||
onClick: handleClearAllFilters,
|
||||
},
|
||||
}
|
||||
: {
|
||||
title: emptyStateDetail.title,
|
||||
description: emptyStateDetail.description,
|
||||
image: emptyStateImage,
|
||||
primaryButton: {
|
||||
text: emptyStateDetail.primaryButton.text,
|
||||
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
|
||||
onClick: () => {
|
||||
setTrackElement("Cycle issue empty state");
|
||||
toggleCreateIssueModal(true, EIssuesStoreType.CYCLE);
|
||||
},
|
||||
},
|
||||
secondaryButton: {
|
||||
text: emptyStateDetail.secondaryButton.text,
|
||||
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
|
||||
onClick: () => setCycleIssuesListModal(true),
|
||||
},
|
||||
size: "sm",
|
||||
disabled: !isEditingAllowed,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ExistingIssuesListModal
|
||||
@@ -73,30 +124,7 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
|
||||
handleOnSubmit={handleAddIssuesToCycle}
|
||||
/>
|
||||
<div className="grid h-full w-full place-items-center">
|
||||
<EmptyState
|
||||
title="Cycle issues will appear here"
|
||||
description="Issues help you track individual pieces of work. With Issues, keep track of what's going on, who is working on it, and what's done."
|
||||
image={emptyIssue}
|
||||
primaryButton={{
|
||||
text: "New issue",
|
||||
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
|
||||
onClick: () => {
|
||||
setTrackElement("Cycle issue empty state");
|
||||
toggleCreateIssueModal(true, EIssuesStoreType.CYCLE);
|
||||
},
|
||||
}}
|
||||
secondaryButton={
|
||||
<Button
|
||||
variant="neutral-primary"
|
||||
prependIcon={<PlusIcon className="h-3 w-3" strokeWidth={2} />}
|
||||
onClick={() => setCycleIssuesListModal(true)}
|
||||
disabled={!isEditingAllowed}
|
||||
>
|
||||
Add an existing issue
|
||||
</Button>
|
||||
}
|
||||
disabled={!isEditingAllowed}
|
||||
/>
|
||||
<EmptyState {...emptyStateProps} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -9,6 +9,7 @@ import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
// constants
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
|
||||
import { EMPTY_FILTER_STATE_DETAILS, EMPTY_ISSUE_STATE_DETAILS } from "constants/empty-state";
|
||||
// types
|
||||
import { IIssueFilterOptions } from "@plane/types";
|
||||
|
||||
@@ -41,7 +42,7 @@ export const ProjectDraftEmptyState: React.FC = observer(() => {
|
||||
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const currentLayoutEmptyStateImagePath = getEmptyStateImagePath("empty-filters", activeLayout ?? "list", isLightMode);
|
||||
const EmptyStateImagePath = getEmptyStateImagePath("draft", "empty-issues", isLightMode);
|
||||
const EmptyStateImagePath = getEmptyStateImagePath("draft", "draft-issues-empty", isLightMode);
|
||||
|
||||
const issueFilterCount = size(
|
||||
Object.fromEntries(
|
||||
@@ -65,17 +66,16 @@ export const ProjectDraftEmptyState: React.FC = observer(() => {
|
||||
const emptyStateProps: EmptyStateProps =
|
||||
issueFilterCount > 0
|
||||
? {
|
||||
title: "No issues found matching the filters applied",
|
||||
title: EMPTY_FILTER_STATE_DETAILS["draft"].title,
|
||||
image: currentLayoutEmptyStateImagePath,
|
||||
secondaryButton: {
|
||||
text: "Clear all filters",
|
||||
text: EMPTY_FILTER_STATE_DETAILS["draft"].secondaryButton.text,
|
||||
onClick: handleClearAllFilters,
|
||||
},
|
||||
}
|
||||
: {
|
||||
title: "No draft issues yet",
|
||||
description:
|
||||
"Quickly stepping away but want to keep your place? No worries – save a draft now. Your issues will be right here waiting for you.",
|
||||
title: EMPTY_ISSUE_STATE_DETAILS["draft"].title,
|
||||
description: EMPTY_ISSUE_STATE_DETAILS["draft"].description,
|
||||
image: EmptyStateImagePath,
|
||||
size: "sm",
|
||||
disabled: !isEditingAllowed,
|
||||
|
||||
@@ -1,41 +1,55 @@
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
// hooks
|
||||
import { useApplication, useEventTracker, useIssues, useUser } from "hooks/store";
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { EmptyState } from "components/common";
|
||||
import { ExistingIssuesListModal } from "components/core";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// assets
|
||||
import emptyIssue from "public/empty-state/issue.svg";
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
// types
|
||||
import { ISearchIssueResponse } from "@plane/types";
|
||||
import { ISearchIssueResponse, TIssueLayouts } from "@plane/types";
|
||||
// constants
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
import { EIssuesStoreType } from "constants/issue";
|
||||
import { EMPTY_FILTER_STATE_DETAILS, MODULE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string | undefined;
|
||||
projectId: string | undefined;
|
||||
moduleId: string | undefined;
|
||||
activeLayout: TIssueLayouts | undefined;
|
||||
handleClearAllFilters: () => void;
|
||||
isEmptyFilters?: boolean;
|
||||
};
|
||||
|
||||
interface EmptyStateProps {
|
||||
title: string;
|
||||
image: string;
|
||||
description?: string;
|
||||
comicBox?: { title: string; description: string };
|
||||
primaryButton?: { text: string; icon?: React.ReactNode; onClick: () => void };
|
||||
secondaryButton?: { text: string; icon?: React.ReactNode; onClick: () => void };
|
||||
size?: "lg" | "sm" | undefined;
|
||||
disabled?: boolean | undefined;
|
||||
}
|
||||
|
||||
export const ModuleEmptyState: React.FC<Props> = observer((props) => {
|
||||
const { workspaceSlug, projectId, moduleId } = props;
|
||||
const { workspaceSlug, projectId, moduleId, activeLayout, handleClearAllFilters, isEmptyFilters = false } = props;
|
||||
// states
|
||||
const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false);
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store hooks
|
||||
const { issues } = useIssues(EIssuesStoreType.MODULE);
|
||||
|
||||
const {
|
||||
commandPalette: { toggleCreateIssueModal },
|
||||
} = useApplication();
|
||||
const { setTrackElement } = useEventTracker();
|
||||
const {
|
||||
membership: { currentProjectRole: userRole },
|
||||
currentUser,
|
||||
} = useUser();
|
||||
// toast alert
|
||||
const { setToastAlert } = useToast();
|
||||
@@ -55,8 +69,43 @@ export const ModuleEmptyState: React.FC<Props> = observer((props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const emptyStateDetail = MODULE_EMPTY_STATE_DETAILS["no-issues"];
|
||||
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const currentLayoutEmptyStateImagePath = getEmptyStateImagePath("empty-filters", activeLayout ?? "list", isLightMode);
|
||||
const emptyStateImage = getEmptyStateImagePath("module-issues", activeLayout ?? "list", isLightMode);
|
||||
|
||||
const isEditingAllowed = !!userRole && userRole >= EUserProjectRoles.MEMBER;
|
||||
|
||||
const emptyStateProps: EmptyStateProps = isEmptyFilters
|
||||
? {
|
||||
title: EMPTY_FILTER_STATE_DETAILS["project"].title,
|
||||
image: currentLayoutEmptyStateImagePath,
|
||||
secondaryButton: {
|
||||
text: EMPTY_FILTER_STATE_DETAILS["project"].secondaryButton.text,
|
||||
onClick: handleClearAllFilters,
|
||||
},
|
||||
}
|
||||
: {
|
||||
title: emptyStateDetail.title,
|
||||
description: emptyStateDetail.description,
|
||||
image: emptyStateImage,
|
||||
primaryButton: {
|
||||
text: emptyStateDetail.primaryButton.text,
|
||||
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
|
||||
onClick: () => {
|
||||
setTrackElement("Module issue empty state");
|
||||
toggleCreateIssueModal(true, EIssuesStoreType.MODULE);
|
||||
},
|
||||
},
|
||||
secondaryButton: {
|
||||
text: emptyStateDetail.secondaryButton.text,
|
||||
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
|
||||
onClick: () => setModuleIssuesListModal(true),
|
||||
},
|
||||
disabled: !isEditingAllowed,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ExistingIssuesListModal
|
||||
@@ -64,34 +113,11 @@ export const ModuleEmptyState: React.FC<Props> = observer((props) => {
|
||||
projectId={projectId}
|
||||
isOpen={moduleIssuesListModal}
|
||||
handleClose={() => setModuleIssuesListModal(false)}
|
||||
searchParams={{ module: moduleId != undefined ? [moduleId.toString()] : [] }}
|
||||
searchParams={{ module: moduleId != undefined ? moduleId.toString() : "" }}
|
||||
handleOnSubmit={handleAddIssuesToModule}
|
||||
/>
|
||||
<div className="grid h-full w-full place-items-center">
|
||||
<EmptyState
|
||||
title="Module issues will appear here"
|
||||
description="Issues help you track individual pieces of work. With Issues, keep track of what's going on, who is working on it, and what's done."
|
||||
image={emptyIssue}
|
||||
primaryButton={{
|
||||
text: "New issue",
|
||||
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
|
||||
onClick: () => {
|
||||
setTrackElement("Module issue empty state");
|
||||
toggleCreateIssueModal(true, EIssuesStoreType.MODULE);
|
||||
},
|
||||
}}
|
||||
secondaryButton={
|
||||
<Button
|
||||
variant="neutral-primary"
|
||||
prependIcon={<PlusIcon className="h-3 w-3" strokeWidth={2} />}
|
||||
onClick={() => setModuleIssuesListModal(true)}
|
||||
disabled={!isEditingAllowed}
|
||||
>
|
||||
Add an existing issue
|
||||
</Button>
|
||||
}
|
||||
disabled={!isEditingAllowed}
|
||||
/>
|
||||
<EmptyState {...emptyStateProps} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -9,6 +9,7 @@ import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
// constants
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
|
||||
import { EMPTY_FILTER_STATE_DETAILS, EMPTY_ISSUE_STATE_DETAILS } from "constants/empty-state";
|
||||
// types
|
||||
import { IIssueFilterOptions } from "@plane/types";
|
||||
|
||||
@@ -67,26 +68,23 @@ export const ProjectEmptyState: React.FC = observer(() => {
|
||||
const emptyStateProps: EmptyStateProps =
|
||||
issueFilterCount > 0
|
||||
? {
|
||||
title: "No issues found matching the filters applied",
|
||||
title: EMPTY_FILTER_STATE_DETAILS["project"].title,
|
||||
image: currentLayoutEmptyStateImagePath,
|
||||
secondaryButton: {
|
||||
text: "Clear all filters",
|
||||
text: EMPTY_FILTER_STATE_DETAILS["project"].secondaryButton.text,
|
||||
onClick: handleClearAllFilters,
|
||||
},
|
||||
}
|
||||
: {
|
||||
title: "Create an issue and assign it to someone, even yourself",
|
||||
description:
|
||||
"Think of issues as jobs, tasks, work, or JTBD. Which we like. An issue and its sub-issues are usually time-based actionables assigned to members of your team. Your team creates, assigns, and completes issues to move your project towards its goal.",
|
||||
title: EMPTY_ISSUE_STATE_DETAILS["project"].title,
|
||||
description: EMPTY_ISSUE_STATE_DETAILS["project"].description,
|
||||
image: EmptyStateImagePath,
|
||||
comicBox: {
|
||||
title: "Issues are building blocks in Plane.",
|
||||
description:
|
||||
"Redesign the Plane UI, Rebrand the company, or Launch the new fuel injection system are examples of issues that likely have sub-issues.",
|
||||
title: EMPTY_ISSUE_STATE_DETAILS["project"].comicBox.title,
|
||||
description: EMPTY_ISSUE_STATE_DETAILS["project"].comicBox.description,
|
||||
},
|
||||
primaryButton: {
|
||||
text: "Create your first issue",
|
||||
|
||||
text: EMPTY_ISSUE_STATE_DETAILS["project"].primaryButton.text,
|
||||
onClick: () => {
|
||||
setTrackElement("Project issue empty state");
|
||||
commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);
|
||||
|
||||
@@ -36,22 +36,34 @@ export const CycleAppliedFiltersRoot: React.FC = observer(() => {
|
||||
const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => {
|
||||
if (!workspaceSlug || !projectId || !cycleId) return;
|
||||
if (!value) {
|
||||
updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, {
|
||||
[key]: null,
|
||||
});
|
||||
updateFilters(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
EIssueFilterType.FILTERS,
|
||||
{
|
||||
[key]: null,
|
||||
},
|
||||
cycleId
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let newValues = issueFilters?.filters?.[key] ?? [];
|
||||
newValues = newValues.filter((val) => val !== value);
|
||||
|
||||
updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, {
|
||||
[key]: newValues,
|
||||
});
|
||||
updateFilters(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
EIssueFilterType.FILTERS,
|
||||
{
|
||||
[key]: newValues,
|
||||
},
|
||||
cycleId
|
||||
);
|
||||
};
|
||||
|
||||
const handleClearAllFilters = () => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
if (!workspaceSlug || !projectId || !cycleId) return;
|
||||
const newFilters: IIssueFilterOptions = {};
|
||||
Object.keys(userFilters ?? {}).forEach((key) => {
|
||||
newFilters[key as keyof IIssueFilterOptions] = null;
|
||||
|
||||
@@ -33,24 +33,36 @@ export const ModuleAppliedFiltersRoot: React.FC = observer(() => {
|
||||
});
|
||||
|
||||
const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
if (!workspaceSlug || !projectId || !moduleId) return;
|
||||
if (!value) {
|
||||
updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, {
|
||||
[key]: null,
|
||||
});
|
||||
updateFilters(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
EIssueFilterType.FILTERS,
|
||||
{
|
||||
[key]: null,
|
||||
},
|
||||
moduleId
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let newValues = issueFilters?.filters?.[key] ?? [];
|
||||
newValues = newValues.filter((val) => val !== value);
|
||||
|
||||
updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, {
|
||||
[key]: newValues,
|
||||
});
|
||||
updateFilters(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
EIssueFilterType.FILTERS,
|
||||
{
|
||||
[key]: newValues,
|
||||
},
|
||||
moduleId
|
||||
);
|
||||
};
|
||||
|
||||
const handleClearAllFilters = () => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
if (!workspaceSlug || !projectId || !moduleId) return;
|
||||
const newFilters: IIssueFilterOptions = {};
|
||||
Object.keys(userFilters ?? {}).forEach((key) => {
|
||||
newFilters[key as keyof IIssueFilterOptions] = null;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useIssues, useUser } from "hooks/store";
|
||||
// components
|
||||
import { IssueGanttBlock } from "components/issues";
|
||||
import { GanttQuickAddIssueForm, IssueGanttBlock } from "components/issues";
|
||||
import {
|
||||
GanttChartRoot,
|
||||
IBlockUpdateData,
|
||||
@@ -70,21 +70,17 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
|
||||
blocks={issues ? renderIssueBlocksStructure(issues as TIssue[]) : null}
|
||||
blockUpdateHandler={updateIssueBlockStructure}
|
||||
blockToRender={(data: TIssue) => <IssueGanttBlock issueId={data.id} />}
|
||||
sidebarToRender={(props) => (
|
||||
<IssueGanttSidebar
|
||||
{...props}
|
||||
quickAddCallback={issueStore.quickAddIssue}
|
||||
viewId={viewId}
|
||||
enableQuickIssueCreate
|
||||
disableIssueCreation={!enableIssueCreation || !isAllowed}
|
||||
showAllBlocks
|
||||
/>
|
||||
)}
|
||||
sidebarToRender={(props) => <IssueGanttSidebar {...props} showAllBlocks />}
|
||||
enableBlockLeftResize={isAllowed}
|
||||
enableBlockRightResize={isAllowed}
|
||||
enableBlockMove={isAllowed}
|
||||
enableReorder={appliedDisplayFilters?.order_by === "sort_order" && isAllowed}
|
||||
enableAddBlock={isAllowed}
|
||||
quickAdd={
|
||||
enableIssueCreation && isAllowed ? (
|
||||
<GanttQuickAddIssueForm quickAddCallback={issueStore.quickAddIssue} viewId={viewId} />
|
||||
) : undefined
|
||||
}
|
||||
showAllBlocks
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -159,7 +159,7 @@ export const GanttQuickAddIssueForm: React.FC<IGanttQuickAddIssueForm> = observe
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="sticky bottom-0 z-[1] flex w-full cursor-pointer items-center gap-2 p-3 py-3 text-custom-primary-100 bg-custom-background-100"
|
||||
className="sticky bottom-0 z-[1] flex w-full cursor-pointer items-center gap-2 p-3 py-3 text-custom-primary-100 bg-custom-background-100 border-custom-border-200 border-t-[1px]"
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<PlusIcon className="h-3.5 w-3.5 stroke-2" />
|
||||
|
||||
@@ -138,14 +138,14 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = memo((props) => {
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 px-3 py-2 text-sm transition-all hover:border-custom-border-400",
|
||||
"rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 text-sm transition-all hover:border-custom-border-400",
|
||||
{ "hover:cursor-grab": !isDragDisabled },
|
||||
{ "border-custom-primary-100": snapshot.isDragging },
|
||||
{ "border border-custom-primary-70 hover:border-custom-primary-70": peekIssueId === issue.id }
|
||||
)}
|
||||
>
|
||||
<RenderIfVisible
|
||||
classNames="space-y-2"
|
||||
classNames="space-y-2 px-3 py-2"
|
||||
root={scrollableContainerRef}
|
||||
defaultHeight="100px"
|
||||
horizonatlOffset={50}
|
||||
|
||||
@@ -59,7 +59,7 @@ export const HeaderGroupByCard: FC<IHeaderGroupByCard> = observer((props) => {
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const renderExistingIssueModal = moduleId || cycleId;
|
||||
const ExistingIssuesListModalPayload = moduleId ? { module: [moduleId.toString()] } : { cycle: true };
|
||||
const ExistingIssuesListModalPayload = moduleId ? { module: moduleId.toString() } : { cycle: true };
|
||||
|
||||
const handleAddIssuesToView = async (data: ISearchIssueResponse[]) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
@@ -67,10 +67,13 @@ export const handleDragDrop = async (
|
||||
|
||||
let updateIssue: any = {};
|
||||
|
||||
const sourceColumnId = (source?.droppableId && source?.droppableId.split("__")) || null;
|
||||
const destinationColumnId = (destination?.droppableId && destination?.droppableId.split("__")) || null;
|
||||
const sourceDroppableId = source?.droppableId;
|
||||
const destinationDroppableId = destination?.droppableId;
|
||||
|
||||
if (!sourceColumnId || !destinationColumnId) return;
|
||||
const sourceColumnId = (sourceDroppableId && sourceDroppableId.split("__")) || null;
|
||||
const destinationColumnId = (destinationDroppableId && destinationDroppableId.split("__")) || null;
|
||||
|
||||
if (!sourceColumnId || !destinationColumnId || !sourceDroppableId || !destinationDroppableId) return;
|
||||
|
||||
const sourceGroupByColumnId = sourceColumnId[0] || null;
|
||||
const destinationGroupByColumnId = destinationColumnId[0] || null;
|
||||
@@ -101,9 +104,13 @@ export const handleDragDrop = async (
|
||||
else return await store?.removeIssue(workspaceSlug, projectId, removed);
|
||||
}
|
||||
} else {
|
||||
const sourceIssues = subGroupBy
|
||||
? (issueWithIds as TSubGroupedIssues)[sourceSubGroupByColumnId][sourceGroupByColumnId]
|
||||
: (issueWithIds as TGroupedIssues)[sourceGroupByColumnId];
|
||||
//spreading the array to stop changing the original reference
|
||||
//since we are removing an id from array further down
|
||||
const sourceIssues = [
|
||||
...(subGroupBy
|
||||
? (issueWithIds as TSubGroupedIssues)[sourceSubGroupByColumnId][sourceGroupByColumnId]
|
||||
: (issueWithIds as TGroupedIssues)[sourceGroupByColumnId]),
|
||||
];
|
||||
const destinationIssues = subGroupBy
|
||||
? (issueWithIds as TSubGroupedIssues)[sourceSubGroupByColumnId][destinationGroupByColumnId]
|
||||
: (issueWithIds as TGroupedIssues)[destinationGroupByColumnId];
|
||||
@@ -119,7 +126,11 @@ export const handleDragDrop = async (
|
||||
// for both horizontal and vertical dnd
|
||||
updateIssue = {
|
||||
...updateIssue,
|
||||
...handleSortOrder(destinationIssues, destination.index, issueMap),
|
||||
...handleSortOrder(
|
||||
sourceDroppableId === destinationDroppableId ? sourceIssues : destinationIssues,
|
||||
destination.index,
|
||||
issueMap
|
||||
),
|
||||
};
|
||||
|
||||
if (subGroupBy && sourceSubGroupByColumnId && destinationSubGroupByColumnId) {
|
||||
|
||||
@@ -50,9 +50,8 @@ export const IssueBlock: React.FC<IssueBlockProps> = observer((props: IssueBlock
|
||||
return (
|
||||
<div
|
||||
className={cn("min-h-12 relative flex items-center gap-3 bg-custom-background-100 p-3 text-sm", {
|
||||
"border border-custom-primary-70 hover:border-custom-primary-70":
|
||||
peekIssue && peekIssue.issueId === issue.id,
|
||||
"last:border-b-transparent": peekIssue?.issueId !== issue.id
|
||||
"border border-custom-primary-70 hover:border-custom-primary-70": peekIssue && peekIssue.issueId === issue.id,
|
||||
"last:border-b-transparent": peekIssue?.issueId !== issue.id,
|
||||
})}
|
||||
>
|
||||
{displayProperties && displayProperties?.key && (
|
||||
|
||||
@@ -41,7 +41,7 @@ export const HeaderGroupByCard = observer(
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const renderExistingIssueModal = moduleId || cycleId;
|
||||
const ExistingIssuesListModalPayload = moduleId ? { module: [moduleId.toString()] } : { cycle: true };
|
||||
const ExistingIssuesListModalPayload = moduleId ? { module: moduleId.toString() } : { cycle: true };
|
||||
|
||||
const handleAddIssuesToView = async (data: ISearchIssueResponse[]) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { TIssue } from "@plane/types";
|
||||
|
||||
export interface IQuickActionProps {
|
||||
issue: TIssue;
|
||||
handleDelete: () => Promise<void>;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
import { Copy, Link, Pencil, Trash2 } from "lucide-react";
|
||||
import omit from "lodash/omit";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
import { useEventTracker } from "hooks/store";
|
||||
@@ -30,7 +31,7 @@ export const AllIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const handleCopyIssueLink = () => {
|
||||
copyUrlToClipboard(`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`).then(() =>
|
||||
copyUrlToClipboard(`/${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`).then(() =>
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Link copied",
|
||||
@@ -39,11 +40,13 @@ export const AllIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const duplicateIssuePayload = {
|
||||
...issue,
|
||||
name: `${issue.name} (copy)`,
|
||||
};
|
||||
delete duplicateIssuePayload.id;
|
||||
const duplicateIssuePayload = omit(
|
||||
{
|
||||
...issue,
|
||||
name: `${issue.name} (copy)`,
|
||||
},
|
||||
["id"]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -87,7 +90,7 @@ export const AllIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setTrackElement("Global issues");
|
||||
setIssueToEdit(issue);
|
||||
setIssueToEdit(issue);
|
||||
setCreateUpdateIssueModal(true);
|
||||
}}
|
||||
>
|
||||
@@ -99,7 +102,7 @@ export const AllIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setTrackElement("Global issues");
|
||||
setCreateUpdateIssueModal(true);
|
||||
setCreateUpdateIssueModal(true);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -110,7 +113,7 @@ export const AllIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setTrackElement("Global issues");
|
||||
setDeleteIssueModal(true);
|
||||
setDeleteIssueModal(true);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -4,7 +4,7 @@ import { CustomMenu } from "@plane/ui";
|
||||
import { Link, Trash2 } from "lucide-react";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
import { useEventTracker, useIssues ,useUser} from "hooks/store";
|
||||
import { useEventTracker, useIssues, useUser } from "hooks/store";
|
||||
// components
|
||||
import { DeleteArchivedIssueModal } from "components/issues";
|
||||
// helpers
|
||||
@@ -37,7 +37,7 @@ export const ArchivedIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
|
||||
const activeLayout = `${issuesFilter.issueFilters?.displayFilters?.layout} layout`;
|
||||
|
||||
const handleCopyIssueLink = () => {
|
||||
copyUrlToClipboard(`${workspaceSlug}/projects/${issue.project}/archived-issues/${issue.id}`).then(() =>
|
||||
copyUrlToClipboard(`${workspaceSlug}/projects/${issue.project_id}/archived-issues/${issue.id}`).then(() =>
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Link copied",
|
||||
@@ -75,7 +75,7 @@ export const ArchivedIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setTrackElement(activeLayout);
|
||||
setDeleteIssueModal(true);
|
||||
setDeleteIssueModal(true);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -2,9 +2,10 @@ import { useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
import { Copy, Link, Pencil, Trash2, XCircle } from "lucide-react";
|
||||
import omit from "lodash/omit";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
import { useEventTracker, useIssues,useUser } from "hooks/store";
|
||||
import { useEventTracker, useIssues, useUser } from "hooks/store";
|
||||
// components
|
||||
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
||||
// helpers
|
||||
@@ -49,7 +50,7 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
const activeLayout = `${issuesFilter.issueFilters?.displayFilters?.layout} layout`;
|
||||
|
||||
const handleCopyIssueLink = () => {
|
||||
copyUrlToClipboard(`${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`).then(() =>
|
||||
copyUrlToClipboard(`${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`).then(() =>
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Link copied",
|
||||
@@ -58,11 +59,13 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const duplicateIssuePayload = {
|
||||
...issue,
|
||||
name: `${issue.name} (copy)`,
|
||||
};
|
||||
delete duplicateIssuePayload.id;
|
||||
const duplicateIssuePayload = omit(
|
||||
{
|
||||
...issue,
|
||||
name: `${issue.name} (copy)`,
|
||||
},
|
||||
["id"]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -107,10 +110,10 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
onClick={() => {
|
||||
setIssueToEdit({
|
||||
...issue,
|
||||
cycle: cycleId?.toString() ?? null,
|
||||
cycle_id: cycleId?.toString() ?? null,
|
||||
});
|
||||
setTrackElement(activeLayout);
|
||||
setCreateUpdateIssueModal(true);
|
||||
setCreateUpdateIssueModal(true);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -131,7 +134,7 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setTrackElement(activeLayout);
|
||||
setCreateUpdateIssueModal(true);
|
||||
setCreateUpdateIssueModal(true);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -142,7 +145,7 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setTrackElement(activeLayout);
|
||||
setDeleteIssueModal(true);
|
||||
setDeleteIssueModal(true);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -2,9 +2,10 @@ import { useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
import { Copy, Link, Pencil, Trash2, XCircle } from "lucide-react";
|
||||
import omit from "lodash/omit";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
import { useIssues, useEventTracker ,useUser } from "hooks/store";
|
||||
import { useIssues, useEventTracker, useUser } from "hooks/store";
|
||||
// components
|
||||
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
||||
// helpers
|
||||
@@ -49,7 +50,7 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
const activeLayout = `${issuesFilter.issueFilters?.displayFilters?.layout} layout`;
|
||||
|
||||
const handleCopyIssueLink = () => {
|
||||
copyUrlToClipboard(`${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`).then(() =>
|
||||
copyUrlToClipboard(`${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`).then(() =>
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Link copied",
|
||||
@@ -58,11 +59,13 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const duplicateIssuePayload = {
|
||||
...issue,
|
||||
name: `${issue.name} (copy)`,
|
||||
};
|
||||
delete duplicateIssuePayload.id;
|
||||
const duplicateIssuePayload = omit(
|
||||
{
|
||||
...issue,
|
||||
name: `${issue.name} (copy)`,
|
||||
},
|
||||
["id"]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -105,9 +108,9 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
<>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setIssueToEdit({ ...issue, module: moduleId?.toString() ?? null });
|
||||
setIssueToEdit({ ...issue, module_ids: moduleId ? [moduleId.toString()] : [] });
|
||||
setTrackElement(activeLayout);
|
||||
setCreateUpdateIssueModal(true);
|
||||
setCreateUpdateIssueModal(true);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -128,7 +131,7 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setTrackElement(activeLayout);
|
||||
setCreateUpdateIssueModal(true);
|
||||
setCreateUpdateIssueModal(true);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -141,7 +144,7 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setTrackElement(activeLayout);
|
||||
setDeleteIssueModal(true);
|
||||
setDeleteIssueModal(true);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
import { Copy, Link, Pencil, Trash2 } from "lucide-react";
|
||||
import omit from "lodash/omit";
|
||||
// hooks
|
||||
import { useEventTracker, useIssues, useUser } from "hooks/store";
|
||||
import useToast from "hooks/use-toast";
|
||||
@@ -39,7 +40,7 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const handleCopyIssueLink = () => {
|
||||
copyUrlToClipboard(`${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`).then(() =>
|
||||
copyUrlToClipboard(`${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`).then(() =>
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Link copied",
|
||||
@@ -48,11 +49,13 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
|
||||
);
|
||||
};
|
||||
|
||||
const duplicateIssuePayload = {
|
||||
...issue,
|
||||
name: `${issue.name} (copy)`,
|
||||
};
|
||||
delete duplicateIssuePayload.id;
|
||||
const duplicateIssuePayload = omit(
|
||||
{
|
||||
...issue,
|
||||
name: `${issue.name} (copy)`,
|
||||
},
|
||||
["id"]
|
||||
);
|
||||
|
||||
const isDraftIssue = router?.asPath?.includes("draft-issues") || false;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useMemo } from "react";
|
||||
import React, { Fragment, useCallback, useMemo } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import useSWR from "swr";
|
||||
@@ -12,15 +12,15 @@ import { GlobalViewsAppliedFiltersRoot, IssuePeekOverview } from "components/iss
|
||||
import { SpreadsheetView } from "components/issues/issue-layouts";
|
||||
import { AllIssueQuickActions } from "components/issues/issue-layouts/quick-action-dropdowns";
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
// ui
|
||||
import { Spinner } from "@plane/ui";
|
||||
import { SpreadsheetLayoutLoader } from "components/ui";
|
||||
// types
|
||||
import { TIssue, IIssueDisplayFilterOptions } from "@plane/types";
|
||||
import { EIssueActions } from "../types";
|
||||
// constants
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
|
||||
import { ALL_ISSUES_EMPTY_STATE_DETAILS, EUserWorkspaceRoles } from "constants/workspace";
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
import { ALL_ISSUES_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
|
||||
export const AllIssueLayoutRoot: React.FC = observer(() => {
|
||||
// router
|
||||
@@ -159,7 +159,7 @@ export const AllIssueLayoutRoot: React.FC = observer(() => {
|
||||
globalViewId.toString()
|
||||
);
|
||||
},
|
||||
[updateFilters, workspaceSlug]
|
||||
[updateFilters, workspaceSlug, globalViewId]
|
||||
);
|
||||
|
||||
const renderQuickActions = useCallback(
|
||||
@@ -177,66 +177,62 @@ export const AllIssueLayoutRoot: React.FC = observer(() => {
|
||||
|
||||
const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
||||
|
||||
if (loader === "init-loader" || !globalViewId || globalViewId !== dataViewId || !issueIds) {
|
||||
return <SpreadsheetLayoutLoader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full w-full flex-col overflow-hidden">
|
||||
{!globalViewId || globalViewId !== dataViewId || loader === "init-loader" || !issueIds ? (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<GlobalViewsAppliedFiltersRoot globalViewId={globalViewId} />
|
||||
|
||||
{(issueIds ?? {}).length == 0 ? (
|
||||
<EmptyState
|
||||
image={emptyStateImage}
|
||||
title={(workspaceProjectIds ?? []).length > 0 ? currentViewDetails.title : "No project"}
|
||||
description={
|
||||
(workspaceProjectIds ?? []).length > 0
|
||||
? currentViewDetails.description
|
||||
: "To create issues or manage your work, you need to create a project or be a part of one."
|
||||
}
|
||||
size="sm"
|
||||
primaryButton={
|
||||
(workspaceProjectIds ?? []).length > 0
|
||||
? currentView !== "custom-view" && currentView !== "subscribed"
|
||||
? {
|
||||
text: "Create new issue",
|
||||
onClick: () => {
|
||||
setTrackElement("All issues empty state");
|
||||
commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);
|
||||
},
|
||||
}
|
||||
: undefined
|
||||
: {
|
||||
text: "Start your first project",
|
||||
<div className="relative h-full w-full overflow-auto">
|
||||
<GlobalViewsAppliedFiltersRoot globalViewId={globalViewId} />
|
||||
{issueIds.length === 0 ? (
|
||||
<EmptyState
|
||||
image={emptyStateImage}
|
||||
title={(workspaceProjectIds ?? []).length > 0 ? currentViewDetails.title : "No project"}
|
||||
description={
|
||||
(workspaceProjectIds ?? []).length > 0
|
||||
? currentViewDetails.description
|
||||
: "To create issues or manage your work, you need to create a project or be a part of one."
|
||||
}
|
||||
size="sm"
|
||||
primaryButton={
|
||||
(workspaceProjectIds ?? []).length > 0
|
||||
? currentView !== "custom-view" && currentView !== "subscribed"
|
||||
? {
|
||||
text: "Create new issue",
|
||||
onClick: () => {
|
||||
setTrackElement("All issues empty state");
|
||||
commandPaletteStore.toggleCreateProjectModal(true);
|
||||
commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);
|
||||
},
|
||||
}
|
||||
}
|
||||
disabled={!isEditingAllowed}
|
||||
: undefined
|
||||
: {
|
||||
text: "Start your first project",
|
||||
onClick: () => {
|
||||
setTrackElement("All issues empty state");
|
||||
commandPaletteStore.toggleCreateProjectModal(true);
|
||||
},
|
||||
}
|
||||
}
|
||||
disabled={!isEditingAllowed}
|
||||
/>
|
||||
) : (
|
||||
<Fragment>
|
||||
<SpreadsheetView
|
||||
displayProperties={issueFilters?.displayProperties ?? {}}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
|
||||
issueIds={issueIds}
|
||||
quickActions={renderQuickActions}
|
||||
handleIssues={handleIssues}
|
||||
canEditProperties={canEditProperties}
|
||||
viewId={globalViewId}
|
||||
/>
|
||||
) : (
|
||||
<div className="relative h-full w-full overflow-auto">
|
||||
<SpreadsheetView
|
||||
displayProperties={issueFilters?.displayProperties ?? {}}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
|
||||
issueIds={issueIds}
|
||||
quickActions={renderQuickActions}
|
||||
handleIssues={handleIssues}
|
||||
canEditProperties={canEditProperties}
|
||||
viewId={globalViewId}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* peek overview */}
|
||||
<IssuePeekOverview />
|
||||
{/* peek overview */}
|
||||
<IssuePeekOverview />
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React, { Fragment } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import useSWR from "swr";
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
} from "components/issues";
|
||||
import { EIssuesStoreType } from "constants/issue";
|
||||
// ui
|
||||
import { Spinner } from "@plane/ui";
|
||||
import { ListLayoutLoader } from "components/ui";
|
||||
|
||||
export const ArchivedIssueLayoutRoot: React.FC = observer(() => {
|
||||
// router
|
||||
@@ -36,30 +36,26 @@ export const ArchivedIssueLayoutRoot: React.FC = observer(() => {
|
||||
}
|
||||
);
|
||||
|
||||
if (issues?.loader === "init-loader" || !issues?.groupedIssueIds) {
|
||||
return <ListLayoutLoader />;
|
||||
}
|
||||
|
||||
if (!workspaceSlug || !projectId) return <></>;
|
||||
return (
|
||||
<div className="relative flex h-full w-full flex-col overflow-hidden">
|
||||
<ArchivedIssueAppliedFiltersRoot />
|
||||
|
||||
{issues?.loader === "init-loader" ? (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Spinner />
|
||||
{issues?.groupedIssueIds?.length === 0 ? (
|
||||
<div className="relative h-full w-full overflow-y-auto">
|
||||
<ProjectArchivedEmptyState />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{!issues?.groupedIssueIds ? (
|
||||
<ProjectArchivedEmptyState />
|
||||
) : (
|
||||
<>
|
||||
<div className="relative h-full w-full overflow-auto">
|
||||
<ArchivedIssueListLayout />
|
||||
</div>
|
||||
|
||||
{/* peek overview */}
|
||||
<IssuePeekOverview is_archived />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
<Fragment>
|
||||
<div className="relative h-full w-full overflow-auto">
|
||||
<ArchivedIssueListLayout />
|
||||
</div>
|
||||
<IssuePeekOverview is_archived={true} />
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { useState } from "react";
|
||||
import React, { Fragment, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import useSWR from "swr";
|
||||
import size from "lodash/size";
|
||||
// hooks
|
||||
import { useCycle, useIssues } from "hooks/store";
|
||||
// components
|
||||
@@ -16,10 +17,11 @@ import {
|
||||
IssuePeekOverview,
|
||||
} from "components/issues";
|
||||
import { TransferIssues, TransferIssuesModal } from "components/cycles";
|
||||
// ui
|
||||
import { Spinner } from "@plane/ui";
|
||||
import { ActiveLoader } from "components/ui";
|
||||
// constants
|
||||
import { EIssuesStoreType } from "constants/issue";
|
||||
import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
|
||||
// types
|
||||
import { IIssueFilterOptions } from "@plane/types";
|
||||
|
||||
export const CycleLayoutRoot: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
@@ -52,47 +54,73 @@ export const CycleLayoutRoot: React.FC = observer(() => {
|
||||
const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined;
|
||||
const cycleStatus = cycleDetails?.status?.toLocaleLowerCase() ?? "draft";
|
||||
|
||||
const userFilters = issuesFilter?.issueFilters?.filters;
|
||||
|
||||
const issueFilterCount = size(
|
||||
Object.fromEntries(
|
||||
Object.entries(userFilters ?? {}).filter(([, value]) => value && Array.isArray(value) && value.length > 0)
|
||||
)
|
||||
);
|
||||
|
||||
const handleClearAllFilters = () => {
|
||||
if (!workspaceSlug || !projectId || !cycleId) return;
|
||||
const newFilters: IIssueFilterOptions = {};
|
||||
Object.keys(userFilters ?? {}).forEach((key) => {
|
||||
newFilters[key as keyof IIssueFilterOptions] = null;
|
||||
});
|
||||
issuesFilter.updateFilters(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
EIssueFilterType.FILTERS,
|
||||
{
|
||||
...newFilters,
|
||||
},
|
||||
cycleId.toString()
|
||||
);
|
||||
};
|
||||
|
||||
if (!workspaceSlug || !projectId || !cycleId) return <></>;
|
||||
|
||||
if (issues?.loader === "init-loader" || !issues?.groupedIssueIds) {
|
||||
return <>{activeLayout && <ActiveLoader layout={activeLayout} />}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<TransferIssuesModal handleClose={() => setTransferIssuesModal(false)} isOpen={transferIssuesModal} />
|
||||
|
||||
<div className="relative flex h-full w-full flex-col overflow-hidden">
|
||||
{cycleStatus === "completed" && <TransferIssues handleClick={() => setTransferIssuesModal(true)} />}
|
||||
<CycleAppliedFiltersRoot />
|
||||
|
||||
{issues?.loader === "init-loader" || !issues?.groupedIssueIds ? (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Spinner />
|
||||
{issues?.groupedIssueIds?.length === 0 ? (
|
||||
<div className="relative h-full w-full overflow-y-auto">
|
||||
<CycleEmptyState
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
cycleId={cycleId.toString()}
|
||||
activeLayout={activeLayout}
|
||||
handleClearAllFilters={handleClearAllFilters}
|
||||
isEmptyFilters={issueFilterCount > 0}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{issues?.groupedIssueIds?.length === 0 ? (
|
||||
<CycleEmptyState
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
cycleId={cycleId.toString()}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<div className="h-full w-full overflow-auto">
|
||||
{activeLayout === "list" ? (
|
||||
<CycleListLayout />
|
||||
) : activeLayout === "kanban" ? (
|
||||
<CycleKanBanLayout />
|
||||
) : activeLayout === "calendar" ? (
|
||||
<CycleCalendarLayout />
|
||||
) : activeLayout === "gantt_chart" ? (
|
||||
<CycleGanttLayout />
|
||||
) : activeLayout === "spreadsheet" ? (
|
||||
<CycleSpreadsheetLayout />
|
||||
) : null}
|
||||
</div>
|
||||
{/* peek overview */}
|
||||
<IssuePeekOverview />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
<Fragment>
|
||||
<div className="h-full w-full overflow-auto">
|
||||
{activeLayout === "list" ? (
|
||||
<CycleListLayout />
|
||||
) : activeLayout === "kanban" ? (
|
||||
<CycleKanBanLayout />
|
||||
) : activeLayout === "calendar" ? (
|
||||
<CycleCalendarLayout />
|
||||
) : activeLayout === "gantt_chart" ? (
|
||||
<CycleGanttLayout />
|
||||
) : activeLayout === "spreadsheet" ? (
|
||||
<CycleSpreadsheetLayout />
|
||||
) : null}
|
||||
</div>
|
||||
{/* peek overview */}
|
||||
<IssuePeekOverview />
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -9,8 +9,8 @@ import { DraftIssueAppliedFiltersRoot } from "../filters/applied-filters/roots/d
|
||||
import { DraftIssueListLayout } from "../list/roots/draft-issue-root";
|
||||
import { ProjectDraftEmptyState } from "../empty-states";
|
||||
import { IssuePeekOverview } from "components/issues/peek-overview";
|
||||
import { ActiveLoader } from "components/ui";
|
||||
// ui
|
||||
import { Spinner } from "@plane/ui";
|
||||
import { DraftKanBanLayout } from "../kanban/roots/draft-issue-root";
|
||||
// constants
|
||||
import { EIssuesStoreType } from "constants/issue";
|
||||
@@ -39,30 +39,29 @@ export const DraftIssueLayoutRoot: React.FC = observer(() => {
|
||||
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout || undefined;
|
||||
|
||||
if (!workspaceSlug || !projectId) return <></>;
|
||||
|
||||
if (issues?.loader === "init-loader" || !issues?.groupedIssueIds) {
|
||||
return <>{activeLayout && <ActiveLoader layout={activeLayout} />}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full w-full flex-col overflow-hidden">
|
||||
<DraftIssueAppliedFiltersRoot />
|
||||
|
||||
{issues?.loader === "init-loader" ? (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Spinner />
|
||||
{issues?.groupedIssueIds?.length === 0 ? (
|
||||
<div className="relative h-full w-full overflow-y-auto">
|
||||
<ProjectDraftEmptyState />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{!issues?.groupedIssueIds ? (
|
||||
<ProjectDraftEmptyState />
|
||||
) : (
|
||||
<div className="relative h-full w-full overflow-auto">
|
||||
{activeLayout === "list" ? (
|
||||
<DraftIssueListLayout />
|
||||
) : activeLayout === "kanban" ? (
|
||||
<DraftKanBanLayout />
|
||||
) : null}
|
||||
{/* issue peek overview */}
|
||||
<IssuePeekOverview />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
<div className="relative h-full w-full overflow-auto">
|
||||
{activeLayout === "list" ? (
|
||||
<DraftIssueListLayout />
|
||||
) : activeLayout === "kanban" ? (
|
||||
<DraftKanBanLayout />
|
||||
) : null}
|
||||
{/* issue peek overview */}
|
||||
<IssuePeekOverview />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React from "react";
|
||||
import React, { Fragment } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import useSWR from "swr";
|
||||
import size from "lodash/size";
|
||||
// mobx store
|
||||
import { useIssues } from "hooks/store";
|
||||
// components
|
||||
@@ -15,10 +16,11 @@ import {
|
||||
ModuleListLayout,
|
||||
ModuleSpreadsheetLayout,
|
||||
} from "components/issues";
|
||||
// ui
|
||||
import { Spinner } from "@plane/ui";
|
||||
import { ActiveLoader } from "components/ui";
|
||||
// constants
|
||||
import { EIssuesStoreType } from "constants/issue";
|
||||
import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
|
||||
// types
|
||||
import { IIssueFilterOptions } from "@plane/types";
|
||||
|
||||
export const ModuleLayoutRoot: React.FC = observer(() => {
|
||||
// router
|
||||
@@ -44,45 +46,72 @@ export const ModuleLayoutRoot: React.FC = observer(() => {
|
||||
}
|
||||
);
|
||||
|
||||
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout || undefined;
|
||||
const userFilters = issuesFilter?.issueFilters?.filters;
|
||||
|
||||
const issueFilterCount = size(
|
||||
Object.fromEntries(
|
||||
Object.entries(userFilters ?? {}).filter(([, value]) => value && Array.isArray(value) && value.length > 0)
|
||||
)
|
||||
);
|
||||
|
||||
const handleClearAllFilters = () => {
|
||||
if (!workspaceSlug || !projectId || !moduleId) return;
|
||||
const newFilters: IIssueFilterOptions = {};
|
||||
Object.keys(userFilters ?? {}).forEach((key) => {
|
||||
newFilters[key as keyof IIssueFilterOptions] = null;
|
||||
});
|
||||
issuesFilter.updateFilters(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
EIssueFilterType.FILTERS,
|
||||
{
|
||||
...newFilters,
|
||||
},
|
||||
moduleId.toString()
|
||||
);
|
||||
};
|
||||
|
||||
if (!workspaceSlug || !projectId || !moduleId) return <></>;
|
||||
|
||||
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout || undefined;
|
||||
|
||||
if (issues?.loader === "init-loader" || !issues?.groupedIssueIds) {
|
||||
return <>{activeLayout && <ActiveLoader layout={activeLayout} />}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full w-full flex-col overflow-hidden">
|
||||
<ModuleAppliedFiltersRoot />
|
||||
|
||||
{issues?.loader === "init-loader" || !issues?.groupedIssueIds ? (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Spinner />
|
||||
{issues?.groupedIssueIds?.length === 0 ? (
|
||||
<div className="relative h-full w-full overflow-y-auto">
|
||||
<ModuleEmptyState
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
moduleId={moduleId.toString()}
|
||||
activeLayout={activeLayout}
|
||||
handleClearAllFilters={handleClearAllFilters}
|
||||
isEmptyFilters={issueFilterCount > 0}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{issues?.groupedIssueIds?.length === 0 ? (
|
||||
<ModuleEmptyState
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
moduleId={moduleId.toString()}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<div className="h-full w-full overflow-auto">
|
||||
{activeLayout === "list" ? (
|
||||
<ModuleListLayout />
|
||||
) : activeLayout === "kanban" ? (
|
||||
<ModuleKanBanLayout />
|
||||
) : activeLayout === "calendar" ? (
|
||||
<ModuleCalendarLayout />
|
||||
) : activeLayout === "gantt_chart" ? (
|
||||
<ModuleGanttLayout />
|
||||
) : activeLayout === "spreadsheet" ? (
|
||||
<ModuleSpreadsheetLayout />
|
||||
) : null}
|
||||
</div>
|
||||
{/* peek overview */}
|
||||
<IssuePeekOverview />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
<Fragment>
|
||||
<div className="h-full w-full overflow-auto">
|
||||
{activeLayout === "list" ? (
|
||||
<ModuleListLayout />
|
||||
) : activeLayout === "kanban" ? (
|
||||
<ModuleKanBanLayout />
|
||||
) : activeLayout === "calendar" ? (
|
||||
<ModuleCalendarLayout />
|
||||
) : activeLayout === "gantt_chart" ? (
|
||||
<ModuleGanttLayout />
|
||||
) : activeLayout === "spreadsheet" ? (
|
||||
<ModuleSpreadsheetLayout />
|
||||
) : null}
|
||||
</div>
|
||||
{/* peek overview */}
|
||||
<IssuePeekOverview />
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FC } from "react";
|
||||
import { FC, Fragment } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import useSWR from "swr";
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
import { Spinner } from "@plane/ui";
|
||||
// hooks
|
||||
import { useIssues } from "hooks/store";
|
||||
// helpers
|
||||
import { ActiveLoader } from "components/ui";
|
||||
// constants
|
||||
import { EIssuesStoreType } from "constants/issue";
|
||||
|
||||
@@ -41,48 +43,42 @@ export const ProjectLayoutRoot: FC = observer(() => {
|
||||
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
|
||||
|
||||
if (!workspaceSlug || !projectId) return <></>;
|
||||
|
||||
if (issues?.loader === "init-loader" || !issues?.groupedIssueIds) {
|
||||
return <>{activeLayout && <ActiveLoader layout={activeLayout} />}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full w-full flex-col overflow-hidden">
|
||||
<ProjectAppliedFiltersRoot />
|
||||
|
||||
{issues?.loader === "init-loader" || !issues?.groupedIssueIds ? (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
{issues?.groupedIssueIds?.length === 0 ? (
|
||||
<ProjectEmptyState />
|
||||
) : (
|
||||
<>
|
||||
{issues?.groupedIssueIds?.length === 0 ? (
|
||||
<div className="relative h-full w-full overflow-y-auto">
|
||||
<ProjectEmptyState />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="relative h-full w-full overflow-auto bg-custom-background-90">
|
||||
{/* mutation loader */}
|
||||
{issues?.loader === "mutation" && (
|
||||
<div className="fixed w-[40px] h-[40px] z-50 right-[20px] top-[70px] flex justify-center items-center bg-custom-background-80 shadow-sm rounded">
|
||||
<Spinner className="w-4 h-4" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeLayout === "list" ? (
|
||||
<ListLayout />
|
||||
) : activeLayout === "kanban" ? (
|
||||
<KanBanLayout />
|
||||
) : activeLayout === "calendar" ? (
|
||||
<CalendarLayout />
|
||||
) : activeLayout === "gantt_chart" ? (
|
||||
<GanttLayout />
|
||||
) : activeLayout === "spreadsheet" ? (
|
||||
<ProjectSpreadsheetLayout />
|
||||
) : null}
|
||||
<Fragment>
|
||||
<div className="relative h-full w-full overflow-auto bg-custom-background-90">
|
||||
{/* mutation loader */}
|
||||
{issues?.loader === "mutation" && (
|
||||
<div className="fixed w-[40px] h-[40px] z-50 right-[20px] top-[70px] flex justify-center items-center bg-custom-background-80 shadow-sm rounded">
|
||||
<Spinner className="w-4 h-4" />
|
||||
</div>
|
||||
)}
|
||||
{activeLayout === "list" ? (
|
||||
<ListLayout />
|
||||
) : activeLayout === "kanban" ? (
|
||||
<KanBanLayout />
|
||||
) : activeLayout === "calendar" ? (
|
||||
<CalendarLayout />
|
||||
) : activeLayout === "gantt_chart" ? (
|
||||
<GanttLayout />
|
||||
) : activeLayout === "spreadsheet" ? (
|
||||
<ProjectSpreadsheetLayout />
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{/* peek overview */}
|
||||
<IssuePeekOverview />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
{/* peek overview */}
|
||||
<IssuePeekOverview />
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useMemo } from "react";
|
||||
import React, { Fragment, useMemo } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import useSWR from "swr";
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
ProjectViewListLayout,
|
||||
ProjectViewSpreadsheetLayout,
|
||||
} from "components/issues";
|
||||
import { Spinner } from "@plane/ui";
|
||||
import { ActiveLoader } from "components/ui";
|
||||
// constants
|
||||
import { EIssuesStoreType } from "constants/issue";
|
||||
// types
|
||||
@@ -63,39 +63,38 @@ export const ProjectViewLayoutRoot: React.FC = observer(() => {
|
||||
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
|
||||
|
||||
if (!workspaceSlug || !projectId || !viewId) return <></>;
|
||||
|
||||
if (issues?.loader === "init-loader" || !issues?.groupedIssueIds) {
|
||||
return <>{activeLayout && <ActiveLoader layout={activeLayout} />}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full w-full flex-col overflow-hidden">
|
||||
<ProjectViewAppliedFiltersRoot />
|
||||
|
||||
{issues?.loader === "init-loader" || !issues?.groupedIssueIds ? (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Spinner />
|
||||
{issues?.groupedIssueIds?.length === 0 ? (
|
||||
<div className="relative h-full w-full overflow-y-auto">
|
||||
<ProjectViewEmptyState />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{issues?.groupedIssueIds?.length === 0 ? (
|
||||
<ProjectViewEmptyState />
|
||||
) : (
|
||||
<>
|
||||
<div className="relative h-full w-full overflow-auto">
|
||||
{activeLayout === "list" ? (
|
||||
<ProjectViewListLayout issueActions={issueActions} />
|
||||
) : activeLayout === "kanban" ? (
|
||||
<ProjectViewKanBanLayout issueActions={issueActions} />
|
||||
) : activeLayout === "calendar" ? (
|
||||
<ProjectViewCalendarLayout issueActions={issueActions} />
|
||||
) : activeLayout === "gantt_chart" ? (
|
||||
<ProjectViewGanttLayout issueActions={issueActions} />
|
||||
) : activeLayout === "spreadsheet" ? (
|
||||
<ProjectViewSpreadsheetLayout issueActions={issueActions} />
|
||||
) : null}
|
||||
</div>
|
||||
<Fragment>
|
||||
<div className="relative h-full w-full overflow-auto">
|
||||
{activeLayout === "list" ? (
|
||||
<ProjectViewListLayout issueActions={issueActions} />
|
||||
) : activeLayout === "kanban" ? (
|
||||
<ProjectViewKanBanLayout issueActions={issueActions} />
|
||||
) : activeLayout === "calendar" ? (
|
||||
<ProjectViewCalendarLayout issueActions={issueActions} />
|
||||
) : activeLayout === "gantt_chart" ? (
|
||||
<ProjectViewGanttLayout issueActions={issueActions} />
|
||||
) : activeLayout === "spreadsheet" ? (
|
||||
<ProjectViewSpreadsheetLayout issueActions={issueActions} />
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{/* peek overview */}
|
||||
<IssuePeekOverview />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
{/* peek overview */}
|
||||
<IssuePeekOverview />
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -88,7 +88,7 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => {
|
||||
viewId
|
||||
);
|
||||
},
|
||||
[issueFiltersStore, projectId, workspaceSlug, viewId]
|
||||
[issueFiltersStore?.updateFilters, projectId, workspaceSlug, viewId]
|
||||
);
|
||||
|
||||
const renderQuickActions = useCallback(
|
||||
|
||||
@@ -38,7 +38,7 @@ export const IssueColumn = observer((props: Props) => {
|
||||
>
|
||||
<td
|
||||
tabIndex={0}
|
||||
className="h-11 w-full min-w-[8rem] bg-custom-background-100 text-sm after:absolute after:w-full after:bottom-[-1px] after:border after:border-custom-border-100 border-r-[1px] border-custom-border-100 focus:border-custom-primary-70"
|
||||
className="h-11 w-full min-w-[8rem] bg-custom-background-100 text-sm after:absolute after:w-full after:bottom-[-1px] after:border after:border-custom-border-100 border-r-[1px] border-custom-border-100"
|
||||
ref={tableCellRef}
|
||||
>
|
||||
<Column
|
||||
|
||||
@@ -28,7 +28,7 @@ export const SpreadsheetHeaderColumn = observer((props: Props) => {
|
||||
shouldRenderProperty={shouldRenderProperty}
|
||||
>
|
||||
<th
|
||||
className="h-11 w-full min-w-[8rem] items-center bg-custom-background-90 text-sm font-medium px-4 py-1 border border-b-0 border-t-0 border-custom-border-100 focus:border-custom-primary-70"
|
||||
className="h-11 w-full min-w-[8rem] items-center bg-custom-background-90 text-sm font-medium px-4 py-1 border border-b-0 border-t-0 border-custom-border-100"
|
||||
ref={tableHeaderCellRef}
|
||||
tabIndex={0}
|
||||
>
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { FC } from "react";
|
||||
// hooks
|
||||
import { useIssueDetail, useProject, useUser } from "hooks/store";
|
||||
// components
|
||||
import { IssueDescriptionForm, TIssueOperations } from "components/issues";
|
||||
import { IssueReaction } from "../issue-detail/reactions";
|
||||
import { FC, useCallback, useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// store hooks
|
||||
import { useIssueDetail, useProject, useUser } from "hooks/store";
|
||||
// hooks
|
||||
import useReloadConfirmations from "hooks/use-reload-confirmation";
|
||||
// components
|
||||
import { TIssueOperations } from "components/issues";
|
||||
import { IssueReaction } from "../issue-detail/reactions";
|
||||
import { IssueTitleInput } from "../title-input";
|
||||
import { IssueDescriptionInput } from "../description-input";
|
||||
import { debounce } from "lodash";
|
||||
|
||||
interface IPeekOverviewIssueDetails {
|
||||
workspaceSlug: string;
|
||||
@@ -17,17 +22,18 @@ interface IPeekOverviewIssueDetails {
|
||||
}
|
||||
|
||||
export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = observer((props) => {
|
||||
const { workspaceSlug, projectId, issueId, issueOperations, disabled, isSubmitting, setIsSubmitting } = props;
|
||||
const { workspaceSlug, issueId, issueOperations, disabled, setIsSubmitting } = props;
|
||||
// store hooks
|
||||
const { getProjectById } = useProject();
|
||||
const { currentUser } = useUser();
|
||||
const {
|
||||
issue: { getIssueById },
|
||||
} = useIssueDetail();
|
||||
|
||||
// derived values
|
||||
const issue = getIssueById(issueId);
|
||||
|
||||
if (!issue) return <></>;
|
||||
|
||||
const projectDetails = getProjectById(issue?.project_id);
|
||||
|
||||
return (
|
||||
@@ -35,20 +41,28 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = observer(
|
||||
<span className="text-base font-medium text-custom-text-400">
|
||||
{projectDetails?.identifier}-{issue?.sequence_id}
|
||||
</span>
|
||||
<IssueDescriptionForm
|
||||
<IssueTitleInput
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
projectId={issue.project_id}
|
||||
issueId={issue.id}
|
||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||
isSubmitting={isSubmitting}
|
||||
issue={issue}
|
||||
issueOperations={issueOperations}
|
||||
disabled={disabled}
|
||||
value={issue.name}
|
||||
/>
|
||||
<IssueDescriptionInput
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={issue.project_id}
|
||||
issueId={issue.id}
|
||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||
issueOperations={issueOperations}
|
||||
disabled={disabled}
|
||||
value={issue.description_html}
|
||||
/>
|
||||
{currentUser && (
|
||||
<IssueReaction
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
projectId={issue.project_id}
|
||||
issueId={issueId}
|
||||
currentUser={currentUser}
|
||||
/>
|
||||
|
||||
@@ -69,20 +69,11 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
||||
// state
|
||||
const [loader, setLoader] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (peekIssue) {
|
||||
setLoader(true);
|
||||
fetchIssue(peekIssue.workspaceSlug, peekIssue.projectId, peekIssue.issueId).finally(() => {
|
||||
setLoader(false);
|
||||
});
|
||||
}
|
||||
}, [peekIssue, fetchIssue]);
|
||||
|
||||
const issueOperations: TIssuePeekOperations = useMemo(
|
||||
() => ({
|
||||
fetch: async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||
try {
|
||||
await fetchIssue(workspaceSlug, projectId, issueId);
|
||||
await fetchIssue(workspaceSlug, projectId, issueId, is_archived);
|
||||
} catch (error) {
|
||||
console.error("Error fetching the parent issue");
|
||||
}
|
||||
@@ -324,9 +315,20 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
||||
removeModulesFromIssue,
|
||||
setToastAlert,
|
||||
onIssueUpdate,
|
||||
captureIssueEvent,
|
||||
router.asPath,
|
||||
]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (peekIssue) {
|
||||
setLoader(true);
|
||||
issueOperations.fetch(peekIssue.workspaceSlug, peekIssue.projectId, peekIssue.issueId).finally(() => {
|
||||
setLoader(false);
|
||||
});
|
||||
}
|
||||
}, [peekIssue, issueOperations]);
|
||||
|
||||
if (!peekIssue?.workspaceSlug || !peekIssue?.projectId || !peekIssue?.issueId) return <></>;
|
||||
|
||||
const issue = getIssueById(peekIssue.issueId) || undefined;
|
||||
|
||||
@@ -126,7 +126,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="w-full truncate !text-base">
|
||||
<div className="w-full !text-base">
|
||||
{issueId && (
|
||||
<div
|
||||
ref={issuePeekOverviewRef}
|
||||
@@ -230,11 +230,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
<IssueActivity
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
/>
|
||||
<IssueActivity workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} />
|
||||
</div>
|
||||
) : (
|
||||
<div className={`flex h-full w-full overflow-auto`}>
|
||||
@@ -250,11 +246,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||
/>
|
||||
|
||||
<IssueActivity
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
/>
|
||||
<IssueActivity workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { Fragment, useRef, useState } from "react";
|
||||
import React, { Fragment, useEffect, useRef, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
import { usePopper } from "react-popper";
|
||||
@@ -36,6 +36,7 @@ export const IssueLabelSelect: React.FC<Props> = observer((props) => {
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
// refs
|
||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
// popper
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: "bottom-start",
|
||||
@@ -76,6 +77,12 @@ export const IssueLabelSelect: React.FC<Props> = observer((props) => {
|
||||
|
||||
useOutsideClickDetector(dropdownRef, handleClose);
|
||||
|
||||
useEffect(() => {
|
||||
if (isDropdownOpen && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [isDropdownOpen]);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as="div"
|
||||
@@ -125,6 +132,8 @@ export const IssueLabelSelect: React.FC<Props> = observer((props) => {
|
||||
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
|
||||
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
|
||||
<Combobox.Input
|
||||
as="input"
|
||||
ref={inputRef}
|
||||
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
placeholder="Search"
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
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";
|
||||
import useReloadConfirmations from "hooks/use-reload-confirmation";
|
||||
|
||||
export type IssueTitleInputProps = {
|
||||
disabled?: boolean;
|
||||
value: string | undefined | null;
|
||||
workspaceSlug: string;
|
||||
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 { setShowAlert } = useReloadConfirmations();
|
||||
const debouncedValue = useDebounce(title, 1500);
|
||||
|
||||
useEffect(() => {
|
||||
if (value) setTitle(value);
|
||||
}, [value]);
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedValue) {
|
||||
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>) => {
|
||||
setShowAlert(true);
|
||||
setIsSubmitting("submitting");
|
||||
setTitle(e.target.value);
|
||||
},
|
||||
[setIsSubmitting, setShowAlert]
|
||||
);
|
||||
|
||||
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>
|
||||
);
|
||||
});
|
||||
@@ -9,8 +9,9 @@ import {
|
||||
DropResult,
|
||||
Droppable,
|
||||
} from "@hello-pangea/dnd";
|
||||
import { useTheme } from "next-themes";
|
||||
// hooks
|
||||
import { useLabel } from "hooks/store";
|
||||
import { useLabel, useUser } from "hooks/store";
|
||||
import useDraggableInPortal from "hooks/use-draggable-portal";
|
||||
// components
|
||||
import {
|
||||
@@ -19,13 +20,13 @@ import {
|
||||
ProjectSettingLabelGroup,
|
||||
ProjectSettingLabelItem,
|
||||
} from "components/labels";
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
// ui
|
||||
import { Button, Loader } from "@plane/ui";
|
||||
import { EmptyState } from "components/common";
|
||||
// images
|
||||
import emptyLabel from "public/empty-state/label.svg";
|
||||
// types
|
||||
import { IIssueLabel } from "@plane/types";
|
||||
// constants
|
||||
import { PROJECT_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
|
||||
const LABELS_ROOT = "labels.root";
|
||||
|
||||
@@ -40,7 +41,10 @@ export const ProjectSettingsLabelList: React.FC = observer(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store hooks
|
||||
const { currentUser } = useUser();
|
||||
const { projectLabels, updateLabelPosition, projectLabelsTree } = useLabel();
|
||||
// portal
|
||||
const renderDraggable = useDraggableInPortal();
|
||||
@@ -50,6 +54,10 @@ export const ProjectSettingsLabelList: React.FC = observer(() => {
|
||||
setLabelForm(true);
|
||||
};
|
||||
|
||||
const emptyStateDetail = PROJECT_SETTINGS_EMPTY_STATE_DETAILS["labels"];
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const emptyStateImage = getEmptyStateImagePath("project-settings", "labels", isLightMode);
|
||||
|
||||
const onDragEnd = (result: DropResult) => {
|
||||
const { combine, draggableId, destination, source } = result;
|
||||
|
||||
@@ -94,7 +102,7 @@ export const ProjectSettingsLabelList: React.FC = observer(() => {
|
||||
Add label
|
||||
</Button>
|
||||
</div>
|
||||
<div className="w-full py-8">
|
||||
<div className="h-full w-full py-8">
|
||||
{showLabelForm && (
|
||||
<div className="w-full rounded border border-custom-border-200 px-3.5 py-2 my-2">
|
||||
<CreateUpdateLabelInline
|
||||
@@ -111,15 +119,14 @@ export const ProjectSettingsLabelList: React.FC = observer(() => {
|
||||
)}
|
||||
{projectLabels ? (
|
||||
projectLabels.length === 0 && !showLabelForm ? (
|
||||
<EmptyState
|
||||
title="No labels yet"
|
||||
description="Create labels to help organize and filter issues in you project"
|
||||
image={emptyLabel}
|
||||
primaryButton={{
|
||||
text: "Add label",
|
||||
onClick: () => newLabel(),
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-center justify-center h-full w-full">
|
||||
<EmptyState
|
||||
title={emptyStateDetail.title}
|
||||
description={emptyStateDetail.description}
|
||||
image={emptyStateImage}
|
||||
size="lg"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
projectLabelsTree && (
|
||||
<DragDropContext
|
||||
|
||||
@@ -8,9 +8,10 @@ import useLocalStorage from "hooks/use-local-storage";
|
||||
import { ModuleCardItem, ModuleListItem, ModulePeekOverview, ModulesListGanttChartView } from "components/modules";
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
// ui
|
||||
import { Loader, Spinner } from "@plane/ui";
|
||||
import { CycleModuleBoardLayout, CycleModuleListLayout, GanttLayoutLoader } from "components/ui";
|
||||
// constants
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
import { MODULE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
|
||||
export const ModulesListView: React.FC = observer(() => {
|
||||
// router
|
||||
@@ -34,23 +35,13 @@ export const ModulesListView: React.FC = observer(() => {
|
||||
|
||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||
|
||||
if (loader)
|
||||
if (loader || !projectModuleIds)
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full w-full">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
|
||||
if (!projectModuleIds)
|
||||
return (
|
||||
<Loader className="grid grid-cols-3 gap-4 p-8">
|
||||
<Loader.Item height="176px" />
|
||||
<Loader.Item height="176px" />
|
||||
<Loader.Item height="176px" />
|
||||
<Loader.Item height="176px" />
|
||||
<Loader.Item height="176px" />
|
||||
<Loader.Item height="176px" />
|
||||
</Loader>
|
||||
<>
|
||||
{modulesView === "list" && <CycleModuleListLayout />}
|
||||
{modulesView === "grid" && <CycleModuleBoardLayout />}
|
||||
{modulesView === "gantt_chart" && <GanttLayoutLoader />}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -97,16 +88,15 @@ export const ModulesListView: React.FC = observer(() => {
|
||||
</>
|
||||
) : (
|
||||
<EmptyState
|
||||
title="Map your project milestones to Modules and track aggregated work easily."
|
||||
description="A group of issues that belong to a logical, hierarchical parent form a module. Think of them as a way to track work by project milestones. They have their own periods and deadlines as well as analytics to help you see how close or far you are from a milestone."
|
||||
title={MODULE_EMPTY_STATE_DETAILS["modules"].title}
|
||||
description={MODULE_EMPTY_STATE_DETAILS["modules"].description}
|
||||
image={EmptyStateImagePath}
|
||||
comicBox={{
|
||||
title: "Modules help group work by hierarchy.",
|
||||
description:
|
||||
"A cart module, a chassis module, and a warehouse module are all good example of this grouping.",
|
||||
title: MODULE_EMPTY_STATE_DETAILS["modules"].comicBox.title,
|
||||
description: MODULE_EMPTY_STATE_DETAILS["modules"].comicBox.description,
|
||||
}}
|
||||
primaryButton={{
|
||||
text: "Build your first module",
|
||||
text: MODULE_EMPTY_STATE_DETAILS["modules"].primaryButton.text,
|
||||
onClick: () => {
|
||||
setTrackElement("Module empty state");
|
||||
commandPaletteStore.toggleCreateModuleModal(true);
|
||||
|
||||
@@ -9,7 +9,8 @@ import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
// components
|
||||
import { EmptyState } from "components/common";
|
||||
import { SnoozeNotificationModal, NotificationCard, NotificationHeader } from "components/notifications";
|
||||
import { Loader, Tooltip } from "@plane/ui";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
import { NotificationsLoader } from "components/ui";
|
||||
// images
|
||||
import emptyNotification from "public/empty-state/notification.svg";
|
||||
// helpers
|
||||
@@ -188,13 +189,7 @@ export const NotificationPopover = observer(() => {
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<Loader className="space-y-4 overflow-y-auto p-5">
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
</Loader>
|
||||
<NotificationsLoader />
|
||||
)}
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
|
||||
@@ -14,6 +14,7 @@ import { Spinner } from "@plane/ui";
|
||||
// constants
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
import { PRODUCT_TOUR_COMPLETED } from "constants/event-tracker";
|
||||
import { WORKSPACE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
|
||||
export const WorkspaceDashboardView = observer(() => {
|
||||
// theme
|
||||
@@ -78,20 +79,18 @@ export const WorkspaceDashboardView = observer(() => {
|
||||
) : (
|
||||
<EmptyState
|
||||
image={emptyStateImage}
|
||||
title="Overview of your projects, activity, and metrics"
|
||||
description=" Welcome to Plane, we are excited to have you here. Create your first project and track your issues, and this
|
||||
page will transform into a space that helps you progress. Admins will also see items which help their team
|
||||
progress."
|
||||
title={WORKSPACE_EMPTY_STATE_DETAILS["dashboard"].title}
|
||||
description={WORKSPACE_EMPTY_STATE_DETAILS["dashboard"].description}
|
||||
primaryButton={{
|
||||
text: "Build your first project",
|
||||
text: WORKSPACE_EMPTY_STATE_DETAILS["dashboard"].primaryButton.text,
|
||||
onClick: () => {
|
||||
setTrackElement("Dashboard empty state");
|
||||
toggleCreateProjectModal(true);
|
||||
},
|
||||
}}
|
||||
comicBox={{
|
||||
title: "Everything starts with a project in Plane",
|
||||
description: "A project could be a product’s roadmap, a marketing campaign, or launching a new car.",
|
||||
title: WORKSPACE_EMPTY_STATE_DETAILS["dashboard"].comicBox.title,
|
||||
description: WORKSPACE_EMPTY_STATE_DETAILS["dashboard"].comicBox.description,
|
||||
}}
|
||||
size="lg"
|
||||
disabled={!isEditingAllowed}
|
||||
|
||||
@@ -68,7 +68,6 @@ export const CreateUpdatePageModal: FC<Props> = (props) => {
|
||||
state: "SUCCESS",
|
||||
},
|
||||
});
|
||||
console.log("Page updated successfully", pageStore);
|
||||
} else {
|
||||
await createProjectPage(formData);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { PagesListItem } from "./list-item";
|
||||
import { Loader } from "@plane/ui";
|
||||
// constants
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
import { PAGE_EMPTY_STATE_DETAILS } from "constants/page";
|
||||
import { PAGE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
|
||||
type IPagesListView = {
|
||||
pageIds: string[];
|
||||
|
||||
@@ -13,6 +13,7 @@ import { Loader } from "@plane/ui";
|
||||
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||
// constants
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
import { PAGE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
|
||||
export const RecentPagesList: FC = observer(() => {
|
||||
// theme
|
||||
@@ -63,11 +64,11 @@ export const RecentPagesList: FC = observer(() => {
|
||||
) : (
|
||||
<>
|
||||
<EmptyState
|
||||
title="Write a note, a doc, or a full knowledge base"
|
||||
description="Pages help you organise your thoughts to create wikis, discussions or even document heated takes for your project. Use it wisely! Pages will be sorted and grouped by last updated."
|
||||
title={PAGE_EMPTY_STATE_DETAILS["Recent"].title}
|
||||
description={PAGE_EMPTY_STATE_DETAILS["Recent"].description}
|
||||
image={EmptyStateImagePath}
|
||||
primaryButton={{
|
||||
text: "Create new page",
|
||||
text: PAGE_EMPTY_STATE_DETAILS["Recent"].primaryButton.text,
|
||||
onClick: () => commandPaletteStore.toggleCreatePageModal(true),
|
||||
}}
|
||||
size="sm"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React, { useEffect } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import useSWR from "swr";
|
||||
import { observer } from "mobx-react-lite";
|
||||
@@ -7,14 +7,14 @@ import { useTheme } from "next-themes";
|
||||
import { ProfileIssuesListLayout } from "components/issues/issue-layouts/list/roots/profile-issues-root";
|
||||
import { ProfileIssuesKanBanLayout } from "components/issues/issue-layouts/kanban/roots/profile-issues-root";
|
||||
import { IssuePeekOverview, ProfileIssuesAppliedFiltersRoot } from "components/issues";
|
||||
import { Spinner } from "@plane/ui";
|
||||
import { KanbanLayoutLoader, ListLayoutLoader } from "components/ui";
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
// hooks
|
||||
import { useIssues, useUser } from "hooks/store";
|
||||
// constants
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
import { PROFILE_EMPTY_STATE_DETAILS } from "constants/profile";
|
||||
import { EIssuesStoreType } from "constants/issue";
|
||||
import { PROFILE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
|
||||
interface IProfileIssuesPage {
|
||||
type: "assigned" | "subscribed" | "created";
|
||||
@@ -36,10 +36,14 @@ export const ProfileIssuesPage = observer((props: IProfileIssuesPage) => {
|
||||
currentUser,
|
||||
} = useUser();
|
||||
const {
|
||||
issues: { loader, groupedIssueIds, fetchIssues },
|
||||
issues: { loader, groupedIssueIds, fetchIssues, setViewId },
|
||||
issuesFilter: { issueFilters, fetchFilters },
|
||||
} = useIssues(EIssuesStoreType.PROFILE);
|
||||
|
||||
useEffect(() => {
|
||||
setViewId(type);
|
||||
}, [type]);
|
||||
|
||||
useSWR(
|
||||
workspaceSlug && userId ? `CURRENT_WORKSPACE_PROFILE_ISSUES_${workspaceSlug}_${userId}_${type}` : null,
|
||||
async () => {
|
||||
@@ -57,38 +61,33 @@ export const ProfileIssuesPage = observer((props: IProfileIssuesPage) => {
|
||||
|
||||
const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
||||
|
||||
if (!groupedIssueIds || loader === "init-loader")
|
||||
return <>{activeLayout === "list" ? <ListLayoutLoader /> : <KanbanLayoutLoader />}</>;
|
||||
|
||||
if (groupedIssueIds.length === 0) {
|
||||
return (
|
||||
<EmptyState
|
||||
image={emptyStateImage}
|
||||
title={PROFILE_EMPTY_STATE_DETAILS[type].title}
|
||||
description={PROFILE_EMPTY_STATE_DETAILS[type].description}
|
||||
size="sm"
|
||||
disabled={!isEditingAllowed}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{loader === "init-loader" ? (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{groupedIssueIds ? (
|
||||
<>
|
||||
<ProfileIssuesAppliedFiltersRoot />
|
||||
<div className="-z-1 relative h-full w-full overflow-auto">
|
||||
{activeLayout === "list" ? (
|
||||
<ProfileIssuesListLayout />
|
||||
) : activeLayout === "kanban" ? (
|
||||
<ProfileIssuesKanBanLayout />
|
||||
) : null}
|
||||
</div>
|
||||
{/* peek overview */}
|
||||
<IssuePeekOverview />
|
||||
</>
|
||||
) : (
|
||||
<EmptyState
|
||||
image={emptyStateImage}
|
||||
title={PROFILE_EMPTY_STATE_DETAILS[type].title}
|
||||
description={PROFILE_EMPTY_STATE_DETAILS[type].description}
|
||||
size="sm"
|
||||
disabled={!isEditingAllowed}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<ProfileIssuesAppliedFiltersRoot />
|
||||
<div className="-z-1 relative h-full w-full overflow-auto">
|
||||
{activeLayout === "list" ? (
|
||||
<ProfileIssuesListLayout />
|
||||
) : activeLayout === "kanban" ? (
|
||||
<ProfileIssuesKanBanLayout />
|
||||
) : null}
|
||||
</div>
|
||||
{/* peek overview */}
|
||||
<IssuePeekOverview />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -4,10 +4,11 @@ import { useTheme } from "next-themes";
|
||||
import { useApplication, useEventTracker, useProject, useUser } from "hooks/store";
|
||||
// components
|
||||
import { ProjectCard } from "components/project";
|
||||
import { Loader } from "@plane/ui";
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
import { ProjectsLoader } from "components/ui";
|
||||
// constants
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
import { WORKSPACE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
|
||||
export const ProjectCardList = observer(() => {
|
||||
// theme
|
||||
@@ -26,17 +27,7 @@ export const ProjectCardList = observer(() => {
|
||||
|
||||
const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
||||
|
||||
if (!workspaceProjectIds)
|
||||
return (
|
||||
<Loader className="grid grid-cols-3 gap-4">
|
||||
<Loader.Item height="100px" />
|
||||
<Loader.Item height="100px" />
|
||||
<Loader.Item height="100px" />
|
||||
<Loader.Item height="100px" />
|
||||
<Loader.Item height="100px" />
|
||||
<Loader.Item height="100px" />
|
||||
</Loader>
|
||||
);
|
||||
if (!workspaceProjectIds) return <ProjectsLoader />;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -59,18 +50,18 @@ export const ProjectCardList = observer(() => {
|
||||
) : (
|
||||
<EmptyState
|
||||
image={emptyStateImage}
|
||||
title="Start a Project"
|
||||
description="Think of each project as the parent for goal-oriented work. Projects are where Jobs, Cycles, and Modules live and, along with your colleagues, help you achieve that goal."
|
||||
title={WORKSPACE_EMPTY_STATE_DETAILS["projects"].title}
|
||||
description={WORKSPACE_EMPTY_STATE_DETAILS["projects"].description}
|
||||
primaryButton={{
|
||||
text: "Start your first project",
|
||||
text: WORKSPACE_EMPTY_STATE_DETAILS["projects"].primaryButton.text,
|
||||
onClick: () => {
|
||||
setTrackElement("Project empty state");
|
||||
commandPaletteStore.toggleCreateProjectModal(true);
|
||||
},
|
||||
}}
|
||||
comicBox={{
|
||||
title: "Everything starts with a project in Plane",
|
||||
description: "A project could be a product’s roadmap, a marketing campaign, or launching a new car.",
|
||||
title: WORKSPACE_EMPTY_STATE_DETAILS["projects"].comicBox.title,
|
||||
description: WORKSPACE_EMPTY_STATE_DETAILS["projects"].comicBox.description,
|
||||
}}
|
||||
size="lg"
|
||||
disabled={!isEditingAllowed}
|
||||
|
||||
@@ -79,7 +79,7 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
|
||||
return updateProject(workspaceSlug.toString(), project.id, payload)
|
||||
.then((res) => {
|
||||
const changed_properties = Object.keys(dirtyFields);
|
||||
console.log(dirtyFields);
|
||||
|
||||
captureProjectEvent({
|
||||
eventName: PROJECT_UPDATED,
|
||||
payload: {
|
||||
|
||||
@@ -6,7 +6,8 @@ import { useEventTracker, useMember } from "hooks/store";
|
||||
// components
|
||||
import { ProjectMemberListItem, SendProjectInvitationModal } from "components/project";
|
||||
// ui
|
||||
import { Button, Loader } from "@plane/ui";
|
||||
import { Button } from "@plane/ui";
|
||||
import { MembersSettingsLoader } from "components/ui";
|
||||
|
||||
export const ProjectMemberList: React.FC = observer(() => {
|
||||
// states
|
||||
@@ -56,12 +57,7 @@ export const ProjectMemberList: React.FC = observer(() => {
|
||||
</Button>
|
||||
</div>
|
||||
{!projectMemberIds ? (
|
||||
<Loader className="space-y-5">
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
</Loader>
|
||||
<MembersSettingsLoader />
|
||||
) : (
|
||||
<div className="divide-y divide-custom-border-100">
|
||||
{projectMemberIds.length > 0
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
MoreHorizontal,
|
||||
} from "lucide-react";
|
||||
// hooks
|
||||
import { useApplication,useEventTracker, useProject } from "hooks/store";
|
||||
import { useApplication, useEventTracker, useProject } from "hooks/store";
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
import useToast from "hooks/use-toast";
|
||||
// helpers
|
||||
@@ -131,7 +131,7 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
|
||||
|
||||
const handleProjectClick = () => {
|
||||
if (window.innerWidth < 768) {
|
||||
themeStore.toggleSidebar();
|
||||
themeStore.toggleMobileSidebar();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -147,9 +147,8 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
|
||||
{({ open }) => (
|
||||
<>
|
||||
<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" : ""
|
||||
} ${isMenuActive ? "!bg-custom-sidebar-background-80" : ""}`}
|
||||
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" : ""
|
||||
} ${isMenuActive ? "!bg-custom-sidebar-background-80" : ""}`}
|
||||
>
|
||||
{provided && (
|
||||
<Tooltip
|
||||
@@ -158,11 +157,9 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
|
||||
>
|
||||
<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"
|
||||
} ${project.sort_order === null ? "cursor-not-allowed opacity-60" : ""} ${
|
||||
isMenuActive ? "!flex" : ""
|
||||
}`}
|
||||
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"
|
||||
} ${project.sort_order === null ? "cursor-not-allowed opacity-60" : ""} ${isMenuActive ? "!flex" : ""
|
||||
}`}
|
||||
{...provided?.dragHandleProps}
|
||||
>
|
||||
<MoreVertical className="h-3.5" />
|
||||
@@ -173,14 +170,12 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
|
||||
<Tooltip tooltipContent={`${project.name}`} position="right" className="ml-2" disabled={!isCollapsed}>
|
||||
<Disclosure.Button
|
||||
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
|
||||
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 ? (
|
||||
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded uppercase">
|
||||
@@ -200,9 +195,8 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
|
||||
</div>
|
||||
{!isCollapsed && (
|
||||
<ChevronDown
|
||||
className={`hidden h-4 w-4 flex-shrink-0 ${open ? "rotate-180" : ""} ${
|
||||
isMenuActive ? "!block" : ""
|
||||
} mb-0.5 text-custom-sidebar-text-400 duration-300 group-hover:!block`}
|
||||
className={`hidden h-4 w-4 flex-shrink-0 ${open ? "rotate-180" : ""} ${isMenuActive ? "!block" : ""
|
||||
} mb-0.5 text-custom-sidebar-text-400 duration-300 group-hover:!block`}
|
||||
/>
|
||||
)}
|
||||
</Disclosure.Button>
|
||||
@@ -326,11 +320,10 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
|
||||
disabled={!isCollapsed}
|
||||
>
|
||||
<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)
|
||||
? "bg-custom-primary-100/10 text-custom-primary-100"
|
||||
: "text-custom-sidebar-text-300 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
|
||||
} ${isCollapsed ? "justify-center" : ""}`}
|
||||
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)
|
||||
? "bg-custom-primary-100/10 text-custom-primary-100"
|
||||
: "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]" />
|
||||
{!isCollapsed && item.name}
|
||||
|
||||
@@ -7,3 +7,4 @@ export * from "./markdown-to-component";
|
||||
export * from "./integration-and-import-export-banner";
|
||||
export * from "./range-datepicker";
|
||||
export * from "./profile-empty-state";
|
||||
export * from "./loader";
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
export const CycleModuleBoardLayout = () => (
|
||||
<div className="h-full w-full animate-pulse">
|
||||
<div className="flex h-full w-full justify-between">
|
||||
<div className="grid h-full w-full grid-cols-1 gap-6 overflow-y-auto p-8 lg:grid-cols-2 xl:grid-cols-3 3xl:grid-cols-4 auto-rows-max transition-all">
|
||||
{[...Array(5)].map(() => (
|
||||
<div className="flex h-44 w-full flex-col justify-between rounded border border-custom-border-100 bg-custom-background-100 p-4 text-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="h-6 w-24 bg-custom-background-80 rounded" />
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="h-6 w-20 bg-custom-background-80 rounded" />
|
||||
<span className="h-6 w-6 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="h-5 w-5 bg-custom-background-80 rounded" />
|
||||
<span className="h-5 w-20 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
<span className="h-5 w-5 bg-custom-background-80 rounded-full" />
|
||||
</div>
|
||||
<span className="h-1.5 bg-custom-background-80 rounded" />
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<span className="h-4 w-16 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="h-4 w-4 bg-custom-background-80 rounded" />
|
||||
<span className="h-4 w-4 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,28 @@
|
||||
export const CycleModuleListLayout = () => (
|
||||
<div className="h-full overflow-y-auto animate-pulse">
|
||||
<div className="flex h-full w-full justify-between">
|
||||
<div className="flex h-full w-full flex-col overflow-y-auto">
|
||||
{[...Array(5)].map(() => (
|
||||
<div className="flex w-full items-center justify-between gap-5 border-b border-custom-border-100 flex-col sm:flex-row px-5 py-6">
|
||||
<div className="relative flex w-full items-center gap-3 justify-between overflow-hidden">
|
||||
<div className="relative w-full flex items-center gap-3 overflow-hidden">
|
||||
<div className="flex items-center gap-4 truncate">
|
||||
<span className="h-10 w-10 bg-custom-background-80 rounded-full" />
|
||||
<span className="h-5 w-20 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
<span className="h-6 w-20 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
<div className="flex w-full sm:w-auto relative overflow-hidden items-center gap-2.5 justify-between sm:justify-end sm:flex-shrink-0 ">
|
||||
<div className="flex-shrink-0 relative flex items-center gap-3">
|
||||
<span className="h-5 w-5 bg-custom-background-80 rounded" />
|
||||
<span className="h-5 w-5 bg-custom-background-80 rounded" />
|
||||
<span className="h-5 w-5 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,9 @@
|
||||
export * from "./layouts";
|
||||
export * from "./settings";
|
||||
export * from "./pages-loader";
|
||||
export * from "./notification-loader";
|
||||
export * from "./cycle-module-board-loader";
|
||||
export * from "./cycle-module-list-loader";
|
||||
export * from "./view-list-loader";
|
||||
export * from "./projects-loader";
|
||||
export * from "./utils";
|
||||
@@ -0,0 +1,48 @@
|
||||
import { getRandomInt } from "../utils";
|
||||
|
||||
const CalendarDay = () => {
|
||||
const dataCount = getRandomInt(0, 1);
|
||||
const dataBlocks = Array.from({ length: dataCount }, (_, index) => (
|
||||
<span key={index} className="h-8 w-full bg-custom-background-80 rounded mb-2" />
|
||||
));
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col min-h-[9rem]">
|
||||
<div className="flex items-center justify-end p-2 w-full">
|
||||
<span className="h-6 w-6 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2.5 p-2">{dataBlocks}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const CalendarLayoutLoader = () => (
|
||||
<div className="h-full w-full overflow-y-auto bg-custom-background-100 pt-4 animate-pulse">
|
||||
<div className="mb-4 flex items-center justify-between gap-2 px-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="h-7 w-10 bg-custom-background-80 rounded" />
|
||||
<span className="h-7 w-32 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="h-7 w-12 bg-custom-background-80 rounded" />
|
||||
<span className="h-7 w-20 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
<span className="relative grid divide-x-[0.5px] divide-custom-border-200 text-sm font-medium grid-cols-5">
|
||||
{[...Array(5)].map((_, index) => (
|
||||
<span key={index} className="h-11 w-full bg-custom-background-80" />
|
||||
))}
|
||||
</span>
|
||||
<div className="h-full w-full overflow-y-auto">
|
||||
<div className="grid h-full w-full grid-cols-1 divide-y-[0.5px] divide-custom-border-200 overflow-y-auto">
|
||||
{[...Array(6)].map((_, index) => (
|
||||
<div key={index} className="grid divide-x-[0.5px] divide-custom-border-200 grid-cols-5">
|
||||
{[...Array(5)].map((_, index) => (
|
||||
<CalendarDay key={index} />
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,50 @@
|
||||
import { getRandomLength } from "../utils";
|
||||
|
||||
export const GanttLayoutLoader = () => (
|
||||
<div className="flex flex-col h-full overflow-x-auto animate-pulse">
|
||||
<div className="min-h-10 w-full border-b border-custom-border-200 ">
|
||||
<span className="h-6 w-12 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
<div className="flex h-full">
|
||||
<div className="h-full w-[25.5rem] border-r border-custom-border-200">
|
||||
<div className="flex items-end h-[3.75rem] py-2 px-4 border-b border-custom-border-200">
|
||||
<div className="flex items-center pl-6 justify-between w-full">
|
||||
<span className="h-5 w-14 bg-custom-background-80 rounded" />
|
||||
<span className="h-5 w-16 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 h-11 p-4 w-full">
|
||||
{[...Array(6)].map((_, index) => (
|
||||
<div key={index} className="flex items-center gap-3 h-11 pl-6 w-full">
|
||||
<span className="h-6 w-6 bg-custom-background-80 rounded" />
|
||||
<span className={`h-6 w-${getRandomLength(["32", "52", "72"])} bg-custom-background-80 rounded`} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-full w-full border-r border-custom-border-200">
|
||||
<div className="flex flex-col justify-between gap-2 h-[3.75rem] py-1.5 px-4 border-b border-custom-border-200">
|
||||
<div className="flex items-center justify-center">
|
||||
<span className="h-5 w-20 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
<div className="flex items-center gap-3 justify-between w-full">
|
||||
{[...Array(15)].map((_, index) => (
|
||||
<span key={index} className="h-5 w-10 bg-custom-background-80 rounded" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 h-11 p-4 w-full">
|
||||
{[...Array(6)].map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`flex items-center gap-3 h-11 w-full`}
|
||||
style={{ paddingLeft: getRandomLength(["115px", "208px", "260px"]) }}
|
||||
>
|
||||
<span className={`h-6 w-40 w-${getRandomLength(["32", "52", "72"])} bg-custom-background-80 rounded`} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from "./list-layout-loader";
|
||||
export * from "./kanban-layout-loader";
|
||||
export * from "./calendar-layout-loader";
|
||||
export * from "./spreadsheet-layout-loader";
|
||||
export * from "./gantt-layout-loader";
|
||||
@@ -0,0 +1,18 @@
|
||||
export const KanbanLayoutLoader = ({ cardsInEachColumn = [2, 3, 2, 4, 3] }: { cardsInEachColumn?: number[] }) => (
|
||||
<div className="flex gap-5 px-3.5 py-1.5 overflow-x-auto">
|
||||
{cardsInEachColumn.map((cardsInColumn, columnIndex) => (
|
||||
<div key={columnIndex} className="flex flex-col gap-3 animate-pulse">
|
||||
<div className="flex items-center justify-between h-9 w-80">
|
||||
<div className="flex item-center gap-1.5">
|
||||
<span className="h-6 w-6 bg-custom-background-80 rounded" />
|
||||
<span className="h-6 w-24 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
<span className="h-6 w-6 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
{Array.from({ length: cardsInColumn }, (_, cardIndex) => (
|
||||
<span key={cardIndex} className="h-28 w-80 bg-custom-background-80 rounded" />
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,45 @@
|
||||
import { getRandomInt, getRandomLength } from "../utils";
|
||||
|
||||
const ListItemRow = () => (
|
||||
<div className="flex items-center justify-between h-11 p-3 border-b border-custom-border-200">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="h-5 w-10 bg-custom-background-80 rounded" />
|
||||
<span className={`h-5 w-${getRandomLength(["32", "52", "72"])} bg-custom-background-80 rounded`} />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{[...Array(6)].map((_, index) => (
|
||||
<>
|
||||
{getRandomInt(1, 2) % 2 === 0 ? (
|
||||
<span key={index} className="h-5 w-5 bg-custom-background-80 rounded" />
|
||||
) : (
|
||||
<span className="h-5 w-16 bg-custom-background-80 rounded" />
|
||||
)}
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const ListSection = ({ itemCount }: { itemCount: number }) => (
|
||||
<div className="flex flex-shrink-0 flex-col">
|
||||
<div className="sticky top-0 z-[2] w-full flex-shrink-0 border-b border-custom-border-200 bg-custom-background-90 px-3 py-1">
|
||||
<div className="flex items-center gap-2 py-1.5 w-full">
|
||||
<span className="h-6 w-6 bg-custom-background-80 rounded" />
|
||||
<span className="h-6 w-24 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative h-full w-full">
|
||||
{[...Array(itemCount)].map((_, index) => (
|
||||
<ListItemRow key={index} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const ListLayoutLoader = () => (
|
||||
<div className="flex flex-shrink-0 flex-col animate-pulse">
|
||||
{[6, 5, 2].map((itemCount, index) => (
|
||||
<ListSection key={index} itemCount={itemCount} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,38 @@
|
||||
import { getRandomLength } from "../utils";
|
||||
|
||||
export const SpreadsheetLayoutLoader = () => (
|
||||
<div className="horizontal-scroll-enable h-full w-full animate-pulse">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="h-11 min-w-[28rem] bg-custom-background-90 border-r border-custom-border-100" />
|
||||
{[...Array(10)].map((_, index) => (
|
||||
<th
|
||||
key={index}
|
||||
className="h-11 w-full min-w-[8rem] bg-custom-background-90 border-r border-custom-border-100"
|
||||
/>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{[...Array(16)].map((_, rowIndex) => (
|
||||
<tr key={rowIndex} className="border-b border-custom-border-100">
|
||||
<td className="h-11 min-w-[28rem] border-r border-custom-border-100">
|
||||
<div className="flex items-center gap-3 px-3">
|
||||
<span className="h-5 w-10 bg-custom-background-80 rounded" />
|
||||
<span className={`h-5 w-${getRandomLength(["32", "52", "72"])} bg-custom-background-80 rounded`} />
|
||||
</div>
|
||||
</td>
|
||||
{[...Array(10)].map((_, colIndex) => (
|
||||
<td key={colIndex} className="h-11 w-full min-w-[8rem] border-r border-custom-border-100">
|
||||
<div className="flex items-center justify-center gap-3 px-3">
|
||||
<span className="h-5 w-20 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,16 @@
|
||||
export const NotificationsLoader = () => (
|
||||
<div className="divide-y divide-custom-border-100 animate-pulse overflow-hidden">
|
||||
{[...Array(3)].map(() => (
|
||||
<div className="flex w-full items-center gap-4 p-3">
|
||||
<span className="min-h-12 min-w-12 bg-custom-background-80 rounded-full" />
|
||||
<div className="flex flex-col gap-2.5 w-full">
|
||||
<span className="h-5 w-36 bg-custom-background-80 rounded" />
|
||||
<div className="flex items-center justify-between gap-2 w-full">
|
||||
<span className="h-5 w-28 bg-custom-background-80 rounded" />
|
||||
<span className="h-5 w-16 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,28 @@
|
||||
export const PagesLoader = () => (
|
||||
<div className="flex h-full flex-col space-y-5 overflow-hidden p-6">
|
||||
<div className="flex justify-between gap-4">
|
||||
<h3 className="text-2xl font-semibold text-custom-text-100">Pages</h3>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{[...Array(5)].map(() => (
|
||||
<span className="h-8 w-20 bg-custom-background-80 rounded-full" />
|
||||
))}
|
||||
</div>
|
||||
<div className="divide-y divide-custom-border-200">
|
||||
{[...Array(5)].map(() => (
|
||||
<div className="h-12 w-full flex items-center justify-between px-3">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="h-5 w-5 bg-custom-background-80 rounded" />
|
||||
<span className="h-5 w-20 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="h-5 w-16 bg-custom-background-80 rounded" />
|
||||
<span className="h-5 w-5 bg-custom-background-80 rounded" />
|
||||
<span className="h-5 w-5 bg-custom-background-80 rounded" />
|
||||
<span className="h-5 w-5 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,34 @@
|
||||
export const ProjectsLoader = () => (
|
||||
<div className="h-full w-full overflow-y-auto p-8 animate-pulse">
|
||||
<div className="grid grid-cols-1 gap-9 md:grid-cols-2 lg:grid-cols-3">
|
||||
{[...Array(3)].map(() => (
|
||||
<div className="flex cursor-pointer flex-col rounded border border-custom-border-200 bg-custom-background-100">
|
||||
<div className="relative min-h-[118px] w-full rounded-t border-b border-custom-border-200 ">
|
||||
<div className="absolute inset-0 z-[1] bg-gradient-to-t from-black/20 to-transparent">
|
||||
<div className="absolute bottom-4 z-10 flex h-10 w-full items-center justify-between gap-3 px-4">
|
||||
<div className="flex flex-grow items-center gap-2.5 truncate">
|
||||
<span className="min-h-9 min-w-9 bg-custom-background-80 rounded" />
|
||||
<div className="flex w-full flex-col justify-between gap-0.5 truncate">
|
||||
<span className="h-4 w-28 bg-custom-background-80 rounded" />
|
||||
<span className="h-4 w-16 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-full flex-shrink-0 items-center gap-2">
|
||||
<span className="h-6 w-6 bg-custom-background-80 rounded" />
|
||||
<span className="h-6 w-6 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-[104px] w-full flex-col justify-between rounded-b p-4">
|
||||
<span className="h-4 w-36 bg-custom-background-80 rounded" />
|
||||
<div className="item-center flex justify-between">
|
||||
<span className="h-5 w-20 bg-custom-background-80 rounded" />
|
||||
<span className="h-5 w-5 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user