Add event forwarding stories to the front component renderer (#19721)
Add Storybook stories and example components to test event forwarding through the front component renderer: form events (text input, checkbox, focus/blur, submit), keyboard events (key/code/modifiers), and host API calls (navigate, snackbar, progress, close panel)
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
node_modules
|
||||
storybook-static
|
||||
src/__stories__/example-sources-built
|
||||
src/__stories__/example-sources-built/*
|
||||
!src/__stories__/example-sources-built/bundle-sizes.json.d.ts
|
||||
src/__stories__/example-sources-built-preact
|
||||
|
||||
+4
-1
@@ -60,7 +60,7 @@ const twentySharedAliases = Object.fromEntries(
|
||||
const storyAlias = {
|
||||
react: path.join(rootNodeModules, 'react'),
|
||||
'react-dom': path.join(rootNodeModules, 'react-dom'),
|
||||
'@/sdk': sdkIndividualIndex,
|
||||
'twenty-sdk': sdkIndividualIndex,
|
||||
'twenty-sdk/ui': twentyUiIndividualIndex,
|
||||
...twentySharedAliases,
|
||||
};
|
||||
@@ -77,6 +77,9 @@ const STORY_COMPONENTS = [
|
||||
'mui-example.front-component',
|
||||
'twenty-ui-example.front-component',
|
||||
'sdk-context-example.front-component',
|
||||
'form-events.front-component',
|
||||
'keyboard-events.front-component',
|
||||
'host-api-calls.front-component',
|
||||
];
|
||||
|
||||
const resolveEntryPoints = (): Record<string, string> => {
|
||||
|
||||
@@ -0,0 +1,348 @@
|
||||
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
||||
import { expect, fn, userEvent, waitFor, within } from 'storybook/test';
|
||||
|
||||
import { FrontComponentRenderer } from '../host/components/FrontComponentRenderer';
|
||||
|
||||
import { getBuiltStoryComponentPathForRender } from './utils/getBuiltStoryComponentPathForRender';
|
||||
|
||||
const errorHandler = fn();
|
||||
|
||||
const createHostApiMocks = () => ({
|
||||
navigate: fn().mockResolvedValue(undefined),
|
||||
enqueueSnackbar: fn().mockResolvedValue(undefined),
|
||||
openSidePanelPage: fn().mockResolvedValue(undefined),
|
||||
closeSidePanel: fn().mockResolvedValue(undefined),
|
||||
unmountFrontComponent: fn().mockResolvedValue(undefined),
|
||||
updateProgress: fn().mockResolvedValue(undefined),
|
||||
requestAccessTokenRefresh: fn().mockResolvedValue('refreshed-token'),
|
||||
openCommandConfirmationModal: fn().mockResolvedValue(undefined),
|
||||
});
|
||||
|
||||
const meta: Meta<typeof FrontComponentRenderer> = {
|
||||
title: 'FrontComponent/EventForwarding',
|
||||
component: FrontComponentRenderer,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
args: {
|
||||
onError: errorHandler,
|
||||
applicationAccessToken: 'fake-token',
|
||||
executionContext: {
|
||||
frontComponentId: 'storybook-test',
|
||||
userId: null,
|
||||
recordId: null,
|
||||
},
|
||||
colorScheme: 'light',
|
||||
frontComponentHostCommunicationApi: createHostApiMocks(),
|
||||
},
|
||||
beforeEach: () => {
|
||||
errorHandler.mockClear();
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof FrontComponentRenderer>;
|
||||
|
||||
const MOUNT_TIMEOUT = 30000;
|
||||
const INTERACTION_TIMEOUT = 5000;
|
||||
const HOST_API_TIMEOUT = 10000;
|
||||
|
||||
const createComponentStory = (
|
||||
name: string,
|
||||
options?: { play?: Story['play'] },
|
||||
): Story => ({
|
||||
args: {
|
||||
componentUrl: getBuiltStoryComponentPathForRender(
|
||||
`${name}.front-component`,
|
||||
),
|
||||
},
|
||||
...(options?.play ? { play: options.play } : {}),
|
||||
});
|
||||
|
||||
const createHostApiStory = (play: Story['play']): Story => ({
|
||||
...createComponentStory('host-api-calls'),
|
||||
args: {
|
||||
...createComponentStory('host-api-calls').args,
|
||||
frontComponentHostCommunicationApi: createHostApiMocks(),
|
||||
},
|
||||
play,
|
||||
});
|
||||
|
||||
export const FormTextInput: Story = createComponentStory('form-events', {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await canvas.findByTestId(
|
||||
'form-events-component',
|
||||
{},
|
||||
{ timeout: MOUNT_TIMEOUT },
|
||||
);
|
||||
|
||||
const textInput = await canvas.findByTestId('text-input');
|
||||
await userEvent.type(textInput, 'hello');
|
||||
|
||||
expect(
|
||||
await canvas.findByText('hello', {}, { timeout: INTERACTION_TIMEOUT }),
|
||||
).toBeVisible();
|
||||
},
|
||||
});
|
||||
|
||||
export const FormCheckbox: Story = createComponentStory('form-events', {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await canvas.findByTestId(
|
||||
'form-events-component',
|
||||
{},
|
||||
{ timeout: MOUNT_TIMEOUT },
|
||||
);
|
||||
|
||||
const checkbox = await canvas.findByTestId('checkbox-input');
|
||||
await userEvent.click(checkbox);
|
||||
|
||||
expect(
|
||||
await canvas.findByText('true', {}, { timeout: INTERACTION_TIMEOUT }),
|
||||
).toBeVisible();
|
||||
},
|
||||
});
|
||||
|
||||
export const FormFocusAndBlur: Story = createComponentStory('form-events', {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await canvas.findByTestId(
|
||||
'form-events-component',
|
||||
{},
|
||||
{ timeout: MOUNT_TIMEOUT },
|
||||
);
|
||||
|
||||
const textInput = await canvas.findByTestId('text-input');
|
||||
await userEvent.click(textInput);
|
||||
|
||||
expect(
|
||||
await canvas.findByText('focused', {}, { timeout: INTERACTION_TIMEOUT }),
|
||||
).toBeVisible();
|
||||
|
||||
await userEvent.click(await canvas.findByTestId('form-events-component'));
|
||||
|
||||
expect(
|
||||
await canvas.findByText('blurred', {}, { timeout: INTERACTION_TIMEOUT }),
|
||||
).toBeVisible();
|
||||
},
|
||||
});
|
||||
|
||||
export const FormSubmission: Story = createComponentStory('form-events', {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await canvas.findByTestId(
|
||||
'form-events-component',
|
||||
{},
|
||||
{ timeout: MOUNT_TIMEOUT },
|
||||
);
|
||||
|
||||
const textInput = await canvas.findByTestId('text-input');
|
||||
await userEvent.type(textInput, 'hello');
|
||||
|
||||
const checkbox = await canvas.findByTestId('checkbox-input');
|
||||
await userEvent.click(checkbox);
|
||||
|
||||
const submitButton = await canvas.findByTestId('submit-button');
|
||||
await userEvent.click(submitButton);
|
||||
|
||||
expect(
|
||||
await canvas.findByText(
|
||||
'{"text":"hello","checkbox":true}',
|
||||
{},
|
||||
{ timeout: INTERACTION_TIMEOUT },
|
||||
),
|
||||
).toBeVisible();
|
||||
},
|
||||
});
|
||||
|
||||
export const KeyboardBasicInput: Story = createComponentStory(
|
||||
'keyboard-events',
|
||||
{
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await canvas.findByTestId(
|
||||
'keyboard-events-component',
|
||||
{},
|
||||
{ timeout: MOUNT_TIMEOUT },
|
||||
);
|
||||
|
||||
const input = await canvas.findByTestId('keyboard-input');
|
||||
await userEvent.click(input);
|
||||
|
||||
await userEvent.keyboard('a');
|
||||
|
||||
expect(
|
||||
await canvas.findByText('a', {}, { timeout: INTERACTION_TIMEOUT }),
|
||||
).toBeVisible();
|
||||
|
||||
expect(
|
||||
await canvas.findByText('KeyA', {}, { timeout: INTERACTION_TIMEOUT }),
|
||||
).toBeVisible();
|
||||
|
||||
expect(
|
||||
await canvas.findByText(
|
||||
/^[1-9]\d*$/,
|
||||
{},
|
||||
{ timeout: INTERACTION_TIMEOUT },
|
||||
),
|
||||
).toBeVisible();
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export const KeyboardModifiers: Story = createComponentStory(
|
||||
'keyboard-events',
|
||||
{
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await canvas.findByTestId(
|
||||
'keyboard-events-component',
|
||||
{},
|
||||
{ timeout: MOUNT_TIMEOUT },
|
||||
);
|
||||
|
||||
const input = await canvas.findByTestId('keyboard-input');
|
||||
await userEvent.click(input);
|
||||
|
||||
await userEvent.keyboard('{Shift>}b{/Shift}');
|
||||
|
||||
expect(
|
||||
await canvas.findByText('shift', {}, { timeout: INTERACTION_TIMEOUT }),
|
||||
).toBeVisible();
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export const HostApiNavigate: Story = createHostApiStory(
|
||||
async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const api = args.frontComponentHostCommunicationApi!;
|
||||
|
||||
await canvas.findByTestId(
|
||||
'host-api-calls-component',
|
||||
{},
|
||||
{ timeout: MOUNT_TIMEOUT },
|
||||
);
|
||||
|
||||
const navigateBtn = await canvas.findByTestId('btn-navigate');
|
||||
await userEvent.click(navigateBtn);
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(api.navigate).toHaveBeenCalled();
|
||||
},
|
||||
{ timeout: HOST_API_TIMEOUT },
|
||||
);
|
||||
|
||||
expect(
|
||||
await canvas.findByText(
|
||||
'navigate:success',
|
||||
{},
|
||||
{ timeout: INTERACTION_TIMEOUT },
|
||||
),
|
||||
).toBeVisible();
|
||||
},
|
||||
);
|
||||
|
||||
export const HostApiSnackbar: Story = createHostApiStory(
|
||||
async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const api = args.frontComponentHostCommunicationApi!;
|
||||
|
||||
await canvas.findByTestId(
|
||||
'host-api-calls-component',
|
||||
{},
|
||||
{ timeout: MOUNT_TIMEOUT },
|
||||
);
|
||||
|
||||
const snackbarBtn = await canvas.findByTestId('btn-snackbar');
|
||||
await userEvent.click(snackbarBtn);
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(api.enqueueSnackbar).toHaveBeenCalledWith({
|
||||
message: 'Test notification',
|
||||
variant: 'success',
|
||||
});
|
||||
},
|
||||
{ timeout: HOST_API_TIMEOUT },
|
||||
);
|
||||
|
||||
expect(
|
||||
await canvas.findByText(
|
||||
'snackbar:success',
|
||||
{},
|
||||
{ timeout: INTERACTION_TIMEOUT },
|
||||
),
|
||||
).toBeVisible();
|
||||
},
|
||||
);
|
||||
|
||||
export const HostApiProgress: Story = createHostApiStory(
|
||||
async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const api = args.frontComponentHostCommunicationApi!;
|
||||
|
||||
await canvas.findByTestId(
|
||||
'host-api-calls-component',
|
||||
{},
|
||||
{ timeout: MOUNT_TIMEOUT },
|
||||
);
|
||||
|
||||
const progressBtn = await canvas.findByTestId('btn-progress');
|
||||
await userEvent.click(progressBtn);
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(api.updateProgress).toHaveBeenCalledWith(50);
|
||||
},
|
||||
{ timeout: HOST_API_TIMEOUT },
|
||||
);
|
||||
|
||||
expect(
|
||||
await canvas.findByText(
|
||||
'progress:success',
|
||||
{},
|
||||
{ timeout: INTERACTION_TIMEOUT },
|
||||
),
|
||||
).toBeVisible();
|
||||
},
|
||||
);
|
||||
|
||||
export const HostApiClosePanel: Story = createHostApiStory(
|
||||
async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const api = args.frontComponentHostCommunicationApi!;
|
||||
|
||||
await canvas.findByTestId(
|
||||
'host-api-calls-component',
|
||||
{},
|
||||
{ timeout: MOUNT_TIMEOUT },
|
||||
);
|
||||
|
||||
const closePanelBtn = await canvas.findByTestId('btn-close-panel');
|
||||
await userEvent.click(closePanelBtn);
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(api.closeSidePanel).toHaveBeenCalled();
|
||||
},
|
||||
{ timeout: HOST_API_TIMEOUT },
|
||||
);
|
||||
|
||||
expect(
|
||||
await canvas.findByText(
|
||||
'closePanel:success',
|
||||
{},
|
||||
{ timeout: INTERACTION_TIMEOUT },
|
||||
),
|
||||
).toBeVisible();
|
||||
},
|
||||
);
|
||||
+9
-8
@@ -16,7 +16,11 @@ const meta: Meta<typeof FrontComponentRenderer> = {
|
||||
args: {
|
||||
onError: errorHandler,
|
||||
applicationAccessToken: 'fake-token',
|
||||
executionContext: { frontComponentId: 'storybook-test', userId: null },
|
||||
executionContext: {
|
||||
frontComponentId: 'storybook-test',
|
||||
userId: null,
|
||||
recordId: null,
|
||||
},
|
||||
},
|
||||
beforeEach: () => {
|
||||
errorHandler.mockClear();
|
||||
@@ -92,13 +96,9 @@ export const Lifecycle: Story = {
|
||||
|
||||
expect(await canvas.findByText('Mounted')).toBeVisible();
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
const tickElement = canvas.getByTestId('tick-count');
|
||||
expect(tickElement.textContent).toMatch(/Ticks: [1-9]\d*/);
|
||||
},
|
||||
{ timeout: 10000 },
|
||||
);
|
||||
expect(
|
||||
await canvas.findByText(/Ticks: [1-9]\d*/, {}, { timeout: 10000 }),
|
||||
).toBeVisible();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -121,6 +121,7 @@ export const SdkContext: Story = {
|
||||
executionContext: {
|
||||
frontComponentId: 'sdk-context-test',
|
||||
userId: 'test-user-abc-123',
|
||||
recordId: null,
|
||||
},
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
|
||||
@@ -16,7 +16,11 @@ const meta: Meta<typeof FrontComponentRenderer> = {
|
||||
args: {
|
||||
onError: errorHandler,
|
||||
applicationAccessToken: 'fake-token',
|
||||
executionContext: { frontComponentId: 'storybook-test', userId: null },
|
||||
executionContext: {
|
||||
frontComponentId: 'storybook-test',
|
||||
userId: null,
|
||||
recordId: null,
|
||||
},
|
||||
},
|
||||
beforeEach: () => {
|
||||
errorHandler.mockClear();
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
declare const bundleSizes: {
|
||||
name: string;
|
||||
reactBytes: number;
|
||||
preactBytes: number;
|
||||
}[];
|
||||
|
||||
export default bundleSizes;
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { defineFrontComponent } from 'twenty-sdk';
|
||||
import styled from '@emotion/styled';
|
||||
import { useState } from 'react';
|
||||
import { defineFrontComponent } from '@/sdk';
|
||||
|
||||
const Card = styled.div`
|
||||
padding: 24px;
|
||||
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
import { defineFrontComponent } from 'twenty-sdk';
|
||||
import { type ChangeEvent, useState } from 'react';
|
||||
|
||||
const CARD_STYLE = {
|
||||
padding: 24,
|
||||
backgroundColor: '#f0fdf4',
|
||||
border: '2px solid #22c55e',
|
||||
borderRadius: 12,
|
||||
fontFamily: 'system-ui, sans-serif',
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
gap: 16,
|
||||
maxWidth: 400,
|
||||
};
|
||||
|
||||
const HEADING_STYLE = {
|
||||
color: '#166534',
|
||||
fontWeight: 700,
|
||||
fontSize: 18,
|
||||
margin: 0,
|
||||
};
|
||||
|
||||
const LABEL_STYLE = {
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
color: '#374151',
|
||||
};
|
||||
|
||||
const HINT_STYLE = {
|
||||
fontSize: 13,
|
||||
color: '#6b7280',
|
||||
};
|
||||
|
||||
const INPUT_STYLE = {
|
||||
padding: '8px 12px',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: 6,
|
||||
fontSize: 14,
|
||||
};
|
||||
|
||||
const SUBMIT_BUTTON_STYLE = {
|
||||
padding: '10px 20px',
|
||||
backgroundColor: '#16a34a',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: 6,
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer',
|
||||
};
|
||||
|
||||
const FormEventsComponent = () => {
|
||||
const [textValue, setTextValue] = useState('');
|
||||
const [checkboxValue, setCheckboxValue] = useState(false);
|
||||
const [focusState, setFocusState] = useState('none');
|
||||
const [submittedData, setSubmittedData] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<div data-testid="form-events-component" style={CARD_STYLE}>
|
||||
<h2 style={HEADING_STYLE}>Form Events</h2>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
<label style={LABEL_STYLE}>Text Input</label>
|
||||
<input
|
||||
data-testid="text-input"
|
||||
type="text"
|
||||
placeholder="Type here..."
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
const detail = (event as unknown as { detail: { value?: string } })
|
||||
.detail;
|
||||
setTextValue(detail?.value ?? '');
|
||||
}}
|
||||
onFocus={() => setFocusState('focused')}
|
||||
onBlur={() => setFocusState('blurred')}
|
||||
style={INPUT_STYLE}
|
||||
/>
|
||||
<span data-testid="text-value" style={HINT_STYLE}>
|
||||
{textValue}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<input
|
||||
data-testid="checkbox-input"
|
||||
type="checkbox"
|
||||
checked={checkboxValue}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
const detail = (
|
||||
event as unknown as { detail: { checked?: boolean } }
|
||||
).detail;
|
||||
setCheckboxValue(detail?.checked ?? false);
|
||||
}}
|
||||
/>
|
||||
<label style={LABEL_STYLE}>Check me</label>
|
||||
<span data-testid="checkbox-value" style={HINT_STYLE}>
|
||||
{String(checkboxValue)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span data-testid="focus-state" style={HINT_STYLE}>
|
||||
{focusState}
|
||||
</span>
|
||||
|
||||
<button
|
||||
data-testid="submit-button"
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setSubmittedData(
|
||||
JSON.stringify({ text: textValue, checkbox: checkboxValue }),
|
||||
)
|
||||
}
|
||||
style={SUBMIT_BUTTON_STYLE}
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
|
||||
{submittedData !== null && (
|
||||
<pre
|
||||
data-testid="submitted-data"
|
||||
style={{
|
||||
fontSize: 13,
|
||||
background: '#dcfce7',
|
||||
padding: 12,
|
||||
borderRadius: 8,
|
||||
margin: 0,
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
{submittedData}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'test-form-00000000-0000-0000-0000-000000000020',
|
||||
name: 'form-events-component',
|
||||
description:
|
||||
'Component testing form input events (onChange, onFocus, onBlur, submit)',
|
||||
component: FormEventsComponent,
|
||||
});
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
import {
|
||||
AppPath,
|
||||
closeSidePanel,
|
||||
defineFrontComponent,
|
||||
enqueueSnackbar,
|
||||
navigate,
|
||||
openSidePanelPage,
|
||||
SidePanelPages,
|
||||
unmountFrontComponent,
|
||||
updateProgress,
|
||||
} from 'twenty-sdk';
|
||||
import { useState } from 'react';
|
||||
|
||||
const CARD_STYLE = {
|
||||
padding: 24,
|
||||
backgroundColor: '#faf5ff',
|
||||
border: '2px solid #a78bfa',
|
||||
borderRadius: 12,
|
||||
fontFamily: 'system-ui, sans-serif',
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
gap: 10,
|
||||
maxWidth: 400,
|
||||
};
|
||||
|
||||
const HEADING_STYLE = {
|
||||
color: '#5b21b6',
|
||||
fontWeight: 700,
|
||||
fontSize: 18,
|
||||
margin: 0,
|
||||
};
|
||||
|
||||
const BUTTON_STYLE = {
|
||||
padding: '8px 16px',
|
||||
backgroundColor: '#7c3aed',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: 6,
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer',
|
||||
fontSize: 13,
|
||||
};
|
||||
|
||||
const STATUS_STYLE = {
|
||||
fontSize: 13,
|
||||
color: '#6b7280',
|
||||
fontFamily: 'monospace',
|
||||
};
|
||||
|
||||
const HostApiCallsComponent = () => {
|
||||
const [apiStatus, setApiStatus] = useState('idle');
|
||||
|
||||
const callApi = async (name: string, apiFunction: () => Promise<void>) => {
|
||||
try {
|
||||
await apiFunction();
|
||||
setApiStatus(`${name}:success`);
|
||||
} catch (error) {
|
||||
setApiStatus(
|
||||
`${name}:error:${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div data-testid="host-api-calls-component" style={CARD_STYLE}>
|
||||
<h2 style={HEADING_STYLE}>Host API Calls</h2>
|
||||
|
||||
<button
|
||||
data-testid="btn-navigate"
|
||||
type="button"
|
||||
onClick={() =>
|
||||
callApi('navigate', () =>
|
||||
navigate(AppPath.RecordIndexPage, {
|
||||
objectNamePlural: 'companies',
|
||||
}),
|
||||
)
|
||||
}
|
||||
style={BUTTON_STYLE}
|
||||
>
|
||||
Navigate
|
||||
</button>
|
||||
|
||||
<button
|
||||
data-testid="btn-snackbar"
|
||||
type="button"
|
||||
onClick={() =>
|
||||
callApi('snackbar', () =>
|
||||
enqueueSnackbar({
|
||||
message: 'Test notification',
|
||||
variant: 'success',
|
||||
}),
|
||||
)
|
||||
}
|
||||
style={BUTTON_STYLE}
|
||||
>
|
||||
Snackbar
|
||||
</button>
|
||||
|
||||
<button
|
||||
data-testid="btn-side-panel"
|
||||
type="button"
|
||||
onClick={() =>
|
||||
callApi('sidePanel', () =>
|
||||
openSidePanelPage({
|
||||
page: SidePanelPages.ViewRecord,
|
||||
pageTitle: 'Test Record',
|
||||
}),
|
||||
)
|
||||
}
|
||||
style={BUTTON_STYLE}
|
||||
>
|
||||
Open Side Panel
|
||||
</button>
|
||||
|
||||
<button
|
||||
data-testid="btn-close-panel"
|
||||
type="button"
|
||||
onClick={() => callApi('closePanel', () => closeSidePanel())}
|
||||
style={BUTTON_STYLE}
|
||||
>
|
||||
Close Side Panel
|
||||
</button>
|
||||
|
||||
<button
|
||||
data-testid="btn-unmount"
|
||||
type="button"
|
||||
onClick={() => callApi('unmount', () => unmountFrontComponent())}
|
||||
style={BUTTON_STYLE}
|
||||
>
|
||||
Unmount
|
||||
</button>
|
||||
|
||||
<button
|
||||
data-testid="btn-progress"
|
||||
type="button"
|
||||
onClick={() => callApi('progress', () => updateProgress(50))}
|
||||
style={BUTTON_STYLE}
|
||||
>
|
||||
Update Progress (50)
|
||||
</button>
|
||||
|
||||
<span data-testid="api-status" style={STATUS_STYLE}>
|
||||
{apiStatus}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'test-hapi-00000000-0000-0000-0000-000000000022',
|
||||
name: 'host-api-calls-component',
|
||||
description:
|
||||
'Component testing host communication API calls (navigate, snackbar, side panel, etc.)',
|
||||
component: HostApiCallsComponent,
|
||||
});
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { defineFrontComponent } from '@/sdk';
|
||||
import { defineFrontComponent } from 'twenty-sdk';
|
||||
import { useState } from 'react';
|
||||
|
||||
const InteractiveComponent = () => {
|
||||
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
import { defineFrontComponent } from 'twenty-sdk';
|
||||
import { type KeyboardEvent, useState } from 'react';
|
||||
|
||||
type RemoteKeyboardEventDetail = {
|
||||
key?: string;
|
||||
code?: string;
|
||||
shiftKey?: boolean;
|
||||
ctrlKey?: boolean;
|
||||
metaKey?: boolean;
|
||||
altKey?: boolean;
|
||||
};
|
||||
|
||||
const KeyboardEventsComponent = () => {
|
||||
const [lastKey, setLastKey] = useState('');
|
||||
const [lastCode, setLastCode] = useState('');
|
||||
const [modifiers, setModifiers] = useState('');
|
||||
const [keyCount, setKeyCount] = useState(0);
|
||||
|
||||
// remote-dom serializes keyboard events into CustomEvent.detail
|
||||
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
||||
const data = (event as unknown as { detail: RemoteKeyboardEventDetail })
|
||||
.detail;
|
||||
|
||||
setLastKey(data.key ?? '');
|
||||
setLastCode(data.code ?? '');
|
||||
setKeyCount((previousCount) => previousCount + 1);
|
||||
|
||||
const activeModifiers: string[] = [];
|
||||
if (data.shiftKey) activeModifiers.push('shift');
|
||||
if (data.ctrlKey) activeModifiers.push('ctrl');
|
||||
if (data.metaKey) activeModifiers.push('meta');
|
||||
if (data.altKey) activeModifiers.push('alt');
|
||||
setModifiers(activeModifiers.join(','));
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="keyboard-events-component"
|
||||
style={{
|
||||
padding: 24,
|
||||
backgroundColor: '#fefce8',
|
||||
border: '2px solid #eab308',
|
||||
borderRadius: 12,
|
||||
fontFamily: 'system-ui, sans-serif',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 12,
|
||||
maxWidth: 400,
|
||||
}}
|
||||
>
|
||||
<h2
|
||||
style={{
|
||||
color: '#854d0e',
|
||||
fontWeight: 700,
|
||||
fontSize: 18,
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
Keyboard Events
|
||||
</h2>
|
||||
|
||||
<input
|
||||
data-testid="keyboard-input"
|
||||
type="text"
|
||||
placeholder="Press keys here..."
|
||||
onKeyDown={handleKeyDown}
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: 6,
|
||||
fontSize: 14,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
<div style={{ display: 'flex', gap: 16 }}>
|
||||
<span style={{ fontSize: 13, color: '#6b7280' }}>
|
||||
Key:{' '}
|
||||
<span
|
||||
data-testid="last-key"
|
||||
style={{ fontWeight: 700, color: '#854d0e' }}
|
||||
>
|
||||
{lastKey}
|
||||
</span>
|
||||
</span>
|
||||
<span style={{ fontSize: 13, color: '#6b7280' }}>
|
||||
Code:{' '}
|
||||
<span
|
||||
data-testid="last-code"
|
||||
style={{ fontWeight: 700, color: '#854d0e' }}
|
||||
>
|
||||
{lastCode}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<span style={{ fontSize: 13, color: '#6b7280' }}>
|
||||
Modifiers:{' '}
|
||||
<span
|
||||
data-testid="modifiers"
|
||||
style={{ fontWeight: 700, color: '#854d0e' }}
|
||||
>
|
||||
{modifiers}
|
||||
</span>
|
||||
</span>
|
||||
<span style={{ fontSize: 13, color: '#6b7280' }}>
|
||||
Key count:{' '}
|
||||
<span
|
||||
data-testid="key-count"
|
||||
style={{ fontWeight: 700, color: '#854d0e' }}
|
||||
>
|
||||
{keyCount}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'test-kbd0-00000000-0000-0000-0000-000000000021',
|
||||
name: 'keyboard-events-component',
|
||||
description:
|
||||
'Component testing keyboard event serialization (key, code, modifiers)',
|
||||
component: KeyboardEventsComponent,
|
||||
});
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { defineFrontComponent } from '@/sdk';
|
||||
import { defineFrontComponent } from 'twenty-sdk';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
const LifecycleComponent = () => {
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { defineFrontComponent } from 'twenty-sdk';
|
||||
import Button from '@mui/material/Button';
|
||||
import MuiChip from '@mui/material/Chip';
|
||||
import { useState } from 'react';
|
||||
import { defineFrontComponent } from '@/sdk';
|
||||
|
||||
const MuiComponent = () => {
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
+3
-3
@@ -1,9 +1,9 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
defineFrontComponent,
|
||||
useFrontComponentExecutionContext,
|
||||
useUserId,
|
||||
} from '@/sdk';
|
||||
} from 'twenty-sdk';
|
||||
import { useState } from 'react';
|
||||
|
||||
const CARD_STYLE = {
|
||||
padding: 24,
|
||||
@@ -72,7 +72,7 @@ const SdkContextComponent = () => {
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
{JSON.stringify(fullContext, null, 2) ?? 'undefined'}
|
||||
{JSON.stringify(fullContext, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { defineFrontComponent } from 'twenty-sdk';
|
||||
import { useState } from 'react';
|
||||
import { defineFrontComponent } from '@/sdk';
|
||||
|
||||
const SHADCN_CSS = `
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { defineFrontComponent } from '@/sdk';
|
||||
import { defineFrontComponent } from 'twenty-sdk';
|
||||
|
||||
const StaticComponent = () => (
|
||||
<div
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { defineFrontComponent } from 'twenty-sdk';
|
||||
import { useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { defineFrontComponent } from '@/sdk';
|
||||
|
||||
const Card = styled.div`
|
||||
padding: 24px;
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { defineFrontComponent } from '@/sdk';
|
||||
import { defineFrontComponent } from 'twenty-sdk';
|
||||
import { useState } from 'react';
|
||||
|
||||
const TAILWIND_CSS = `
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { defineFrontComponent } from 'twenty-sdk';
|
||||
import { useState } from 'react';
|
||||
import { defineFrontComponent } from '@/sdk';
|
||||
import {
|
||||
Button,
|
||||
Chip,
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
"src/remote/mock/**/*",
|
||||
"src/host/generated/host-component-registry.ts",
|
||||
"src/remote/generated/remote-components.ts",
|
||||
"src/remote/generated/remote-elements.ts",
|
||||
"src/__stories__/**/*"
|
||||
"src/remote/generated/remote-elements.ts"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user