Merge branch 'chore-cycles-upgrade' of github.com:makeplane/plane into chore-cycles-upgrade

This commit is contained in:
NarayanBavisetti
2024-09-18 18:54:10 +05:30
19 changed files with 136 additions and 61 deletions

View File

@@ -37,7 +37,7 @@ class IssueReactionViewSet(BaseViewSet):
.distinct()
)
@allow_permission(ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def create(self, request, slug, project_id, issue_id):
serializer = IssueReactionSerializer(data=request.data)
if serializer.is_valid():
@@ -60,7 +60,7 @@ class IssueReactionViewSet(BaseViewSet):
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@allow_permission(ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def destroy(self, request, slug, project_id, issue_id, reaction_code):
issue_reaction = IssueReaction.objects.get(
workspace__slug=slug,

View File

@@ -1,4 +1,4 @@
import React, { useRef, useState, useCallback, useLayoutEffect } from "react";
import React, { useRef, useState, useCallback, useLayoutEffect, useEffect } from "react";
import { NodeSelection } from "@tiptap/pm/state";
// extensions
import { CustomImageNodeViewProps } from "@/extensions/custom-image";
@@ -13,52 +13,84 @@ export const CustomImageBlock: React.FC<CustomImageNodeViewProps> = (props) => {
const [size, setSize] = useState({ width: width || "35%", height: height || "auto" });
const [isLoading, setIsLoading] = useState(true);
const [initialResizeComplete, setInitialResizeComplete] = useState(false);
const isShimmerVisible = isLoading || !initialResizeComplete;
const [editorContainer, setEditorContainer] = useState<HTMLElement | null>(null);
const containerRef = useRef<HTMLDivElement>(null);
const containerRect = useRef<DOMRect | null>(null);
const imageRef = useRef<HTMLImageElement>(null);
const isResizing = useRef(false);
const aspectRatio = useRef(1);
const aspectRatioRef = useRef<number | null>(null);
useLayoutEffect(() => {
if (imageRef.current) {
const img = imageRef.current;
img.onload = () => {
if (node.attrs.width === "35%" && node.attrs.height === "auto") {
aspectRatio.current = img.naturalWidth / img.naturalHeight;
const initialWidth = Math.max(img.naturalWidth * 0.35, MIN_SIZE);
const initialHeight = initialWidth / aspectRatio.current;
setSize({ width: `${initialWidth}px`, height: `${initialHeight}px` });
const closestEditorContainer = img.closest(".editor-container");
if (!closestEditorContainer) {
console.error("Editor container not found");
return;
}
setEditorContainer(closestEditorContainer as HTMLElement);
if (width === "35%") {
const editorWidth = closestEditorContainer.clientWidth;
const initialWidth = Math.max(editorWidth * 0.35, MIN_SIZE);
const aspectRatio = img.naturalWidth / img.naturalHeight;
const initialHeight = initialWidth / aspectRatio;
const newSize = {
width: `${Math.round(initialWidth)}px`,
height: `${Math.round(initialHeight)}px`,
};
setSize(newSize);
updateAttributes(newSize);
}
setInitialResizeComplete(true);
setIsLoading(false);
};
}
}, [src]);
const handleResizeStart = useCallback((e: React.MouseEvent | React.TouchEvent) => {
e.preventDefault();
e.stopPropagation();
isResizing.current = true;
if (containerRef.current) {
containerRect.current = containerRef.current.getBoundingClientRect();
}
}, []);
}, [width, height, updateAttributes]);
useLayoutEffect(() => {
// for realtime resizing and undo/redo
setSize({ width, height });
}, [width, height]);
const handleResize = useCallback((e: MouseEvent | TouchEvent) => {
if (!isResizing.current || !containerRef.current || !containerRect.current) return;
const handleResizeStart = useCallback(
(e: React.MouseEvent | React.TouchEvent) => {
e.preventDefault();
e.stopPropagation();
isResizing.current = true;
if (containerRef.current && editorContainer) {
aspectRatioRef.current = Number(size.width.replace("px", "")) / Number(size.height.replace("px", ""));
containerRect.current = containerRef.current.getBoundingClientRect();
}
},
[size, editorContainer]
);
const clientX = "touches" in e ? e.touches[0].clientX : e.clientX;
const handleResize = useCallback(
(e: MouseEvent | TouchEvent) => {
if (!isResizing.current || !containerRef.current || !containerRect.current) return;
const newWidth = Math.max(clientX - containerRect.current.left, MIN_SIZE);
const newHeight = newWidth / aspectRatio.current;
if (size) {
aspectRatioRef.current = Number(size.width.replace("px", "")) / Number(size.height.replace("px", ""));
}
setSize({ width: `${newWidth}px`, height: `${newHeight}px` });
}, []);
if (!aspectRatioRef.current) return;
const clientX = "touches" in e ? e.touches[0].clientX : e.clientX;
const newWidth = Math.max(clientX - containerRect.current.left, MIN_SIZE);
const newHeight = newWidth / aspectRatioRef.current;
setSize({ width: `${newWidth}px`, height: `${newHeight}px` });
},
[size]
);
const handleResizeEnd = useCallback(() => {
if (isResizing.current) {
@@ -77,18 +109,23 @@ export const CustomImageBlock: React.FC<CustomImageNodeViewProps> = (props) => {
[editor, getPos]
);
useLayoutEffect(() => {
const handleGlobalMouseMove = (e: MouseEvent) => handleResize(e);
const handleGlobalMouseUp = () => handleResizeEnd();
useEffect(() => {
if (!editorContainer) return;
document.addEventListener("mousemove", handleGlobalMouseMove);
document.addEventListener("mouseup", handleGlobalMouseUp);
const handleMouseMove = (e: MouseEvent) => handleResize(e);
const handleMouseUp = () => handleResizeEnd();
const handleMouseLeave = () => handleResizeEnd();
editorContainer.addEventListener("mousemove", handleMouseMove);
editorContainer.addEventListener("mouseup", handleMouseUp);
editorContainer.addEventListener("mouseleave", handleMouseLeave);
return () => {
document.removeEventListener("mousemove", handleGlobalMouseMove);
document.removeEventListener("mouseup", handleGlobalMouseUp);
editorContainer.removeEventListener("mousemove", handleMouseMove);
editorContainer.removeEventListener("mouseup", handleMouseUp);
editorContainer.removeEventListener("mouseleave", handleMouseLeave);
};
}, [handleResize, handleResizeEnd]);
}, [handleResize, handleResizeEnd, editorContainer]);
return (
<div
@@ -100,12 +137,16 @@ export const CustomImageBlock: React.FC<CustomImageNodeViewProps> = (props) => {
height: size.height,
}}
>
{isLoading && <div className="animate-pulse bg-custom-background-80 rounded-md" style={{ width, height }} />}
{isShimmerVisible && (
<div className="animate-pulse bg-custom-background-80 rounded-md" style={{ width, height }} />
)}
<img
ref={imageRef}
src={src}
width={size.width}
height={size.height}
className={cn("block rounded-md", {
hidden: isLoading,
hidden: isShimmerVisible,
"read-only-image": !editor.isEditable,
})}
style={{

View File

@@ -46,6 +46,7 @@ export const TrackImageDeletionPlugin = (editor: Editor, deleteImage: DeleteImag
async function onNodeDeleted(src: string, deleteImage: DeleteImage): Promise<void> {
try {
if (!src) return;
const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1);
await deleteImage(assetUrlWithWorkspaceId);
} catch (error) {

View File

@@ -49,6 +49,7 @@ export const TrackImageRestorationPlugin = (editor: Editor, restoreImage: Restor
async function onNodeRestored(src: string, restoreImage: RestoreImage): Promise<void> {
try {
if (!src) return;
const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1);
await restoreImage(assetUrlWithWorkspaceId);
} catch (error) {

View File

@@ -5,7 +5,6 @@ import useSWR from "swr";
import { IAnalyticsParams } from "@plane/types";
// services
// components
import { ContentWrapper } from "@plane/ui";
import { CustomAnalyticsSelectBar, CustomAnalyticsMainContent, CustomAnalyticsSidebar } from "@/components/analytics";
// types
// fetch-keys
@@ -54,7 +53,7 @@ export const CustomAnalytics: React.FC<Props> = observer((props) => {
return (
<div className={cn("relative flex h-full w-full overflow-hidden", isProjectLevel ? "flex-col-reverse" : "")}>
<ContentWrapper>
<div className="flex h-full w-full flex-col overflow-hidden">
<CustomAnalyticsSelectBar
control={control}
setValue={setValue}
@@ -68,7 +67,7 @@ export const CustomAnalytics: React.FC<Props> = observer((props) => {
params={params}
fullScreen={fullScreen}
/>
</ContentWrapper>
</div>
<div
className={cn(

View File

@@ -5,6 +5,7 @@ import { IAnalyticsParams } from "@plane/types";
import { SelectProject, SelectSegment, SelectXAxis, SelectYAxis } from "@/components/analytics";
import { ANALYTICS_X_AXIS_VALUES } from "@/constants/analytics";
import { useProject } from "@/hooks/store";
import { Row } from "@plane/ui";
// components
// types
@@ -30,10 +31,10 @@ export const CustomAnalyticsSelectBar: React.FC<Props> = observer((props) => {
: ANALYTICS_X_AXIS_VALUES;
return (
<div
className={`grid items-center gap-4 pb-2.5 ${
<Row
className={`grid items-center gap-4 py-2.5 ${
isProjectLevel ? "grid-cols-1 sm:grid-cols-3" : "grid-cols-2"
} ${fullScreen ? "md:pb-5 lg:grid-cols-4" : ""}`}
} ${fullScreen ? "md:py-5 lg:grid-cols-4" : ""}`}
>
{!isProjectLevel && (
<div>
@@ -88,6 +89,6 @@ export const CustomAnalyticsSelectBar: React.FC<Props> = observer((props) => {
)}
/>
</div>
</div>
</Row>
);
});

View File

@@ -19,7 +19,7 @@ type Props = {
};
export const AnalyticsTable: React.FC<Props> = ({ analytics, barGraphData, params, yAxisKey }) => (
<div className="w-full overflow-hidden overflow-x-auto absolute left-0">
<div className="w-full overflow-hidden overflow-x-auto">
<table className="w-full overflow-hidden divide-y divide-custom-border-200 whitespace-nowrap border-y border-custom-border-200">
<thead className="bg-custom-background-80">
<tr className="divide-x divide-custom-border-200 text-sm text-custom-text-100">

View File

@@ -54,7 +54,7 @@ export const CycleDeleteModal: React.FC<ICycleDelete> = observer((props) => {
});
})
.catch((errors) => {
const isPermissionError = errors?.error === "Only admin or owner can delete the cycle";
const isPermissionError = errors?.error === "You don't have the required permissions.";
const currentError = isPermissionError
? PROJECT_ERROR_MESSAGES.permissionError
: PROJECT_ERROR_MESSAGES.cycleDeleteError;

View File

@@ -66,7 +66,7 @@ export const OverviewStatsWidget: React.FC<WidgetProps> = observer((props) => {
return (
<Card
spacing={ECardSpacing.SM}
className="flex-row grid lg:grid-cols-4 md:grid-cols-2 sm:grid-cols-2 grid-cols-2 space-y-0
className="flex-row grid lg:grid-cols-4 md:grid-cols-2 sm:grid-cols-2 grid-cols-2 space-y-0 p-0.5
[&>div>a>div]:border-r
[&>div:last-child>a>div]:border-0
[&>div>a>div]:border-custom-border-200
@@ -79,8 +79,8 @@ export const OverviewStatsWidget: React.FC<WidgetProps> = observer((props) => {
key={stat.key}
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 === 0 ? "rounded-l-md" : "",
index === STATS_LIST.length - 1 ? "rounded-r-md" : "",
index === 1 ? "rounded-tr-xl lg:rounded-[0px]" : "",
index == 2 ? "rounded-bl-xl lg:rounded-[0px]" : ""
)}

View File

@@ -4,6 +4,7 @@ import { MutableRefObject, useEffect, useRef, useState } from "react";
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { cn } from "@plane/editor";
// plane packages
import {
@@ -90,6 +91,7 @@ export const ListGroup = observer((props: Props) => {
const [isExpanded, setIsExpanded] = useState(true);
const groupRef = useRef<HTMLDivElement | null>(null);
const { projectId } = useParams();
const projectState = useProjectState();
const {
@@ -216,7 +218,8 @@ export const ListGroup = observer((props: Props) => {
);
}, [groupRef?.current, group, orderBy, getGroupIndex, setDragColumnOrientation, setIsDraggingOverColumn]);
const isDragAllowed = !!group_by && DRAG_ALLOWED_GROUPS.includes(group_by);
const isDragAllowed =
!!group_by && DRAG_ALLOWED_GROUPS.includes(group_by) && canEditProperties(projectId?.toString());
const canOverlayBeVisible = orderBy !== "sort_order" || !!group.isDropDisabled;
const isGroupByCreatedBy = group_by === "created_by";

View File

@@ -6,12 +6,28 @@ import { ProjectIssueQuickActions } from "@/components/issues";
// components
// types
// constants
import { useUserPermissions } from "@/hooks/store";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
import { BaseListRoot } from "../base-list-root";
export const ListLayout: FC = observer(() => {
const { workspaceSlug, projectId } = useParams();
const { allowPermissions } = useUserPermissions();
if (!workspaceSlug || !projectId) return null;
return <BaseListRoot QuickActions={ProjectIssueQuickActions} />;
const canEditPropertiesBasedOnProject = (projectId: string) =>
allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
EUserPermissionsLevel.PROJECT,
workspaceSlug.toString(),
projectId
);
return (
<BaseListRoot
QuickActions={ProjectIssueQuickActions}
canEditPropertiesBasedOnProject={canEditPropertiesBasedOnProject}
/>
);
});

View File

@@ -25,10 +25,20 @@ type Props = {
droppedLabelId: string | undefined,
dropAtEndOfList: boolean
) => void;
isEditable?: boolean;
};
export const ProjectSettingLabelGroup: React.FC<Props> = observer((props) => {
const { label, labelChildren, handleLabelDelete, isUpdating, setIsUpdating, isLastChild, onDrop } = props;
const {
label,
labelChildren,
handleLabelDelete,
isUpdating,
setIsUpdating,
isLastChild,
onDrop,
isEditable = false,
} = props;
// states
const [isEditLabelForm, setEditLabelForm] = useState(false);
@@ -123,6 +133,7 @@ export const ProjectSettingLabelGroup: React.FC<Props> = observer((props) => {
isChild
isLastChild={index === labelChildren.length - 1}
onDrop={onDrop}
isEditable={isEditable}
/>
</div>
</div>

View File

@@ -111,6 +111,7 @@ export const ProjectSettingsLabelList: React.FC = observer(() => {
setIsUpdating={setIsUpdating}
isLastChild={index === projectLabelsTree.length - 1}
onDrop={onDrop}
isEditable={isEditable}
/>
);
}
@@ -123,6 +124,7 @@ export const ProjectSettingsLabelList: React.FC = observer(() => {
isChild={false}
isLastChild={index === projectLabelsTree.length - 1}
onDrop={onDrop}
isEditable={isEditable}
/>
);
})}

View File

@@ -56,7 +56,7 @@ export const DeleteModuleModal: React.FC<Props> = observer((props) => {
});
})
.catch((errors) => {
const isPermissionError = errors?.error === "Only admin or creator can delete the module";
const isPermissionError = errors?.error === "You don't have the required permissions.";
const currentError = isPermissionError
? PROJECT_ERROR_MESSAGES.permissionError
: PROJECT_ERROR_MESSAGES.moduleDeleteError;

View File

@@ -67,7 +67,7 @@ export const WorkspaceDashboardView = observer(() => {
<>
<IssuePeekOverview />
<ContentWrapper
className={cn("gap-7", {
className={cn("gap-7 bg-custom-background-90", {
"vertical-scrollbar scrollbar-lg": windowWidth >= 768,
})}
>

View File

@@ -60,10 +60,10 @@ export const LeaveProjectModal: FC<ILeaveProjectModal> = observer((props) => {
if (data) {
if (data.projectName === project?.name) {
if (data.confirmLeave === "Leave Project") {
router.push(`/${workspaceSlug}/projects`);
return leaveProject(workspaceSlug.toString(), project.id)
.then(() => {
handleClose();
router.push(`/${workspaceSlug}/projects`);
captureEvent(PROJECT_MEMBER_LEAVE, {
state: "SUCCESS",
element: "Project settings members page",

View File

@@ -38,6 +38,7 @@ export const ProjectMemberListItem: React.FC<Props> = observer((props) => {
if (!workspaceSlug || !projectId || !memberId) return;
if (memberId === currentUser?.id) {
router.push(`/${workspaceSlug}/projects`);
await leaveProject(workspaceSlug.toString(), projectId.toString())
.then(async () => {
captureEvent(PROJECT_MEMBER_LEAVE, {
@@ -45,7 +46,6 @@ export const ProjectMemberListItem: React.FC<Props> = observer((props) => {
element: "Project settings members page",
});
await fetchProjects(workspaceSlug.toString());
router.push(`/${workspaceSlug}/projects`);
})
.catch((err) =>
setToast({

View File

@@ -97,9 +97,11 @@ export const AccountTypeColumn: React.FC<AccountTypeProps> = observer((props) =>
// derived values
const isCurrentUser = currentUser?.id === rowData.member.id;
const isAdminOrGuest = [EUserPermissions.ADMIN, EUserPermissions.GUEST].includes(rowData.role);
const userWorkspaceRole = getWorkspaceMemberDetails(rowData.member.id)?.role;
const isRoleNonEditable = isCurrentUser || (isAdminOrGuest && userWorkspaceRole !== EUserPermissions.MEMBER);
const isProjectAdminOrGuest = [EUserPermissions.ADMIN, EUserPermissions.GUEST].includes(rowData.role);
const isWorkspaceMember = [EUserPermissions.MEMBER].includes(
Number(getWorkspaceMemberDetails(rowData.member.id)?.role) ?? EUserPermissions.GUEST
);
const isRoleNonEditable = isCurrentUser || (isProjectAdminOrGuest && !isWorkspaceMember);
const checkCurrentOptionWorkspaceRole = (value: string) => {
const currentMemberWorkspaceRole = getWorkspaceMemberDetails(value)?.role as EUserPermissions | undefined;

View File

@@ -114,9 +114,7 @@ export const AccountTypeColumn: React.FC<AccountTypeProps> = observer((props) =>
<CustomSelect
value={value}
onChange={(value: EUserPermissions) => {
console.log({ value, workspaceSlug }, "onChange");
if (!workspaceSlug) return;
updateMember(workspaceSlug.toString(), rowData.member.id, {
role: value as unknown as EUserPermissions, // Cast value to unknown first, then to EUserPermissions
}).catch((err) => {