Merge pull request #5788 from makeplane/preview

release: v0.23.1
This commit is contained in:
sriram veeraghanta 2024-10-10 15:11:04 +05:30 committed by GitHub
commit 9bab108329
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
37 changed files with 251 additions and 195 deletions

View file

@ -1,6 +1,6 @@
{
"name": "admin",
"version": "0.23.0",
"version": "0.23.1",
"private": true,
"scripts": {
"dev": "turbo run develop",

View file

@ -1,4 +1,4 @@
{
"name": "plane-api",
"version": "0.23.0"
"version": "0.23.1"
}

View file

@ -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
)

View file

@ -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
)

View file

@ -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)

View file

@ -40,6 +40,10 @@ app.conf.beat_schedule = {
"task": "plane.bgtasks.deletion_task.hard_delete",
"schedule": crontab(hour=0, minute=0),
},
"run-every-6-hours-for-instance-trace": {
"task": "plane.license.bgtasks.tracer.instance_traces",
"schedule": crontab(hour="*/6"),
},
}
# Load task modules from all registered Django app configs.

View file

@ -1,7 +1,7 @@
# base requirements
# django
Django==4.2.15
Django==4.2.16
# rest framework
djangorestframework==3.15.2
# postgres

View file

@ -1,6 +1,6 @@
{
"name": "live",
"version": "0.23.0",
"version": "0.23.1",
"description": "",
"main": "./src/server.ts",
"private": true,

View file

@ -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<void> => {
const { documentType } = args;
throw Error(`Authentication failed: Invalid document type ${documentType} provided.`);
}

View file

@ -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,
});
};

View file

@ -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,

View file

