Allow copy to clipboard and pointer/mousemove events in front components (#20858)
Follow-up to #20525, picks up the clipboard + mouse/pointer events asks from the "Allow to copy to clipboard in front-component" Slack thread. `navigator.geolocation` and `getBoundingClientRect` are intentionally out of scope until we have a permission model. ### `copyToClipboard` host API New SDK function `copyToClipboard` (in `twenty-sdk/front-component`) that goes through the host bridge to `useCopyToClipboard` in `twenty-front`: ```ts import { copyToClipboard } from 'twenty-sdk/front-component'; await copyToClipboard('hello'); ``` Host-side hardening (front-component code is untrusted): - Drops anything that isn't a non-empty string - Caps payload at 64KB - Throttles to 1 call/sec per front-component instance - Snackbar shows a truncated preview so the user can spot a mismatch between the affordance they clicked and what actually got copied ### `mousemove` and pointer events Added to `COMMON_HTML_EVENTS` (and the React mapping) so they fire on every HTML tag the renderer ships: `mousemove`, `pointerdown/up/move`, `pointerover/out/enter/leave/cancel`. Generator rerun for `remote-elements.ts` and `remote-components.ts`. `SerializedEventData` now also forwards pointer geometry: `pointerId`, `pointerType`, `pressure`, `tangentialPressure`, `tiltX/Y`, `twist`, `width/height`, `isPrimary`. Existing positional fields are unchanged. ### Coverage - New Storybook stories: `HostApi/CopyToClipboard` and `HtmlTag/Grouping/Div/Events::PointerMove` - `useFrontComponentExecutionContext` unit tests cover the API call, preview truncation, type guard, length cap, and rate limit - Renderer Storybook suite 227 → 229, prebuild bundle count 219 → 221
This commit is contained in:
+43
@@ -1,4 +1,7 @@
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useRef } from 'react';
|
||||
import {
|
||||
type FrontComponentExecutionContext,
|
||||
type FrontComponentHostCommunicationApi,
|
||||
@@ -18,8 +21,13 @@ import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomState
|
||||
import { useSetAtomFamilyState } from '@/ui/utilities/state/jotai/hooks/useSetAtomFamilyState';
|
||||
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
|
||||
import { useNavigateApp } from '~/hooks/useNavigateApp';
|
||||
|
||||
const FRONT_COMPONENT_CLIPBOARD_MAX_LENGTH = 64 * 1024;
|
||||
const FRONT_COMPONENT_CLIPBOARD_RATE_LIMIT_MS = 1000;
|
||||
const FRONT_COMPONENT_CLIPBOARD_PREVIEW_LENGTH = 30;
|
||||
|
||||
export const useFrontComponentExecutionContext = ({
|
||||
frontComponentId,
|
||||
commandMenuItemId,
|
||||
@@ -49,6 +57,10 @@ export const useFrontComponentExecutionContext = ({
|
||||
enqueueWarningSnackBar,
|
||||
} = useSnackBar();
|
||||
const { closeSidePanelMenu } = useSidePanelMenu();
|
||||
const { copyToClipboard: copyToClipboardWithSnackbar } = useCopyToClipboard();
|
||||
const { t } = useLingui();
|
||||
// oxlint-disable-next-line twenty/no-state-useref
|
||||
const lastCopyToClipboardCallAtRef = useRef<number>(Number.NEGATIVE_INFINITY);
|
||||
const setCommandMenuItemProgress = useSetAtomFamilyState(
|
||||
commandMenuItemProgressFamilyState,
|
||||
commandMenuItemId ?? '',
|
||||
@@ -152,6 +164,36 @@ export const useFrontComponentExecutionContext = ({
|
||||
setCommandMenuItemProgress(Math.max(0, Math.min(100, progress)));
|
||||
};
|
||||
|
||||
const copyToClipboard: FrontComponentHostCommunicationApi['copyToClipboard'] =
|
||||
async (text) => {
|
||||
if (!isNonEmptyString(text)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (text.length > FRONT_COMPONENT_CLIPBOARD_MAX_LENGTH) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
if (
|
||||
now - lastCopyToClipboardCallAtRef.current <
|
||||
FRONT_COMPONENT_CLIPBOARD_RATE_LIMIT_MS
|
||||
) {
|
||||
return;
|
||||
}
|
||||
lastCopyToClipboardCallAtRef.current = now;
|
||||
|
||||
const preview =
|
||||
text.length > FRONT_COMPONENT_CLIPBOARD_PREVIEW_LENGTH
|
||||
? `${text.slice(0, FRONT_COMPONENT_CLIPBOARD_PREVIEW_LENGTH)}…`
|
||||
: text;
|
||||
|
||||
await copyToClipboardWithSnackbar(
|
||||
text,
|
||||
t`Application copied "${preview}" to your clipboard`,
|
||||
);
|
||||
};
|
||||
|
||||
const frontComponentHostCommunicationApi: FrontComponentHostCommunicationApi =
|
||||
{
|
||||
navigate,
|
||||
@@ -162,6 +204,7 @@ export const useFrontComponentExecutionContext = ({
|
||||
unmountFrontComponent,
|
||||
closeSidePanel,
|
||||
updateProgress,
|
||||
copyToClipboard,
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user