[WEB-4342] fix: copy to clipboard text + added redirection for pages (#3447)

* fix: copy to clipboard text + added redirection for pages

* fix: translations

* fix: feature flagged

* fix: refactor

* chore: featureflag check

---------

Co-authored-by: sangeethailango <sangeethailango21@gmail.com>
This commit is contained in:
Akshita Goyal
2025-06-18 20:56:07 +05:30
committed by GitHub
parent 1c2e5ddaab
commit ee96abd5f2
8 changed files with 64 additions and 34 deletions

View File

@@ -11,6 +11,8 @@ from plane.ee.models.issue import WorkItemPage
from plane.db.models import Page, Workspace
from plane.ee.serializers import WorkItemPageSerializer
from plane.app.permissions import ProjectLitePermission
from plane.payment.flags.flag import FeatureFlag
from plane.payment.flags.flag_decorator import check_feature_flag
class IssuePageViewSet(BaseAPIView):
@@ -21,6 +23,7 @@ class IssuePageViewSet(BaseAPIView):
workspace=workspace, project_id=project_id, issue_id=issue_id
)
@check_feature_flag(FeatureFlag.LINK_PAGES)
def post(self, request, slug, project_id, issue_id):
workspace = Workspace.objects.get(slug=slug)
pages_ids = request.data.get("pages_ids", [])
@@ -61,12 +64,14 @@ class IssuePageViewSet(BaseAPIView):
serializer = WorkItemPageSerializer(work_item_pages, many=True)
return Response(serializer.data, status=status.HTTP_201_CREATED)
@check_feature_flag(FeatureFlag.LINK_PAGES)
def get(self, request, slug, project_id, issue_id, page_id=None):
workspace = Workspace.objects.get(slug=slug)
work_item_pages = self.filter_work_item_pages(workspace, project_id, issue_id)
serializer = WorkItemPageSerializer(work_item_pages, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
@check_feature_flag(FeatureFlag.LINK_PAGES)
def delete(self, request, slug, project_id, issue_id, page_id):
workspace = Workspace.objects.get(slug=slug)
work_item_page = self.filter_work_item_pages(
@@ -79,6 +84,7 @@ class IssuePageViewSet(BaseAPIView):
class PageSearchViewSet(BaseAPIView):
permission_classes = [ProjectLitePermission]
@check_feature_flag(FeatureFlag.LINK_PAGES)
def get(self, request, slug, project_id):
is_global = request.query_params.get("is_global", False)
search = request.query_params.get("search", "")

View File

@@ -78,6 +78,8 @@ class FeatureFlag(Enum):
ANALYTICS_ADVANCED = "ANALYTICS_ADVANCED"
# Notion Importer
NOTION_IMPORTER = "NOTION_IMPORTER"
# Link Pages
LINK_PAGES = "LINK_PAGES"
class AdminFeatureFlag(Enum):

View File

@@ -39,6 +39,7 @@ export enum E_FEATURE_FLAGS {
TIME_ESTIMATES = "TIME_ESTIMATES",
PROJECT_TEMPLATES_PUBLISH = "PROJECT_TEMPLATES_PUBLISH",
COPY_WORK_ITEM = "COPY_WORK_ITEM",
LINK_PAGES = "LINK_PAGES",
// ====== silo importers ======
SILO_IMPORTERS = "SILO_IMPORTERS",

View File

@@ -34,7 +34,7 @@
"duplicate": {
"modal": {
"title": "Make a copy to another project",
"description1": "This creates a copy of the work item, its attachments, links.",
"description1": "This creates a copy of the work item.",
"description2": "All property data will be removed while duplicating.",
"placeholder": "Select a project"
}
@@ -557,7 +557,7 @@
},
"duplicate": {
"modal": {
"description1": "This creates a copy of the epic, its attachments, links.",
"description1": "This creates a copy of the epic.",
"description2": "All property data will be removed while duplicating.",
"epics_not_enabled": "Epics not enabled"
}

View File

@@ -1,28 +1,33 @@
import { FC } from "react";
import { FileText } from "lucide-react";
import { E_FEATURE_FLAGS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { TWorkItemAdditionalWidgetActionButtonsProps } from "@/ce/components/issues/issue-detail-widgets/action-buttons";
import { IssueDetailWidgetButton } from "@/components/issues";
import { WithFeatureFlagHOC } from "@/plane-web/components/feature-flags";
import { PagesActionButton } from "./pages";
export const WorkItemAdditionalWidgetActionButtons: FC<TWorkItemAdditionalWidgetActionButtonsProps> = (props) => {
const { workspaceSlug, workItemId, disabled, issueServiceType, hideWidgets } = props;
const { t } = useTranslation();
return (
<>
{!hideWidgets?.includes("pages") && (
<PagesActionButton
issueServiceType={issueServiceType}
disabled={disabled}
workItemId={workItemId}
customButton={
<IssueDetailWidgetButton
title={t("issue.pages.link_pages")}
icon={<FileText className="h-3.5 w-3.5 flex-shrink-0" strokeWidth={2} />}
disabled={disabled}
/>
}
/>
)}
<WithFeatureFlagHOC workspaceSlug={workspaceSlug} flag={E_FEATURE_FLAGS.LINK_PAGES} fallback={<></>}>
{!hideWidgets?.includes("pages") && (
<PagesActionButton
issueServiceType={issueServiceType}
disabled={disabled}
workItemId={workItemId}
customButton={
<IssueDetailWidgetButton
title={t("issue.pages.link_pages")}
icon={<FileText className="h-3.5 w-3.5 flex-shrink-0" strokeWidth={2} />}
disabled={disabled}
/>
}
/>
)}
</WithFeatureFlagHOC>
</>
);
};

View File

@@ -1,10 +1,12 @@
import { FC } from "react";
import { observer } from "mobx-react";
// ce imports
import { E_FEATURE_FLAGS } from "@plane/constants";
import { TWorkItemAdditionalWidgetCollapsiblesProps } from "@/ce/components/issues/issue-detail-widgets/collapsibles";
// hooks
import { useIssueDetail } from "@/hooks/store";
// plane web imports
import { WithFeatureFlagHOC } from "@/plane-web/components/feature-flags";
import { CustomerRequestsCollapsible } from "@/plane-web/components/issues/issue-detail-widgets";
import { useCustomers } from "@/plane-web/hooks/store";
import { PagesCollapsible } from "./pages";
@@ -27,15 +29,17 @@ export const WorkItemAdditionalWidgetCollapsibles: FC<TWorkItemAdditionalWidgetC
{shouldRenderCustomerRequest && isCustomersFeatureEnabled && (
<CustomerRequestsCollapsible workItemId={workItemId} workspaceSlug={workspaceSlug} disabled={disabled} />
)}
{shouldRenderPages && (
<PagesCollapsible
workItemId={workItemId}
workspaceSlug={workspaceSlug}
disabled={disabled}
projectId={issue?.project_id}
issueServiceType={issueServiceType}
/>
)}
<WithFeatureFlagHOC workspaceSlug={workspaceSlug} flag={E_FEATURE_FLAGS.LINK_PAGES} fallback={<></>}>
{shouldRenderPages && (
<PagesCollapsible
workItemId={workItemId}
workspaceSlug={workspaceSlug}
disabled={disabled}
projectId={issue?.project_id}
issueServiceType={issueServiceType}
/>
)}
</WithFeatureFlagHOC>
</>
);
}

View File

@@ -1,12 +1,13 @@
import React, { FC } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { CircleX, Files, FileText, Link2 } from "lucide-react";
import { useTranslation } from "@plane/i18n";
import { TIssuePage, TIssueServiceType, TLogoProps } from "@plane/types";
import { setToast, TContextMenuItem, TOAST_TYPE, CustomMenu, Logo } from "@plane/ui";
import { calculateTimeAgo, cn, copyUrlToClipboard } from "@plane/utils";
import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
import { useIssueDetail, useProject, useWorkspace } from "@/hooks/store";
import { useIssueDetail, useProject } from "@/hooks/store";
type TProps = {
issueServiceType: TIssueServiceType;
@@ -21,12 +22,10 @@ export const PagesCollapsibleContentBlock: FC<TProps> = observer((props) => {
// hooks
const { t } = useTranslation();
const { getProjectById } = useProject();
const { getWorkspaceBySlug } = useWorkspace();
const {
pages: { deleteIssuePages },
} = useIssueDetail(issueServiceType);
// derived
const workspace = getWorkspaceBySlug(workspaceSlug);
const project = getProjectById(projectId);
const handleCopyText = () => {
@@ -36,7 +35,7 @@ export const PagesCollapsibleContentBlock: FC<TProps> = observer((props) => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: t("link_copied"),
message: t("project_link_copied_to_clipboard"),
message: t("entity.link_copied_to_clipboard", { entity: t("page") }),
});
});
};
@@ -79,7 +78,15 @@ export const PagesCollapsibleContentBlock: FC<TProps> = observer((props) => {
key={page.id}
className="flex flex-col gap-2 rounded-xl border border-custom-border-100 p-4 pb-2 min-h-[166px]"
>
<div className="border-b border-custom-border-100 pb-2 flex flex-col gap-2 flex-1">
<Link
href={
page.is_global
? `/${workspaceSlug}/pages/${page.id}`
: `/${workspaceSlug}/projects/${projectId}/pages/${page.id}`
}
target="_blank"
className="border-b border-custom-border-100 pb-2 flex flex-col gap-2 flex-1"
>
<div className="flex gap-2 items-center max-w-full w-fit overflow-hidden bg-custom-background-90 p-1 rounded">
<div className="my-auto">
{page.is_global ? (
@@ -105,7 +112,7 @@ export const PagesCollapsibleContentBlock: FC<TProps> = observer((props) => {
{page.description_stripped === "" ? t("issue.pages.no_description") : page.description_stripped}
</div>
</div>
</div>
</Link>
<div className="flex gap-2 justify-between items-center">
<div>{page.created_by && <ButtonAvatars showTooltip userIds={[page.created_by]} />}</div>
<div className="flex gap-2">

View File

@@ -1,7 +1,9 @@
import useSWR from "swr";
// plane web imports
import { E_FEATURE_FLAGS } from "@plane/constants";
import { TIssueServiceType } from "@plane/types";
import { useIssueDetail } from "@/hooks/store";
import { useFlag } from "./store/use-flag";
export const useWorkItemProperties = (
projectId: string | null | undefined,
workspaceSlug: string | null | undefined,
@@ -13,11 +15,14 @@ export const useWorkItemProperties = (
pages: { fetchPagesByIssueId },
} = useIssueDetail(issueServiceType);
const isPagesInWorkitemWidgetEnabled = useFlag(workspaceSlug?.toString(), E_FEATURE_FLAGS.LINK_PAGES);
useSWR(
workspaceSlug && projectId && workItemId ? `WORK_ITEM_PAGES_${workspaceSlug}_${projectId}_${workItemId}` : null,
workspaceSlug && projectId && workItemId
workspaceSlug && projectId && workItemId && isPagesInWorkitemWidgetEnabled
? `WORK_ITEM_PAGES_${workspaceSlug}_${projectId}_${workItemId}_${isPagesInWorkitemWidgetEnabled}`
: null,
workspaceSlug && projectId && workItemId && isPagesInWorkitemWidgetEnabled
? () => fetchPagesByIssueId(workspaceSlug, projectId?.toString() ?? "", workItemId)
: null,
{ revalidateIfStale: false, revalidateOnFocus: true }
);
};
};