Merge branch 'develop' of gurusainath:makeplane/plane into feat/mobx-global-views

This commit is contained in:
gurusainath 2024-02-03 21:23:56 +05:30
commit fe505e6b31
15 changed files with 207 additions and 32 deletions

View File

@ -243,6 +243,29 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
): ):
serializer = CycleSerializer(data=request.data) serializer = CycleSerializer(data=request.data)
if serializer.is_valid(): if serializer.is_valid():
if (
request.data.get("external_id")
and request.data.get("external_source")
and Cycle.objects.filter(
project_id=project_id,
workspace__slug=slug,
external_source=request.data.get("external_source"),
external_id=request.data.get("external_id"),
).exists()
):
cycle = Cycle.objects.filter(
workspace__slug=slug,
project_id=project_id,
external_source=request.data.get("external_source"),
external_id=request.data.get("external_id"),
).first()
return Response(
{
"error": "Cycle with the same external id and external source already exists",
"cycle": str(cycle.id),
},
status=status.HTTP_409_CONFLICT,
)
serializer.save( serializer.save(
project_id=project_id, project_id=project_id,
owned_by=request.user, owned_by=request.user,
@ -289,6 +312,23 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
serializer = CycleSerializer(cycle, data=request.data, partial=True) serializer = CycleSerializer(cycle, data=request.data, partial=True)
if serializer.is_valid(): if serializer.is_valid():
if (
request.data.get("external_id")
and (cycle.external_id != request.data.get("external_id"))
and Cycle.objects.filter(
project_id=project_id,
workspace__slug=slug,
external_source=request.data.get("external_source", cycle.external_source),
external_id=request.data.get("external_id"),
).exists()
):
return Response(
{
"error": "Cycle with the same external id and external source already exists",
"cycle_id": str(cycle.id),
},
status=status.HTTP_409_CONFLICT,
)
serializer.save() serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

View File

@ -220,6 +220,30 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
) )
if serializer.is_valid(): if serializer.is_valid():
if (
request.data.get("external_id")
and request.data.get("external_source")
and Issue.objects.filter(
project_id=project_id,
workspace__slug=slug,
external_source=request.data.get("external_source"),
external_id=request.data.get("external_id"),
).exists()
):
issue = Issue.objects.filter(
workspace__slug=slug,
project_id=project_id,
external_id=request.data.get("external_id"),
external_source=request.data.get("external_source"),
).first()
return Response(
{
"error": "Issue with the same external id and external source already exists",
"issue_id": str(issue.id),
},
status=status.HTTP_409_CONFLICT,
)
serializer.save() serializer.save()
# Track the issue # Track the issue
@ -256,6 +280,24 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
partial=True, partial=True,
) )
if serializer.is_valid(): if serializer.is_valid():
if (
str(request.data.get("external_id"))
and (issue.external_id != str(request.data.get("external_id")))
and Issue.objects.filter(
project_id=project_id,
workspace__slug=slug,
external_source=request.data.get("external_source", issue.external_source),
external_id=request.data.get("external_id"),
).exists()
):
return Response(
{
"error": "Issue with the same external id and external source already exists",
"issue_id": str(issue.id),
},
status=status.HTTP_409_CONFLICT,
)
serializer.save() serializer.save()
issue_activity.delay( issue_activity.delay(
type="issue.activity.updated", type="issue.activity.updated",
@ -263,6 +305,8 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
actor_id=str(request.user.id), actor_id=str(request.user.id),
issue_id=str(pk), issue_id=str(pk),
project_id=str(project_id), project_id=str(project_id),
external_id__isnull=False,
external_source__isnull=False,
current_instance=current_instance, current_instance=current_instance,
epoch=int(timezone.now().timestamp()), epoch=int(timezone.now().timestamp()),
) )

View File

