diff --git a/apiserver/plane/app/views/issue/reaction.py b/apiserver/plane/app/views/issue/reaction.py index 5146118f30..a43f7bda6a 100644 --- a/apiserver/plane/app/views/issue/reaction.py +++ b/apiserver/plane/app/views/issue/reaction.py @@ -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, diff --git a/packages/editor/src/core/extensions/custom-image/components/image-block.tsx b/packages/editor/src/core/extensions/custom-image/components/image-block.tsx index 22a793a958..c951c3bf2c 100644 --- a/packages/editor/src/core/extensions/custom-image/components/image-block.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/image-block.tsx @@ -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 = (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(null); const containerRef = useRef(null); const containerRect = useRef(null); const imageRef = useRef(null); const isResizing = useRef(false); - const aspectRatio = useRef(1); + const aspectRatioRef = useRef(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 = (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 (
= (props) => { height: size.height, }} > - {isLoading &&
} + {isShimmerVisible && ( +
+ )} { try { + if (!src) return; const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1); await deleteImage(assetUrlWithWorkspaceId); } catch (error) { diff --git a/packages/editor/src/core/plugins/image/restore-image.ts b/packages/editor/src/core/plugins/image/restore-image.ts index cbcd335322..d722e53a63 100644 --- a/packages/editor/src/core/plugins/image/restore-image.ts +++ b/packages/editor/src/core/plugins/image/restore-image.ts @@ -49,6 +49,7 @@ export const TrackImageRestorationPlugin = (editor: Editor, restoreImage: Restor async function onNodeRestored(src: string, restoreImage: RestoreImage): Promise { try { + if (!src) return; const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1); await restoreImage(assetUrlWithWorkspaceId); } catch (error) { diff --git a/web/core/components/analytics/custom-analytics/custom-analytics.tsx b/web/core/components/analytics/custom-analytics/custom-analytics.tsx index bfb5398ec6..413126c482 100644 --- a/web/core/components/analytics/custom-analytics/custom-analytics.tsx +++ b/web/core/components/analytics/custom-analytics/custom-analytics.tsx @@ -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 = observer((props) => { return (
- +
= observer((props) => { params={params} fullScreen={fullScreen} /> - +
= observer((props) => { : ANALYTICS_X_AXIS_VALUES; return ( -
{!isProjectLevel && (
@@ -88,6 +89,6 @@ export const CustomAnalyticsSelectBar: React.FC = observer((props) => { )} />
-
+ ); }); diff --git a/web/core/components/analytics/custom-analytics/table.tsx b/web/core/components/analytics/custom-analytics/table.tsx index ca3c17846f..49039ed02b 100644 --- a/web/core/components/analytics/custom-analytics/table.tsx +++ b/web/core/components/analytics/custom-analytics/table.tsx @@ -19,7 +19,7 @@ type Props = { }; export const AnalyticsTable: React.FC = ({ analytics, barGraphData, params, yAxisKey }) => ( -
+
diff --git a/web/core/components/cycles/delete-modal.tsx b/web/core/components/cycles/delete-modal.tsx index e31a785b53..f89a1cea77 100644 --- a/web/core/components/cycles/delete-modal.tsx +++ b/web/core/components/cycles/delete-modal.tsx @@ -54,7 +54,7 @@ export const CycleDeleteModal: React.FC = 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; diff --git a/web/core/components/dashboard/widgets/overview-stats.tsx b/web/core/components/dashboard/widgets/overview-stats.tsx index d355da644d..6220010277 100644 --- a/web/core/components/dashboard/widgets/overview-stats.tsx +++ b/web/core/components/dashboard/widgets/overview-stats.tsx @@ -66,7 +66,7 @@ export const OverviewStatsWidget: React.FC = observer((props) => { return ( 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 = 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]" : "" )} diff --git a/web/core/components/issues/issue-layouts/list/list-group.tsx b/web/core/components/issues/issue-layouts/list/list-group.tsx index ca0753bef9..7e417e471a 100644 --- a/web/core/components/issues/issue-layouts/list/list-group.tsx +++ b/web/core/components/issues/issue-layouts/list/list-group.tsx @@ -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(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"; diff --git a/web/core/components/issues/issue-layouts/list/roots/project-root.tsx b/web/core/components/issues/issue-layouts/list/roots/project-root.tsx index 4e7ad97c9a..5fe8bceab3 100644 --- a/web/core/components/issues/issue-layouts/list/roots/project-root.tsx +++ b/web/core/components/issues/issue-layouts/list/roots/project-root.tsx @@ -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 ; + const canEditPropertiesBasedOnProject = (projectId: string) => + allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.PROJECT, + workspaceSlug.toString(), + projectId + ); + + return ( + + ); }); diff --git a/web/core/components/labels/project-setting-label-group.tsx b/web/core/components/labels/project-setting-label-group.tsx index efa0facc1b..898931b344 100644 --- a/web/core/components/labels/project-setting-label-group.tsx +++ b/web/core/components/labels/project-setting-label-group.tsx @@ -25,10 +25,20 @@ type Props = { droppedLabelId: string | undefined, dropAtEndOfList: boolean ) => void; + isEditable?: boolean; }; export const ProjectSettingLabelGroup: React.FC = 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 = observer((props) => { isChild isLastChild={index === labelChildren.length - 1} onDrop={onDrop} + isEditable={isEditable} /> diff --git a/web/core/components/labels/project-setting-label-list.tsx b/web/core/components/labels/project-setting-label-list.tsx index b11d8249ae..2fc9c83827 100644 --- a/web/core/components/labels/project-setting-label-list.tsx +++ b/web/core/components/labels/project-setting-label-list.tsx @@ -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} /> ); })} diff --git a/web/core/components/modules/delete-module-modal.tsx b/web/core/components/modules/delete-module-modal.tsx index 290b7dc2be..c165c2e4c7 100644 --- a/web/core/components/modules/delete-module-modal.tsx +++ b/web/core/components/modules/delete-module-modal.tsx @@ -56,7 +56,7 @@ export const DeleteModuleModal: React.FC = 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; diff --git a/web/core/components/page-views/workspace-dashboard.tsx b/web/core/components/page-views/workspace-dashboard.tsx index a6affa4ae3..05546af56f 100644 --- a/web/core/components/page-views/workspace-dashboard.tsx +++ b/web/core/components/page-views/workspace-dashboard.tsx @@ -67,7 +67,7 @@ export const WorkspaceDashboardView = observer(() => { <> = 768, })} > diff --git a/web/core/components/project/leave-project-modal.tsx b/web/core/components/project/leave-project-modal.tsx index 74dd189397..470a5fe61b 100644 --- a/web/core/components/project/leave-project-modal.tsx +++ b/web/core/components/project/leave-project-modal.tsx @@ -60,10 +60,10 @@ export const LeaveProjectModal: FC = 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", diff --git a/web/core/components/project/member-list-item.tsx b/web/core/components/project/member-list-item.tsx index 956e95db32..3a7c5ed034 100644 --- a/web/core/components/project/member-list-item.tsx +++ b/web/core/components/project/member-list-item.tsx @@ -38,6 +38,7 @@ export const ProjectMemberListItem: React.FC = 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 = observer((props) => { element: "Project settings members page", }); await fetchProjects(workspaceSlug.toString()); - router.push(`/${workspaceSlug}/projects`); }) .catch((err) => setToast({ diff --git a/web/core/components/project/settings/member-columns.tsx b/web/core/components/project/settings/member-columns.tsx index f6699d7118..b5b48d6315 100644 --- a/web/core/components/project/settings/member-columns.tsx +++ b/web/core/components/project/settings/member-columns.tsx @@ -97,9 +97,11 @@ export const AccountTypeColumn: React.FC = 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; diff --git a/web/core/components/workspace/settings/member-columns.tsx b/web/core/components/workspace/settings/member-columns.tsx index 70491351dd..c2639177d1 100644 --- a/web/core/components/workspace/settings/member-columns.tsx +++ b/web/core/components/workspace/settings/member-columns.tsx @@ -114,9 +114,7 @@ export const AccountTypeColumn: React.FC = observer((props) => { - 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) => {