From 58f3d0a68cda8927075f3feb7957adf066869dd1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Oct 2024 17:26:33 +0530 Subject: [PATCH 01/10] chore(deps): bump django in /apiserver/requirements (#5781) Bumps [django](https://github.com/django/django) from 4.2.15 to 4.2.16. - [Commits](https://github.com/django/django/compare/4.2.15...4.2.16) --- updated-dependencies: - dependency-name: django dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apiserver/requirements/base.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/requirements/base.txt b/apiserver/requirements/base.txt index 8b96cb997..fbe6680d4 100644 --- a/apiserver/requirements/base.txt +++ b/apiserver/requirements/base.txt @@ -1,7 +1,7 @@ # base requirements # django -Django==4.2.15 +Django==4.2.16 # rest framework djangorestframework==3.15.2 # postgres From d92dbaea72c7f550f3ef0424715b51703b416d83 Mon Sep 17 00:00:00 2001 From: Akshita Goyal <36129505+gakshita@users.noreply.github.com> Date: Wed, 9 Oct 2024 17:48:52 +0530 Subject: [PATCH 02/10] [WEB-2589] Chore: inbox issue permissions (#5763) * chore: changed permission in inbox issue * chore: fixed permissions for intake * fix: refactoring * fix: lint --------- Co-authored-by: NarayanBavisetti --- apiserver/plane/api/views/inbox.py | 2 +- apiserver/plane/app/views/inbox/base.py | 4 +- .../inbox/content/inbox-issue-header.tsx | 55 +++++++++++++++++-- .../content/inbox-issue-mobile-header.tsx | 44 +++++++++++++-- web/core/components/inbox/content/root.tsx | 8 +-- web/core/store/inbox/project-inbox.store.ts | 2 +- 6 files changed, 99 insertions(+), 16 deletions(-) diff --git a/apiserver/plane/api/views/inbox.py b/apiserver/plane/api/views/inbox.py index 24eac569d..f7e18dd76 100644 --- a/apiserver/plane/api/views/inbox.py +++ b/apiserver/plane/api/views/inbox.py @@ -285,7 +285,7 @@ class InboxIssueAPIEndpoint(BaseAPIView): ) # Only project admins and members can edit inbox issue attributes - if project_member.role > 5: + if project_member.role > 15: serializer = InboxIssueSerializer( inbox_issue, data=request.data, partial=True ) diff --git a/apiserver/plane/app/views/inbox/base.py b/apiserver/plane/app/views/inbox/base.py index 3bd5332dc..4a32d9930 100644 --- a/apiserver/plane/app/views/inbox/base.py +++ b/apiserver/plane/app/views/inbox/base.py @@ -323,7 +323,7 @@ class InboxIssueViewSet(BaseViewSet): serializer.errors, status=status.HTTP_400_BAD_REQUEST ) - @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) + @allow_permission(allowed_roles=[ROLE.ADMIN], creator=True, model=Issue) def partial_update(self, request, slug, project_id, pk): inbox_id = Inbox.objects.filter( workspace__slug=slug, project_id=project_id @@ -418,7 +418,7 @@ class InboxIssueViewSet(BaseViewSet): ) # Only project admins and members can edit inbox issue attributes - if project_member.role > 5: + if project_member.role > 15: serializer = InboxIssueSerializer( inbox_issue, data=request.data, partial=True ) diff --git a/web/core/components/inbox/content/inbox-issue-header.tsx b/web/core/components/inbox/content/inbox-issue-header.tsx index efd0e8ee1..19a39e5e9 100644 --- a/web/core/components/inbox/content/inbox-issue-header.tsx +++ b/web/core/components/inbox/content/inbox-issue-header.tsx @@ -89,6 +89,12 @@ export const InboxIssueActionsHeader: FC = observer((p const canDelete = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT, workspaceSlug, projectId) || issue?.created_by === currentUser?.id; + const isProjectAdmin = allowPermissions( + [EUserPermissions.ADMIN], + EUserPermissionsLevel.PROJECT, + workspaceSlug, + projectId + ); const isAcceptedOrDeclined = inboxIssue?.status ? [-1, 1, 2].includes(inboxIssue.status) : undefined; // days left for snooze const numberOfDaysLeft = findHowManyDaysLeft(inboxIssue?.snoozed_till); @@ -199,6 +205,17 @@ export const InboxIssueActionsHeader: FC = observer((p [handleInboxIssueNavigation] ); + const handleActionWithPermission = (isAdmin: boolean, action: () => void, errorMessage: string) => { + if (isAdmin) action(); + else { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Permission denied", + message: errorMessage, + }); + } + }; + useEffect(() => { if (!isNotificationEmbed) document.addEventListener("keydown", onKeyDown); return () => { @@ -293,7 +310,13 @@ export const InboxIssueActionsHeader: FC = observer((p size="sm" prependIcon={} className="text-green-500 border-0.5 border-green-500 bg-green-500/20 focus:bg-green-500/20 focus:text-green-500 hover:bg-green-500/40 bg-opacity-20" - onClick={() => setAcceptIssueModal(true)} + onClick={() => + handleActionWithPermission( + isProjectAdmin, + () => setAcceptIssueModal(true), + "Only project admins can accept issues" + ) + } > Accept @@ -307,7 +330,13 @@ export const InboxIssueActionsHeader: FC = observer((p size="sm" prependIcon={} className="text-red-500 border-0.5 border-red-500 bg-red-500/20 focus:bg-red-500/20 focus:text-red-500 hover:bg-red-500/40 bg-opacity-20" - onClick={() => setDeclineIssueModal(true)} + onClick={() => + handleActionWithPermission( + isProjectAdmin, + () => setDeclineIssueModal(true), + "Only project admins can deny issues" + ) + } > Decline @@ -341,7 +370,15 @@ export const InboxIssueActionsHeader: FC = observer((p {isAllowed && ( {canMarkAsAccepted && ( - + + handleActionWithPermission( + isProjectAdmin, + handleIssueSnoozeAction, + "Only project admins can snooze/Un-snooze issues" + ) + } + >
{inboxIssue?.snoozed_till && numberOfDaysLeft && numberOfDaysLeft > 0 @@ -351,7 +388,15 @@ export const InboxIssueActionsHeader: FC = observer((p )} {canMarkAsDuplicate && ( - setSelectDuplicateIssue(true)}> + + handleActionWithPermission( + isProjectAdmin, + () => setSelectDuplicateIssue(true), + "Only project admins can mark issues as duplicate" + ) + } + >
Mark as duplicate @@ -401,6 +446,8 @@ export const InboxIssueActionsHeader: FC = observer((p setIsMobileSidebar={setIsMobileSidebar} isNotificationEmbed={isNotificationEmbed} embedRemoveCurrentNotification={embedRemoveCurrentNotification} + isProjectAdmin={isProjectAdmin} + handleActionWithPermission={handleActionWithPermission} />
diff --git a/web/core/components/inbox/content/inbox-issue-mobile-header.tsx b/web/core/components/inbox/content/inbox-issue-mobile-header.tsx index e87573e9b..7a66d0976 100644 --- a/web/core/components/inbox/content/inbox-issue-mobile-header.tsx +++ b/web/core/components/inbox/content/inbox-issue-mobile-header.tsx @@ -47,6 +47,8 @@ type Props = { setIsMobileSidebar: (value: boolean) => void; isNotificationEmbed: boolean; embedRemoveCurrentNotification?: () => void; + isProjectAdmin: boolean; + handleActionWithPermission: (isAdmin: boolean, action: () => void, errorMessage: string) => void; }; export const InboxIssueActionsMobileHeader: React.FC = observer((props) => { @@ -70,6 +72,8 @@ export const InboxIssueActionsMobileHeader: React.FC = observer((props) = setIsMobileSidebar, isNotificationEmbed, embedRemoveCurrentNotification, + isProjectAdmin, + handleActionWithPermission, } = props; const router = useAppRouter(); const issue = inboxIssue?.issue; @@ -139,7 +143,15 @@ export const InboxIssueActionsMobileHeader: React.FC = observer((props) =
)} {canMarkAsAccepted && !isAcceptedOrDeclined && ( - + + handleActionWithPermission( + isProjectAdmin, + handleIssueSnoozeAction, + "Only project admins can snooze/Un-snooze issues" + ) + } + >
{inboxIssue?.snoozed_till && numberOfDaysLeft && numberOfDaysLeft > 0 ? "Un-snooze" : "Snooze"} @@ -147,7 +159,15 @@ export const InboxIssueActionsMobileHeader: React.FC = observer((props) = )} {canMarkAsDuplicate && !isAcceptedOrDeclined && ( - setSelectDuplicateIssue(true)}> + + handleActionWithPermission( + isProjectAdmin, + () => setSelectDuplicateIssue(true), + "Only project admins can mark issues as duplicate" + ) + } + >
Mark as duplicate @@ -155,7 +175,15 @@ export const InboxIssueActionsMobileHeader: React.FC = observer((props) = )} {canMarkAsAccepted && ( - setAcceptIssueModal(true)}> + + handleActionWithPermission( + isProjectAdmin, + () => setAcceptIssueModal(true), + "Only project admins can accept issues" + ) + } + >
Accept @@ -163,7 +191,15 @@ export const InboxIssueActionsMobileHeader: React.FC = observer((props) = )} {canMarkAsDeclined && ( - setDeclineIssueModal(true)}> + + handleActionWithPermission( + isProjectAdmin, + () => setDeclineIssueModal(true), + "Only project admins can deny issues" + ) + } + >
Decline diff --git a/web/core/components/inbox/content/root.tsx b/web/core/components/inbox/content/root.tsx index 852be8a80..504b1d593 100644 --- a/web/core/components/inbox/content/root.tsx +++ b/web/core/components/inbox/content/root.tsx @@ -62,10 +62,10 @@ export const InboxContentRoot: FC = observer((props) => { } ); - const isEditable = allowPermissions( - [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST], - EUserPermissionsLevel.PROJECT - ); + const isEditable = + allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT) || + inboxIssue.created_by === currentUser?.id; + const isGuest = projectPermissionsByWorkspaceSlugAndProjectId(workspaceSlug, projectId) === EUserPermissions.GUEST; const isOwner = inboxIssue?.issue.created_by === currentUser?.id; const readOnly = !isOwner && isGuest; diff --git a/web/core/store/inbox/project-inbox.store.ts b/web/core/store/inbox/project-inbox.store.ts index bf0a48576..289782539 100644 --- a/web/core/store/inbox/project-inbox.store.ts +++ b/web/core/store/inbox/project-inbox.store.ts @@ -423,7 +423,7 @@ export class ProjectInboxStore implements IProjectInboxStore { if (inboxIssue && issueId) { runInAction(() => { - set(this.inboxIssues, [issueId], new InboxIssueStore(workspaceSlug, projectId, inboxIssue, this.store)); + this.createOrUpdateInboxIssue([inboxIssue], workspaceSlug, projectId); set(this, "loader", undefined); }); await Promise.all([ From 8981e52dcc43c31caaa916a4343e4c8e54fd3db2 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Tue, 8 Oct 2024 18:43:13 +0530 Subject: [PATCH 03/10] [WEB-2601] improvement: add click to copy issue identifier on peek-overview and issue detail page. (#5760) --- .../issues/issue-details/issue-identifier.tsx | 54 +++++++++++++++++-- .../issue-details/issue-type-switcher.tsx | 2 +- 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/web/ce/components/issues/issue-details/issue-identifier.tsx b/web/ce/components/issues/issue-details/issue-identifier.tsx index b12cc6de7..c461e88fa 100644 --- a/web/ce/components/issues/issue-details/issue-identifier.tsx +++ b/web/ce/components/issues/issue-details/issue-identifier.tsx @@ -1,6 +1,8 @@ import { observer } from "mobx-react"; // types import { IIssueDisplayProperties } from "@plane/types"; +// ui +import { setToast, TOAST_TYPE, Tooltip } from "@plane/ui"; // helpers import { cn } from "@/helpers/common.helper"; // hooks @@ -11,6 +13,7 @@ type TIssueIdentifierBaseProps = { size?: "xs" | "sm" | "md" | "lg"; textContainerClassName?: string; displayProperties?: IIssueDisplayProperties | undefined; + enableClickToCopyIdentifier?: boolean; }; type TIssueIdentifierFromStore = TIssueIdentifierBaseProps & { @@ -23,9 +26,48 @@ type TIssueIdentifierWithDetails = TIssueIdentifierBaseProps & { issueSequenceId: string | number; }; -type TIssueIdentifierProps = TIssueIdentifierFromStore | TIssueIdentifierWithDetails; +export type TIssueIdentifierProps = TIssueIdentifierFromStore | TIssueIdentifierWithDetails; + +type TIdentifierTextProps = { + identifier: string; + enableClickToCopyIdentifier?: boolean; + textContainerClassName?: string; +}; + +export const IdentifierText: React.FC = (props) => { + const { identifier, enableClickToCopyIdentifier = false, textContainerClassName } = props; + // handlers + const handleCopyIssueIdentifier = () => { + if (enableClickToCopyIdentifier) { + navigator.clipboard.writeText(identifier).then(() => { + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Issue ID copied to clipboard", + }); + }); + } + }; + + return ( + + + {identifier} + + + ); +}; + export const IssueIdentifier: React.FC = observer((props) => { - const { projectId, textContainerClassName, displayProperties } = props; + const { projectId, textContainerClassName, displayProperties, enableClickToCopyIdentifier = false } = props; // store hooks const { getProjectIdentifierById } = useProject(); const { @@ -43,9 +85,11 @@ export const IssueIdentifier: React.FC = observer((props) return (
- - {projectIdentifier}-{issueSequenceId} - +
); }); diff --git a/web/ce/components/issues/issue-details/issue-type-switcher.tsx b/web/ce/components/issues/issue-details/issue-type-switcher.tsx index 5cbd8e6d6..5d4adeb95 100644 --- a/web/ce/components/issues/issue-details/issue-type-switcher.tsx +++ b/web/ce/components/issues/issue-details/issue-type-switcher.tsx @@ -20,5 +20,5 @@ export const IssueTypeSwitcher: React.FC = observer((pr if (!issue || !issue.project_id) return <>; - return ; + return ; }); From 55f44e0245782aed9f74bd122e6084b8914b91e6 Mon Sep 17 00:00:00 2001 From: Akshita Goyal <36129505+gakshita@users.noreply.github.com> Date: Tue, 8 Oct 2024 17:10:16 +0530 Subject: [PATCH 04/10] fix: spreadsheet flicker issue (#5769) --- .../issues/issue-layouts/spreadsheet/issue-row.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/web/core/components/issues/issue-layouts/spreadsheet/issue-row.tsx b/web/core/components/issues/issue-layouts/spreadsheet/issue-row.tsx index 0ee9d96fd..ab4b73df4 100644 --- a/web/core/components/issues/issue-layouts/spreadsheet/issue-row.tsx +++ b/web/core/components/issues/issue-layouts/spreadsheet/issue-row.tsx @@ -18,7 +18,7 @@ import { SPREADSHEET_SELECT_GROUP } from "@/constants/spreadsheet"; // helper import { cn } from "@/helpers/common.helper"; // hooks -import { useIssueDetail, useProject } from "@/hooks/store"; +import { useIssueDetail, useIssues, useProject } from "@/hooks/store"; import useIssuePeekOverviewRedirection from "@/hooks/use-issue-peek-overview-redirection"; import { TSelectionHelper } from "@/hooks/use-multiple-select"; import { usePlatformOS } from "@/hooks/use-platform-os"; @@ -26,6 +26,7 @@ import { usePlatformOS } from "@/hooks/use-platform-os"; import { IssueIdentifier } from "@/plane-web/components/issues"; // local components import { TRenderQuickActions } from "../list/list-view-types"; +import { isIssueNew } from "../utils"; import { IssueColumn } from "./issue-column"; interface Props { @@ -42,6 +43,7 @@ interface Props { spreadsheetColumnsList: (keyof IIssueDisplayProperties)[]; spacingLeft?: number; selectionHelpers: TSelectionHelper; + shouldRenderByDefault?: boolean; } export const SpreadsheetIssueRow = observer((props: Props) => { @@ -59,11 +61,14 @@ export const SpreadsheetIssueRow = observer((props: Props) => { spreadsheetColumnsList, spacingLeft = 6, selectionHelpers, + shouldRenderByDefault, } = props; // states const [isExpanded, setExpanded] = useState(false); // store hooks const { subIssues: subIssuesStore } = useIssueDetail(); + const { issueMap } = useIssues(); + // derived values const subIssues = subIssuesStore.subIssuesByIssueId(issueId); const isIssueSelected = selectionHelpers.getIsEntitySelected(issueId); @@ -88,6 +93,7 @@ export const SpreadsheetIssueRow = observer((props: Props) => { })} verticalOffset={100} shouldRecordHeights={false} + defaultValue={shouldRenderByDefault || isIssueNew(issueMap[issueId])} > { containerRef={containerRef} spreadsheetColumnsList={spreadsheetColumnsList} selectionHelpers={selectionHelpers} + shouldRenderByDefault={isExpanded} /> ))} From 852fc9bac1a6fb90a5437849a027d1473a627000 Mon Sep 17 00:00:00 2001 From: "M. Palanikannan" <73993394+Palanikannan1437@users.noreply.github.com> Date: Tue, 8 Oct 2024 16:55:26 +0530 Subject: [PATCH 05/10] [WEB-2603] fix: remove validation of roles from the live server (#5761) * fix: remove validation of roles from the live server * chore: remove the service * fix: remove all validation of authorization * fix: props updated --- live/src/ce/lib/authentication.ts | 15 -------- live/src/core/hocuspocus-server.ts | 8 +---- live/src/core/lib/authentication.ts | 47 +------------------------- live/src/core/services/user.service.ts | 35 +------------------ 4 files changed, 3 insertions(+), 102 deletions(-) delete mode 100644 live/src/ce/lib/authentication.ts diff --git a/live/src/ce/lib/authentication.ts b/live/src/ce/lib/authentication.ts deleted file mode 100644 index 3d5a1ea48..000000000 --- a/live/src/ce/lib/authentication.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ConnectionConfiguration } from "@hocuspocus/server"; -// types -import { TDocumentTypes } from "@/core/types/common.js"; - -type TArgs = { - connection: ConnectionConfiguration - cookie: string; - documentType: TDocumentTypes | undefined; - params: URLSearchParams; -} - -export const authenticateUser = async (args: TArgs): Promise => { - const { documentType } = args; - throw Error(`Authentication failed: Invalid document type ${documentType} provided.`); -} \ No newline at end of file diff --git a/live/src/core/hocuspocus-server.ts b/live/src/core/hocuspocus-server.ts index fb30c8f82..0aa411b93 100644 --- a/live/src/core/hocuspocus-server.ts +++ b/live/src/core/hocuspocus-server.ts @@ -12,15 +12,11 @@ export const getHocusPocusServer = async () => { name: serverName, onAuthenticate: async ({ requestHeaders, - requestParameters, - connection, // user id used as token for authentication token, }) => { // request headers const cookie = requestHeaders.cookie?.toString(); - // params - const params = requestParameters; if (!cookie) { throw Error("Credentials not provided"); @@ -28,9 +24,7 @@ export const getHocusPocusServer = async () => { try { await handleAuthentication({ - connection, cookie, - params, token, }); } catch (error) { @@ -38,6 +32,6 @@ export const getHocusPocusServer = async () => { } }, extensions, - debounce: 10000 + debounce: 10000, }); }; diff --git a/live/src/core/lib/authentication.ts b/live/src/core/lib/authentication.ts index dbde17959..ee01b0209 100644 --- a/live/src/core/lib/authentication.ts +++ b/live/src/core/lib/authentication.ts @@ -1,28 +1,17 @@ -import { ConnectionConfiguration } from "@hocuspocus/server"; // services import { UserService } from "@/core/services/user.service.js"; -// types -import { TDocumentTypes } from "@/core/types/common.js"; -// plane live lib -import { authenticateUser } from "@/plane-live/lib/authentication.js"; // core helpers import { manualLogger } from "@/core/helpers/logger.js"; const userService = new UserService(); type Props = { - connection: ConnectionConfiguration; cookie: string; - params: URLSearchParams; token: string; }; export const handleAuthentication = async (props: Props) => { - const { connection, cookie, params, token } = props; - // params - const documentType = params.get("documentType")?.toString() as - | TDocumentTypes - | undefined; + const { cookie, token } = props; // fetch current user info let response; try { @@ -35,40 +24,6 @@ export const handleAuthentication = async (props: Props) => { throw Error("Authentication failed: Token doesn't match the current user."); } - if (documentType === "project_page") { - // params - const workspaceSlug = params.get("workspaceSlug")?.toString(); - const projectId = params.get("projectId")?.toString(); - if (!workspaceSlug || !projectId) { - throw Error( - "Authentication failed: Incomplete query params. Either workspaceSlug or projectId is missing." - ); - } - // fetch current user's project membership info - try { - const projectMembershipInfo = await userService.getUserProjectMembership( - workspaceSlug, - projectId, - cookie - ); - const projectRole = projectMembershipInfo.role; - // make the connection read only for roles lower than a member - if (projectRole < 15) { - connection.readOnly = true; - } - } catch (error) { - manualLogger.error("Failed to fetch project membership info:", error); - throw error; - } - } else { - await authenticateUser({ - connection, - cookie, - documentType, - params, - }); - } - return { user: { id: response.id, diff --git a/live/src/core/services/user.service.ts b/live/src/core/services/user.service.ts index 09412aa53..39d200919 100644 --- a/live/src/core/services/user.service.ts +++ b/live/src/core/services/user.service.ts @@ -1,5 +1,5 @@ // types -import type { IProjectMember, IUser } from "@plane/types"; +import type { IUser } from "@plane/types"; // services import { API_BASE_URL, APIService } from "@/core/services/api.service.js"; @@ -25,37 +25,4 @@ export class UserService extends APIService { throw error; }); } - - async getUserWorkspaceMembership( - workspaceSlug: string, - cookie: string - ): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/workspace-members/me/`, - { - headers: { - Cookie: cookie, - }, - }) - .then((response) => response?.data) - .catch((error) => { - throw error?.response; - }); - } - - async getUserProjectMembership( - workspaceSlug: string, - projectId: string, - cookie: string - ): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/project-members/me/`, - { - headers: { - Cookie: cookie, - }, - }) - .then((response) => response?.data) - .catch((error) => { - throw error?.response; - }); - } } From b97fcfb46da26498f75e4819b9970ed8ecf40ae3 Mon Sep 17 00:00:00 2001 From: "M. Palanikannan" <73993394+Palanikannan1437@users.noreply.github.com> Date: Tue, 8 Oct 2024 16:50:32 +0530 Subject: [PATCH 06/10] fix: show the full screen toolbar in read only instances as well (#5746) --- .../extensions/custom-image/components/image-block.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) 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 f067ee947..ed60f3dab 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 @@ -201,8 +201,10 @@ export const CustomImageBlock: React.FC = (props) => { // show the image loader if the remote image's src or preview image from filesystem is not set yet (while loading the image post upload) (or) // if the initial resize (from 35% width and "auto" height attrs to the actual size in px) is not complete const showImageLoader = !(remoteImageSrc || imageFromFileSystem) || !initialResizeComplete; - // show the image utils only if the editor is editable, the remote image's (post upload) src is set and the initial resize is complete (but not while we're showing the preview imageFromFileSystem) - const showImageUtils = editor.isEditable && remoteImageSrc && initialResizeComplete; + // show the image utils only if the remote image's (post upload) src is set and the initial resize is complete (but not while we're showing the preview imageFromFileSystem) + const showImageUtils = remoteImageSrc && initialResizeComplete; + // show the image resizer only if the editor is editable, the remote image's (post upload) src is set and the initial resize is complete (but not while we're showing the preview imageFromFileSystem) + const showImageResizer = editor.isEditable && remoteImageSrc && initialResizeComplete; // show the preview image from the file system if the remote image's src is not set const displayedImageSrc = remoteImageSrc ?? imageFromFileSystem; @@ -258,7 +260,7 @@ export const CustomImageBlock: React.FC = (props) => { {selected && displayedImageSrc === remoteImageSrc && (
)} - {showImageUtils && ( + {showImageResizer && ( <>
Date: Tue, 8 Oct 2024 16:47:16 +0530 Subject: [PATCH 07/10] [WEB-2532] fix: custom theme mutation logic (#5685) * fix: custom theme mutation logic * chore: update querySelector element --- web/app/profile/appearance/page.tsx | 9 ++------ web/core/lib/wrappers/store-wrapper.tsx | 30 +++---------------------- web/helpers/theme.helper.ts | 19 ++++++++-------- 3 files changed, 15 insertions(+), 43 deletions(-) diff --git a/web/app/profile/appearance/page.tsx b/web/app/profile/appearance/page.tsx index ef19a3342..775ff637b 100644 --- a/web/app/profile/appearance/page.tsx +++ b/web/app/profile/appearance/page.tsx @@ -52,13 +52,8 @@ const ProfileAppearancePage = observer(() => { const applyThemeChange = (theme: Partial) => { setTheme(theme?.theme || "system"); - const customThemeElement = window.document?.querySelector("[data-theme='custom']"); - if (theme?.theme === "custom" && theme?.palette && customThemeElement) { - applyTheme( - theme?.palette !== ",,,," ? theme?.palette : "#0d101b,#c5c5c5,#3f76ff,#0d101b,#c5c5c5", - false, - customThemeElement - ); + if (theme?.theme === "custom" && theme?.palette) { + applyTheme(theme?.palette !== ",,,," ? theme?.palette : "#0d101b,#c5c5c5,#3f76ff,#0d101b,#c5c5c5", false); } else unsetCustomCssVariables(); }; diff --git a/web/core/lib/wrappers/store-wrapper.tsx b/web/core/lib/wrappers/store-wrapper.tsx index f12821c5a..fa60c354e 100644 --- a/web/core/lib/wrappers/store-wrapper.tsx +++ b/web/core/lib/wrappers/store-wrapper.tsx @@ -21,8 +21,6 @@ const StoreWrapper: FC = observer((props) => { const { setQuery } = useRouterParams(); const { sidebarCollapsed, toggleSidebar } = useAppTheme(); const { data: userProfile } = useUserProfile(); - // states - const [dom, setDom] = useState(null); /** * Sidebar collapsed fetching from local storage @@ -44,36 +42,14 @@ const StoreWrapper: FC = observer((props) => { const currentThemePalette = userProfile?.theme?.palette; if (currentTheme) { setTheme(currentTheme); - if (currentTheme === "custom" && currentThemePalette && dom) { + if (currentTheme === "custom" && currentThemePalette) { applyTheme( currentThemePalette !== ",,,," ? currentThemePalette : "#0d101b,#c5c5c5,#3f76ff,#0d101b,#c5c5c5", - false, - dom + false ); } else unsetCustomCssVariables(); } - }, [userProfile?.theme?.theme, userProfile?.theme?.palette, setTheme, dom]); - - useEffect(() => { - if (dom) return; - - const observer = new MutationObserver((mutationsList, observer) => { - for (const mutation of mutationsList) { - if (mutation.type === "childList") { - const customThemeElement = window.document?.querySelector("[data-theme='custom']"); - if (customThemeElement) { - setDom(customThemeElement); - observer.disconnect(); - break; - } - } - } - }); - - observer.observe(document.body, { childList: true, subtree: true }); - - return () => observer.disconnect(); - }, [dom]); + }, [userProfile?.theme?.theme, userProfile?.theme?.palette, setTheme]); useEffect(() => { if (!params) return; diff --git a/web/helpers/theme.helper.ts b/web/helpers/theme.helper.ts index fd3bd07af..9b0638c1f 100644 --- a/web/helpers/theme.helper.ts +++ b/web/helpers/theme.helper.ts @@ -59,8 +59,9 @@ const calculateShades = (hexValue: string): TShades => { return shades as TShades; }; -export const applyTheme = (palette: string, isDarkPalette: boolean, dom: HTMLElement | null) => { +export const applyTheme = (palette: string, isDarkPalette: boolean) => { if (!palette) return; + const themeElement = document?.querySelector("html"); // palette: [bg, text, primary, sidebarBg, sidebarText] const values: string[] = palette.split(","); values.push(isDarkPalette ? "dark" : "light"); @@ -80,27 +81,27 @@ export const applyTheme = (palette: string, isDarkPalette: boolean, dom: HTMLEle const sidebarBackgroundRgbValues = `${sidebarBackgroundShades[shade].r}, ${sidebarBackgroundShades[shade].g}, ${sidebarBackgroundShades[shade].b}`; const sidebarTextRgbValues = `${sidebarTextShades[shade].r}, ${sidebarTextShades[shade].g}, ${sidebarTextShades[shade].b}`; - dom?.style.setProperty(`--color-background-${shade}`, bgRgbValues); - dom?.style.setProperty(`--color-text-${shade}`, textRgbValues); - dom?.style.setProperty(`--color-primary-${shade}`, primaryRgbValues); - dom?.style.setProperty(`--color-sidebar-background-${shade}`, sidebarBackgroundRgbValues); - dom?.style.setProperty(`--color-sidebar-text-${shade}`, sidebarTextRgbValues); + themeElement?.style.setProperty(`--color-background-${shade}`, bgRgbValues); + themeElement?.style.setProperty(`--color-text-${shade}`, textRgbValues); + themeElement?.style.setProperty(`--color-primary-${shade}`, primaryRgbValues); + themeElement?.style.setProperty(`--color-sidebar-background-${shade}`, sidebarBackgroundRgbValues); + themeElement?.style.setProperty(`--color-sidebar-text-${shade}`, sidebarTextRgbValues); if (i >= 100 && i <= 400) { const borderShade = i === 100 ? 70 : i === 200 ? 80 : i === 300 ? 90 : 100; - dom?.style.setProperty( + themeElement?.style.setProperty( `--color-border-${shade}`, `${bgShades[borderShade].r}, ${bgShades[borderShade].g}, ${bgShades[borderShade].b}` ); - dom?.style.setProperty( + themeElement?.style.setProperty( `--color-sidebar-border-${shade}`, `${sidebarBackgroundShades[borderShade].r}, ${sidebarBackgroundShades[borderShade].g}, ${sidebarBackgroundShades[borderShade].b}` ); } } - dom?.style.setProperty("--color-scheme", values[5]); + themeElement?.style.setProperty("--color-scheme", values[5]); }; export const unsetCustomCssVariables = () => { From 2b1da96c3ff3083bd33928285c62e344aae69cf3 Mon Sep 17 00:00:00 2001 From: "M. Palanikannan" <73993394+Palanikannan1437@users.noreply.github.com> Date: Tue, 8 Oct 2024 16:44:05 +0530 Subject: [PATCH 08/10] fix: drag handle scrolling fixed (#5619) * fix: drag handle scrolling fixed * fix: closest scrollable parent found and scrolled * fix: removed overflow auto from framerenderer * fix: make dragging dynamic and smoother --- .../editor/src/core/extensions/side-menu.tsx | 2 +- .../editor/src/core/plugins/drag-handle.ts | 44 ++++++++++++++++--- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/packages/editor/src/core/extensions/side-menu.tsx b/packages/editor/src/core/extensions/side-menu.tsx index 616e315e2..5ab6fbdf5 100644 --- a/packages/editor/src/core/extensions/side-menu.tsx +++ b/packages/editor/src/core/extensions/side-menu.tsx @@ -42,7 +42,7 @@ export const SideMenuExtension = (props: Props) => { ai: aiEnabled, dragDrop: dragDropEnabled, }, - scrollThreshold: { up: 300, down: 100 }, + scrollThreshold: { up: 200, down: 100 }, }), ]; }, diff --git a/packages/editor/src/core/plugins/drag-handle.ts b/packages/editor/src/core/plugins/drag-handle.ts index ba390180b..809802b4f 100644 --- a/packages/editor/src/core/plugins/drag-handle.ts +++ b/packages/editor/src/core/plugins/drag-handle.ts @@ -233,14 +233,46 @@ export const DragHandlePlugin = (options: SideMenuPluginProps): SideMenuHandleOp dragHandleElement.addEventListener("click", (e) => handleClick(e, view)); dragHandleElement.addEventListener("contextmenu", (e) => handleClick(e, view)); + const isScrollable = (node: HTMLElement | SVGElement) => { + if (!(node instanceof HTMLElement || node instanceof SVGElement)) { + return false; + } + const style = getComputedStyle(node); + return ["overflow", "overflow-y"].some((propertyName) => { + const value = style.getPropertyValue(propertyName); + return value === "auto" || value === "scroll"; + }); + }; + + const getScrollParent = (node: HTMLElement | SVGElement) => { + let currentParent = node.parentElement; + while (currentParent) { + if (isScrollable(currentParent)) { + return currentParent; + } + currentParent = currentParent.parentElement; + } + return document.scrollingElement || document.documentElement; + }; + + const maxScrollSpeed = 100; + dragHandleElement.addEventListener("drag", (e) => { hideDragHandle(); - const frameRenderer = document.querySelector(".frame-renderer"); - if (!frameRenderer) return; - if (e.clientY < options.scrollThreshold.up) { - frameRenderer.scrollBy({ top: -70, behavior: "smooth" }); - } else if (window.innerHeight - e.clientY < options.scrollThreshold.down) { - frameRenderer.scrollBy({ top: 70, behavior: "smooth" }); + const scrollableParent = getScrollParent(dragHandleElement); + if (!scrollableParent) return; + const scrollThreshold = options.scrollThreshold; + + if (e.clientY < scrollThreshold.up) { + const overflow = scrollThreshold.up - e.clientY; + const ratio = Math.min(overflow / scrollThreshold.up, 1); + const scrollAmount = -maxScrollSpeed * ratio; + scrollableParent.scrollBy({ top: scrollAmount }); + } else if (window.innerHeight - e.clientY < scrollThreshold.down) { + const overflow = e.clientY - (window.innerHeight - scrollThreshold.down); + const ratio = Math.min(overflow / scrollThreshold.down, 1); + const scrollAmount = maxScrollSpeed * ratio; + scrollableParent.scrollBy({ top: scrollAmount }); } }); From 7495a7d0cbeed63e6b82b3b56335dba57ac50ea4 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Tue, 8 Oct 2024 13:20:27 +0530 Subject: [PATCH 09/10] [WEB-2605] fix: update URL regex pattern to allow complex links. (#5767) --- web/helpers/string.helper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/helpers/string.helper.ts b/web/helpers/string.helper.ts index 1182feeb0..e4b4dc665 100644 --- a/web/helpers/string.helper.ts +++ b/web/helpers/string.helper.ts @@ -270,7 +270,7 @@ export const isCommentEmpty = (comment: string | undefined): boolean => { export const checkURLValidity = (url: string): boolean => { if (!url) return false; // regex to match valid URLs (with or without http/https) - const urlPattern = /^(https?:\/\/)?([\da-z.-]+)\.([a-z]{2,6})(\/[\w.-]*)*\/?(\?[=&\w.-]*)?$/i; + const urlPattern = /^(https?:\/\/)?([\w.-]+\.[a-z]{2,6})(\/[\w\-.~:/?#[\]@!$&'()*+,;=%]*)?$/i; // test if the URL matches the pattern return urlPattern.test(url); }; From 8a866e440c9cb12faf8f34338cf074cc428bd512 Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Mon, 7 Oct 2024 20:07:24 +0530 Subject: [PATCH 10/10] chore: only admin can changed the project settings (#5766) --- apiserver/plane/app/views/project/base.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/apiserver/plane/app/views/project/base.py b/apiserver/plane/app/views/project/base.py index 6a9afb652..f5ddb2245 100644 --- a/apiserver/plane/app/views/project/base.py +++ b/apiserver/plane/app/views/project/base.py @@ -413,9 +413,20 @@ class ProjectViewSet(BaseViewSet): status=status.HTTP_410_GONE, ) - @allow_permission([ROLE.ADMIN]) def partial_update(self, request, slug, pk=None): try: + if not ProjectMember.objects.filter( + member=request.user, + workspace__slug=slug, + project_id=pk, + role=20, + is_active=True, + ).exists(): + return Response( + {"error": "You don't have the required permissions."}, + status=status.HTTP_403_FORBIDDEN, + ) + workspace = Workspace.objects.get(slug=slug) project = Project.objects.get(pk=pk)