@ -132,6 +132,29 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
}, },
) )
if serializer.is_valid(): if serializer.is_valid():
if (
request.data.get("external_id")
and request.data.get("external_source")
and Module.objects.filter(
project_id=project_id,
workspace__slug=slug,
external_source=request.data.get("external_source"),
external_id=request.data.get("external_id"),
).exists()
):
module = Module.objects.filter(
project_id=project_id,
workspace__slug=slug,
external_source=request.data.get("external_source"),
external_id=request.data.get("external_id"),
).first()
return Response(
{
"error": "Module with the same external id and external source already exists",
"module_id": str(module.id),
},
status=status.HTTP_409_CONFLICT,
)
serializer.save() serializer.save()
module = Module.objects.get(pk=serializer.data["id"]) module = Module.objects.get(pk=serializer.data["id"])
serializer = ModuleSerializer(module) serializer = ModuleSerializer(module)
@ -149,8 +172,25 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
partial=True, partial=True,
) )
if serializer.is_valid(): if serializer.is_valid():
if (
request.data.get("external_id")
and (module.external_id != request.data.get("external_id"))
and Module.objects.filter(
project_id=project_id,
workspace__slug=slug,
external_source=request.data.get("external_source", module.external_source),
external_id=request.data.get("external_id"),
).exists()
):
return Response(
{
"error": "Module with the same external id and external source already exists",
"module_id": str(module.id),
},
status=status.HTTP_409_CONFLICT,
)
serializer.save() serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def get(self, request, slug, project_id, pk=None): def get(self, request, slug, project_id, pk=None):

View File

@ -38,6 +38,30 @@ class StateAPIEndpoint(BaseAPIView):
data=request.data, context={"project_id": project_id} data=request.data, context={"project_id": project_id}
) )
if serializer.is_valid(): if serializer.is_valid():
if (
request.data.get("external_id")
and request.data.get("external_source")
and State.objects.filter(
project_id=project_id,
workspace__slug=slug,
external_source=request.data.get("external_source"),
external_id=request.data.get("external_id"),
).exists()
):
state = State.objects.filter(
workspace__slug=slug,
project_id=project_id,
external_id=request.data.get("external_id"),
external_source=request.data.get("external_source"),
).first()
return Response(
{
"error": "State with the same external id and external source already exists",
"state_id": str(state.id),
},
status=status.HTTP_409_CONFLICT,
)
serializer.save(project_id=project_id) serializer.save(project_id=project_id)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@ -91,6 +115,23 @@ class StateAPIEndpoint(BaseAPIView):
) )
serializer = StateSerializer(state, data=request.data, partial=True) serializer = StateSerializer(state, data=request.data, partial=True)
if serializer.is_valid(): if serializer.is_valid():
if (
str(request.data.get("external_id"))
and (state.external_id != str(request.data.get("external_id")))
and State.objects.filter(
project_id=project_id,
workspace__slug=slug,
external_source=request.data.get("external_source", state.external_source),
external_id=request.data.get("external_id"),
).exists()
):
return Response(
{
"error": "State with the same external id and external source already exists",
"state_id": str(state.id),
},
status=status.HTTP_409_CONFLICT,
)
serializer.save() serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

View File

