Notion text selection (#2062)
* feat: add option to allow text selection on Notion pages Add enableTextSelection field to Link model that allows document owners to toggle text selection/copying for visitors on Notion pages. Changes: - Add enableTextSelection Boolean field to Link Prisma schema - Add database migration for the new field - Create TextSelectionSection toggle component for link settings - Wire the setting through link sheet, link options, API routes - Pass textSelectionEnabled prop to NotionPage viewer component - Add .notion-text-selection-enabled CSS class that overrides user-select: none when text selection is allowed - Update link-active-controls to show when text selection is active - Update webhooks and link data queries to include new field By default, text selection remains disabled (preserving existing behavior). When enabled, visitors can select and copy text content on Notion document pages. Co-authored-by: Marc Seitz <mfts@users.noreply.github.com> * refactor: use team feature flag for text selection instead of per-link toggle Replace the per-link enableTextSelection database field with a team-level feature flag (textSelection) using the existing Vercel Edge Config system. This means text selection on Notion pages is controlled at the team level via the betaFeatures edge config, so there's no per-link toggle to manage. Changes: - Remove enableTextSelection from Link Prisma schema and migration - Remove TextSelectionSection toggle component and link sheet wiring - Add 'textSelection' to BetaFeatures type in featureFlags - Fetch textSelection flag in all view page getStaticProps: - pages/view/[linkId]/index.tsx (document + dataroom paths) - pages/view/domains/[domain]/[slug]/index.tsx (document + dataroom) - pages/view/[linkId]/d/[documentId].tsx (dataroom document) - pages/view/domains/[domain]/[slug]/d/[documentId].tsx (domain dataroom doc) - Pass textSelectionEnabled prop through component chain: DocumentView -> ViewData -> NotionPage DataroomDocumentView -> ViewData -> NotionPage - Keep CSS override (.notion-text-selection-enabled) and NotionPage conditional class application from previous commit To enable: add the team's ID to the 'textSelection' array in Vercel Edge Config betaFeatures. Co-authored-by: Marc Seitz <mfts@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Marc Seitz <mfts@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
co-authored by
Cursor Agent
Marc Seitz
parent
10d58b0832
commit
5401d3d5d2
@@ -68,7 +68,7 @@ export const DEFAULT_LINK_PROPS = (
|
||||
enableNotification: true,
|
||||
enableFeedback: false,
|
||||
enableScreenshotProtection: false,
|
||||
enableCustomMetatag: false,
|
||||
enableCustomMetatag: false,
|
||||
metaTitle: null,
|
||||
metaDescription: null,
|
||||
metaImage: null,
|
||||
|
||||
@@ -84,6 +84,7 @@ export default function DataroomDocumentView({
|
||||
isEmbedded,
|
||||
preview,
|
||||
logoOnAccessForm,
|
||||
textSelectionEnabled,
|
||||
}: {
|
||||
link: LinkWithDataroomDocument;
|
||||
userEmail: string | null | undefined;
|
||||
@@ -104,6 +105,7 @@ export default function DataroomDocumentView({
|
||||
isEmbedded?: boolean;
|
||||
preview?: boolean;
|
||||
logoOnAccessForm?: boolean;
|
||||
textSelectionEnabled?: boolean;
|
||||
}) {
|
||||
useDisablePrint();
|
||||
const {
|
||||
@@ -362,6 +364,7 @@ export default function DataroomDocumentView({
|
||||
undefined
|
||||
}
|
||||
canDownload={viewData.canDownload}
|
||||
textSelectionEnabled={textSelectionEnabled}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-screen items-center justify-center">
|
||||
|
||||
@@ -62,6 +62,7 @@ export default function DataroomView({
|
||||
isEmbedded,
|
||||
preview,
|
||||
dataroomIndexEnabled,
|
||||
textSelectionEnabled,
|
||||
}: {
|
||||
link: LinkWithDataroom;
|
||||
userEmail: string | null | undefined;
|
||||
@@ -77,6 +78,7 @@ export default function DataroomView({
|
||||
preview?: boolean;
|
||||
logoOnAccessForm?: boolean;
|
||||
dataroomIndexEnabled?: boolean;
|
||||
textSelectionEnabled?: boolean;
|
||||
}) {
|
||||
useDisablePrint();
|
||||
const {
|
||||
|
||||
@@ -72,6 +72,7 @@ export default function DocumentView({
|
||||
logoOnAccessForm,
|
||||
isEmbedded,
|
||||
annotationsEnabled,
|
||||
textSelectionEnabled,
|
||||
}: {
|
||||
link: LinkWithDocument;
|
||||
userEmail: string | null | undefined;
|
||||
@@ -94,6 +95,7 @@ export default function DocumentView({
|
||||
isEmbedded?: boolean;
|
||||
logoOnAccessForm?: boolean;
|
||||
annotationsEnabled?: boolean;
|
||||
textSelectionEnabled?: boolean;
|
||||
}) {
|
||||
useDisablePrint();
|
||||
const {
|
||||
@@ -314,6 +316,7 @@ export default function DocumentView({
|
||||
useAdvancedExcelViewer={useAdvancedExcelViewer}
|
||||
viewerEmail={data.email ?? verifiedEmail ?? userEmail ?? undefined}
|
||||
annotationsEnabled={annotationsEnabled}
|
||||
textSelectionEnabled={textSelectionEnabled}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-screen items-center justify-center">
|
||||
|
||||
@@ -66,6 +66,7 @@ export default function ViewData({
|
||||
dataroomId,
|
||||
canDownload,
|
||||
annotationsEnabled,
|
||||
textSelectionEnabled,
|
||||
}: {
|
||||
viewData: DEFAULT_DOCUMENT_VIEW_TYPE | DEFAULT_DATAROOM_DOCUMENT_VIEW_TYPE;
|
||||
link: LinkWithDocument | LinkWithDataroomDocument;
|
||||
@@ -83,6 +84,7 @@ export default function ViewData({
|
||||
dataroomId?: string;
|
||||
canDownload?: boolean;
|
||||
annotationsEnabled?: boolean;
|
||||
textSelectionEnabled?: boolean;
|
||||
}) {
|
||||
const { isMobile } = useMediaQuery();
|
||||
|
||||
@@ -136,6 +138,7 @@ export default function ViewData({
|
||||
versionNumber={document.versions[0].versionNumber}
|
||||
theme={notionData.theme}
|
||||
screenshotProtectionEnabled={link.enableScreenshotProtection!}
|
||||
textSelectionEnabled={textSelectionEnabled ?? false}
|
||||
navData={navData}
|
||||
/>
|
||||
) : viewData.fileType === "link" ? (
|
||||
|
||||
@@ -128,12 +128,14 @@ export const NotionPage = ({
|
||||
versionNumber,
|
||||
theme,
|
||||
screenshotProtectionEnabled,
|
||||
textSelectionEnabled,
|
||||
navData,
|
||||
}: {
|
||||
recordMap: ExtendedRecordMap;
|
||||
versionNumber: number;
|
||||
theme?: NotionTheme | null;
|
||||
screenshotProtectionEnabled: boolean;
|
||||
textSelectionEnabled: boolean;
|
||||
navData: TNavData;
|
||||
}) => {
|
||||
const { isPreview, linkId, documentId, viewId, brand } = navData;
|
||||
@@ -543,6 +545,7 @@ export const NotionPage = ({
|
||||
!isWindowFocused &&
|
||||
screenshotProtectionEnabled &&
|
||||
"blur-xl transition-all duration-300",
|
||||
textSelectionEnabled && "notion-text-selection-enabled",
|
||||
)}
|
||||
>
|
||||
<NotionRenderer
|
||||
|
||||
@@ -15,7 +15,8 @@ export type BetaFeatures =
|
||||
| "dataroomInvitations"
|
||||
| "workflows"
|
||||
| "ai"
|
||||
| "sso";
|
||||
| "sso"
|
||||
| "textSelection";
|
||||
|
||||
type BetaFeaturesRecord = Record<BetaFeatures, string[]>;
|
||||
|
||||
@@ -36,6 +37,7 @@ export const getFeatureFlags = async ({ teamId }: { teamId?: string }) => {
|
||||
workflows: false,
|
||||
ai: false,
|
||||
sso: false,
|
||||
textSelection: false,
|
||||
};
|
||||
|
||||
// Return all features as false if edge config is not available
|
||||
|
||||
@@ -12,6 +12,7 @@ import { parsePageId } from "notion-utils";
|
||||
import z from "zod";
|
||||
|
||||
import { fetchLinkDataById } from "@/lib/api/links/link-data";
|
||||
import { getFeatureFlags } from "@/lib/featureFlags";
|
||||
import notion from "@/lib/notion";
|
||||
import {
|
||||
addSignedUrls,
|
||||
@@ -50,6 +51,7 @@ type DataroomDocumentProps = {
|
||||
useAdvancedExcelViewer: boolean;
|
||||
useCustomAccessForm: boolean;
|
||||
logoOnAccessForm: boolean;
|
||||
textSelectionEnabled?: boolean;
|
||||
error?: boolean;
|
||||
};
|
||||
|
||||
@@ -62,6 +64,7 @@ export default function DataroomDocumentViewPage({
|
||||
useAdvancedExcelViewer,
|
||||
useCustomAccessForm,
|
||||
logoOnAccessForm,
|
||||
textSelectionEnabled,
|
||||
error,
|
||||
}: DataroomDocumentProps) {
|
||||
const router = useRouter();
|
||||
@@ -183,6 +186,7 @@ export default function DataroomDocumentViewPage({
|
||||
token={storedToken}
|
||||
verifiedEmail={verifiedEmail}
|
||||
preview={!!preview}
|
||||
textSelectionEnabled={textSelectionEnabled}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@@ -251,6 +255,10 @@ export async function getStaticProps(context: GetStaticPropsContext) {
|
||||
const { advancedExcelEnabled, ...linkDocument } =
|
||||
linkData.dataroomDocument.document;
|
||||
|
||||
// Check feature flags
|
||||
const featureFlags = await getFeatureFlags({ teamId: teamId || undefined });
|
||||
const textSelectionEnabled = featureFlags.textSelection;
|
||||
|
||||
return {
|
||||
props: {
|
||||
linkData: {
|
||||
@@ -291,6 +299,7 @@ export async function getStaticProps(context: GetStaticPropsContext) {
|
||||
teamId === "cm9ztf0s70005js04i689gefn" ||
|
||||
teamId === "cmk2hnmqh0000k304zcoezt6n",
|
||||
logoOnAccessForm: teamId === "cm7nlkrhm0000qgh0nvyrrywr",
|
||||
textSelectionEnabled,
|
||||
},
|
||||
revalidate: brand || recordMap ? 10 : 60,
|
||||
};
|
||||
|
||||
@@ -72,6 +72,7 @@ export interface ViewPageProps {
|
||||
logoOnAccessForm: boolean;
|
||||
dataroomIndexEnabled?: boolean;
|
||||
annotationsEnabled?: boolean;
|
||||
textSelectionEnabled?: boolean;
|
||||
}
|
||||
|
||||
export const getStaticProps = async (context: GetStaticPropsContext) => {
|
||||
@@ -181,6 +182,7 @@ export const getStaticProps = async (context: GetStaticPropsContext) => {
|
||||
// Check feature flags for document links
|
||||
const featureFlags = await getFeatureFlags({ teamId });
|
||||
const annotationsEnabled = featureFlags.annotations;
|
||||
const textSelectionEnabled = featureFlags.textSelection;
|
||||
|
||||
return {
|
||||
props: {
|
||||
@@ -222,6 +224,7 @@ export const getStaticProps = async (context: GetStaticPropsContext) => {
|
||||
teamId === "cm7nlkrhm0000qgh0nvyrrywr" ||
|
||||
teamId === "clup33by90000oewh4rfvp2eg",
|
||||
annotationsEnabled,
|
||||
textSelectionEnabled,
|
||||
},
|
||||
revalidate: brand || recordMap ? 10 : 60,
|
||||
};
|
||||
@@ -259,6 +262,7 @@ export const getStaticProps = async (context: GetStaticPropsContext) => {
|
||||
const featureFlags = await getFeatureFlags({ teamId });
|
||||
const dataroomIndexEnabled = featureFlags.dataroomIndex;
|
||||
const annotationsEnabled = featureFlags.annotations;
|
||||
const textSelectionEnabled = featureFlags.textSelection;
|
||||
|
||||
const lastUpdatedAt = link.dataroom.documents.reduce(
|
||||
(max: number, doc: any) => {
|
||||
@@ -307,6 +311,7 @@ export const getStaticProps = async (context: GetStaticPropsContext) => {
|
||||
teamId === "clup33by90000oewh4rfvp2eg",
|
||||
dataroomIndexEnabled,
|
||||
annotationsEnabled,
|
||||
textSelectionEnabled,
|
||||
},
|
||||
revalidate: 10,
|
||||
};
|
||||
@@ -336,6 +341,7 @@ export default function ViewPage({
|
||||
logoOnAccessForm,
|
||||
dataroomIndexEnabled,
|
||||
annotationsEnabled,
|
||||
textSelectionEnabled,
|
||||
error,
|
||||
notionError,
|
||||
}: ViewPageProps & { error?: boolean; notionError?: boolean }) {
|
||||
@@ -488,6 +494,7 @@ export default function ViewPage({
|
||||
token={storedToken}
|
||||
verifiedEmail={verifiedEmail}
|
||||
annotationsEnabled={annotationsEnabled}
|
||||
textSelectionEnabled={textSelectionEnabled}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@@ -568,6 +575,7 @@ export default function ViewPage({
|
||||
previewToken={previewToken}
|
||||
preview={!!preview}
|
||||
dataroomIndexEnabled={dataroomIndexEnabled}
|
||||
textSelectionEnabled={textSelectionEnabled}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -12,6 +12,7 @@ import { parsePageId } from "notion-utils";
|
||||
import z from "zod";
|
||||
|
||||
import { fetchLinkDataByDomainSlug } from "@/lib/api/links/link-data";
|
||||
import { getFeatureFlags } from "@/lib/featureFlags";
|
||||
import notion from "@/lib/notion";
|
||||
import {
|
||||
addSignedUrls,
|
||||
@@ -50,6 +51,7 @@ type DataroomDocumentProps = {
|
||||
useAdvancedExcelViewer: boolean;
|
||||
useCustomAccessForm: boolean;
|
||||
logoOnAccessForm: boolean;
|
||||
textSelectionEnabled?: boolean;
|
||||
error?: boolean;
|
||||
};
|
||||
|
||||
@@ -62,6 +64,7 @@ export default function DataroomDocumentViewPage({
|
||||
useAdvancedExcelViewer,
|
||||
useCustomAccessForm,
|
||||
logoOnAccessForm,
|
||||
textSelectionEnabled,
|
||||
error,
|
||||
}: DataroomDocumentProps) {
|
||||
const router = useRouter();
|
||||
@@ -184,6 +187,7 @@ export default function DataroomDocumentViewPage({
|
||||
verifiedEmail={verifiedEmail}
|
||||
preview={!!preview}
|
||||
logoOnAccessForm={logoOnAccessForm}
|
||||
textSelectionEnabled={textSelectionEnabled}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@@ -261,6 +265,10 @@ export async function getStaticProps(context: GetStaticPropsContext) {
|
||||
const { advancedExcelEnabled, ...linkDocument } =
|
||||
linkData.dataroomDocument.document;
|
||||
|
||||
// Check feature flags
|
||||
const featureFlags = await getFeatureFlags({ teamId: teamId || undefined });
|
||||
const textSelectionEnabled = featureFlags.textSelection;
|
||||
|
||||
return {
|
||||
props: {
|
||||
linkData: {
|
||||
@@ -301,6 +309,7 @@ export async function getStaticProps(context: GetStaticPropsContext) {
|
||||
teamId === "cm9ztf0s70005js04i689gefn" ||
|
||||
teamId === "cmk2hnmqh0000k304zcoezt6n",
|
||||
logoOnAccessForm: teamId === "cm7nlkrhm0000qgh0nvyrrywr",
|
||||
textSelectionEnabled,
|
||||
},
|
||||
revalidate: brand || recordMap ? 10 : 60,
|
||||
};
|
||||
|
||||
@@ -154,6 +154,10 @@ export const getStaticProps = async (context: GetStaticPropsContext) => {
|
||||
link.document;
|
||||
const teamPlan = team?.plan || "free";
|
||||
|
||||
// Check feature flags for document links
|
||||
const docFeatureFlags = await getFeatureFlags({ teamId: teamId || undefined });
|
||||
const textSelectionEnabled = docFeatureFlags.textSelection;
|
||||
|
||||
return {
|
||||
props: {
|
||||
linkData: {
|
||||
@@ -192,6 +196,7 @@ export const getStaticProps = async (context: GetStaticPropsContext) => {
|
||||
logoOnAccessForm:
|
||||
teamId === "cm7nlkrhm0000qgh0nvyrrywr" ||
|
||||
teamId === "clup33by90000oewh4rfvp2eg",
|
||||
textSelectionEnabled,
|
||||
},
|
||||
revalidate: 10,
|
||||
};
|
||||
@@ -225,9 +230,10 @@ export const getStaticProps = async (context: GetStaticPropsContext) => {
|
||||
|
||||
const { teamId } = link.dataroom;
|
||||
|
||||
// Check if dataroomIndex feature flag is enabled
|
||||
// Check feature flags
|
||||
const featureFlags = await getFeatureFlags({ teamId });
|
||||
const dataroomIndexEnabled = featureFlags.dataroomIndex;
|
||||
const textSelectionEnabled = featureFlags.textSelection;
|
||||
|
||||
const lastUpdatedAt = link.dataroom.documents.reduce(
|
||||
(max: number, doc: any) => {
|
||||
@@ -275,6 +281,7 @@ export const getStaticProps = async (context: GetStaticPropsContext) => {
|
||||
teamId === "cm7nlkrhm0000qgh0nvyrrywr" ||
|
||||
teamId === "clup33by90000oewh4rfvp2eg",
|
||||
dataroomIndexEnabled,
|
||||
textSelectionEnabled,
|
||||
},
|
||||
revalidate: 10,
|
||||
};
|
||||
@@ -302,6 +309,7 @@ export default function ViewPage({
|
||||
useCustomAccessForm,
|
||||
logoOnAccessForm,
|
||||
dataroomIndexEnabled,
|
||||
textSelectionEnabled,
|
||||
error,
|
||||
}: {
|
||||
linkData: DocumentLinkData | DataroomLinkData | WorkflowLinkData;
|
||||
@@ -323,6 +331,7 @@ export default function ViewPage({
|
||||
useCustomAccessForm: boolean;
|
||||
logoOnAccessForm: boolean;
|
||||
dataroomIndexEnabled?: boolean;
|
||||
textSelectionEnabled?: boolean;
|
||||
error?: boolean;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
@@ -469,6 +478,7 @@ export default function ViewPage({
|
||||
token={storedToken}
|
||||
verifiedEmail={verifiedEmail}
|
||||
logoOnAccessForm={logoOnAccessForm}
|
||||
textSelectionEnabled={textSelectionEnabled}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@@ -549,6 +559,7 @@ export default function ViewPage({
|
||||
preview={!!preview}
|
||||
logoOnAccessForm={logoOnAccessForm}
|
||||
dataroomIndexEnabled={dataroomIndexEnabled}
|
||||
textSelectionEnabled={textSelectionEnabled}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Disable user selection for all text content */
|
||||
/* Disable user selection for all text content (default) */
|
||||
.notion-page,
|
||||
.notion-text,
|
||||
.notion-h1,
|
||||
@@ -103,6 +103,37 @@
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Re-enable user selection when text selection is allowed */
|
||||
.notion-text-selection-enabled .notion-page,
|
||||
.notion-text-selection-enabled .notion-text,
|
||||
.notion-text-selection-enabled .notion-h1,
|
||||
.notion-text-selection-enabled .notion-h2,
|
||||
.notion-text-selection-enabled .notion-h3,
|
||||
.notion-text-selection-enabled .notion-h4,
|
||||
.notion-text-selection-enabled .notion-h5,
|
||||
.notion-text-selection-enabled .notion-h6,
|
||||
.notion-text-selection-enabled .notion-header,
|
||||
.notion-text-selection-enabled .notion-sub_header,
|
||||
.notion-text-selection-enabled .notion-sub_sub_header,
|
||||
.notion-text-selection-enabled .notion-quote,
|
||||
.notion-text-selection-enabled .notion-callout,
|
||||
.notion-text-selection-enabled .notion-toggle,
|
||||
.notion-text-selection-enabled .notion-list,
|
||||
.notion-text-selection-enabled .notion-numbered-list,
|
||||
.notion-text-selection-enabled .notion-to_do,
|
||||
.notion-text-selection-enabled .notion-code,
|
||||
.notion-text-selection-enabled .notion-simple-table,
|
||||
.notion-text-selection-enabled .notion-table,
|
||||
.notion-text-selection-enabled .notion-column-list,
|
||||
.notion-text-selection-enabled .notion-bookmark,
|
||||
.notion-text-selection-enabled .notion-embed,
|
||||
.notion-text-selection-enabled .notion-database {
|
||||
-webkit-user-select: text;
|
||||
-moz-user-select: text;
|
||||
-ms-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
/* Disable image dragging */
|
||||
.notion-image,
|
||||
.notion-asset,
|
||||
|
||||
Reference in New Issue
Block a user