@ -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<IProjectMember> {
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<IProjectMember> {
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/project-members/me/`,
{
headers: {
Cookie: cookie,
},
})
.then((response) => response?.data)
.catch((error) => {
throw error?.response;
});
}
}

View file

@ -1,6 +1,6 @@
{
"repository": "https://github.com/makeplane/plane.git",
"version": "0.23.0",
"version": "0.23.1",
"license": "AGPL-3.0",
"private": true,
"workspaces": [

View file

@ -1,6 +1,6 @@
{
"name": "@plane/constants",
"version": "0.23.0",
"version": "0.23.1",
"private": true,
"main": "./index.ts"
}

View file

@ -1,6 +1,6 @@
{
"name": "@plane/editor",
"version": "0.23.0",
"version": "0.23.1",
"description": "Core Editor that powers Plane",
"private": true,
"main": "./dist/index.mjs",

View file

@ -201,8 +201,10 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (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<CustomImageBlockProps> = (props) => {
{selected && displayedImageSrc === remoteImageSrc && (
<div className="absolute inset-0 size-full bg-custom-primary-500/30" />
)}
{showImageUtils && (
{showImageResizer && (
<>
<div
className={cn(

View file

@ -42,7 +42,7 @@ export const SideMenuExtension = (props: Props) => {
ai: aiEnabled,
dragDrop: dragDropEnabled,
},
scrollThreshold: { up: 300, down: 100 },
scrollThreshold: { up: 200, down: 100 },
}),
];
},

View file

@ -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 });
}
});

View file

@ -1,7 +1,7 @@
{
"name": "@plane/eslint-config",
"private": true,
"version": "0.23.0",
"version": "0.23.1",
"files": [
"library.js",
"next.js",

View file

@ -1,6 +1,6 @@
{
"name": "@plane/helpers",
"version": "0.23.0",
"version": "0.23.1",
"description": "Helper functions shared across multiple apps internally",
"private": true,
"main": "./dist/index.js",

View file

@ -1,6 +1,6 @@
{
"name": "tailwind-config-custom",
"version": "0.23.0",
"version": "0.23.1",
"description": "common tailwind configuration across monorepo",
"main": "index.js",
"private": true,

View file

@ -1,6 +1,6 @@
{
"name": "@plane/types",
"version": "0.23.0",
"version": "0.23.1",
"private": true,
"types": "./src/index.d.ts",
"main": "./src/index.d.ts"

View file

@ -1,6 +1,6 @@
{
"name": "@plane/typescript-config",
"version": "0.23.0",
"version": "0.23.1",
"private": true,
"files": [
"base.json",

View file

@ -2,7 +2,7 @@
"name": "@plane/ui",
"description": "UI components shared across multiple apps internally",
"private": true,
"version": "0.23.0",
"version": "0.23.1",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",

View file

@ -1,6 +1,6 @@
{
"name": "space",
"version": "0.23.0",
"version": "0.23.1",
"private": true,
"scripts": {
"dev": "turbo run develop",

View file

@ -52,13 +52,8 @@ const ProfileAppearancePage = observer(() => {
const applyThemeChange = (theme: Partial<IUserTheme>) => {
setTheme(theme?.theme || "system");
const customThemeElement = window.document?.querySelector<HTMLElement>("[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();
};

View file

@ -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<TIdentifierTextProps> = (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 (
<Tooltip tooltipContent="Click to copy" disabled={!enableClickToCopyIdentifier} position="top">
<span
className={cn(
"text-base font-medium text-custom-text-300",
{
"cursor-pointer": enableClickToCopyIdentifier,
},
textContainerClassName
)}
onClick={handleCopyIssueIdentifier}
>
{identifier}
</span>
</Tooltip>
);
};
export const IssueIdentifier: React.FC<TIssueIdentifierProps> = 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<TIssueIdentifierProps> = observer((props)
return (
<div className="flex items-center space-x-2">
<span className={cn("text-base font-medium text-custom-text-300", textContainerClassName)}>
{projectIdentifier}-{issueSequenceId}
</span>
<IdentifierText
identifier={`${projectIdentifier}-${issueSequenceId}`}
enableClickToCopyIdentifier={enableClickToCopyIdentifier}
textContainerClassName={textContainerClassName}
/>
</div>
);
});

View file

@ -20,5 +20,5 @@ export const IssueTypeSwitcher: React.FC<TIssueTypeSwitcherProps> = observer((pr
if (!issue || !issue.project_id) return <></>;
return <IssueIdentifier issueId={issueId} projectId={issue.project_id} size="md" />;
return <IssueIdentifier issueId={issueId} projectId={issue.project_id} size="md" enableClickToCopyIdentifier />;
});

View file

@ -89,6 +89,12 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = 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<TInboxIssueActionsHeader> = 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<TInboxIssueActionsHeader> = observer((p
size="sm"
prependIcon={<CircleCheck className="w-3 h-3" />}
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
</Button>
@ -307,7 +330,13 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
size="sm"
prependIcon={<CircleX className="w-3 h-3" />}
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
</Button>
@ -341,7 +370,15 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
{isAllowed && (
<CustomMenu verticalEllipsis placement="bottom-start">
{canMarkAsAccepted && (
<CustomMenu.MenuItem onClick={handleIssueSnoozeAction}>
<CustomMenu.MenuItem
onClick={() =>
handleActionWithPermission(
isProjectAdmin,
handleIssueSnoozeAction,
"Only project admins can snooze/Un-snooze issues"
)
}
>
<div className="flex items-center gap-2">
<Clock size={14} strokeWidth={2} />
{inboxIssue?.snoozed_till && numberOfDaysLeft && numberOfDaysLeft > 0
@ -351,7 +388,15 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
</CustomMenu.MenuItem>
)}
{canMarkAsDuplicate && (
<CustomMenu.MenuItem onClick={() => setSelectDuplicateIssue(true)}>
<CustomMenu.MenuItem
onClick={() =>
handleActionWithPermission(
isProjectAdmin,
() => setSelectDuplicateIssue(true),
"Only project admins can mark issues as duplicate"
)
}
>
<div className="flex items-center gap-2">
<FileStack size={14} strokeWidth={2} />
Mark as duplicate
@ -401,6 +446,8 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
setIsMobileSidebar={setIsMobileSidebar}
isNotificationEmbed={isNotificationEmbed}
embedRemoveCurrentNotification={embedRemoveCurrentNotification}
isProjectAdmin={isProjectAdmin}
handleActionWithPermission={handleActionWithPermission}
/>
</div>
</>

View file

@ -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<Props> = observer((props) => {
@ -70,6 +72,8 @@ export const InboxIssueActionsMobileHeader: React.FC<Props> = observer((props) =
setIsMobileSidebar,
isNotificationEmbed,
embedRemoveCurrentNotification,
isProjectAdmin,
handleActionWithPermission,
} = props;
const router = useAppRouter();
const issue = inboxIssue?.issue;
@ -139,7 +143,15 @@ export const InboxIssueActionsMobileHeader: React.FC<Props> = observer((props) =
</CustomMenu.MenuItem>
)}
{canMarkAsAccepted && !isAcceptedOrDeclined && (
<CustomMenu.MenuItem onClick={handleIssueSnoozeAction}>
<CustomMenu.MenuItem
onClick={() =>
handleActionWithPermission(
isProjectAdmin,
handleIssueSnoozeAction,
"Only project admins can snooze/Un-snooze issues"
)
}
>
<div className="flex items-center gap-2">
<Clock size={14} strokeWidth={2} />
{inboxIssue?.snoozed_till && numberOfDaysLeft && numberOfDaysLeft > 0 ? "Un-snooze" : "Snooze"}
@ -147,7 +159,15 @@ export const InboxIssueActionsMobileHeader: React.FC<Props> = observer((props) =
</CustomMenu.MenuItem>
)}
{canMarkAsDuplicate && !isAcceptedOrDeclined && (
<CustomMenu.MenuItem onClick={() => setSelectDuplicateIssue(true)}>
<CustomMenu.MenuItem
onClick={() =>
handleActionWithPermission(
isProjectAdmin,
() => setSelectDuplicateIssue(true),
"Only project admins can mark issues as duplicate"
)
}
>
<div className="flex items-center gap-2">
<FileStack size={14} strokeWidth={2} />
Mark as duplicate
@ -155,7 +175,15 @@ export const InboxIssueActionsMobileHeader: React.FC<Props> = observer((props) =
</CustomMenu.MenuItem>
)}
{canMarkAsAccepted && (
<CustomMenu.MenuItem onClick={() => setAcceptIssueModal(true)}>
<CustomMenu.MenuItem
onClick={() =>
handleActionWithPermission(
isProjectAdmin,
() => setAcceptIssueModal(true),
"Only project admins can accept issues"
)
}
>
<div className="flex items-center gap-2 text-green-500">
<CircleCheck size={14} strokeWidth={2} />
Accept
@ -163,7 +191,15 @@ export const InboxIssueActionsMobileHeader: React.FC<Props> = observer((props) =
</CustomMenu.MenuItem>
)}
{canMarkAsDeclined && (
<CustomMenu.MenuItem onClick={() => setDeclineIssueModal(true)}>
<CustomMenu.MenuItem
onClick={() =>
handleActionWithPermission(
isProjectAdmin,
() => setDeclineIssueModal(true),
"Only project admins can deny issues"
)
}
>
<div className="flex items-center gap-2 text-red-500">
<CircleX size={14} strokeWidth={2} />
Decline

View file

@ -62,10 +62,10 @@ export const InboxContentRoot: FC<TInboxContentRoot> = 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;

View file

@ -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<boolean>(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])}
>
<IssueRowDetails
issueId={issueId}
@ -124,6 +130,7 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
containerRef={containerRef}
spreadsheetColumnsList={spreadsheetColumnsList}
selectionHelpers={selectionHelpers}
shouldRenderByDefault={isExpanded}
/>
))}
</>

View file

@ -21,8 +21,6 @@ const StoreWrapper: FC<TStoreWrapper> = observer((props) => {
const { setQuery } = useRouterParams();
const { sidebarCollapsed, toggleSidebar } = useAppTheme();
const { data: userProfile } = useUserProfile();
// states
const [dom, setDom] = useState<HTMLElement | null>(null);
/**
* Sidebar collapsed fetching from local storage
@ -44,36 +42,14 @@ const StoreWrapper: FC<TStoreWrapper> = 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<HTMLElement>("[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;

View file

@ -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([

View file

@ -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);
};

View file

@ -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 = () => {

View file

@ -1,6 +1,6 @@
{
"name": "web",
"version": "0.23.0",
"version": "0.23.1",
"private": true,
"scripts": {
"dev": "turbo run develop",