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";
|
import { queryParamGenerator } from "@/helpers/query-param-generator";
|
||||||
// hooks
|
// hooks
|
||||||
import { useProject, useIssueFilter, useIssueDetails } from "@/hooks/store";
|
import { useProject, useIssueFilter, useIssueDetails } from "@/hooks/store";
|
||||||
|
import useIsInIframe from "@/hooks/use-is-in-iframe";
|
||||||
// types
|
// types
|
||||||
import { TIssueLayout } from "@/types/issue";
|
import { TIssueLayout } from "@/types/issue";
|
||||||
|
|
||||||
@ -39,6 +40,8 @@ export const NavbarControls: FC<NavbarControlsProps> = observer((props) => {
|
|||||||
// derived values
|
// derived values
|
||||||
const activeLayout = issueFilters?.display_filters?.layout || undefined;
|
const activeLayout = issueFilters?.display_filters?.layout || undefined;
|
||||||
|
|
||||||
|
const isInIframe = useIsInIframe();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (workspaceSlug && projectId && settings) {
|
if (workspaceSlug && projectId && settings) {
|
||||||
const viewsAcceptable: string[] = [];
|
const viewsAcceptable: string[] = [];
|
||||||
@ -111,7 +114,7 @@ export const NavbarControls: FC<NavbarControlsProps> = observer((props) => {
|
|||||||
<NavbarTheme />
|
<NavbarTheme />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<UserAvatar />
|
{!isInIframe && <UserAvatar />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -11,6 +11,7 @@ import { CommentReactions } from "@/components/issues/peek-overview";
|
|||||||
import { timeAgo } from "@/helpers/date-time.helper";
|
import { timeAgo } from "@/helpers/date-time.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useIssueDetails, useProject, useUser } from "@/hooks/store";
|
import { useIssueDetails, useProject, useUser } from "@/hooks/store";
|
||||||
|
import useIsInIframe from "@/hooks/use-is-in-iframe";
|
||||||
// types
|
// types
|
||||||
import { Comment } from "@/types/issue";
|
import { Comment } from "@/types/issue";
|
||||||
|
|
||||||
@ -25,6 +26,7 @@ export const CommentCard: React.FC<Props> = observer((props) => {
|
|||||||
const { workspace } = useProject();
|
const { workspace } = useProject();
|
||||||
const { peekId, deleteIssueComment, updateIssueComment } = useIssueDetails();
|
const { peekId, deleteIssueComment, updateIssueComment } = useIssueDetails();
|
||||||
const { data: currentUser } = useUser();
|
const { data: currentUser } = useUser();
|
||||||
|
const isInIframe = useIsInIframe();
|
||||||
// derived values
|
// derived values
|
||||||
const workspaceId = workspace?.id;
|
const workspaceId = workspace?.id;
|
||||||
|
|
||||||
@ -138,7 +140,7 @@ export const CommentCard: React.FC<Props> = observer((props) => {
|
|||||||
</div>
|
</div>
|
||||||
</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 as="div" className="relative w-min text-left">
|
||||||
<Menu.Button
|
<Menu.Button
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -5,10 +5,12 @@ import { Tooltip } from "@plane/ui";
|
|||||||
// ui
|
// ui
|
||||||
import { ReactionSelector } from "@/components/ui";
|
import { ReactionSelector } from "@/components/ui";
|
||||||
// helpers
|
// helpers
|
||||||
|
import { cn } from "@/helpers/common.helper";
|
||||||
import { groupReactions, renderEmoji } from "@/helpers/emoji.helper";
|
import { groupReactions, renderEmoji } from "@/helpers/emoji.helper";
|
||||||
import { queryParamGenerator } from "@/helpers/query-param-generator";
|
import { queryParamGenerator } from "@/helpers/query-param-generator";
|
||||||
// hooks
|
// hooks
|
||||||
import { useIssueDetails, useUser } from "@/hooks/store";
|
import { useIssueDetails, useUser } from "@/hooks/store";
|
||||||
|
import useIsInIframe from "@/hooks/use-is-in-iframe";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
commentId: string;
|
commentId: string;
|
||||||
@ -30,6 +32,7 @@ export const CommentReactions: React.FC<Props> = observer((props) => {
|
|||||||
// hooks
|
// hooks
|
||||||
const { addCommentReaction, removeCommentReaction, details, peekId } = useIssueDetails();
|
const { addCommentReaction, removeCommentReaction, details, peekId } = useIssueDetails();
|
||||||
const { data: user } = useUser();
|
const { data: user } = useUser();
|
||||||
|
const isInIframe = useIsInIframe();
|
||||||
|
|
||||||
const commentReactions = peekId ? details[peekId].comments.find((c) => c.id === commentId)?.comment_reactions : [];
|
const commentReactions = peekId ? details[peekId].comments.find((c) => c.id === commentId)?.comment_reactions : [];
|
||||||
const groupedReactions = peekId ? groupReactions(commentReactions ?? [], "reaction") : {};
|
const groupedReactions = peekId ? groupReactions(commentReactions ?? [], "reaction") : {};
|
||||||
@ -58,6 +61,7 @@ export const CommentReactions: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-2 flex items-center gap-1.5">
|
<div className="mt-2 flex items-center gap-1.5">
|
||||||
|
{!isInIframe && (
|
||||||
<ReactionSelector
|
<ReactionSelector
|
||||||
onSelect={(value) => {
|
onSelect={(value) => {
|
||||||
if (user) handleReactionClick(value);
|
if (user) handleReactionClick(value);
|
||||||
@ -67,6 +71,7 @@ export const CommentReactions: React.FC<Props> = observer((props) => {
|
|||||||
selected={userReactions?.map((r) => r.reaction)}
|
selected={userReactions?.map((r) => r.reaction)}
|
||||||
size="md"
|
size="md"
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{Object.keys(groupedReactions || {}).map((reaction) => {
|
{Object.keys(groupedReactions || {}).map((reaction) => {
|
||||||
const reactions = groupedReactions?.[reaction] ?? [];
|
const reactions = groupedReactions?.[reaction] ?? [];
|
||||||
@ -89,14 +94,20 @@ export const CommentReactions: React.FC<Props> = observer((props) => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
if (isInIframe) return;
|
||||||
if (user) handleReactionClick(reaction);
|
if (user) handleReactionClick(reaction);
|
||||||
else router.push(`/?next_path=${pathName}?${queryParam}`);
|
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 ${
|
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)
|
commentReactions?.some((r) => r?.actor_detail?.id === user?.id && r.reaction === reaction)
|
||||||
? "bg-custom-primary-100/10"
|
? "bg-custom-primary-100/10"
|
||||||
: "bg-custom-background-80"
|
: "bg-custom-background-80"
|
||||||
}`}
|
}`,
|
||||||
|
{
|
||||||
|
"cursor-default": isInIframe,
|
||||||
|
}
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<span>{renderEmoji(reaction)}</span>
|
<span>{renderEmoji(reaction)}</span>
|
||||||
<span
|
<span
|
||||||
|
@ -8,6 +8,7 @@ import { Icon } from "@/components/ui";
|
|||||||
import { copyTextToClipboard } from "@/helpers/string.helper";
|
import { copyTextToClipboard } from "@/helpers/string.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useIssueDetails } from "@/hooks/store";
|
import { useIssueDetails } from "@/hooks/store";
|
||||||
|
import useClipboardWritePermission from "@/hooks/use-clipboard-write-permission";
|
||||||
import useToast from "@/hooks/use-toast";
|
import useToast from "@/hooks/use-toast";
|
||||||
// store
|
// store
|
||||||
import { IPeekMode } from "@/store/issue-detail.store";
|
import { IPeekMode } from "@/store/issue-detail.store";
|
||||||
@ -41,6 +42,7 @@ export const PeekOverviewHeader: React.FC<Props> = observer((props) => {
|
|||||||
const { handleClose } = props;
|
const { handleClose } = props;
|
||||||
|
|
||||||
const { peekMode, setPeekMode } = useIssueDetails();
|
const { peekMode, setPeekMode } = useIssueDetails();
|
||||||
|
const isClipboardWriteAllowed = useClipboardWritePermission();
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
@ -117,7 +119,7 @@ export const PeekOverviewHeader: React.FC<Props> = observer((props) => {
|
|||||||
</Transition>
|
</Transition>
|
||||||
</Listbox>
|
</Listbox>
|
||||||
</div>
|
</div>
|
||||||
{(peekMode === "side" || peekMode === "modal") && (
|
{isClipboardWriteAllowed && (peekMode === "side" || peekMode === "modal") && (
|
||||||
<div className="flex flex-shrink-0 items-center gap-2">
|
<div className="flex flex-shrink-0 items-center gap-2">
|
||||||
<button type="button" onClick={handleCopyLink} className="-rotate-45 focus:outline-none" tabIndex={1}>
|
<button type="button" onClick={handleCopyLink} className="-rotate-45 focus:outline-none" tabIndex={1}>
|
||||||
<Icon iconName="link" className="text-[1rem]" />
|
<Icon iconName="link" className="text-[1rem]" />
|
||||||
|
@ -8,6 +8,7 @@ import { CommentCard, AddComment } from "@/components/issues/peek-overview";
|
|||||||
import { Icon } from "@/components/ui";
|
import { Icon } from "@/components/ui";
|
||||||
// hooks
|
// hooks
|
||||||
import { useIssueDetails, useProject, useUser } from "@/hooks/store";
|
import { useIssueDetails, useProject, useUser } from "@/hooks/store";
|
||||||
|
import useIsInIframe from "@/hooks/use-is-in-iframe";
|
||||||
// types
|
// types
|
||||||
import { IIssue } from "@/types/issue";
|
import { IIssue } from "@/types/issue";
|
||||||
|
|
||||||
@ -25,6 +26,7 @@ export const PeekOverviewIssueActivity: React.FC<Props> = observer((props) => {
|
|||||||
const { canComment } = useProject();
|
const { canComment } = useProject();
|
||||||
const { details, peekId } = useIssueDetails();
|
const { details, peekId } = useIssueDetails();
|
||||||
const { data: currentUser } = useUser();
|
const { data: currentUser } = useUser();
|
||||||
|
const isInIframe = useIsInIframe();
|
||||||
|
|
||||||
const comments = details[peekId || ""]?.comments || [];
|
const comments = details[peekId || ""]?.comments || [];
|
||||||
|
|
||||||
@ -38,7 +40,8 @@ export const PeekOverviewIssueActivity: React.FC<Props> = observer((props) => {
|
|||||||
<CommentCard key={comment.id} comment={comment} workspaceSlug={workspaceSlug?.toString()} />
|
<CommentCard key={comment.id} comment={comment} workspaceSlug={workspaceSlug?.toString()} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{currentUser ? (
|
{!isInIframe &&
|
||||||
|
(currentUser ? (
|
||||||
<>
|
<>
|
||||||
{canComment && (
|
{canComment && (
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
@ -56,7 +59,7 @@ export const PeekOverviewIssueActivity: React.FC<Props> = observer((props) => {
|
|||||||
<Button variant="primary">Sign in</Button>
|
<Button variant="primary">Sign in</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
)}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { IssueEmojiReactions, IssueVotes } from "@/components/issues/peek-overview";
|
import { IssueEmojiReactions, IssueVotes } from "@/components/issues/peek-overview";
|
||||||
import { useProject } from "@/hooks/store";
|
import { useProject } from "@/hooks/store";
|
||||||
|
import useIsInIframe from "@/hooks/use-is-in-iframe";
|
||||||
|
|
||||||
// type IssueReactionsProps = {
|
// type IssueReactionsProps = {
|
||||||
// workspaceSlug: string;
|
// workspaceSlug: string;
|
||||||
@ -11,6 +12,7 @@ export const IssueReactions: React.FC = () => {
|
|||||||
const { workspace_slug: workspaceSlug, project_id: projectId } = useParams<any>();
|
const { workspace_slug: workspaceSlug, project_id: projectId } = useParams<any>();
|
||||||
|
|
||||||
const { canVote, canReact } = useProject();
|
const { canVote, canReact } = useProject();
|
||||||
|
const isInIframe = useIsInIframe();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-4 flex items-center gap-3">
|
<div className="mt-4 flex items-center gap-3">
|
||||||
@ -21,7 +23,7 @@ export const IssueReactions: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{canReact && (
|
{!isInIframe && canReact && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<IssueEmojiReactions workspaceSlug={workspaceSlug} projectId={projectId} />
|
<IssueEmojiReactions workspaceSlug={workspaceSlug} projectId={projectId} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -5,9 +5,11 @@ import { observer } from "mobx-react-lite";
|
|||||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||||
import { Tooltip } from "@plane/ui";
|
import { Tooltip } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
|
import { cn } from "@/helpers/common.helper";
|
||||||
import { queryParamGenerator } from "@/helpers/query-param-generator";
|
import { queryParamGenerator } from "@/helpers/query-param-generator";
|
||||||
// hooks
|
// hooks
|
||||||
import { useIssueDetails, useUser } from "@/hooks/store";
|
import { useIssueDetails, useUser } from "@/hooks/store";
|
||||||
|
import useIsInIframe from "@/hooks/use-is-in-iframe";
|
||||||
|
|
||||||
type TIssueVotes = {
|
type TIssueVotes = {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
@ -32,6 +34,8 @@ export const IssueVotes: React.FC<TIssueVotes> = observer((props) => {
|
|||||||
const issueDetailsStore = useIssueDetails();
|
const issueDetailsStore = useIssueDetails();
|
||||||
const { data: user, fetchCurrentUser } = useUser();
|
const { data: user, fetchCurrentUser } = useUser();
|
||||||
|
|
||||||
|
const isInIframe = useIsInIframe();
|
||||||
|
|
||||||
const issueId = issueDetailsStore.peekId;
|
const issueId = issueDetailsStore.peekId;
|
||||||
|
|
||||||
const votes = issueId ? issueDetailsStore.details[issueId]?.votes : [];
|
const votes = issueId ? issueDetailsStore.details[issueId]?.votes : [];
|
||||||
@ -94,12 +98,18 @@ export const IssueVotes: React.FC<TIssueVotes> = observer((props) => {
|
|||||||
type="button"
|
type="button"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
if (isInIframe) return;
|
||||||
if (user) handleVote(e, 1);
|
if (user) handleVote(e, 1);
|
||||||
else router.push(`/?next_path=${pathName}?${queryParam}`);
|
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 ${
|
className={cn(
|
||||||
isUpVotedByUser ? "border-custom-primary-200 text-custom-primary-200" : "border-custom-border-300"
|
"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="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>
|
<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"
|
type="button"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
if (isInIframe) return;
|
||||||
if (user) handleVote(e, -1);
|
if (user) handleVote(e, -1);
|
||||||
else router.push(`/?next_path=${pathName}?${queryParam}`);
|
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 ${
|
className={cn(
|
||||||
isDownVotedByUser ? "border-red-600 text-red-600" : "border-custom-border-300"
|
"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="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>
|
<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