@ -47,6 +47,7 @@ export const PriorityIcon: React.FC<IPriorityIcon> = (props) => {
> >
<Icon <Icon
size={size} size={size}
viewBox="0 0 23.5 24"
className={cn( className={cn(
{ {
"text-white": priority === "urgent", "text-white": priority === "urgent",

View File

@ -89,8 +89,8 @@ export const DeactivateAccountModal: React.FC<Props> = (props) => {
<div className="px-4 pb-4 pt-5 sm:p-6 sm:pb-4"> <div className="px-4 pb-4 pt-5 sm:p-6 sm:pb-4">
<div className=""> <div className="">
<div className="flex items-start gap-x-4"> <div className="flex items-start gap-x-4">
<div className="grid place-items-center rounded-full bg-red-500/20 p-4"> <div className="grid place-items-center rounded-full bg-red-500/20 p-2 sm:p-2 md:p-4 lg:p-4 mt-3 sm:mt-3 md:mt-0 lg:mt-0 ">
<Trash2 className="h-6 w-6 text-red-600" aria-hidden="true" /> <Trash2 className="h-4 w-4 sm:h-4 sm:w-4 md:h-6 md:w-6 lg:h-6 lg:w-6 text-red-600" aria-hidden="true" />
</div> </div>
<div> <div>
<Dialog.Title as="h3" className="my-4 text-2xl font-medium leading-6 text-custom-text-100"> <Dialog.Title as="h3" className="my-4 text-2xl font-medium leading-6 text-custom-text-100">

View File

@ -77,7 +77,7 @@ export const WidgetIssuesList: React.FC<WidgetIssuesListProps> = (props) => {
})} })}
> >
Issues Issues
<span className="flex-shrink-0 bg-custom-primary-100/20 text-custom-primary-100 text-xs font-medium rounded-xl h-4 min-w-6 flex items-center text-center justify-center"> <span className="flex-shrink-0 bg-custom-primary-100/20 text-custom-primary-100 text-xs font-medium rounded-xl px-3 flex items-center text-center justify-center">
{totalIssues} {totalIssues}
</span> </span>
</h6> </h6>

View File

@ -9,6 +9,7 @@ import { WidgetLoader } from "components/dashboard/widgets";
import { renderFormattedPayloadDate } from "helpers/date-time.helper"; import { renderFormattedPayloadDate } from "helpers/date-time.helper";
// types // types
import { TOverviewStatsWidgetResponse } from "@plane/types"; import { TOverviewStatsWidgetResponse } from "@plane/types";
import { cn } from "helpers/common.helper";
export type WidgetProps = { export type WidgetProps = {
dashboardId: string; dashboardId: string;
@ -71,10 +72,18 @@ export const OverviewStatsWidget: React.FC<WidgetProps> = observer((props) => {
[&>div:nth-child(2)>a>div]:lg:border-r [&>div:nth-child(2)>a>div]:lg:border-r
" "
> >
{STATS_LIST.map((stat) => ( {STATS_LIST.map((stat, index) => (
<div className="w-full flex flex-col gap-2 hover:bg-custom-background-80 rounded-[10px]"> <div
className={cn(
`w-full flex flex-col gap-2 hover:bg-custom-background-80`,
index === 0 ? "rounded-tl-xl lg:rounded-l-xl" : "",
index === STATS_LIST.length - 1 ? "rounded-br-xl lg:rounded-r-xl" : "",
index === 1 ? "rounded-tr-xl lg:rounded-[0px]" : "",
index == 2 ? "rounded-bl-xl lg:rounded-[0px]" : ""
)}
>
<Link href={stat.link} className="py-4 duration-300 rounded-[10px] w-full "> <Link href={stat.link} className="py-4 duration-300 rounded-[10px] w-full ">
<div className={`relative flex justify-center items-center`}> <div className={`relative flex pl-10 sm:pl-20 md:pl-20 lg:pl-20 items-center`}>
<div> <div>
<h5 className="font-semibold text-xl">{stat.count}</h5> <h5 className="font-semibold text-xl">{stat.count}</h5>
<p className="text-custom-text-300 text-sm xl:text-base">{stat.title}</p> <p className="text-custom-text-300 text-sm xl:text-base">{stat.title}</p>

View File

@ -18,7 +18,7 @@ export const WorkspaceDashboardHeader = () => {
return ( return (
<> <>
<ProductUpdatesModal isOpen={isProductUpdatesModalOpen} setIsOpen={setIsProductUpdatesModalOpen} /> <ProductUpdatesModal isOpen={isProductUpdatesModalOpen} setIsOpen={setIsProductUpdatesModalOpen} />
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4"> <div className="relative z-20 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
<div className="flex items-center gap-2 overflow-ellipsis whitespace-nowrap"> <div className="flex items-center gap-2 overflow-ellipsis whitespace-nowrap">
<SidebarHamburgerToggle /> <SidebarHamburgerToggle />
<div> <div>

View File

@ -237,7 +237,7 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
)} )}
<div className="horizontal-scroll-enable relative h-full w-full overflow-auto bg-custom-background-90"> <div className="horizontal-scroll-enable relative h-full w-full overflow-auto bg-custom-background-90">
<div className="relative h-full w-max min-w-full bg-custom-background-90 px-2"> <div className="relative h-max w-max min-w-full bg-custom-background-90 px-2">
<DragDropContext onDragStart={onDragStart} onDragEnd={onDragEnd}> <DragDropContext onDragStart={onDragStart} onDragEnd={onDragEnd}>
{/* drag and delete component */} {/* drag and delete component */}
<div <div

View File

@ -148,9 +148,15 @@ export const AllIssueLayoutRoot: React.FC = observer(() => {
const handleDisplayFiltersUpdate = useCallback( const handleDisplayFiltersUpdate = useCallback(
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => { (updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
if (!workspaceSlug) return; if (!workspaceSlug || !globalViewId) return;
updateFilters(workspaceSlug.toString(), undefined, EIssueFilterType.DISPLAY_FILTERS, { ...updatedDisplayFilter }); updateFilters(
workspaceSlug.toString(),
undefined,
EIssueFilterType.DISPLAY_FILTERS,
{ ...updatedDisplayFilter },
globalViewId.toString()
);
}, },
[updateFilters, workspaceSlug] [updateFilters, workspaceSlug]
); );

View File

@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
// hooks // hooks
import { useApplication, useCycle, useIssues, useModule, useProject, useUser, useWorkspace } from "hooks/store"; import { useApplication, useCycle, useIssues, useModule, useProject, useWorkspace } from "hooks/store";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
import useLocalStorage from "hooks/use-local-storage"; import useLocalStorage from "hooks/use-local-storage";
// components // components
@ -32,7 +32,6 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
const { const {
eventTracker: { postHogEventTracker }, eventTracker: { postHogEventTracker },
} = useApplication(); } = useApplication();
const { currentUser } = useUser();
const { const {
router: { workspaceSlug, projectId, cycleId, moduleId, viewId: projectViewId }, router: { workspaceSlug, projectId, cycleId, moduleId, viewId: projectViewId },
} = useApplication(); } = useApplication();
@ -49,27 +48,22 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
const issueStores = { const issueStores = {
[EIssuesStoreType.PROJECT]: { [EIssuesStoreType.PROJECT]: {
store: projectIssues, store: projectIssues,
dataIdToUpdate: activeProjectId,
viewId: undefined, viewId: undefined,
}, },
[EIssuesStoreType.PROJECT_VIEW]: { [EIssuesStoreType.PROJECT_VIEW]: {
store: viewIssues, store: viewIssues,
dataIdToUpdate: activeProjectId,
viewId: projectViewId, viewId: projectViewId,
}, },
[EIssuesStoreType.PROFILE]: { [EIssuesStoreType.PROFILE]: {
store: profileIssues, store: profileIssues,
dataIdToUpdate: currentUser?.id || undefined,
viewId: undefined, viewId: undefined,
}, },
[EIssuesStoreType.CYCLE]: { [EIssuesStoreType.CYCLE]: {
store: cycleIssues, store: cycleIssues,
dataIdToUpdate: activeProjectId,
viewId: cycleId, viewId: cycleId,
}, },
[EIssuesStoreType.MODULE]: { [EIssuesStoreType.MODULE]: {
store: moduleIssues, store: moduleIssues,
dataIdToUpdate: activeProjectId,
viewId: moduleId, viewId: moduleId,
}, },
}; };
@ -78,7 +72,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
// local storage // local storage
const { setValue: setLocalStorageDraftIssue } = useLocalStorage<any>("draftedIssue", {}); const { setValue: setLocalStorageDraftIssue } = useLocalStorage<any>("draftedIssue", {});
// current store details // current store details
const { store: currentIssueStore, viewId, dataIdToUpdate } = issueStores[storeType]; const { store: currentIssueStore, viewId } = issueStores[storeType];
useEffect(() => { useEffect(() => {
// if modal is closed, reset active project to null // if modal is closed, reset active project to null
@ -129,13 +123,13 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
}; };
const handleCreateIssue = async (payload: Partial<TIssue>): Promise<TIssue | undefined> => { const handleCreateIssue = async (payload: Partial<TIssue>): Promise<TIssue | undefined> => {
if (!workspaceSlug || !dataIdToUpdate) return; if (!workspaceSlug || !payload.project_id) return;
try { try {
const response = await currentIssueStore.createIssue(workspaceSlug, dataIdToUpdate, payload, viewId); const response = await currentIssueStore.createIssue(workspaceSlug, payload.project_id, payload, viewId);
if (!response) throw new Error(); if (!response) throw new Error();
currentIssueStore.fetchIssues(workspaceSlug, dataIdToUpdate, "mutation", viewId); currentIssueStore.fetchIssues(workspaceSlug, payload.project_id, "mutation", viewId);
if (payload.cycle_id && payload.cycle_id !== "" && storeType !== EIssuesStoreType.CYCLE) if (payload.cycle_id && payload.cycle_id !== "" && storeType !== EIssuesStoreType.CYCLE)
await addIssueToCycle(response, payload.cycle_id); await addIssueToCycle(response, payload.cycle_id);
@ -182,10 +176,10 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
}; };
const handleUpdateIssue = async (payload: Partial<TIssue>): Promise<TIssue | undefined> => { const handleUpdateIssue = async (payload: Partial<TIssue>): Promise<TIssue | undefined> => {
if (!workspaceSlug || !dataIdToUpdate || !data?.id) return; if (!workspaceSlug || !payload.project_id || !data?.id) return;
try { try {
const response = await currentIssueStore.updateIssue(workspaceSlug, dataIdToUpdate, data.id, payload, viewId); const response = await currentIssueStore.updateIssue(workspaceSlug, payload.project_id, data.id, payload, viewId);
setToastAlert({ setToastAlert({
type: "success", type: "success",
title: "Success!", title: "Success!",
@ -226,7 +220,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
}; };
const handleFormSubmit = async (formData: Partial<TIssue>) => { const handleFormSubmit = async (formData: Partial<TIssue>) => {
if (!workspaceSlug || !dataIdToUpdate || !storeType) return; if (!workspaceSlug || !formData.project_id || !storeType) return;
const payload: Partial<TIssue> = { const payload: Partial<TIssue> = {
...formData, ...formData,

View File

@ -229,7 +229,7 @@ export const PagesListItem: FC<IPagesListItem> = observer(({ pageId, projectId }
)} )}
<Tooltip <Tooltip
position="top-right" position="top-right"
tooltipContent={`Created by ${ownerDetails?.member.display_name} on ${renderFormattedDate( tooltipContent={`Created by ${ownerDetails?.member?.display_name} on ${renderFormattedDate(
created_at created_at
)}`} )}`}
> >

View File

@ -42,7 +42,7 @@ export const AppSidebar: FC<IAppSidebar> = observer(() => {
return ( return (
<div <div
className={`inset-y-0 z-20 flex h-full flex-shrink-0 flex-grow-0 flex-col border-r border-custom-sidebar-border-200 bg-custom-sidebar-background-100 duration-300 className={`inset-y-0 z-30 flex h-full flex-shrink-0 flex-grow-0 flex-col border-r border-custom-sidebar-border-200 bg-custom-sidebar-background-100 duration-300
fixed md:relative fixed md:relative
${themStore.sidebarCollapsed ? "-ml-[280px]" : ""} ${themStore.sidebarCollapsed ? "-ml-[280px]" : ""}
sm:${themStore.sidebarCollapsed ? "-ml-[280px]" : ""} sm:${themStore.sidebarCollapsed ? "-ml-[280px]" : ""}