Compare commits
30
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
506fa18295 | ||
|
|
bf410ae438 | ||
|
|
aecbc89a3f | ||
|
|
446a3923f2 | ||
|
|
4f4f723ed0 | ||
|
|
4103efcb84 | ||
|
|
d3df58046c | ||
|
|
cb6953abe3 | ||
|
|
9bc803d0c7 | ||
|
|
cd31d9e0de | ||
|
|
c0fef0be08 | ||
|
|
270069c3e3 | ||
|
|
2413af0ba9 | ||
|
|
2b5b8a8b13 | ||
|
|
7af82fb6a4 | ||
|
|
9beb1ca326 | ||
|
|
d54092b0e2 | ||
|
|
60701c2cf9 | ||
|
|
381f3ba7d9 | ||
|
|
64470baa1e | ||
|
|
f5e8c05267 | ||
|
|
48c540eb6f | ||
|
|
5583254f58 | ||
|
|
97832af778 | ||
|
|
7ce3e2b065 | ||
|
|
a49d8386aa | ||
|
|
e420ee8746 | ||
|
|
7adab8884b | ||
|
|
6101a4f113 | ||
|
|
da8fe7f6f7 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-twenty-app",
|
||||
"version": "1.22.0-canary.6",
|
||||
"version": "1.22.0",
|
||||
"description": "Command-line interface to create Twenty application",
|
||||
"main": "dist/cli.cjs",
|
||||
"bin": "dist/cli.cjs",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "twenty-client-sdk",
|
||||
"version": "1.22.0-canary.6",
|
||||
"version": "1.22.0",
|
||||
"sideEffects": false,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
|
||||
@@ -1732,6 +1732,12 @@ type PublicWorkspaceData {
|
||||
workspaceUrls: WorkspaceUrls!
|
||||
}
|
||||
|
||||
type PublicWorkspaceDataSummary {
|
||||
id: UUID!
|
||||
logo: String
|
||||
displayName: String
|
||||
}
|
||||
|
||||
type NativeModelCapabilities {
|
||||
webSearch: Boolean
|
||||
twitterSearch: Boolean
|
||||
@@ -2645,6 +2651,7 @@ type CommandMenuItem {
|
||||
hotKeys: [String!]
|
||||
conditionalAvailabilityExpression: String
|
||||
availabilityObjectMetadataId: UUID
|
||||
pageLayoutId: UUID
|
||||
applicationId: UUID
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
@@ -3344,6 +3351,7 @@ type Query {
|
||||
currentUser: User!
|
||||
currentWorkspace: Workspace!
|
||||
getPublicWorkspaceDataByDomain(origin: String): PublicWorkspaceData!
|
||||
getPublicWorkspaceDataById(id: UUID!): PublicWorkspaceDataSummary!
|
||||
getSSOIdentityProviders: [FindAvailableSSOIDP!]!
|
||||
getConnectedImapSmtpCaldavAccount(id: UUID!): ConnectedImapSmtpCaldavAccount!
|
||||
getAutoCompleteAddress(address: String!, token: String!, country: String, isFieldCity: Boolean): [AutocompleteResult!]!
|
||||
@@ -4176,6 +4184,7 @@ input CreateCommandMenuItemInput {
|
||||
conditionalAvailabilityExpression: String
|
||||
availabilityObjectMetadataId: UUID
|
||||
payload: JSON
|
||||
pageLayoutId: UUID
|
||||
}
|
||||
|
||||
input UpdateCommandMenuItemInput {
|
||||
@@ -4189,6 +4198,7 @@ input UpdateCommandMenuItemInput {
|
||||
availabilityObjectMetadataId: UUID
|
||||
engineComponentKey: EngineComponentKey
|
||||
hotKeys: [String!]
|
||||
pageLayoutId: UUID
|
||||
}
|
||||
|
||||
input CreateFrontComponentInput {
|
||||
@@ -4599,6 +4609,7 @@ input UpdateApplicationRegistrationVariableInput {
|
||||
|
||||
input UpdateApplicationRegistrationVariablePayload {
|
||||
value: String
|
||||
resetValue: Boolean
|
||||
description: String
|
||||
}
|
||||
|
||||
|
||||
@@ -1430,6 +1430,13 @@ export interface PublicWorkspaceData {
|
||||
__typename: 'PublicWorkspaceData'
|
||||
}
|
||||
|
||||
export interface PublicWorkspaceDataSummary {
|
||||
id: Scalars['UUID']
|
||||
logo?: Scalars['String']
|
||||
displayName?: Scalars['String']
|
||||
__typename: 'PublicWorkspaceDataSummary'
|
||||
}
|
||||
|
||||
export interface NativeModelCapabilities {
|
||||
webSearch?: Scalars['Boolean']
|
||||
twitterSearch?: Scalars['Boolean']
|
||||
@@ -2361,6 +2368,7 @@ export interface CommandMenuItem {
|
||||
hotKeys?: Scalars['String'][]
|
||||
conditionalAvailabilityExpression?: Scalars['String']
|
||||
availabilityObjectMetadataId?: Scalars['UUID']
|
||||
pageLayoutId?: Scalars['UUID']
|
||||
applicationId?: Scalars['UUID']
|
||||
createdAt: Scalars['DateTime']
|
||||
updatedAt: Scalars['DateTime']
|
||||
@@ -2905,6 +2913,7 @@ export interface Query {
|
||||
currentUser: User
|
||||
currentWorkspace: Workspace
|
||||
getPublicWorkspaceDataByDomain: PublicWorkspaceData
|
||||
getPublicWorkspaceDataById: PublicWorkspaceDataSummary
|
||||
getSSOIdentityProviders: FindAvailableSSOIDP[]
|
||||
getConnectedImapSmtpCaldavAccount: ConnectedImapSmtpCaldavAccount
|
||||
getAutoCompleteAddress: AutocompleteResult[]
|
||||
@@ -4680,6 +4689,14 @@ export interface PublicWorkspaceDataGenqlSelection{
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface PublicWorkspaceDataSummaryGenqlSelection{
|
||||
id?: boolean | number
|
||||
logo?: boolean | number
|
||||
displayName?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface NativeModelCapabilitiesGenqlSelection{
|
||||
webSearch?: boolean | number
|
||||
twitterSearch?: boolean | number
|
||||
@@ -5690,6 +5707,7 @@ export interface CommandMenuItemGenqlSelection{
|
||||
hotKeys?: boolean | number
|
||||
conditionalAvailabilityExpression?: boolean | number
|
||||
availabilityObjectMetadataId?: boolean | number
|
||||
pageLayoutId?: boolean | number
|
||||
applicationId?: boolean | number
|
||||
createdAt?: boolean | number
|
||||
updatedAt?: boolean | number
|
||||
@@ -6274,6 +6292,7 @@ export interface QueryGenqlSelection{
|
||||
currentUser?: UserGenqlSelection
|
||||
currentWorkspace?: WorkspaceGenqlSelection
|
||||
getPublicWorkspaceDataByDomain?: (PublicWorkspaceDataGenqlSelection & { __args?: {origin?: (Scalars['String'] | null)} })
|
||||
getPublicWorkspaceDataById?: (PublicWorkspaceDataSummaryGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
getSSOIdentityProviders?: FindAvailableSSOIDPGenqlSelection
|
||||
getConnectedImapSmtpCaldavAccount?: (ConnectedImapSmtpCaldavAccountGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
getAutoCompleteAddress?: (AutocompleteResultGenqlSelection & { __args: {address: Scalars['String'], token: Scalars['String'], country?: (Scalars['String'] | null), isFieldCity?: (Scalars['Boolean'] | null)} })
|
||||
@@ -6725,9 +6744,9 @@ update: UpdateLogicFunctionFromSourceInputUpdates}
|
||||
|
||||
export interface UpdateLogicFunctionFromSourceInputUpdates {name?: (Scalars['String'] | null),description?: (Scalars['String'] | null),timeoutSeconds?: (Scalars['Float'] | null),sourceHandlerCode?: (Scalars['String'] | null),toolInputSchema?: (Scalars['JSON'] | null),handlerName?: (Scalars['String'] | null),sourceHandlerPath?: (Scalars['String'] | null),isTool?: (Scalars['Boolean'] | null),cronTriggerSettings?: (Scalars['JSON'] | null),databaseEventTriggerSettings?: (Scalars['JSON'] | null),httpRouteTriggerSettings?: (Scalars['JSON'] | null)}
|
||||
|
||||
export interface CreateCommandMenuItemInput {workflowVersionId?: (Scalars['UUID'] | null),frontComponentId?: (Scalars['UUID'] | null),engineComponentKey: EngineComponentKey,label: Scalars['String'],icon?: (Scalars['String'] | null),shortLabel?: (Scalars['String'] | null),position?: (Scalars['Float'] | null),isPinned?: (Scalars['Boolean'] | null),availabilityType?: (CommandMenuItemAvailabilityType | null),hotKeys?: (Scalars['String'][] | null),conditionalAvailabilityExpression?: (Scalars['String'] | null),availabilityObjectMetadataId?: (Scalars['UUID'] | null),payload?: (Scalars['JSON'] | null)}
|
||||
export interface CreateCommandMenuItemInput {workflowVersionId?: (Scalars['UUID'] | null),frontComponentId?: (Scalars['UUID'] | null),engineComponentKey: EngineComponentKey,label: Scalars['String'],icon?: (Scalars['String'] | null),shortLabel?: (Scalars['String'] | null),position?: (Scalars['Float'] | null),isPinned?: (Scalars['Boolean'] | null),availabilityType?: (CommandMenuItemAvailabilityType | null),hotKeys?: (Scalars['String'][] | null),conditionalAvailabilityExpression?: (Scalars['String'] | null),availabilityObjectMetadataId?: (Scalars['UUID'] | null),payload?: (Scalars['JSON'] | null),pageLayoutId?: (Scalars['UUID'] | null)}
|
||||
|
||||
export interface UpdateCommandMenuItemInput {id: Scalars['UUID'],label?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),shortLabel?: (Scalars['String'] | null),position?: (Scalars['Float'] | null),isPinned?: (Scalars['Boolean'] | null),availabilityType?: (CommandMenuItemAvailabilityType | null),availabilityObjectMetadataId?: (Scalars['UUID'] | null),engineComponentKey?: (EngineComponentKey | null),hotKeys?: (Scalars['String'][] | null)}
|
||||
export interface UpdateCommandMenuItemInput {id: Scalars['UUID'],label?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),shortLabel?: (Scalars['String'] | null),position?: (Scalars['Float'] | null),isPinned?: (Scalars['Boolean'] | null),availabilityType?: (CommandMenuItemAvailabilityType | null),availabilityObjectMetadataId?: (Scalars['UUID'] | null),engineComponentKey?: (EngineComponentKey | null),hotKeys?: (Scalars['String'][] | null),pageLayoutId?: (Scalars['UUID'] | null)}
|
||||
|
||||
export interface CreateFrontComponentInput {id?: (Scalars['UUID'] | null),name: Scalars['String'],description?: (Scalars['String'] | null),sourceComponentPath: Scalars['String'],builtComponentPath: Scalars['String'],componentName: Scalars['String'],builtComponentChecksum: Scalars['String']}
|
||||
|
||||
@@ -6859,7 +6878,7 @@ export interface CreateApplicationRegistrationVariableInput {applicationRegistra
|
||||
|
||||
export interface UpdateApplicationRegistrationVariableInput {id: Scalars['String'],update: UpdateApplicationRegistrationVariablePayload}
|
||||
|
||||
export interface UpdateApplicationRegistrationVariablePayload {value?: (Scalars['String'] | null),description?: (Scalars['String'] | null)}
|
||||
export interface UpdateApplicationRegistrationVariablePayload {value?: (Scalars['String'] | null),resetValue?: (Scalars['Boolean'] | null),description?: (Scalars['String'] | null)}
|
||||
|
||||
export interface UpdateWorkspaceMemberSettingsInput {workspaceMemberId: Scalars['UUID'],update: Scalars['JSON']}
|
||||
|
||||
@@ -7850,6 +7869,14 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const PublicWorkspaceDataSummary_possibleTypes: string[] = ['PublicWorkspaceDataSummary']
|
||||
export const isPublicWorkspaceDataSummary = (obj?: { __typename?: any } | null): obj is PublicWorkspaceDataSummary => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isPublicWorkspaceDataSummary"')
|
||||
return PublicWorkspaceDataSummary_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const NativeModelCapabilities_possibleTypes: string[] = ['NativeModelCapabilities']
|
||||
export const isNativeModelCapabilities = (obj?: { __typename?: any } | null): obj is NativeModelCapabilities => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isNativeModelCapabilities"')
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+10
-24
@@ -2,45 +2,31 @@ import { styled } from '@linaria/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { useContext } from 'react';
|
||||
import { IconLock } from 'twenty-ui/display';
|
||||
import { Card, CardContent } from 'twenty-ui/layout';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledVisibilityCardContainer = styled.div`
|
||||
color: ${themeCssVariables.font.color.light};
|
||||
flex: 1;
|
||||
transition: color ${themeCssVariables.animation.duration.normal} ease;
|
||||
width: 100%;
|
||||
|
||||
> * {
|
||||
border-color: ${themeCssVariables.border.color.light};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledVisibilityCardContentContainer = styled.div`
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
background-color: ${themeCssVariables.background.transparent.lighter};
|
||||
background: ${themeCssVariables.background.transparent.lighter};
|
||||
border: 1px solid ${themeCssVariables.border.color.light};
|
||||
border-radius: ${themeCssVariables.border.radius.sm};
|
||||
box-sizing: border-box;
|
||||
color: ${themeCssVariables.font.color.light};
|
||||
display: flex;
|
||||
font-size: ${themeCssVariables.font.size.sm};
|
||||
font-weight: ${themeCssVariables.font.weight.medium};
|
||||
gap: ${themeCssVariables.spacing[1]};
|
||||
height: ${themeCssVariables.spacing[6]};
|
||||
padding: ${themeCssVariables.spacing[0]} ${themeCssVariables.spacing[1]};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const CalendarEventNotSharedContent = () => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
|
||||
return (
|
||||
<StyledVisibilityCardContainer>
|
||||
<Card>
|
||||
<StyledVisibilityCardContentContainer>
|
||||
<CardContent>
|
||||
<IconLock size={theme.icon.size.sm} />
|
||||
<Trans>Not shared</Trans>
|
||||
</CardContent>
|
||||
</StyledVisibilityCardContentContainer>
|
||||
</Card>
|
||||
</StyledVisibilityCardContainer>
|
||||
<StyledContainer>
|
||||
<IconLock size={theme.icon.size.sm} />
|
||||
<Trans>Not shared</Trans>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -204,6 +204,14 @@ const SettingsApplicationRegistrationDetails = lazy(() =>
|
||||
})),
|
||||
);
|
||||
|
||||
const SettingsApplicationRegistrationConfigVariableDetail = lazy(() =>
|
||||
import(
|
||||
'~/pages/settings/applications/components/SettingsApplicationRegistrationConfigVariableDetail'
|
||||
).then((module) => ({
|
||||
default: module.SettingsApplicationRegistrationConfigVariableDetail,
|
||||
})),
|
||||
);
|
||||
|
||||
const SettingsAgentForm = lazy(() =>
|
||||
import('~/pages/settings/ai/SettingsAgentForm').then((module) => ({
|
||||
default: module.SettingsAgentForm,
|
||||
@@ -744,6 +752,10 @@ export const SettingsRoutes = ({ isAdminPageEnabled }: SettingsRoutesProps) => (
|
||||
path={SettingsPath.ApplicationLogicFunctionDetail}
|
||||
element={<SettingsLogicFunctionDetail />}
|
||||
/>
|
||||
<Route
|
||||
path={SettingsPath.ApplicationRegistrationConfigVariableDetails}
|
||||
element={<SettingsApplicationRegistrationConfigVariableDetail />}
|
||||
/>
|
||||
</Route>
|
||||
|
||||
<Route
|
||||
|
||||
@@ -32,6 +32,7 @@ import { useResetFocusStackToFocusItem } from '@/ui/utilities/focus/hooks/useRes
|
||||
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { currentPageLayoutIdState } from '@/page-layout/states/currentPageLayoutIdState';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
@@ -44,6 +45,7 @@ import { AppBasePath, AppPath, SidePanelPages } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { AnalyticsType } from '~/generated-metadata/graphql';
|
||||
import { usePageChangeEffectNavigateLocation } from '~/hooks/usePageChangeEffectNavigateLocation';
|
||||
import { getPageLayoutIdForLocation } from '~/modules/app/utils/getPageLayoutIdForLocation';
|
||||
import { useInitializeQueryParamState } from '~/modules/app/hooks/useInitializeQueryParamState';
|
||||
import { isMatchingLocation } from '~/utils/isMatchingLocation';
|
||||
import { getPageTitleFromPath } from '~/utils/title-utils';
|
||||
@@ -146,10 +148,15 @@ export const PageChangeEffect = () => {
|
||||
if (!previousLocation || previousLocation !== location.pathname) {
|
||||
setPreviousLocation(location.pathname);
|
||||
executeTasksOnAnyLocationChange();
|
||||
} else {
|
||||
return;
|
||||
|
||||
const newPageLayoutId = getPageLayoutIdForLocation({
|
||||
location,
|
||||
store,
|
||||
});
|
||||
|
||||
store.set(currentPageLayoutIdState.atom, newPageLayoutId);
|
||||
}
|
||||
}, [location, previousLocation, executeTasksOnAnyLocationChange]);
|
||||
}, [location, previousLocation, executeTasksOnAnyLocationChange, store]);
|
||||
|
||||
useEffect(() => {
|
||||
initializeQueryParamState();
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { type getDefaultStore } from 'jotai';
|
||||
import { type Location, matchPath } from 'react-router-dom';
|
||||
import { AppPath, CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector';
|
||||
import { recordPageLayoutByObjectMetadataIdFamilySelector } from '@/page-layout/states/selectors/recordPageLayoutByObjectMetadataIdFamilySelector';
|
||||
import { getDefaultRecordPageLayoutId } from '@/page-layout/utils/getDefaultRecordPageLayoutId';
|
||||
|
||||
const DASHBOARD_NAME_SINGULAR = CoreObjectNameSingular.Dashboard;
|
||||
|
||||
export const getPageLayoutIdForLocation = ({
|
||||
location,
|
||||
store,
|
||||
}: {
|
||||
location: Location;
|
||||
store: ReturnType<typeof getDefaultStore>;
|
||||
}): string | null => {
|
||||
const recordShowMatch = matchPath(AppPath.RecordShowPage, location.pathname);
|
||||
|
||||
if (isDefined(recordShowMatch?.params.objectNameSingular)) {
|
||||
const objectNameSingular = recordShowMatch.params.objectNameSingular;
|
||||
|
||||
if (objectNameSingular === DASHBOARD_NAME_SINGULAR) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const objectMetadataItem = store.get(
|
||||
objectMetadataItemFamilySelector.selectorFamily({
|
||||
objectName: objectNameSingular,
|
||||
objectNameType: 'singular',
|
||||
}),
|
||||
);
|
||||
|
||||
if (!isDefined(objectMetadataItem)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const recordPageLayout = store.get(
|
||||
recordPageLayoutByObjectMetadataIdFamilySelector.selectorFamily({
|
||||
objectMetadataId: objectMetadataItem.id,
|
||||
}),
|
||||
);
|
||||
|
||||
return isDefined(recordPageLayout)
|
||||
? recordPageLayout.id
|
||||
: getDefaultRecordPageLayoutId({
|
||||
targetObjectNameSingular: objectNameSingular,
|
||||
});
|
||||
}
|
||||
|
||||
const pageLayoutMatch = matchPath(AppPath.PageLayoutPage, location.pathname);
|
||||
|
||||
if (isDefined(pageLayoutMatch?.params.pageLayoutId)) {
|
||||
return pageLayoutMatch.params.pageLayoutId;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const FIND_ONE_APPLICATION_SUMMARY = gql`
|
||||
query FindOneApplicationSummary($universalIdentifier: UUID!) {
|
||||
findOneApplication(universalIdentifier: $universalIdentifier) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
+5
-1
@@ -5,7 +5,9 @@ import { PinnedCommandMenuItemButtons } from '@/command-menu-item/display/compon
|
||||
import { CommandMenuItemEditButton } from '@/command-menu-item/edit/components/CommandMenuItemEditButton';
|
||||
import { commandMenuItemsSelector } from '@/command-menu-item/states/commandMenuItemsSelector';
|
||||
import { doesCommandMenuItemMatchObjectMetadataId } from '@/command-menu-item/utils/doesCommandMenuItemMatchObjectMetadataId';
|
||||
import { doesCommandMenuItemMatchPageLayoutId } from '@/command-menu-item/utils/doesCommandMenuItemMatchPageLayoutId';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { currentPageLayoutIdState } from '@/page-layout/states/currentPageLayoutIdState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useStore } from 'jotai';
|
||||
import { useMemo } from 'react';
|
||||
@@ -22,6 +24,7 @@ export const StandalonePageCommandMenu = () => {
|
||||
const isMobile = useIsMobile();
|
||||
const commandMenuItems = useAtomStateValue(commandMenuItemsSelector);
|
||||
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
|
||||
const currentPageLayoutId = useAtomStateValue(currentPageLayoutIdState);
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const commandMenuContextApi = useMemo<CommandMenuContextApi>(() => {
|
||||
@@ -83,6 +86,7 @@ export const StandalonePageCommandMenu = () => {
|
||||
item.availabilityType !==
|
||||
CommandMenuItemAvailabilityType.GLOBAL_OBJECT_CONTEXT,
|
||||
)
|
||||
.filter(doesCommandMenuItemMatchPageLayoutId(currentPageLayoutId))
|
||||
.filter((item) =>
|
||||
evaluateConditionalAvailabilityExpression(
|
||||
item.conditionalAvailabilityExpression,
|
||||
@@ -92,7 +96,7 @@ export const StandalonePageCommandMenu = () => {
|
||||
.sort(
|
||||
(firstItem, secondItem) => firstItem.position - secondItem.position,
|
||||
);
|
||||
}, [commandMenuItems, commandMenuContextApi]);
|
||||
}, [commandMenuItems, commandMenuContextApi, currentPageLayoutId]);
|
||||
|
||||
return (
|
||||
<CommandMenuContext.Provider
|
||||
|
||||
+5
-1
@@ -4,7 +4,9 @@ import {
|
||||
} from '@/command-menu-item/contexts/CommandMenuContext';
|
||||
import { commandMenuItemsSelector } from '@/command-menu-item/states/commandMenuItemsSelector';
|
||||
import { doesCommandMenuItemMatchObjectMetadataId } from '@/command-menu-item/utils/doesCommandMenuItemMatchObjectMetadataId';
|
||||
import { doesCommandMenuItemMatchPageLayoutId } from '@/command-menu-item/utils/doesCommandMenuItemMatchPageLayoutId';
|
||||
import { doesCommandMenuItemMatchPageType } from '@/command-menu-item/utils/doesCommandMenuItemMatchPageType';
|
||||
import { currentPageLayoutIdState } from '@/page-layout/states/currentPageLayoutIdState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useMemo } from 'react';
|
||||
import { type CommandMenuContextApi } from 'twenty-shared/types';
|
||||
@@ -24,6 +26,7 @@ export const CommandMenuContextProviderContent = ({
|
||||
commandMenuContextApi,
|
||||
}: CommandMenuContextProviderContentProps) => {
|
||||
const commandMenuItems = useAtomStateValue(commandMenuItemsSelector);
|
||||
const currentPageLayoutId = useAtomStateValue(currentPageLayoutIdState);
|
||||
|
||||
const filteredCommandMenuItems = useMemo(() => {
|
||||
const currentObjectMetadataItemId =
|
||||
@@ -34,6 +37,7 @@ export const CommandMenuContextProviderContent = ({
|
||||
doesCommandMenuItemMatchObjectMetadataId(currentObjectMetadataItemId),
|
||||
)
|
||||
.filter(doesCommandMenuItemMatchPageType(commandMenuContextApi.pageType))
|
||||
.filter(doesCommandMenuItemMatchPageLayoutId(currentPageLayoutId))
|
||||
.filter((item) =>
|
||||
evaluateConditionalAvailabilityExpression(
|
||||
item.conditionalAvailabilityExpression,
|
||||
@@ -43,7 +47,7 @@ export const CommandMenuContextProviderContent = ({
|
||||
.sort(
|
||||
(firstItem, secondItem) => firstItem.position - secondItem.position,
|
||||
);
|
||||
}, [commandMenuItems, commandMenuContextApi]);
|
||||
}, [commandMenuItems, commandMenuContextApi, currentPageLayoutId]);
|
||||
|
||||
return (
|
||||
<CommandMenuContext.Provider
|
||||
|
||||
+5
-24
@@ -3,10 +3,7 @@ import { Suspense, lazy } from 'react';
|
||||
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
|
||||
import { CommandComponentInstanceContext } from '@/command-menu-item/engine-command/states/contexts/CommandComponentInstanceContext';
|
||||
import { isHeadlessFrontComponentCommandContextApi } from '@/command-menu-item/engine-command/utils/isHeadlessFrontComponentCommandContextApi';
|
||||
import { LayoutRenderingProvider } from '@/ui/layout/contexts/LayoutRenderingContext';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { PageLayoutType } from '~/generated-metadata/graphql';
|
||||
|
||||
const FrontComponentRenderer = lazy(() =>
|
||||
import('@/front-components/components/FrontComponentRenderer').then(
|
||||
@@ -27,34 +24,18 @@ export const HeadlessFrontComponentRendererEngineCommand = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const objectNameSingular = context.objectMetadataItem?.nameSingular;
|
||||
|
||||
const recordId =
|
||||
context.selectedRecords.length === 1
|
||||
? context.selectedRecords[0].id
|
||||
: undefined;
|
||||
|
||||
// TODO: Remove layout rendering provider once we have refactored FrontComponentRenderer to have one headless renderer and a standard renderer
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<LayoutRenderingProvider
|
||||
value={{
|
||||
targetRecordIdentifier:
|
||||
isDefined(objectNameSingular) && isDefined(recordId)
|
||||
? {
|
||||
id: recordId,
|
||||
targetObjectNameSingular: objectNameSingular,
|
||||
}
|
||||
: undefined,
|
||||
layoutType: PageLayoutType.DASHBOARD,
|
||||
isInSidePanel: false,
|
||||
}}
|
||||
>
|
||||
<FrontComponentRenderer
|
||||
frontComponentId={context.frontComponentId}
|
||||
commandMenuItemId={commandMenuItemId}
|
||||
/>
|
||||
</LayoutRenderingProvider>
|
||||
<FrontComponentRenderer
|
||||
frontComponentId={context.frontComponentId}
|
||||
commandMenuItemId={commandMenuItemId}
|
||||
recordId={recordId}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
+6
-117
@@ -9,8 +9,6 @@ import {
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
const mockFindOneWorkflowVersion = jest.fn();
|
||||
const mockEnqueueWarningSnackBar = jest.fn();
|
||||
const mockBuildTriggerWorkflowVersionPayloads = jest.fn();
|
||||
|
||||
jest.mock('@/object-record/hooks/useLazyFindOneRecord', () => ({
|
||||
useLazyFindOneRecord: () => ({
|
||||
@@ -18,20 +16,6 @@ jest.mock('@/object-record/hooks/useLazyFindOneRecord', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('@/ui/feedback/snack-bar-manager/hooks/useSnackBar', () => ({
|
||||
useSnackBar: () => ({
|
||||
enqueueWarningSnackBar: mockEnqueueWarningSnackBar,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock(
|
||||
'@/command-menu-item/engine-command/utils/buildTriggerWorkflowVersionPayloads',
|
||||
() => ({
|
||||
buildTriggerWorkflowVersionPayloads: (...args: unknown[]) =>
|
||||
mockBuildTriggerWorkflowVersionPayloads(...args),
|
||||
}),
|
||||
);
|
||||
|
||||
const getWrapper =
|
||||
(store = createStore()) =>
|
||||
({ children }: { children: ReactNode }) => (
|
||||
@@ -58,7 +42,7 @@ describe('useEnrichHeadlessCommandContextApiWithWorkflowVersionTriggerInformatio
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return enriched context API with workflow info and payloads', async () => {
|
||||
it('should return enriched context with workflow metadata', async () => {
|
||||
const store = createStore();
|
||||
const wrapper = getWrapper(store);
|
||||
|
||||
@@ -75,9 +59,6 @@ describe('useEnrichHeadlessCommandContextApiWithWorkflowVersionTriggerInformatio
|
||||
},
|
||||
);
|
||||
|
||||
const expectedPayloads = [{ recordId: 'rec-1' }];
|
||||
mockBuildTriggerWorkflowVersionPayloads.mockReturnValue(expectedPayloads);
|
||||
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useEnrichHeadlessCommandContextApiWithWorkflowVersionTriggerInformation(),
|
||||
@@ -94,7 +75,8 @@ describe('useEnrichHeadlessCommandContextApiWithWorkflowVersionTriggerInformatio
|
||||
{
|
||||
headlessEngineCommandContextApi,
|
||||
workflowVersionId: 'wf-version-1',
|
||||
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
|
||||
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
|
||||
availabilityObjectMetadataId: 'obj-1',
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -103,7 +85,9 @@ describe('useEnrichHeadlessCommandContextApiWithWorkflowVersionTriggerInformatio
|
||||
...headlessEngineCommandContextApi,
|
||||
workflowId: 'workflow-1',
|
||||
workflowVersionId: 'wf-version-1',
|
||||
payloads: expectedPayloads,
|
||||
trigger: { type: 'MANUAL' },
|
||||
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
|
||||
availabilityObjectMetadataId: 'obj-1',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -134,99 +118,4 @@ describe('useEnrichHeadlessCommandContextApiWithWorkflowVersionTriggerInformatio
|
||||
|
||||
expect(enrichedResult).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined for RECORD_SELECTION type when payloads are empty', async () => {
|
||||
const store = createStore();
|
||||
const wrapper = getWrapper(store);
|
||||
|
||||
const workflowVersionRecord = {
|
||||
id: 'wf-version-1',
|
||||
workflowId: 'workflow-1',
|
||||
trigger: { type: 'MANUAL' },
|
||||
__typename: 'WorkflowVersion' as const,
|
||||
};
|
||||
|
||||
mockFindOneWorkflowVersion.mockImplementation(
|
||||
async ({ onCompleted }: { onCompleted: (data: unknown) => void }) => {
|
||||
onCompleted(workflowVersionRecord);
|
||||
},
|
||||
);
|
||||
|
||||
mockBuildTriggerWorkflowVersionPayloads.mockReturnValue([]);
|
||||
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useEnrichHeadlessCommandContextApiWithWorkflowVersionTriggerInformation(),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
let enrichedResult: unknown;
|
||||
|
||||
await act(async () => {
|
||||
enrichedResult =
|
||||
await result.current.enrichHeadlessCommandContextApiWithWorkflowVersionTriggerInformation(
|
||||
{
|
||||
headlessEngineCommandContextApi: buildBaseContextApi(),
|
||||
workflowVersionId: 'wf-version-1',
|
||||
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
expect(enrichedResult).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should show warning snackbar when selected records exceed QUERY_MAX_RECORDS', async () => {
|
||||
const store = createStore();
|
||||
const wrapper = getWrapper(store);
|
||||
|
||||
const workflowVersionRecord = {
|
||||
id: 'wf-version-1',
|
||||
workflowId: 'workflow-1',
|
||||
trigger: { type: 'MANUAL' },
|
||||
__typename: 'WorkflowVersion' as const,
|
||||
};
|
||||
|
||||
mockFindOneWorkflowVersion.mockImplementation(
|
||||
async ({ onCompleted }: { onCompleted: (data: unknown) => void }) => {
|
||||
onCompleted(workflowVersionRecord);
|
||||
},
|
||||
);
|
||||
|
||||
mockBuildTriggerWorkflowVersionPayloads.mockReturnValue([
|
||||
{ recordId: 'rec-1' },
|
||||
]);
|
||||
|
||||
const selectedRecordIds = Array.from({ length: 201 }, (_, index) =>
|
||||
String(index),
|
||||
);
|
||||
|
||||
const headlessEngineCommandContextApi = buildBaseContextApi({
|
||||
targetedRecordsRule: { mode: 'selection', selectedRecordIds },
|
||||
});
|
||||
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useEnrichHeadlessCommandContextApiWithWorkflowVersionTriggerInformation(),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.enrichHeadlessCommandContextApiWithWorkflowVersionTriggerInformation(
|
||||
{
|
||||
headlessEngineCommandContextApi,
|
||||
workflowVersionId: 'wf-version-1',
|
||||
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
expect(mockEnqueueWarningSnackBar).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
options: {
|
||||
dedupeKey: 'workflow-manual-trigger-selection-limit',
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
+2
-1
@@ -84,7 +84,8 @@ describe('useMountCommand', () => {
|
||||
...baseContextApi,
|
||||
workflowId: 'workflow-1',
|
||||
workflowVersionId: 'wf-version-1',
|
||||
payloads: [{ recordId: 'rec-1' }],
|
||||
trigger: { type: 'MANUAL' },
|
||||
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
|
||||
};
|
||||
mockEnrichFn.mockResolvedValue(enrichedState);
|
||||
|
||||
|
||||
+8
-73
@@ -4,27 +4,18 @@ import {
|
||||
type HeadlessCommandContextApi,
|
||||
type HeadlessEngineCommandContextApi,
|
||||
} from '@/command-menu-item/engine-command/types/HeadlessCommandContextApi';
|
||||
import { buildTriggerWorkflowVersionPayloads } from '@/command-menu-item/engine-command/utils/buildTriggerWorkflowVersionPayloads';
|
||||
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
|
||||
import { useLazyFindOneRecord } from '@/object-record/hooks/useLazyFindOneRecord';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { type WorkflowVersion } from '@/workflow/types/Workflow';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useStore } from 'jotai';
|
||||
import { QUERY_MAX_RECORDS } from 'twenty-shared/constants';
|
||||
import { CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import { isDefined, isNonEmptyArray } from 'twenty-shared/utils';
|
||||
import {
|
||||
CommandMenuItemAvailabilityType as CommandMenuItemAvailabilityTypeEnum,
|
||||
type CommandMenuItemAvailabilityType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type CommandMenuItemAvailabilityType } from '~/generated-metadata/graphql';
|
||||
|
||||
type WorkflowVersionRecord = Pick<
|
||||
WorkflowVersion,
|
||||
'id' | 'workflowId' | 'trigger' | '__typename'
|
||||
>;
|
||||
|
||||
type BuildTriggerWorkflowVersionCommandStateParams = {
|
||||
type EnrichParams = {
|
||||
headlessEngineCommandContextApi: HeadlessEngineCommandContextApi;
|
||||
workflowVersionId: string;
|
||||
availabilityType: CommandMenuItemAvailabilityType;
|
||||
@@ -33,9 +24,6 @@ type BuildTriggerWorkflowVersionCommandStateParams = {
|
||||
|
||||
export const useEnrichHeadlessCommandContextApiWithWorkflowVersionTriggerInformation =
|
||||
() => {
|
||||
const store = useStore();
|
||||
const { enqueueWarningSnackBar } = useSnackBar();
|
||||
|
||||
const { findOneRecord: findOneWorkflowVersion } =
|
||||
useLazyFindOneRecord<WorkflowVersionRecord>({
|
||||
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
||||
@@ -65,76 +53,23 @@ export const useEnrichHeadlessCommandContextApiWithWorkflowVersionTriggerInforma
|
||||
workflowVersionId,
|
||||
availabilityType,
|
||||
availabilityObjectMetadataId,
|
||||
}: BuildTriggerWorkflowVersionCommandStateParams): Promise<
|
||||
HeadlessCommandContextApi | undefined
|
||||
> => {
|
||||
}: EnrichParams): Promise<HeadlessCommandContextApi | undefined> => {
|
||||
const workflowVersion = await fetchWorkflowVersion(workflowVersionId);
|
||||
|
||||
if (!isDefined(workflowVersion)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (
|
||||
headlessEngineCommandContextApi.targetedRecordsRule.mode ===
|
||||
'exclusion'
|
||||
) {
|
||||
enqueueWarningSnackBar({
|
||||
message: t`Running workflows on all records is not yet supported. Please select records manually.`,
|
||||
options: {
|
||||
dedupeKey: 'workflow-manual-trigger-select-all-not-supported',
|
||||
},
|
||||
});
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const selectedRecordIds =
|
||||
headlessEngineCommandContextApi.targetedRecordsRule
|
||||
.selectedRecordIds;
|
||||
|
||||
if (selectedRecordIds.length > QUERY_MAX_RECORDS) {
|
||||
const selectedCountFormatted =
|
||||
selectedRecordIds.length.toLocaleString();
|
||||
|
||||
const limitFormatted = QUERY_MAX_RECORDS.toLocaleString();
|
||||
|
||||
enqueueWarningSnackBar({
|
||||
message: t`You selected ${selectedCountFormatted} records but manual triggers can run on at most ${limitFormatted} records at once. Only the first ${limitFormatted} records will be processed.`,
|
||||
options: {
|
||||
dedupeKey: 'workflow-manual-trigger-selection-limit',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const objectMetadataItems = store.get(
|
||||
objectMetadataItemsSelector.atom,
|
||||
);
|
||||
|
||||
const payloads = buildTriggerWorkflowVersionPayloads({
|
||||
store,
|
||||
trigger: workflowVersion.trigger,
|
||||
availabilityType,
|
||||
availabilityObjectMetadataId,
|
||||
objectMetadataItems,
|
||||
selectedRecordIds,
|
||||
});
|
||||
|
||||
if (
|
||||
availabilityType ===
|
||||
CommandMenuItemAvailabilityTypeEnum.RECORD_SELECTION &&
|
||||
!isNonEmptyArray(payloads)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
...headlessEngineCommandContextApi,
|
||||
workflowId: workflowVersion.workflowId,
|
||||
workflowVersionId: workflowVersion.id,
|
||||
payloads,
|
||||
trigger: workflowVersion.trigger,
|
||||
availabilityType,
|
||||
availabilityObjectMetadataId,
|
||||
};
|
||||
},
|
||||
[store, fetchWorkflowVersion, enqueueWarningSnackBar],
|
||||
[fetchWorkflowVersion],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
+47
-7
@@ -1,23 +1,63 @@
|
||||
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
|
||||
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
|
||||
import { buildTriggerWorkflowVersionPayloads } from '@/command-menu-item/engine-command/utils/buildTriggerWorkflowVersionPayloads';
|
||||
import { isHeadlessTriggerWorkflowVersionCommandContextApi } from '@/command-menu-item/engine-command/utils/isHeadlessTriggerWorkflowVersionCommandContextApi';
|
||||
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
|
||||
import { DEFAULT_QUERY_PAGE_SIZE } from '@/object-record/constants/DefaultQueryPageSize';
|
||||
import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRecords';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { useRunWorkflowVersion } from '@/workflow/hooks/useRunWorkflowVersion';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import {
|
||||
CoreObjectNameSingular,
|
||||
type RecordGqlOperationFilter,
|
||||
} from 'twenty-shared/types';
|
||||
import { isNonEmptyArray } from 'twenty-shared/utils';
|
||||
|
||||
export const TriggerWorkflowVersionEngineCommand = () => {
|
||||
const mountedCommandState = useHeadlessCommandContextApi();
|
||||
const store = useStore();
|
||||
|
||||
if (!isHeadlessTriggerWorkflowVersionCommandContextApi(mountedCommandState)) {
|
||||
throw new Error(
|
||||
'TriggerWorkflowVersionEngineCommand requires a workflow trigger context',
|
||||
);
|
||||
}
|
||||
|
||||
const noMatchFilter: RecordGqlOperationFilter = { id: { in: [] } };
|
||||
|
||||
const { fetchAllRecords } = useLazyFetchAllRecords({
|
||||
objectNameSingular:
|
||||
mountedCommandState.objectMetadataItem?.nameSingular ??
|
||||
CoreObjectNameSingular.Person,
|
||||
filter: mountedCommandState.graphqlFilter ?? noMatchFilter,
|
||||
limit: DEFAULT_QUERY_PAGE_SIZE,
|
||||
});
|
||||
|
||||
const { runWorkflowVersion } = useRunWorkflowVersion();
|
||||
|
||||
const execute = useCallback(async () => {
|
||||
if (
|
||||
!isHeadlessTriggerWorkflowVersionCommandContextApi(mountedCommandState)
|
||||
) {
|
||||
return;
|
||||
let selectedRecords: ObjectRecord[];
|
||||
|
||||
if (mountedCommandState.targetedRecordsRule.mode === 'selection') {
|
||||
selectedRecords = mountedCommandState.selectedRecords;
|
||||
} else {
|
||||
selectedRecords = await fetchAllRecords();
|
||||
}
|
||||
|
||||
if (!isNonEmptyArray(mountedCommandState.payloads)) {
|
||||
const objectMetadataItems = store.get(objectMetadataItemsSelector.atom);
|
||||
|
||||
const payloads = buildTriggerWorkflowVersionPayloads({
|
||||
trigger: mountedCommandState.trigger,
|
||||
availabilityType: mountedCommandState.availabilityType,
|
||||
availabilityObjectMetadataId:
|
||||
mountedCommandState.availabilityObjectMetadataId,
|
||||
objectMetadataItems,
|
||||
selectedRecords,
|
||||
});
|
||||
|
||||
if (!isNonEmptyArray(payloads)) {
|
||||
await runWorkflowVersion({
|
||||
workflowId: mountedCommandState.workflowId,
|
||||
workflowVersionId: mountedCommandState.workflowVersionId,
|
||||
@@ -26,14 +66,14 @@ export const TriggerWorkflowVersionEngineCommand = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const payload of mountedCommandState.payloads) {
|
||||
for (const payload of payloads) {
|
||||
await runWorkflowVersion({
|
||||
workflowId: mountedCommandState.workflowId,
|
||||
workflowVersionId: mountedCommandState.workflowVersionId,
|
||||
payload,
|
||||
});
|
||||
}
|
||||
}, [runWorkflowVersion, mountedCommandState]);
|
||||
}, [mountedCommandState, fetchAllRecords, runWorkflowVersion, store]);
|
||||
|
||||
return <HeadlessEngineCommandWrapperEffect execute={execute} />;
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user