[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:
Prateek Shourya 2024-05-22 12:39:34 +05:30 committed by GitHub
parent 0c80cf3d54
commit f13c190676
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 127 additions and 43 deletions

View File

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

View File

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

View File

@ -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,6 +61,7 @@ export const CommentReactions: React.FC<Props> = observer((props) => {
return (
<div className="mt-2 flex items-center gap-1.5">
{!isInIframe && (
<ReactionSelector
onSelect={(value) => {
if (user) handleReactionClick(value);
@ -67,6 +71,7 @@ export const CommentReactions: React.FC<Props> = observer((props) => {
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 ${
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

View File

@ -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]" />

View File

@ -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,7 +40,8 @@ export const PeekOverviewIssueActivity: React.FC<Props> = observer((props) => {
<CommentCard key={comment.id} comment={comment} workspaceSlug={workspaceSlug?.toString()} />
))}
</div>
{currentUser ? (
{!isInIframe &&
(currentUser ? (
<>
{canComment && (
<div className="mt-4">
@ -56,7 +59,7 @@ export const PeekOverviewIssueActivity: React.FC<Props> = observer((props) => {
<Button variant="primary">Sign in</Button>
</Link>
</div>
)}
))}
</div>
)}
</div>

View File

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

View File

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

View 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;

View 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;