mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
[WEB-1396] chore: remove/ disable all actionable items from deploy url in case url is embedded using Iframe. (#4544)
* fix: is in iframe validation check * chore: remove/ disable all actionable items from deploy url in case url is embeded using Iframe. * chore: remove copy issue link option if clipboard write access is not granted. --------- Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
This commit is contained in:
parent
0c80cf3d54
commit
f13c190676
@ -12,6 +12,7 @@ import { UserAvatar } from "@/components/issues/navbar/user-avatar";
|
||||
import { queryParamGenerator } from "@/helpers/query-param-generator";
|
||||
// hooks
|
||||
import { useProject, useIssueFilter, useIssueDetails } from "@/hooks/store";
|
||||
import useIsInIframe from "@/hooks/use-is-in-iframe";
|
||||
// types
|
||||
import { TIssueLayout } from "@/types/issue";
|
||||
|
||||
@ -39,6 +40,8 @@ export const NavbarControls: FC<NavbarControlsProps> = observer((props) => {
|
||||
// derived values
|
||||
const activeLayout = issueFilters?.display_filters?.layout || undefined;
|
||||
|
||||
const isInIframe = useIsInIframe();
|
||||
|
||||
useEffect(() => {
|
||||
if (workspaceSlug && projectId && settings) {
|
||||
const viewsAcceptable: string[] = [];
|
||||
@ -111,7 +114,7 @@ export const NavbarControls: FC<NavbarControlsProps> = observer((props) => {
|
||||
<NavbarTheme />
|
||||
</div>
|
||||
|
||||
<UserAvatar />
|
||||
{!isInIframe && <UserAvatar />}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
@ -11,6 +11,7 @@ import { CommentReactions } from "@/components/issues/peek-overview";
|
||||
import { timeAgo } from "@/helpers/date-time.helper";
|
||||
// hooks
|
||||
import { useIssueDetails, useProject, useUser } from "@/hooks/store";
|
||||
import useIsInIframe from "@/hooks/use-is-in-iframe";
|
||||
// types
|
||||
import { Comment } from "@/types/issue";
|
||||
|
||||
@ -25,6 +26,7 @@ export const CommentCard: React.FC<Props> = observer((props) => {
|
||||
const { workspace } = useProject();
|
||||
const { peekId, deleteIssueComment, updateIssueComment } = useIssueDetails();
|
||||
const { data: currentUser } = useUser();
|
||||
const isInIframe = useIsInIframe();
|
||||
// derived values
|
||||
const workspaceId = workspace?.id;
|
||||
|
||||
@ -138,7 +140,7 @@ export const CommentCard: React.FC<Props> = observer((props) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{currentUser?.id === comment?.actor_detail?.id && (
|
||||
{!isInIframe && currentUser?.id === comment?.actor_detail?.id && (
|
||||
<Menu as="div" className="relative w-min text-left">
|
||||
<Menu.Button
|
||||
type="button"
|
||||
|
@ -5,10 +5,12 @@ import { Tooltip } from "@plane/ui";
|
||||
// ui
|
||||
import { ReactionSelector } from "@/components/ui";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { groupReactions, renderEmoji } from "@/helpers/emoji.helper";
|
||||
import { queryParamGenerator } from "@/helpers/query-param-generator";
|
||||
// hooks
|
||||
import { useIssueDetails, useUser } from "@/hooks/store";
|
||||
import useIsInIframe from "@/hooks/use-is-in-iframe";
|
||||
|
||||
type Props = {
|
||||
commentId: string;
|
||||
@ -30,6 +32,7 @@ export const CommentReactions: React.FC<Props> = observer((props) => {
|
||||
// hooks
|
||||
const { addCommentReaction, removeCommentReaction, details, peekId } = useIssueDetails();
|
||||
const { data: user } = useUser();
|
||||
const isInIframe = useIsInIframe();
|
||||
|
||||
const commentReactions = peekId ? details[peekId].comments.find((c) => c.id === commentId)?.comment_reactions : [];
|
||||
const groupedReactions = peekId ? groupReactions(commentReactions ?? [], "reaction") : {};
|
||||
@ -58,15 +61,17 @@ export const CommentReactions: React.FC<Props> = observer((props) => {
|
||||
|
||||
return (
|
||||
<div className="mt-2 flex items-center gap-1.5">
|
||||
<ReactionSelector
|
||||
onSelect={(value) => {
|
||||
if (user) handleReactionClick(value);
|
||||
else router.push(`/?next_path=${pathName}?${queryParam}`);
|
||||
}}
|
||||
position="top"
|
||||
selected={userReactions?.map((r) => r.reaction)}
|
||||
size="md"
|
||||
/>
|
||||
{!isInIframe && (
|
||||
<ReactionSelector
|
||||
onSelect={(value) => {
|
||||
if (user) handleReactionClick(value);
|
||||
else router.push(`/?next_path=${pathName}?${queryParam}`);
|
||||
}}
|
||||
position="top"
|
||||
selected={userReactions?.map((r) => r.reaction)}
|
||||
size="md"
|
||||
/>
|
||||
)}
|
||||
|
||||
{Object.keys(groupedReactions || {}).map((reaction) => {
|
||||
const reactions = groupedReactions?.[reaction] ?? [];
|
||||
@ -89,14 +94,20 @@ export const CommentReactions: React.FC<Props> = observer((props) => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (isInIframe) return;
|
||||
if (user) handleReactionClick(reaction);
|
||||
else router.push(`/?next_path=${pathName}?${queryParam}`);
|
||||
}}
|
||||
className={`flex h-full items-center gap-1 rounded-md px-2 py-1 text-sm text-custom-text-100 ${
|
||||
commentReactions?.some((r) => r?.actor_detail?.id === user?.id && r.reaction === reaction)
|
||||
? "bg-custom-primary-100/10"
|
||||
: "bg-custom-background-80"
|
||||
}`}
|
||||
className={cn(
|
||||
`flex h-full items-center gap-1 rounded-md px-2 py-1 text-sm text-custom-text-100 ${
|
||||
commentReactions?.some((r) => r?.actor_detail?.id === user?.id && r.reaction === reaction)
|
||||
? "bg-custom-primary-100/10"
|
||||
: "bg-custom-background-80"
|
||||
}`,
|
||||
{
|
||||
"cursor-default": isInIframe,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<span>{renderEmoji(reaction)}</span>
|
||||
<span
|
||||
|
@ -8,6 +8,7 @@ import { Icon } from "@/components/ui";
|
||||
import { copyTextToClipboard } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import { useIssueDetails } from "@/hooks/store";
|
||||
import useClipboardWritePermission from "@/hooks/use-clipboard-write-permission";
|
||||
import useToast from "@/hooks/use-toast";
|
||||
// store
|
||||
import { IPeekMode } from "@/store/issue-detail.store";
|
||||
@ -41,6 +42,7 @@ export const PeekOverviewHeader: React.FC<Props> = observer((props) => {
|
||||
const { handleClose } = props;
|
||||
|
||||
const { peekMode, setPeekMode } = useIssueDetails();
|
||||
const isClipboardWriteAllowed = useClipboardWritePermission();
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
@ -117,7 +119,7 @@ export const PeekOverviewHeader: React.FC<Props> = observer((props) => {
|
||||
</Transition>
|
||||
</Listbox>
|
||||
</div>
|
||||
{(peekMode === "side" || peekMode === "modal") && (
|
||||
{isClipboardWriteAllowed && (peekMode === "side" || peekMode === "modal") && (
|
||||
<div className="flex flex-shrink-0 items-center gap-2">
|
||||
<button type="button" onClick={handleCopyLink} className="-rotate-45 focus:outline-none" tabIndex={1}>
|
||||
<Icon iconName="link" className="text-[1rem]" />
|
||||
|
@ -8,6 +8,7 @@ import { CommentCard, AddComment } from "@/components/issues/peek-overview";
|
||||
import { Icon } from "@/components/ui";
|
||||
// hooks
|
||||
import { useIssueDetails, useProject, useUser } from "@/hooks/store";
|
||||
import useIsInIframe from "@/hooks/use-is-in-iframe";
|
||||
// types
|
||||
import { IIssue } from "@/types/issue";
|
||||
|
||||
@ -25,6 +26,7 @@ export const PeekOverviewIssueActivity: React.FC<Props> = observer((props) => {
|
||||
const { canComment } = useProject();
|
||||
const { details, peekId } = useIssueDetails();
|
||||
const { data: currentUser } = useUser();
|
||||
const isInIframe = useIsInIframe();
|
||||
|
||||
const comments = details[peekId || ""]?.comments || [];
|
||||
|
||||
@ -38,25 +40,26 @@ export const PeekOverviewIssueActivity: React.FC<Props> = observer((props) => {
|
||||
<CommentCard key={comment.id} comment={comment} workspaceSlug={workspaceSlug?.toString()} />
|
||||
))}
|
||||
</div>
|
||||
{currentUser ? (
|
||||
<>
|
||||
{canComment && (
|
||||
<div className="mt-4">
|
||||
<AddComment disabled={!currentUser} workspaceSlug={workspaceSlug} projectId={projectId} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="mt-4 flex items-center justify-between gap-2 rounded border border-custom-border-300 bg-custom-background-80 px-2 py-2.5">
|
||||
<p className="flex gap-2 overflow-hidden break-words text-sm text-custom-text-200">
|
||||
<Icon iconName="lock" className="!text-sm" />
|
||||
Sign in to add your comment
|
||||
</p>
|
||||
<Link href={`/?next_path=${pathname}`}>
|
||||
<Button variant="primary">Sign in</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
{!isInIframe &&
|
||||
(currentUser ? (
|
||||
<>
|
||||
{canComment && (
|
||||
<div className="mt-4">
|
||||
<AddComment disabled={!currentUser} workspaceSlug={workspaceSlug} projectId={projectId} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="mt-4 flex items-center justify-between gap-2 rounded border border-custom-border-300 bg-custom-background-80 px-2 py-2.5">
|
||||
<p className="flex gap-2 overflow-hidden break-words text-sm text-custom-text-200">
|
||||
<Icon iconName="lock" className="!text-sm" />
|
||||
Sign in to add your comment
|
||||
</p>
|
||||
<Link href={`/?next_path=${pathname}`}>
|
||||
<Button variant="primary">Sign in</Button>
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { useParams } from "next/navigation";
|
||||
import { IssueEmojiReactions, IssueVotes } from "@/components/issues/peek-overview";
|
||||
import { useProject } from "@/hooks/store";
|
||||
import useIsInIframe from "@/hooks/use-is-in-iframe";
|
||||
|
||||
// type IssueReactionsProps = {
|
||||
// workspaceSlug: string;
|
||||
@ -11,6 +12,7 @@ export const IssueReactions: React.FC = () => {
|
||||
const { workspace_slug: workspaceSlug, project_id: projectId } = useParams<any>();
|
||||
|
||||
const { canVote, canReact } = useProject();
|
||||
const isInIframe = useIsInIframe();
|
||||
|
||||
return (
|
||||
<div className="mt-4 flex items-center gap-3">
|
||||
@ -21,7 +23,7 @@ export const IssueReactions: React.FC = () => {
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{canReact && (
|
||||
{!isInIframe && canReact && (
|
||||
<div className="flex items-center gap-2">
|
||||
<IssueEmojiReactions workspaceSlug={workspaceSlug} projectId={projectId} />
|
||||
</div>
|
||||
|
@ -5,9 +5,11 @@ import { observer } from "mobx-react-lite";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { queryParamGenerator } from "@/helpers/query-param-generator";
|
||||
// hooks
|
||||
import { useIssueDetails, useUser } from "@/hooks/store";
|
||||
import useIsInIframe from "@/hooks/use-is-in-iframe";
|
||||
|
||||
type TIssueVotes = {
|
||||
workspaceSlug: string;
|
||||
@ -32,6 +34,8 @@ export const IssueVotes: React.FC<TIssueVotes> = observer((props) => {
|
||||
const issueDetailsStore = useIssueDetails();
|
||||
const { data: user, fetchCurrentUser } = useUser();
|
||||
|
||||
const isInIframe = useIsInIframe();
|
||||
|
||||
const issueId = issueDetailsStore.peekId;
|
||||
|
||||
const votes = issueId ? issueDetailsStore.details[issueId]?.votes : [];
|
||||
@ -94,12 +98,18 @@ export const IssueVotes: React.FC<TIssueVotes> = observer((props) => {
|
||||
type="button"
|
||||
disabled={isSubmitting}
|
||||
onClick={(e) => {
|
||||
if (isInIframe) return;
|
||||
if (user) handleVote(e, 1);
|
||||
else router.push(`/?next_path=${pathName}?${queryParam}`);
|
||||
}}
|
||||
className={`flex items-center justify-center gap-x-1 overflow-hidden rounded border px-2 h-7 focus:outline-none ${
|
||||
isUpVotedByUser ? "border-custom-primary-200 text-custom-primary-200" : "border-custom-border-300"
|
||||
}`}
|
||||
className={cn(
|
||||
"flex items-center justify-center gap-x-1 overflow-hidden rounded border px-2 h-7 focus:outline-none",
|
||||
{
|
||||
"border-custom-primary-200 text-custom-primary-200": isUpVotedByUser,
|
||||
"border-custom-border-300": !isUpVotedByUser,
|
||||
"cursor-default": isInIframe,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<span className="material-symbols-rounded !m-0 !p-0 text-base">arrow_upward_alt</span>
|
||||
<span className="text-sm font-normal transition-opacity ease-in-out">{allUpVotes.length}</span>
|
||||
@ -128,12 +138,18 @@ export const IssueVotes: React.FC<TIssueVotes> = observer((props) => {
|
||||
type="button"
|
||||
disabled={isSubmitting}
|
||||
onClick={(e) => {
|
||||
if (isInIframe) return;
|
||||
if (user) handleVote(e, -1);
|
||||
else router.push(`/?next_path=${pathName}?${queryParam}`);
|
||||
}}
|
||||
className={`flex items-center justify-center gap-x-1 h-7 overflow-hidden rounded border px-2 focus:outline-none ${
|
||||
isDownVotedByUser ? "border-red-600 text-red-600" : "border-custom-border-300"
|
||||
}`}
|
||||
className={cn(
|
||||
"flex items-center justify-center gap-x-1 h-7 overflow-hidden rounded border px-2 focus:outline-none",
|
||||
{
|
||||
"border-red-600 text-red-600": isDownVotedByUser,
|
||||
"border-custom-border-300": !isDownVotedByUser,
|
||||
"cursor-default": isInIframe,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<span className="material-symbols-rounded !m-0 !p-0 text-base">arrow_downward_alt</span>
|
||||
<span className="text-sm font-normal transition-opacity ease-in-out">{allDownVotes.length}</span>
|
||||
|
28
space/hooks/use-clipboard-write-permission.tsx
Normal file
28
space/hooks/use-clipboard-write-permission.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
const useClipboardWritePermission = () => {
|
||||
const [isClipboardWriteAllowed, setClipboardWriteAllowed] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkClipboardWriteAccess = () => {
|
||||
navigator.permissions
|
||||
.query({ name: "clipboard-write" as PermissionName })
|
||||
.then((result) => {
|
||||
if (result.state === "granted") {
|
||||
setClipboardWriteAllowed(true);
|
||||
} else {
|
||||
setClipboardWriteAllowed(false);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setClipboardWriteAllowed(false);
|
||||
});
|
||||
};
|
||||
|
||||
checkClipboardWriteAccess();
|
||||
}, []);
|
||||
|
||||
return isClipboardWriteAllowed;
|
||||
};
|
||||
|
||||
export default useClipboardWritePermission;
|
17
space/hooks/use-is-in-iframe.tsx
Normal file
17
space/hooks/use-is-in-iframe.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
const useIsInIframe = () => {
|
||||
const [isInIframe, setIsInIframe] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkIfInIframe = () => {
|
||||
setIsInIframe(window.self !== window.top);
|
||||
};
|
||||
|
||||
checkIfInIframe();
|
||||
}, []);
|
||||
|
||||
return isInIframe;
|
||||
};
|
||||
|
||||
export default useIsInIframe;
|
Loading…
Reference in New Issue
Block a user