Compare commits

...
Author SHA1 Message Date
Félix Malfait 094cc09878 fix(automation): replace isSystem check with explicit blocklist
Workflows and AI tools share record-crud services. The previous guard
treated every isSystem object as off-limits, which incorrectly blocked
legitimate targets like noteTarget/taskTarget/attachment/timelineActivity
when an AI tool tried to create them and surfaced a misleading
"cannot be created by workflow" error.

- Replace canObjectBeManagedByWorkflow with canObjectBeManagedByAutomation
  backed by OBJECTS_BLOCKED_FROM_AUTOMATION (workflow*, workspaceMember,
  dashboard, message*, calendarEvent*).
- Update CRUD service errors to say "by automation" instead of "by workflow".
- Filter blocked objects out of write/delete AI tool descriptors in
  DatabaseToolProvider so the model never sees tools it can't call.
2026-05-27 18:38:20 +02:00
46e7f23df1 fix(contact-creation): handle common email display-name shapes when auto-creating People (#20639)
## Summary

When messages are imported, Twenty auto-creates a Person record for any
recipient that doesn't exist yet. The display-name parser used at that
point is `displayName.split(' ')[0] / [1]`, which silently mangles
several common header shapes:

| Header | Old result |

|-------------------------------------------------|-----------------------------------------|
| `"Doe, John" <...>` | `firstName="Doe,"`, `lastName="John"` |
| `"John.Doe Doe" <...>` | `firstName="John.Doe"`, `lastName="Doe"`|
| `"Mary Jane Watson" <...>` | `lastName="Jane"` ("Watson" dropped) |
| `"[email protected]" <[email protected]>` (forwarder) | full address in
`firstName` |
| `"Doe, John:GROUP" <...>` (group-tag servers) |
`firstName="John:GROUP"` |

This PR rewrites `getFirstNameAndLastNameFromHandleAndDisplayName` to
handle each pattern. Behaviour in order:

1. Trim + strip wrapping quotes
2. Swap `"Last, First"` comma form
3. Fall back to handle parsing when display name contains `@` (real
names don't)
4. Split single dotted tokens (`"john.doe"` → `"John"`, `"Doe"`)
5. Preserve multi-word last names (`tokens.slice(1).join(' ')`)
6. De-synthesize dot-glued first names (`"John.Doe Doe"` → `"John"`,
`"Doe"`)
7. Strip `:XXX` trailing tag suffix from each parsed field

## Test plan

- [x] 16 new unit test cases covering each shape
(`__tests__/get-first-name-and-last-name-from-handle-and-display-name.util.spec.ts`)
- [x] Lint + typecheck clean
- [ ] No regression in the messaging import flow

---------

Co-authored-by: neo773 <[email protected]>
Co-authored-by: neo773 <[email protected]>
2026-05-27 15:49:46 +00:00
6f3541fd7c i18n - translations (#20975)
Created by Github action

Co-authored-by: github-actions <[email protected]>
2026-05-27 17:47:07 +02:00
nitinandGitHub 863f3f29a2 Fix page layout widget tab moves (#20915)
Fixes widget moves between page layout tabs by making pageLayoutTabId
part of the flat-entity diff, so the save mutation no longer silently
drops the new tab assignment.

https://discord.com/channels/1130383047699738754/1508737039128985680
2026-05-27 15:29:12 +00:00
martmullandGitHub 9264480824 Display which remote is used when twenty-sdk runs command (#20969)
## After
<img width="669" height="186" alt="image"
src="https://github.com/user-attachments/assets/c6ce25c9-35a9-4b2f-a473-59803b8931cc"
/>
2026-05-27 15:23:17 +00:00
nitinandGitHub 5ff8c3b219 revert(navigation-drawer): unwanted desktop design changes from #20634 (#20955)
Follow-up to #20634. Removes three desktop design changes that bled in
unintentionally:

- `font-weight: regular` → restored to `medium` on nav item labels
- `MenuItemIconBoxContainer` wrap around bare icons → removed
- Section title `padding-right/top` tweak → restored to original values

Mobile-specific fixes from #20634 (slide-over drawer width, min-width
overflow fixes, breadcrumb cleanup, etc.) are preserved.
2026-05-27 15:06:45 +00:00
Thomas TrompetteandGitHub ff28547a36 tt-fix-logic-function-error-handling (#20928)
## Summary
- **Fix `callWithTimeout` timer leak**: the `setTimeout` was never
cleared when the callback resolved first, leaving orphaned timers (up to
15 minutes) in the Node.js event loop. Now uses `try/finally` with
`clearTimeout`.
- **Properly classify timeout errors**: introduced
`ExecutionTimedOutError` so the Lambda driver can throw
`LOGIC_FUNCTION_EXECUTION_TIMEOUT` instead of
`LOGIC_FUNCTION_EXECUTION_FAILED` — users now see "Function execution
timed out" instead of the generic "Function execution failed."

## Test plan
- [x] Execute a logic function normally and verify it still works
- [x] Trigger a logic function timeout and verify the error message says
"Function execution timed out" (not "Function execution failed")
- [x] Verify that a deleted logic function invocation logs a specific
"was deleted" warning
- [x] Verify no orphaned timers remain after logic function execution
completes
2026-05-27 14:32:12 +00:00
Félix MalfaitandGitHub dde6df7a26 refactor(website): replace axios with native fetch (#20967)
## Summary

Follow-up to #20966 (Stripe fetch client). axios's default Node http
adapter on workerd has the same TLS hang the Stripe SDK had — it just
doesn't surface today because all three call sites are wrapped in
\`unstable_cache(revalidate: 3600)\` and the cache is populated at build
time, so misses are rare and the failure mode is a silent \`null\` to
the layout.

This swaps the three remaining axios calls in \`twenty-website\` for
native \`fetch\` and removes axios from
\`packages/twenty-website/package.json\`. The package is still used by
other workspaces, so yarn.lock keeps the other resolution.

Touched call sites:
- \`src/lib/releases/fetch-latest-release-tag.ts\` (GitHub releases —
runs at build time, cosmetic)
- \`src/lib/community/fetch-github-star-count.ts\` (GitHub star count in
menu)
- \`src/lib/community/fetch-discord-member-count.ts\` (Discord member
count in menu)

## Test plan

- [ ] After merge + deploy: confirm GitHub star + Discord member counts
render in the site menu (non-zero, formatted)
- [ ] Confirm \`/releases\` shows the latest tag-gated visible release
notes
- [ ] No \`axios\` in worker bundle (\`grep axios .open-next/worker.js\`
should be empty)
2026-05-27 16:31:39 +02:00
DeviSriSaiCharanandGitHub 92b0e07617 fix: separate initial timeline loading from fetchMore loading state (#20896)
Fixes: #20742 

# Issue:
In the timeline activity inside the side panel, when we scroll down it
fetches more data and it displays a skeleton and after fetching finishes
the scroll position always jumps back to the top. Because of this, we
have to scroll to the bottom again to load more data.



https://github.com/user-attachments/assets/40d99df7-bdfb-4351-bc4f-baec2a035f13

## Root Cause
`useTimelineActivities` exposed a single `loading` state from
`useFindManyRecords`, which became true for both:
- Initial timeline fetch
- Pagination / fetchMore requests

`loadingTimelineActivities` becomes true whenever a network request is
triggered, including pagination requests where timeline records are
already available.

Because of this, the UI could not distinguish between the first query
loading state and subsequent fetchMore loading states.

## Fix
Added a separate firstQueryLoading state to detect only the first
timeline request.

The first query is identified by checking:

- the request is still loading
- and no timeline activities have been loaded yet

Once activities are already available, any future loading state is
treated as pagination/loading more records instead of initial loading.

This allows the UI to correctly handle:

- Skeleton loaders for first load
- Infinite scroll loaders for pagination
- Empty states after loading finishes



https://github.com/user-attachments/assets/48e8e078-82e2-43d8-823f-2f71e4f4f6e1
2026-05-27 14:20:59 +00:00
951ead5a1e fix(front): ignore IME composition Enter in input hotkeys (#20958)
## What

Pressing Enter to confirm an IME (CJK) composition no longer submits the
input. The Enter / Escape / Tab handlers now ignore key events fired
while a composition is in progress (`isComposing`, or the legacy
`keyCode === 229`).

Fixes #20954

## Why

`isComposing` was not checked anywhere in `twenty-front`, so the Enter
that confirms a Japanese / Chinese / Korean conversion was also consumed
as a submit / escape / tab hotkey — making it very hard to type CJK text
into any input that submits on Enter.

## Changes

- `useHotkeysOnFocusedElement` — central guard; covers every input wired
through `useRegisterInputEvents` (~13 components) and all hotkeys routed
through this hook.
- Direct `onKeyDown` Enter handlers: `CreateWorkspace`,
`SettingsDevelopersApiKeysNew`, `SettingsAccountsBlocklistInput`.

## Notes

- No effect on non-IME (Latin) typing — `isComposing` is only true
during an active composition. It also improves accented / dead-key input
on Latin layouts.
- `react-hotkeys-hook@4` does not handle IME composition on its own, so
the guard is explicit.

## Testing

Manually verified with a Japanese IME on Chrome (macOS) against the
v2.8.3 self-hosted image: romaji + Enter now only confirms the
conversion; a second Enter on committed text submits as expected. The
GIF in #20954 shows the original buggy behavior.

Co-authored-by: Claude Opus 4.7 <[email protected]>
2026-05-27 14:09:10 +00:00
Félix MalfaitandGitHub e79f43813b fix(website): use Stripe fetch client on Cloudflare Workers (#20966)
## Summary

Every `/api/enterprise/*` route on `twenty-website-prod` is currently
timing out at exactly 80s with `Request aborted due to timeout being
reached (80000ms)` — that's `Stripe.DEFAULT_TIMEOUT` aborting itself,
not Cloudflare.

The Stripe SDK's default transport uses Node's `http`/`https` module.
Under workerd, even with `nodejs_compat`, the outbound TLS connection to
`api.stripe.com` hangs and the SDK eventually times out at its built-in
80s ceiling. Worked on EKS (real Node), breaks on Cloudflare Workers.

Fix is one line: pass `httpClient: Stripe.createFetchHttpClient()` so
the SDK uses workerd's native `fetch` instead of the polyfilled Node
transport.

## Blast radius

`getStripeClient()` is shared across every enterprise route. All of
these are silently broken on prod right now:

- `POST /api/enterprise/checkout` (confirmed in logs)
- `POST /api/enterprise/portal`
- `POST /api/enterprise/seats`
- `GET  /api/enterprise/status`
- `POST /api/enterprise/activate`
- `POST /api/enterprise/validate`

Single change in `stripe-client.ts` unblocks all of them.

## Test plan

- [ ] After merge + deploy to prod: `wrangler tail twenty-website-prod`
while hitting the enterprise checkout flow; same call should complete in
<2s instead of 80s
- [ ] Verify a real Stripe checkout session is created (Stripe dashboard
→ Payments → Checkout sessions)
- [ ] Spot-check `/api/enterprise/status` and `/api/enterprise/portal`
are no longer timing out
2026-05-27 16:03:48 +02:00
90 changed files with 1241 additions and 245 deletions
@@ -48,12 +48,16 @@ const StyledSidePanelPlaceholderWrapper = styled.div`
export const TimelineCard = () => {
const targetRecord = useTargetRecord();
const { isInSidePanel } = useLayoutRenderingContext();
const { timelineActivities, loading, fetchMoreRecords } =
useTimelineActivities(targetRecord);
const {
timelineActivities,
firstQueryLoading,
loadingMore,
fetchMoreRecords,
} = useTimelineActivities(targetRecord);
const isTimelineActivitiesEmpty = timelineActivities.length === 0;
if (loading === true) {
if (firstQueryLoading === true) {
return <SkeletonLoader withSubSections />;
}
@@ -92,7 +96,7 @@ export const TimelineCard = () => {
events={timelineActivities ?? []}
/>
<CustomResolverFetchMoreLoader
loading={loading}
loading={loadingMore}
onLastRowVisible={fetchMoreRecords}
/>
</StyledMainContainer>
@@ -2,9 +2,9 @@ import { useLinkedObjectsTitle } from '@/activities/timeline-activities/hooks/us
import { type TimelineActivity } from '@/activities/timeline-activities/types/TimelineActivity';
import { type ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { useGenerateDepthRecordGqlFieldsFromObject } from '@/object-record/graphql/record-gql-fields/hooks/useGenerateDepthRecordGqlFieldsFromObject';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { capitalize, isDefined } from 'twenty-shared/utils';
// do we need to test this?
@@ -62,11 +62,16 @@ export const useTimelineActivities = (
useLinkedObjectsTitle(activityIds);
const loading = loadingTimelineActivities;
const firstQueryLoading =
loadingTimelineActivities && timelineActivities.length === 0;
const loadingMore =
loadingTimelineActivities && timelineActivities.length > 0;
return {
timelineActivities,
loading,
firstQueryLoading,
loadingMore,
fetchMoreRecords,
};
};
@@ -74,6 +74,9 @@ export const SettingsAccountsBlocklistInput = ({
});
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.nativeEvent.isComposing || e.keyCode === 229) {
return;
}
if (e.key === Key.Enter) {
submit();
}
@@ -24,7 +24,6 @@ import {
TooltipDelay,
TooltipPosition,
} from 'twenty-ui/display';
import { MenuItemIconBoxContainer } from 'twenty-ui/navigation';
import {
MOBILE_VIEWPORT,
ThemeContext,
@@ -162,7 +161,7 @@ const StyledLabelParent = styled.div`
`;
const StyledItemLabel = styled.span`
font-weight: ${themeCssVariables.font.weight.regular};
font-weight: ${themeCssVariables.font.weight.medium};
`;
const StyledItemSecondaryLabel = styled.span`
@@ -350,20 +349,18 @@ export const NavigationDrawerItem = ({
</StyledIcon>
) : (
<StyledIcon>
<MenuItemIconBoxContainer>
<Icon
style={{
minWidth: theme.icon.size.md,
}}
size={theme.icon.size.md}
stroke={theme.icon.stroke.md}
color={
showBreadcrumb && !isExpanded
? theme.font.color.light
: 'currentColor'
}
/>
</MenuItemIconBoxContainer>
<Icon
style={{
minWidth: theme.icon.size.md,
}}
size={theme.icon.size.md}
stroke={theme.icon.stroke.md}
color={
showBreadcrumb && !isExpanded
? theme.font.color.light
: 'currentColor'
}
/>
</StyledIcon>
))}
@@ -17,8 +17,8 @@ const StyledTitle = styled.div`
justify-content: space-between;
padding-bottom: ${themeCssVariables.spacing[1]};
padding-left: ${themeCssVariables.spacing[1]};
padding-right: ${themeCssVariables.spacing[1]};
padding-top: ${themeCssVariables.spacing[2]};
padding-right: ${themeCssVariables.spacing['0.5']};
padding-top: ${themeCssVariables.spacing[1]};
&:hover {
background-color: ${themeCssVariables.background.transparent.light};
@@ -48,6 +48,10 @@ export const useHotkeysOnFocusedElement = ({
return useHotkeys(
keys,
(keyboardEvent, hotkeysEvent) => {
if (keyboardEvent.isComposing || keyboardEvent.keyCode === 229) {
return;
}
callScopedHotkeyCallback({
keyboardEvent,
hotkeysEvent,
@@ -15,7 +15,7 @@ import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components
import { t } from '@lingui/core/macro';
import { useEffect, useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { canObjectBeManagedByWorkflow } from 'twenty-shared/workflow';
import { canObjectBeManagedByAutomation } from 'twenty-shared/workflow';
import { HorizontalSeparator } from 'twenty-ui/display';
import { type SelectOption } from 'twenty-ui/input';
import { type JsonValue } from 'type-fest';
@@ -74,9 +74,8 @@ export const WorkflowEditActionCreateRecord = ({
const availableMetadata: Array<SelectOption<string>> =
activeNonSystemObjectMetadataItems
.filter((objectMetadataItem) =>
canObjectBeManagedByWorkflow({
canObjectBeManagedByAutomation({
nameSingular: objectMetadataItem.nameSingular,
isSystem: objectMetadataItem.isSystem,
}),
)
.map((item) => ({
@@ -11,7 +11,7 @@ import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowS
import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter';
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
import { isDefined } from 'twenty-shared/utils';
import { canObjectBeManagedByWorkflow } from 'twenty-shared/workflow';
import { canObjectBeManagedByAutomation } from 'twenty-shared/workflow';
import { HorizontalSeparator } from 'twenty-ui/display';
import { type SelectOption } from 'twenty-ui/input';
import { type JsonValue } from 'type-fest';
@@ -46,9 +46,8 @@ export const WorkflowEditActionDeleteRecord = ({
const availableMetadata: Array<SelectOption<string>> =
activeNonSystemObjectMetadataItems
.filter((objectMetadataItem) =>
canObjectBeManagedByWorkflow({
canObjectBeManagedByAutomation({
nameSingular: objectMetadataItem.nameSingular,
isSystem: objectMetadataItem.isSystem,
}),
)
.map((item) => ({
@@ -16,7 +16,7 @@ import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components
import { t } from '@lingui/core/macro';
import { useEffect, useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { canObjectBeManagedByWorkflow } from 'twenty-shared/workflow';
import { canObjectBeManagedByAutomation } from 'twenty-shared/workflow';
import { HorizontalSeparator } from 'twenty-ui/display';
import { type SelectOption } from 'twenty-ui/input';
import { type JsonValue } from 'type-fest';
@@ -47,9 +47,8 @@ export const WorkflowEditActionUpdateRecord = ({
const availableMetadata: Array<SelectOption<string>> =
activeNonSystemObjectMetadataItems
.filter((objectMetadataItem) =>
canObjectBeManagedByWorkflow({
canObjectBeManagedByAutomation({
nameSingular: objectMetadataItem.nameSingular,
isSystem: objectMetadataItem.isSystem,
}),
)
.map((item) => ({
@@ -17,7 +17,7 @@ import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components
import { t } from '@lingui/core/macro';
import { useEffect, useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { canObjectBeManagedByWorkflow } from 'twenty-shared/workflow';
import { canObjectBeManagedByAutomation } from 'twenty-shared/workflow';
import { HorizontalSeparator } from 'twenty-ui/display';
import { type SelectOption } from 'twenty-ui/input';
import { type JsonValue } from 'type-fest';
@@ -84,9 +84,8 @@ export const WorkflowEditActionUpsertRecord = ({
const availableMetadata: Array<SelectOption<string>> =
activeNonSystemObjectMetadataItems
.filter((objectMetadataItem) =>
canObjectBeManagedByWorkflow({
canObjectBeManagedByAutomation({
nameSingular: objectMetadataItem.nameSingular,
isSystem: objectMetadataItem.isSystem,
}),
)
.map((item) => ({
@@ -141,6 +141,9 @@ export const CreateWorkspace = () => {
);
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.nativeEvent.isComposing || event.keyCode === 229) {
return;
}
if (event.key === Key.Enter) {
event.preventDefault();
handleSubmit(onSubmit)();
@@ -159,6 +159,9 @@ export const SettingsDevelopersApiKeysNew = () => {
placeholder={t`E.g. backoffice integration`}
value={formValues.name}
onKeyDown={(e) => {
if (e.nativeEvent.isComposing || e.keyCode === 229) {
return;
}
if (e.key === Key.Enter) {
handleSave();
}
@@ -52,7 +52,9 @@ export class DeployCommand {
await checkSdkVersionCompatibility(appPath);
console.log(chalk.blue('Deploying application...'));
const remoteName = options.remote ?? ConfigService.getActiveRemote();
console.log(chalk.blue(`Deploying application on ${remoteName}...`));
console.log(chalk.gray(`App path: ${appPath}\n`));
const onProgress = (message: string) => console.log(chalk.gray(message));
@@ -85,7 +87,6 @@ export class DeployCommand {
const appName = packageJson?.name ?? result.data.name;
const appVersion = packageJson?.version ?? 'unknown';
const remoteName = ConfigService.getActiveRemote();
console.log(
chalk.green(`\n✓ Published ${appName} v${appVersion} to ${remoteName}\n`),
@@ -1,4 +1,5 @@
import { appInstall } from '@/cli/operations/install';
import { ConfigService } from '@/cli/utilities/config/config-service';
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
import { checkSdkVersionCompatibility } from '@/cli/utilities/version/check-sdk-version-compatibility';
import { checkServerVersionCompatibility } from '@/cli/utilities/version/check-server-version-compatibility';
@@ -16,7 +17,9 @@ export class AppInstallCommand {
await checkSdkVersionCompatibility(appPath);
await checkServerVersionCompatibility();
console.log(chalk.blue('Installing application...'));
const remoteName = options.remote ?? ConfigService.getActiveRemote();
console.log(chalk.blue(`Installing application on ${remoteName}...`));
console.log(chalk.gray(`App path: ${appPath}\n`));
const result = await appInstall({
@@ -1,5 +1,6 @@
import { appUninstall } from '@/cli/operations/uninstall';
import { type ApiResponse } from '@/cli/utilities/api/api-response-type';
import { ConfigService } from '@/cli/utilities/config/config-service';
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
import chalk from 'chalk';
import inquirer from 'inquirer';
@@ -12,7 +13,9 @@ export class AppUninstallCommand {
appPath?: string;
askForConfirmation: boolean;
}): Promise<ApiResponse<any>> {
console.log(chalk.blue('🚀 Uninstall Twenty Application'));
const remoteName = ConfigService.getActiveRemote();
console.log(chalk.blue(`🚀 Uninstall Twenty Application on ${remoteName}`));
console.log(chalk.gray(`📁 App Path: ${appPath}\n`));
if (askForConfirmation && !(await this.confirmationPrompt())) {
@@ -12,7 +12,9 @@ export class CatalogSyncCommand {
ConfigService.setActiveRemote(options.remote);
}
console.log(chalk.blue('Syncing marketplace catalog...'));
const remoteName = ConfigService.getActiveRemote();
console.log(chalk.blue(`Syncing marketplace catalog on ${remoteName}...`));
const apiService = new ApiService();
@@ -1,4 +1,5 @@
import { appDevOnce } from '@/cli/operations/dev-once';
import { ConfigService } from '@/cli/utilities/config/config-service';
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
import { checkSdkVersionCompatibility } from '@/cli/utilities/version/check-sdk-version-compatibility';
import chalk from 'chalk';
@@ -14,7 +15,9 @@ export class AppDevOnceCommand {
await checkSdkVersionCompatibility(appPath);
console.log(chalk.blue('Syncing application...'));
const remoteName = ConfigService.getActiveRemote();
console.log(chalk.blue(`Syncing application on ${remoteName}...`));
console.log(chalk.gray(`App path: ${appPath}\n`));
const result = await appDevOnce({
@@ -1,5 +1,6 @@
import { functionExecute } from '@/cli/operations/execute';
import { APP_ERROR_CODES, FUNCTION_ERROR_CODES } from '@/cli/types';
import { ConfigService } from '@/cli/utilities/config/config-service';
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
import chalk from 'chalk';
import { isDefined } from 'twenty-shared/utils';
@@ -36,7 +37,11 @@ export class LogicFunctionExecuteCommand {
? 'pre install'
: (functionUniversalIdentifier ?? functionName);
console.log(chalk.blue(`🚀 Executing function "${identifier}"...`));
const remoteName = ConfigService.getActiveRemote();
console.log(
chalk.blue(`🚀 Executing function "${identifier}" on ${remoteName}...`),
);
console.log(chalk.gray(` Payload: ${JSON.stringify(parsedPayload)}\n`));
const executeOptions = postInstall
@@ -1,4 +1,5 @@
import { ApiService } from '@/cli/utilities/api/api-service';
import { ConfigService } from '@/cli/utilities/config/config-service';
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
import chalk from 'chalk';
import { readManifestFromFile } from '@/cli/utilities/build/manifest/manifest-reader';
@@ -58,8 +59,12 @@ export class LogicFunctionLogsCommand {
? `function "${functionUniversalIdentifier || functionName}"`
: 'functions';
const remoteName = ConfigService.getActiveRemote();
console.log(
chalk.blue(`🚀 Watching ${appPath} ${functionIdentifier} logs:\n`),
chalk.blue(
`🚀 Watching ${appPath} ${functionIdentifier} logs on ${remoteName}:\n`,
),
);
}
}
@@ -91,14 +91,20 @@ export class DevModeOrchestrator {
await ensureDir(outputDir);
await emptyDir(outputDir);
this.state.addEvent({
message: `Using remote "${ConfigService.getActiveRemote()}"`,
status: 'info',
});
if (!this.verbose) {
this.state.addEvent({
message: 'Add --verbose to see fully detailed logs',
status: 'info',
});
this.state.notify();
}
this.state.notify();
await this.startWatchersStep.start();
this.serverCheckInterval = setInterval(() => {
@@ -5208,6 +5208,8 @@ msgstr "Bladsyuitleg nie gevind nie."
#. js-lingui-id: qJ9V6q
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
@@ -5208,6 +5208,8 @@ msgstr "لم يتم العثور على تخطيط الصفحة."
#. js-lingui-id: qJ9V6q
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
@@ -5208,6 +5208,8 @@ msgstr "No s'ha trobat la disposició de pàgina."
#. js-lingui-id: qJ9V6q
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
@@ -5208,6 +5208,8 @@ msgstr "Rozložení stránky nebylo nalezeno."
#. js-lingui-id: qJ9V6q
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
@@ -5208,6 +5208,8 @@ msgstr "Sidelayoutet blev ikke fundet."
#. js-lingui-id: qJ9V6q
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
@@ -5208,6 +5208,8 @@ msgstr "Seitenlayout nicht gefunden."
#. js-lingui-id: qJ9V6q
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
@@ -5208,6 +5208,8 @@ msgstr "Δεν βρέθηκε η διάταξη σελίδας."
#. js-lingui-id: qJ9V6q
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
@@ -5203,6 +5203,8 @@ msgstr "Page layout not found."
#. js-lingui-id: qJ9V6q
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
@@ -5208,6 +5208,8 @@ msgstr "Diseño de página no encontrado."
#. js-lingui-id: qJ9V6q
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
@@ -5208,6 +5208,8 @@ msgstr "Sivuasettelua ei löytynyt."
#. js-lingui-id: qJ9V6q
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
@@ -5208,6 +5208,8 @@ msgstr "Mise en page introuvable."
#. js-lingui-id: qJ9V6q
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
@@ -5208,6 +5208,8 @@ msgstr "פריסת העמוד לא נמצאה."
#. js-lingui-id: qJ9V6q
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
@@ -5208,6 +5208,8 @@ msgstr "Az oldalelrendezés nem található."
#. js-lingui-id: qJ9V6q
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
@@ -5208,6 +5208,8 @@ msgstr "Layout della pagina non trovato."
#. js-lingui-id: qJ9V6q
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
@@ -5208,6 +5208,8 @@ msgstr "ページレイアウトが見つかりません."
#. js-lingui-id: qJ9V6q
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
@@ -5208,6 +5208,8 @@ msgstr "페이지 레이아웃을 찾을 수 없습니다."
#. js-lingui-id: qJ9V6q
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
@@ -5208,6 +5208,8 @@ msgstr "Pagina-indeling niet gevonden."
#. js-lingui-id: qJ9V6q
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
@@ -5208,6 +5208,8 @@ msgstr "Sideoppsett ikke funnet."
#. js-lingui-id: qJ9V6q
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
@@ -5208,6 +5208,8 @@ msgstr "Nie znaleziono układu strony."
#. js-lingui-id: qJ9V6q
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
@@ -5203,6 +5203,8 @@ msgstr ""
#. js-lingui-id: qJ9V6q
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
@@ -5208,6 +5208,8 @@ msgstr "Layout de página não encontrado."
#. js-lingui-id: qJ9V6q
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
@@ -5208,6 +5208,8 @@ msgstr "Layout de página não encontrado."
#. js-lingui-id: qJ9V6q
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
@@ -5208,6 +5208,8 @@ msgstr "Aspectul paginii nu a fost găsit."
#. js-lingui-id: qJ9V6q
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
@@ -5208,6 +5208,8 @@ msgstr "Макет страницы не найден."
#. js-lingui-id: qJ9V6q
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
@@ -5208,6 +5208,8 @@ msgstr "Распоред странице није пронађен."
#. js-lingui-id: qJ9V6q
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
@@ -5210,6 +5210,8 @@ msgstr "Sidlayouten hittades inte."
#. js-lingui-id: qJ9V6q
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
@@ -5208,6 +5208,8 @@ msgstr "Sayfa düzeni bulunamadı."
#. js-lingui-id: qJ9V6q
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
@@ -5208,6 +5208,8 @@ msgstr "Макет сторінки не знайдено."
#. js-lingui-id: qJ9V6q
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
@@ -5208,6 +5208,8 @@ msgstr "Không tìm thấy bố cục trang."
#. js-lingui-id: qJ9V6q
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
@@ -5208,6 +5208,8 @@ msgstr "未找到页面布局."
#. js-lingui-id: qJ9V6q
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
@@ -5208,6 +5208,8 @@ msgstr "找不到頁面版面配置."
#. js-lingui-id: qJ9V6q
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service.ts
@@ -41,7 +41,6 @@ import { ASSET_PATH } from 'src/constants/assets-path';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { type CacheLockService } from 'src/engine/core-modules/cache-lock/cache-lock.service';
import { COMMON_LAYER_DEPENDENCIES_DIRNAME } from 'src/engine/core-modules/logic-function/logic-function-drivers/constants/common-layer-dependencies-dirname';
import { callWithTimeout } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/call-with-timeout';
import { copyBuilder } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/copy-builder';
import { copyCommonLayerDependencies } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/copy-common-layer-dependencies';
import { copyExecutor } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/copy-executor';
@@ -429,17 +428,18 @@ export class LambdaDriver implements LogicFunctionDriver {
const yarnInstallFunctionName = await this.getYarnInstallFunctionName();
const result = await callWithTimeout({
callback: () =>
lambdaClient.send(
new InvokeCommand({
FunctionName: yarnInstallFunctionName,
Payload: JSON.stringify(payload),
LogType: LogType.Tail,
}),
const result = await lambdaClient.send(
new InvokeCommand({
FunctionName: yarnInstallFunctionName,
Payload: JSON.stringify(payload),
LogType: LogType.Tail,
}),
{
abortSignal: AbortSignal.timeout(
YARN_INSTALL_LAMBDA_TIMEOUT_SECONDS * 1000,
),
timeoutMs: YARN_INSTALL_LAMBDA_TIMEOUT_SECONDS * 1000,
});
},
);
if (result.FunctionError) {
const parsedResult = result.Payload
@@ -529,17 +529,16 @@ export class LambdaDriver implements LogicFunctionDriver {
builtFileName,
};
const result = await callWithTimeout({
callback: () =>
lambdaClient.send(
new InvokeCommand({
FunctionName: builderFunctionName,
Payload: JSON.stringify(payload),
LogType: LogType.Tail,
}),
),
timeoutMs: BUILDER_LAMBDA_TIMEOUT_SECONDS * 1000,
});
const result = await lambdaClient.send(
new InvokeCommand({
FunctionName: builderFunctionName,
Payload: JSON.stringify(payload),
LogType: LogType.Tail,
}),
{
abortSignal: AbortSignal.timeout(BUILDER_LAMBDA_TIMEOUT_SECONDS * 1000),
},
);
if (result.FunctionError) {
const parsedResult = result.Payload
@@ -1195,9 +1194,8 @@ export class LambdaDriver implements LogicFunctionDriver {
const lambdaClient = await this.getLambdaClient();
const invokeStart = Date.now();
const result = await callWithTimeout({
callback: () => lambdaClient.send(command),
timeoutMs,
const result = await lambdaClient.send(command, {
abortSignal: AbortSignal.timeout(timeoutMs),
});
const invokeSendMs = Date.now() - invokeStart;
@@ -1248,6 +1246,13 @@ export class LambdaDriver implements LogicFunctionDriver {
);
}
if (error instanceof Error && error.name === 'TimeoutError') {
throw new LogicFunctionException(
`Lambda invocation timed out for function '${flatLogicFunction.id}': ${error.message}`,
LogicFunctionExceptionCode.LOGIC_FUNCTION_EXECUTION_TIMEOUT,
);
}
if (error instanceof LogicFunctionException) {
throw error;
}
@@ -1,14 +0,0 @@
export const callWithTimeout = async <T>({
callback,
timeoutMs,
}: {
callback: () => Promise<T>;
timeoutMs: number;
}): Promise<T> => {
return Promise.race([
callback(),
new Promise<T>((_, reject) =>
setTimeout(() => reject(new Error('Execution timed out')), timeoutMs),
),
]);
};
@@ -1,7 +1,7 @@
import { Injectable, Logger } from '@nestjs/common';
import { FieldActorSource } from 'twenty-shared/types';
import { canObjectBeManagedByWorkflow } from 'twenty-shared/workflow';
import { canObjectBeManagedByAutomation } from 'twenty-shared/workflow';
import { CommonCreateManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-create-many-query-runner/common-create-many-query-runner.service';
import {
@@ -38,13 +38,12 @@ export class CreateManyRecordsService {
});
if (
!canObjectBeManagedByWorkflow({
!canObjectBeManagedByAutomation({
nameSingular: flatObjectMetadata.nameSingular,
isSystem: flatObjectMetadata.isSystem,
})
) {
throw new RecordCrudException(
'Failed to create: Object cannot be created by workflow',
'Failed to create: Object cannot be created by automation',
RecordCrudExceptionCode.INVALID_REQUEST,
);
}
@@ -1,7 +1,7 @@
import { Injectable, Logger } from '@nestjs/common';
import { FieldActorSource } from 'twenty-shared/types';
import { canObjectBeManagedByWorkflow } from 'twenty-shared/workflow';
import { canObjectBeManagedByAutomation } from 'twenty-shared/workflow';
import { CommonCreateOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-create-one-query-runner.service';
import {
@@ -38,13 +38,12 @@ export class CreateRecordService {
});
if (
!canObjectBeManagedByWorkflow({
!canObjectBeManagedByAutomation({
nameSingular: flatObjectMetadata.nameSingular,
isSystem: flatObjectMetadata.isSystem,
})
) {
throw new RecordCrudException(
'Failed to create: Object cannot be created by workflow',
'Failed to create: Object cannot be created by automation',
RecordCrudExceptionCode.INVALID_REQUEST,
);
}
@@ -1,7 +1,7 @@
import { Injectable, Logger } from '@nestjs/common';
import { isDefined, isValidUuid } from 'twenty-shared/utils';
import { canObjectBeManagedByWorkflow } from 'twenty-shared/workflow';
import { canObjectBeManagedByAutomation } from 'twenty-shared/workflow';
import { CommonDeleteOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-delete-one-query-runner.service';
import { CommonDestroyOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-destroy-one-query-runner.service';
@@ -42,13 +42,12 @@ export class DeleteRecordService {
});
if (
!canObjectBeManagedByWorkflow({
!canObjectBeManagedByAutomation({
nameSingular: flatObjectMetadata.nameSingular,
isSystem: flatObjectMetadata.isSystem,
})
) {
throw new RecordCrudException(
'Failed to delete: Object cannot be deleted by workflow',
'Failed to delete: Object cannot be deleted by automation',
RecordCrudExceptionCode.INVALID_REQUEST,
);
}
@@ -1,6 +1,6 @@
import { Injectable, Logger } from '@nestjs/common';
import { canObjectBeManagedByWorkflow } from 'twenty-shared/workflow';
import { canObjectBeManagedByAutomation } from 'twenty-shared/workflow';
import { CommonUpdateManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-update-many-query-runner.service';
import {
@@ -37,13 +37,12 @@ export class UpdateManyRecordsService {
});
if (
!canObjectBeManagedByWorkflow({
!canObjectBeManagedByAutomation({
nameSingular: flatObjectMetadata.nameSingular,
isSystem: flatObjectMetadata.isSystem,
})
) {
throw new RecordCrudException(
'Failed to update: Object cannot be updated by workflow',
'Failed to update: Object cannot be updated by automation',
RecordCrudExceptionCode.INVALID_REQUEST,
);
}
@@ -1,7 +1,7 @@
import { Injectable, Logger } from '@nestjs/common';
import { isDefined, isValidUuid } from 'twenty-shared/utils';
import { canObjectBeManagedByWorkflow } from 'twenty-shared/workflow';
import { canObjectBeManagedByAutomation } from 'twenty-shared/workflow';
import { CommonUpdateOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-update-one-query-runner.service';
import {
@@ -52,13 +52,12 @@ export class UpdateRecordService {
});
if (
!canObjectBeManagedByWorkflow({
!canObjectBeManagedByAutomation({
nameSingular: flatObjectMetadata.nameSingular,
isSystem: flatObjectMetadata.isSystem,
})
) {
throw new RecordCrudException(
'Failed to update: Object cannot be updated by workflow',
'Failed to update: Object cannot be updated by automation',
RecordCrudExceptionCode.INVALID_REQUEST,
);
}
@@ -1,6 +1,6 @@
import { Injectable, Logger } from '@nestjs/common';
import { canObjectBeManagedByWorkflow } from 'twenty-shared/workflow';
import { canObjectBeManagedByAutomation } from 'twenty-shared/workflow';
import { CommonCreateOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-create-one-query-runner.service';
import {
@@ -32,13 +32,12 @@ export class UpsertRecordService {
});
if (
!canObjectBeManagedByWorkflow({
!canObjectBeManagedByAutomation({
nameSingular: flatObjectMetadata.nameSingular,
isSystem: flatObjectMetadata.isSystem,
})
) {
throw new RecordCrudException(
'Failed to update: Object cannot be updated by workflow',
'Failed to upsert: Object cannot be upserted by automation',
RecordCrudExceptionCode.INVALID_REQUEST,
);
}
@@ -5,6 +5,7 @@ import {
type ObjectsPermissionsByRoleId,
} from 'twenty-shared/types';
import { camelToSnakeCase, isDefined } from 'twenty-shared/utils';
import { canObjectBeManagedByAutomation } from 'twenty-shared/workflow';
import { z } from 'zod';
import { type GenerateDescriptorOptions } from 'src/engine/core-modules/tool-provider/interfaces/generate-descriptor-options.type';
@@ -116,6 +117,9 @@ export class DatabaseToolProvider implements ToolProvider {
const restrictedFields = permission.restrictedFields;
const snakePlural = camelToSnakeCase(objectMetadata.namePlural);
const snakeSingular = camelToSnakeCase(objectMetadata.nameSingular);
const canBeManagedByAutomation = canObjectBeManagedByAutomation({
nameSingular: objectMetadata.nameSingular,
});
if (permission.canReadObjectRecords) {
descriptors.push({
@@ -182,7 +186,7 @@ export class DatabaseToolProvider implements ToolProvider {
}
}
if (permission.canUpdateObjectRecords) {
if (permission.canUpdateObjectRecords && canBeManagedByAutomation) {
descriptors.push({
name: `create_${snakeSingular}`,
description: `Create a new ${objectMetadata.labelSingular} record. Provide all required fields and any optional fields you want to set. The system will automatically handle timestamps and IDs. Returns the created record with all its data.`,
@@ -266,7 +270,7 @@ export class DatabaseToolProvider implements ToolProvider {
});
}
if (permission.canSoftDeleteObjectRecords) {
if (permission.canSoftDeleteObjectRecords && canBeManagedByAutomation) {
descriptors.push({
name: `delete_${snakeSingular}`,
description: `Delete a ${objectMetadata.labelSingular} record by marking it as deleted. The record is hidden from normal queries. This is reversible. Use this to remove records.`,
@@ -214,6 +214,7 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma
"position",
"universalConfiguration",
"deletedAt",
"pageLayoutTabUniversalIdentifier",
"conditionalDisplay",
"conditionalAvailabilityExpression",
"isActive",
@@ -930,7 +930,7 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
universalProperty: undefined,
},
pageLayoutTabId: {
toCompare: false,
toCompare: true,
toStringify: false,
universalProperty: 'pageLayoutTabUniversalIdentifier',
isOverridable: true,
@@ -0,0 +1,146 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { PageLayoutTabLayoutMode } from 'twenty-shared/types';
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
import { FlatPageLayoutWidgetTypeValidatorService } from 'src/engine/metadata-modules/flat-page-layout-widget/services/flat-page-layout-widget-type-validator.service';
import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
import { PageLayoutTabExceptionCode } from 'src/engine/metadata-modules/page-layout-tab/exceptions/page-layout-tab.exception';
import { type UniversalFlatPageLayoutTab } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-page-layout-tab.type';
import { FlatPageLayoutWidgetValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service';
const EXISTING_TAB_UNIVERSAL_IDENTIFIER =
'00000000-0000-0000-0000-000000000aa1';
const MISSING_TAB_UNIVERSAL_IDENTIFIER = '00000000-0000-0000-0000-000000000aa2';
const DESTINATION_TAB_UNIVERSAL_IDENTIFIER =
'00000000-0000-0000-0000-000000000aa3';
const WIDGET_UNIVERSAL_IDENTIFIER = '00000000-0000-0000-0000-000000000111';
const tab = (
universalIdentifier = EXISTING_TAB_UNIVERSAL_IDENTIFIER,
): UniversalFlatPageLayoutTab =>
({
universalIdentifier,
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
}) as unknown as UniversalFlatPageLayoutTab;
const widget = (
universalIdentifier = WIDGET_UNIVERSAL_IDENTIFIER,
pageLayoutTabUniversalIdentifier = EXISTING_TAB_UNIVERSAL_IDENTIFIER,
): FlatPageLayoutWidget =>
({
universalIdentifier,
pageLayoutTabUniversalIdentifier,
title: 'widget',
type: 'FRONT_COMPONENT',
gridPosition: { row: 0, column: 0, rowSpan: 12, columnSpan: 12 },
position: { layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST, index: 0 },
}) as unknown as FlatPageLayoutWidget;
const mapsFrom = (entities: { universalIdentifier: string }[]): any => {
const maps = createEmptyFlatEntityMaps() as any;
for (const entity of entities) {
maps.byUniversalIdentifier[entity.universalIdentifier] = entity;
}
return maps;
};
const buildUpdateArgs = ({
update,
tabs = [tab()],
existingWidgets = [widget()],
}: {
update: Record<string, unknown>;
tabs?: UniversalFlatPageLayoutTab[];
existingWidgets?: FlatPageLayoutWidget[];
}) =>
({
universalIdentifier: WIDGET_UNIVERSAL_IDENTIFIER,
flatEntityUpdate: update,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatPageLayoutTabMaps: mapsFrom(tabs),
flatPageLayoutWidgetMaps: mapsFrom(existingWidgets),
},
additionalCacheDataMaps: { featureFlagsMap: {} },
workspaceId: 'workspace-id',
buildOptions: {} as never,
}) as unknown as Parameters<
FlatPageLayoutWidgetValidatorService['validateFlatPageLayoutWidgetUpdate']
>[0];
describe('FlatPageLayoutWidgetValidatorService', () => {
let service: FlatPageLayoutWidgetValidatorService;
beforeEach(async () => {
const moduleRef: TestingModule = await Test.createTestingModule({
providers: [
FlatPageLayoutWidgetValidatorService,
{
provide: FlatPageLayoutWidgetTypeValidatorService,
useValue: {
validateFlatPageLayoutWidgetTypeSpecificitiesForCreation: () => [],
validateFlatPageLayoutWidgetTypeSpecificitiesForUpdate: () => [],
},
},
],
}).compile();
service = moduleRef.get(FlatPageLayoutWidgetValidatorService);
});
describe('validateFlatPageLayoutWidgetUpdate', () => {
it('rejects moving a widget to an unknown tab', async () => {
const result = await service.validateFlatPageLayoutWidgetUpdate(
buildUpdateArgs({
update: {
pageLayoutTabUniversalIdentifier: MISSING_TAB_UNIVERSAL_IDENTIFIER,
},
}),
);
expect(result.errors.map((error) => error.code)).toContain(
PageLayoutTabExceptionCode.PAGE_LAYOUT_TAB_NOT_FOUND,
);
});
it('rejects moving an overridden widget to an unknown tab', async () => {
const result = await service.validateFlatPageLayoutWidgetUpdate(
buildUpdateArgs({
update: {
universalOverrides: {
pageLayoutTabUniversalIdentifier:
MISSING_TAB_UNIVERSAL_IDENTIFIER,
},
},
}),
);
expect(result.errors.map((error) => error.code)).toContain(
PageLayoutTabExceptionCode.PAGE_LAYOUT_TAB_NOT_FOUND,
);
});
it('accepts moving an overridden widget to a known tab and exposes the override target as the effective tab', async () => {
const result = await service.validateFlatPageLayoutWidgetUpdate(
buildUpdateArgs({
update: {
universalOverrides: {
pageLayoutTabUniversalIdentifier:
DESTINATION_TAB_UNIVERSAL_IDENTIFIER,
},
},
tabs: [tab(), tab(DESTINATION_TAB_UNIVERSAL_IDENTIFIER)],
}),
);
expect(result.errors.map((error) => error.code)).not.toContain(
PageLayoutTabExceptionCode.PAGE_LAYOUT_TAB_NOT_FOUND,
);
expect(
result.flatEntityMinimalInformation.pageLayoutTabUniversalIdentifier,
).toBe(DESTINATION_TAB_UNIVERSAL_IDENTIFIER);
});
});
});
@@ -17,6 +17,7 @@ import { validatePageLayoutWidgetGridPosition } from 'src/engine/metadata-module
import { validatePageLayoutWidgetVerticalListPosition } from 'src/engine/metadata-modules/page-layout-widget/utils/validate-page-layout-widget-vertical-list-position.util';
import { validateWidgetGridPosition } from 'src/engine/metadata-modules/page-layout-widget/utils/validate-widget-grid-position.util';
import { type UniversalFlatPageLayoutTab } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-page-layout-tab.type';
import { type UniversalFlatPageLayoutWidget } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-page-layout-widget.type';
import {
FailedFlatEntityValidation,
FlatEntityValidationError,
@@ -70,19 +71,31 @@ export class FlatPageLayoutWidgetValidatorService {
...flatEntityUpdate,
};
const effectivePageLayoutTabUniversalIdentifier =
this.getEffectivePageLayoutTabUniversalIdentifier(
updatedFlatPageLayoutWidget,
);
validationResult.flatEntityMinimalInformation = {
...validationResult.flatEntityMinimalInformation,
pageLayoutTabUniversalIdentifier:
updatedFlatPageLayoutWidget.pageLayoutTabUniversalIdentifier,
effectivePageLayoutTabUniversalIdentifier,
};
const referencedPageLayoutTab = findFlatEntityByUniversalIdentifier({
universalIdentifier:
updatedFlatPageLayoutWidget.pageLayoutTabUniversalIdentifier,
universalIdentifier: effectivePageLayoutTabUniversalIdentifier,
flatEntityMaps:
optimisticFlatEntityMapsAndRelatedFlatEntityMaps.flatPageLayoutTabMaps,
});
if (!isDefined(referencedPageLayoutTab)) {
validationResult.errors.push({
code: PageLayoutTabExceptionCode.PAGE_LAYOUT_TAB_NOT_FOUND,
message: t`Page layout tab not found`,
userFriendlyMessage: msg`Page layout tab not found`,
});
}
const gridPositionErrors = this.validateGridPosition({
gridPosition: updatedFlatPageLayoutWidget.gridPosition,
widgetTitle: updatedFlatPageLayoutWidget.title,
@@ -235,6 +248,18 @@ export class FlatPageLayoutWidgetValidatorService {
return validationResult;
}
private getEffectivePageLayoutTabUniversalIdentifier(
widget: Pick<
UniversalFlatPageLayoutWidget,
'pageLayoutTabUniversalIdentifier' | 'universalOverrides'
>,
): string {
return (
widget.universalOverrides?.pageLayoutTabUniversalIdentifier ??
widget.pageLayoutTabUniversalIdentifier
);
}
private validateGridPosition({
gridPosition,
widgetTitle,
@@ -0,0 +1,4 @@
export type ParsedName = {
firstName: string;
lastName: string;
};
@@ -0,0 +1,155 @@
import { type EachTestingContext } from 'twenty-shared/testing';
import { getFirstNameAndLastNameFromHandleAndDisplayName } from 'src/modules/contact-creation-manager/utils/get-first-name-and-last-name-from-handle-and-display-name.util';
type TestCase = EachTestingContext<{
handle: string;
displayName: string;
expected: { firstName: string; lastName: string };
}>;
describe('getFirstNameAndLastNameFromHandleAndDisplayName', () => {
const testCases: TestCase[] = [
{
title: 'should parse a standard "First Last" display name',
context: {
handle: '[email protected]',
displayName: 'John Doe',
expected: { firstName: 'John', lastName: 'Doe' },
},
},
{
title: 'should swap "Last, First" comma format',
context: {
handle: '[email protected]',
displayName: 'Doe, John',
expected: { firstName: 'John', lastName: 'Doe' },
},
},
{
title: 'should de-duplicate "First.Last Last" synthesized form',
context: {
handle: '[email protected]',
displayName: 'John.Doe Doe',
expected: { firstName: 'John', lastName: 'Doe' },
},
},
{
title: 'should keep all trailing tokens as a multi-word last name',
context: {
handle: '[email protected]',
displayName: 'Mary Jane Watson',
expected: { firstName: 'Mary', lastName: 'Jane Watson' },
},
},
{
title: 'should split a single dotted token into first and last',
context: {
handle: '[email protected]',
displayName: 'john.doe',
expected: { firstName: 'John', lastName: 'Doe' },
},
},
{
title:
'should accept a single-word display name and fall back to handle for last',
context: {
handle: '[email protected]',
displayName: 'First',
expected: { firstName: 'First', lastName: 'Someone' },
},
},
{
title: 'should fall back to handle when display name is empty',
context: {
handle: '[email protected]',
displayName: '',
expected: { firstName: 'John', lastName: 'Doe' },
},
},
{
title: 'should fall back to handle even when handle has no dot',
context: {
handle: '[email protected]',
displayName: '',
expected: { firstName: 'Noname', lastName: '' },
},
},
{
title: 'should strip wrapping quotes before parsing',
context: {
handle: '[email protected]',
displayName: '"Doe, John"',
expected: { firstName: 'John', lastName: 'Doe' },
},
},
{
title: 'should collapse extra whitespace',
context: {
handle: '[email protected]',
displayName: ' John Doe ',
expected: { firstName: 'John', lastName: 'Doe' },
},
},
{
title: 'should expand "John.Doe Smith" into "John" + "Doe Smith"',
context: {
handle: '[email protected]',
displayName: 'John.Doe Smith',
expected: { firstName: 'John', lastName: 'Doe Smith' },
},
},
{
title: 'should preserve multi-dot handle local-parts on fallback',
context: {
handle: '[email protected]',
displayName: '',
expected: { firstName: 'Jean', lastName: 'Luc picard' },
},
},
{
title:
'should fall back to handle when display name contains the address',
context: {
handle: '[email protected]',
displayName: '[email protected] Smith',
expected: { firstName: 'Jane', lastName: 'Smith' },
},
},
{
title:
'should fall back to handle for single-token email-as-display-name',
context: {
handle: '[email protected]',
displayName: '[email protected]',
expected: { firstName: 'Janesmith', lastName: '' },
},
},
{
title:
'should strip ":XXX" group-code suffix from comma-format first name',
context: {
handle: '[email protected]',
displayName: 'Smith, Jane:GROUP',
expected: { firstName: 'Jane', lastName: 'Smith' },
},
},
{
title: 'should strip ":XXX" suffix from a multi-token display name',
context: {
handle: '[email protected]',
displayName: 'Jane:GROUP Smith',
expected: { firstName: 'Jane', lastName: 'Smith' },
},
},
];
test.each(testCases)(
'$title',
({ context: { handle, displayName, expected } }) => {
expect(
getFirstNameAndLastNameFromHandleAndDisplayName(handle, displayName),
).toEqual(expected);
},
);
});
@@ -0,0 +1,162 @@
import { type EachTestingContext } from 'twenty-shared/testing';
import { getParsedNameFromDisplayName } from 'src/modules/contact-creation-manager/utils/get-parsed-name-from-display-name.util';
type TestCase = EachTestingContext<{
displayName: string;
expected: { firstName: string; lastName: string };
}>;
describe('getParsedNameFromDisplayName', () => {
describe('common shapes', () => {
const testCases: TestCase[] = [
{
title: 'should split a standard "First Last" display name',
context: {
displayName: 'John Doe',
expected: { firstName: 'John', lastName: 'Doe' },
},
},
{
title:
'should keep all trailing tokens together as a multi-word last name',
context: {
displayName: 'Mary Jane Watson',
expected: { firstName: 'Mary', lastName: 'Jane Watson' },
},
},
{
title: 'should treat a single-word display name as the first name only',
context: {
displayName: 'Cher',
expected: { firstName: 'Cher', lastName: '' },
},
},
{
title: 'should collapse surrounding and inner whitespace',
context: {
displayName: ' John Doe ',
expected: { firstName: 'John', lastName: 'Doe' },
},
},
];
test.each(testCases)('$title', ({ context: { displayName, expected } }) => {
expect(getParsedNameFromDisplayName(displayName)).toEqual(expected);
});
});
describe('comma-inverted form "Last, First"', () => {
const testCases: TestCase[] = [
{
title: 'should swap the two halves into first and last name',
context: {
displayName: 'Doe, John',
expected: { firstName: 'John', lastName: 'Doe' },
},
},
{
title:
'should strip surrounding RFC 5322 quoted-string quotes before swapping',
context: {
displayName: '"Doe, John"',
expected: { firstName: 'John', lastName: 'Doe' },
},
},
];
test.each(testCases)('$title', ({ context: { displayName, expected } }) => {
expect(getParsedNameFromDisplayName(displayName)).toEqual(expected);
});
});
describe('email-derived display names', () => {
const testCases: TestCase[] = [
{
title: 'should split a single dotted token like an email local part',
context: {
displayName: 'john.doe',
expected: { firstName: 'john', lastName: 'doe' },
},
},
{
title:
'should de-duplicate "First.Last Last" forms synthesized by some clients',
context: {
displayName: 'John.Doe Doe',
expected: { firstName: 'John', lastName: 'Doe' },
},
},
{
title:
'should expand "First.Middle Last" forms into a multi-word last name',
context: {
displayName: 'John.Doe Smith',
expected: { firstName: 'John', lastName: 'Doe Smith' },
},
},
];
test.each(testCases)('$title', ({ context: { displayName, expected } }) => {
expect(getParsedNameFromDisplayName(displayName)).toEqual(expected);
});
});
describe('mail-server ":GROUP" tag suffix', () => {
const testCases: TestCase[] = [
{
title:
'should strip the tag from the first name in the comma-inverted form',
context: {
displayName: 'Smith, Jane:GROUP',
expected: { firstName: 'Jane', lastName: 'Smith' },
},
},
{
title:
'should strip the tag from the first token in a space-separated form',
context: {
displayName: 'Jane:GROUP Smith',
expected: { firstName: 'Jane', lastName: 'Smith' },
},
},
];
test.each(testCases)('$title', ({ context: { displayName, expected } }) => {
expect(getParsedNameFromDisplayName(displayName)).toEqual(expected);
});
});
describe('fallback cases (empty result)', () => {
const testCases: TestCase[] = [
{
title:
'should return empty names so the caller can fall back to handle parsing when the display name is empty',
context: {
displayName: '',
expected: { firstName: '', lastName: '' },
},
},
{
title:
'should return empty names when the display name is only whitespace',
context: {
displayName: ' ',
expected: { firstName: '', lastName: '' },
},
},
{
title:
'should return empty names when forwarders inject an email address into the display-name slot',
context: {
displayName: '[email protected]',
expected: { firstName: '', lastName: '' },
},
},
];
test.each(testCases)('$title', ({ context: { displayName, expected } }) => {
expect(getParsedNameFromDisplayName(displayName)).toEqual(expected);
});
});
});
@@ -0,0 +1,80 @@
import { type EachTestingContext } from 'twenty-shared/testing';
import { getParsedNameFromEmailLocalPart } from 'src/modules/contact-creation-manager/utils/get-parsed-name-from-email-local-part.util';
type TestCase = EachTestingContext<{
localPart: string;
expected: { firstName: string; lastName: string };
}>;
describe('getParsedNameFromEmailLocalPart', () => {
const testCases: TestCase[] = [
{
title:
'should split a dotted local part into first and last name segments',
context: {
localPart: 'john.doe',
expected: { firstName: 'john', lastName: 'doe' },
},
},
{
title:
'should keep the trailing dot-segments together as a multi-word last name',
context: {
localPart: 'jean.luc.picard',
expected: { firstName: 'jean', lastName: 'luc picard' },
},
},
{
title:
'should fall back to the whole local part as first name when no dot is present',
context: {
localPart: 'noname',
expected: { firstName: 'noname', lastName: '' },
},
},
{
title:
'should ignore consecutive dots so they do not produce empty segments',
context: {
localPart: 'john..doe',
expected: { firstName: 'john', lastName: 'doe' },
},
},
{
title:
'should ignore a leading dot rather than producing an empty first name',
context: {
localPart: '.john.doe',
expected: { firstName: 'john', lastName: 'doe' },
},
},
{
title: 'should return empty names for an empty local part',
context: {
localPart: '',
expected: { firstName: '', lastName: '' },
},
},
{
title:
'should strip an RFC 5233 plus-address tag so routing metadata does not end up in the name',
context: {
localPart: 'john.doe+sales',
expected: { firstName: 'john', lastName: 'doe' },
},
},
{
title:
'should strip a plus-address tag even when the local part has no dot',
context: {
localPart: 'noname+ticket-123',
expected: { firstName: 'noname', lastName: '' },
},
},
];
test.each(testCases)('$title', ({ context: { localPart, expected } }) => {
expect(getParsedNameFromEmailLocalPart(localPart)).toEqual(expected);
});
});
@@ -0,0 +1,55 @@
import { type EachTestingContext } from 'twenty-shared/testing';
import { getParsedNameFromHandle } from 'src/modules/contact-creation-manager/utils/get-parsed-name-from-handle.util';
type TestCase = EachTestingContext<{
handle: string;
expected: { firstName: string; lastName: string };
}>;
describe('getParsedNameFromHandle', () => {
const testCases: TestCase[] = [
{
title:
'should derive first and last name from the local part of a standard email',
context: {
handle: '[email protected]',
expected: { firstName: 'john', lastName: 'doe' },
},
},
{
title: 'should ignore the domain entirely',
context: {
handle: '[email protected]',
expected: { firstName: 'john', lastName: 'doe' },
},
},
{
title:
'should return only a first name when the local part has no dot separator',
context: {
handle: '[email protected]',
expected: { firstName: 'noname', lastName: '' },
},
},
{
title:
'should treat an input without an @ as a bare local part rather than rejecting it',
context: {
handle: 'lonely.handle',
expected: { firstName: 'lonely', lastName: 'handle' },
},
},
{
title: 'should return empty names for an empty handle',
context: {
handle: '',
expected: { firstName: '', lastName: '' },
},
},
];
test.each(testCases)('$title', ({ context: { handle, expected } }) => {
expect(getParsedNameFromHandle(handle)).toEqual(expected);
});
});
@@ -1,17 +1,18 @@
import { capitalize } from 'twenty-shared/utils';
export function getFirstNameAndLastNameFromHandleAndDisplayName(
import { type ParsedName } from 'src/modules/contact-creation-manager/types/parsed-name.type';
import { getParsedNameFromDisplayName } from 'src/modules/contact-creation-manager/utils/get-parsed-name-from-display-name.util';
import { getParsedNameFromHandle } from 'src/modules/contact-creation-manager/utils/get-parsed-name-from-handle.util';
export const getFirstNameAndLastNameFromHandleAndDisplayName = (
handle: string,
displayName: string,
): { firstName: string; lastName: string } {
const firstName = displayName.split(' ')[0];
const lastName = displayName.split(' ')[1];
const contactFullNameFromHandle = handle.split('@')[0];
const firstNameFromHandle = contactFullNameFromHandle.split('.')[0];
const lastNameFromHandle = contactFullNameFromHandle.split('.')[1];
): ParsedName => {
const fromDisplayName = getParsedNameFromDisplayName(displayName);
const fromHandle = getParsedNameFromHandle(handle);
return {
firstName: capitalize(firstName || firstNameFromHandle || ''),
lastName: capitalize(lastName || lastNameFromHandle || ''),
firstName: capitalize(fromDisplayName.firstName || fromHandle.firstName),
lastName: capitalize(fromDisplayName.lastName || fromHandle.lastName),
};
}
};
@@ -0,0 +1,58 @@
import { isNonEmptyString } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
import { type ParsedName } from 'src/modules/contact-creation-manager/types/parsed-name.type';
import { getParsedNameFromEmailLocalPart } from 'src/modules/contact-creation-manager/utils/get-parsed-name-from-email-local-part.util';
const EMPTY_NAME: ParsedName = { firstName: '', lastName: '' };
export const getParsedNameFromDisplayName = (
displayName: string,
): ParsedName => {
const cleaned = displayName
.trim()
.replace(/^['"]+|['"]+$/g, '')
.trim();
if (!isNonEmptyString(cleaned) || cleaned.includes('@')) return EMPTY_NAME;
const stripTrailingGroupTag = (input: string): string =>
input.replace(/:[^:]+$/, '').trim();
const withGroupTagsStripped = (parsed: ParsedName): ParsedName => ({
firstName: stripTrailingGroupTag(parsed.firstName),
lastName: stripTrailingGroupTag(parsed.lastName),
});
const commaMatch = cleaned.match(/^([^,]+),\s*([^,]+)$/);
if (isDefined(commaMatch)) {
return withGroupTagsStripped({
firstName: commaMatch[2].trim(),
lastName: commaMatch[1].trim(),
});
}
const [firstToken, ...rest] = cleaned.split(/\s+/);
const restAsLastName = rest.join(' ');
const { firstName: head, lastName: dotTail } =
getParsedNameFromEmailLocalPart(firstToken);
if (!isNonEmptyString(dotTail)) {
return withGroupTagsStripped({
firstName: head,
lastName: restAsLastName,
});
}
const dotTailAlreadyInLastName = restAsLastName
.toLowerCase()
.startsWith(dotTail.toLowerCase());
return withGroupTagsStripped({
firstName: head,
lastName: dotTailAlreadyInLastName
? restAsLastName
: `${dotTail} ${restAsLastName}`.trim(),
});
};
@@ -0,0 +1,15 @@
import { isNonEmptyString } from '@sniptt/guards';
import { type ParsedName } from 'src/modules/contact-creation-manager/types/parsed-name.type';
export const getParsedNameFromEmailLocalPart = (
localPart: string,
): ParsedName => {
const [withoutPlusAddressTag = ''] = localPart.split('+');
const parts = withoutPlusAddressTag.split('.').filter(isNonEmptyString);
return {
firstName: parts[0] ?? '',
lastName: parts.slice(1).join(' '),
};
};
@@ -0,0 +1,8 @@
import { type ParsedName } from 'src/modules/contact-creation-manager/types/parsed-name.type';
import { getParsedNameFromEmailLocalPart } from 'src/modules/contact-creation-manager/utils/get-parsed-name-from-email-local-part.util';
export const getParsedNameFromHandle = (handle: string): ParsedName => {
const [localPart = ''] = handle.split('@');
return getParsedNameFromEmailLocalPart(localPart);
};
@@ -67,6 +67,25 @@ describe('parseAndFormatGmailMessage', () => {
]);
});
it('should preserve the display name on the FROM participant', () => {
const result = parseAndFormatGmailMessage(
buildMessage([
{ name: 'From', value: '"Doe, John" <[email protected]>' },
{ name: 'To', value: '[email protected]' },
{ name: 'Message-ID', value: '<[email protected]>' },
]),
connectedAccount,
);
const fromParticipant = result?.participants.find((p) => p.role === 'FROM');
expect(fromParticipant).toEqual({
role: 'FROM',
handle: '[email protected]',
displayName: 'Doe, John',
});
});
it('should mark messages from the connected account as OUTGOING', () => {
const result = parseAndFormatGmailMessage(
buildMessage([
@@ -42,10 +42,7 @@ export const parseAndFormatGmailMessage = (
: [];
const participants = [
...formatAddressObjectAsParticipants(
[{ address: from }],
MessageParticipantRole.FROM,
),
...formatAddressObjectAsParticipants([from], MessageParticipantRole.FROM),
...formatAddressObjectAsParticipants(
toParticipants,
MessageParticipantRole.TO,
@@ -72,7 +69,7 @@ export const parseAndFormatGmailMessage = (
subject: subject || '',
messageThreadExternalId: threadId,
receivedAt: new Date(parseInt(internalDate)),
direction: computeMessageDirection(from || '', connectedAccount),
direction: computeMessageDirection(from.address || '', connectedAccount),
participants,
text: sanitizeString(textWithoutReplyQuotations),
attachments,
@@ -45,7 +45,7 @@ export const parseGmailMessage = (message: gmail_v1.Schema$Message) => {
historyId,
internalDate,
subject,
from: rawFrom ? safeParseEmailAddressAddress(rawFrom) : undefined,
from: rawFrom ? safeParseEmailAddresses(rawFrom)[0] : undefined,
deliveredTo: rawDeliveredTo
? safeParseEmailAddressAddress(rawDeliveredTo)
: undefined,
@@ -1,7 +1,10 @@
import { createOnePageLayoutTab } from 'test/integration/metadata/suites/page-layout-tab/utils/create-one-page-layout-tab.util';
import { destroyOnePageLayoutTab } from 'test/integration/metadata/suites/page-layout-tab/utils/destroy-one-page-layout-tab.util';
import { findPageLayoutTabs } from 'test/integration/metadata/suites/page-layout-tab/utils/find-page-layout-tabs.util';
import { findPageLayoutWidgets } from 'test/integration/metadata/suites/page-layout-widget/utils/find-page-layout-widgets.util';
import { updateOnePageLayoutWidget } from 'test/integration/metadata/suites/page-layout-widget/utils/update-one-page-layout-widget.util';
import { findPageLayouts } from 'test/integration/metadata/suites/page-layout/utils/find-page-layouts.util';
import { PageLayoutTabLayoutMode } from 'twenty-shared/types';
import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum';
@@ -17,7 +20,10 @@ const WIDGET_OVERRIDE_GQL_FIELDS = `
`;
describe('Page layout widget override behavior', () => {
let seededPageLayoutId: string;
let seededWidgetId: string;
let seededWidgetOriginalPageLayoutTabId: string;
let seededWidgetOriginalTabLayoutMode: PageLayoutTabLayoutMode;
let seededWidgetOriginalTitle: string;
beforeAll(async () => {
@@ -32,15 +38,27 @@ describe('Page layout widget override behavior', () => {
expect(recordPageLayout).toBeDefined();
seededPageLayoutId = recordPageLayout!.id;
const { data: tabsData } = await findPageLayoutTabs({
expectToFail: false,
input: { pageLayoutId: recordPageLayout!.id },
input: { pageLayoutId: seededPageLayoutId },
gqlFields: `
id
title
position
layoutMode
pageLayoutId
`,
});
expect(tabsData.getPageLayoutTabs.length).toBeGreaterThanOrEqual(1);
const firstTabId = tabsData.getPageLayoutTabs[0].id;
seededWidgetOriginalTabLayoutMode = tabsData.getPageLayoutTabs[0]
.layoutMode as PageLayoutTabLayoutMode;
const { data: widgetsData } = await findPageLayoutWidgets({
expectToFail: false,
input: { pageLayoutTabId: firstTabId },
@@ -52,6 +70,7 @@ describe('Page layout widget override behavior', () => {
const firstWidget = widgetsData.getPageLayoutWidgets[0];
seededWidgetId = firstWidget.id;
seededWidgetOriginalPageLayoutTabId = firstWidget.pageLayoutTabId;
seededWidgetOriginalTitle = firstWidget.title;
});
@@ -60,6 +79,7 @@ describe('Page layout widget override behavior', () => {
expectToFail: false,
input: {
id: seededWidgetId,
pageLayoutTabId: seededWidgetOriginalPageLayoutTabId,
title: seededWidgetOriginalTitle,
},
gqlFields: WIDGET_OVERRIDE_GQL_FIELDS,
@@ -81,6 +101,74 @@ describe('Page layout widget override behavior', () => {
expect(data.updatePageLayoutWidget.title).toBe(overriddenTitle);
});
it('should move a seeded widget to another tab through an override', async () => {
let destinationTabId: string | undefined;
try {
const { data: tabData } = await createOnePageLayoutTab({
expectToFail: false,
input: {
title: `Widget Move Override ${Date.now()}`,
pageLayoutId: seededPageLayoutId,
layoutMode: seededWidgetOriginalTabLayoutMode,
},
});
destinationTabId = tabData.createPageLayoutTab.id;
const { data } = await updateOnePageLayoutWidget({
expectToFail: false,
input: {
id: seededWidgetId,
pageLayoutTabId: destinationTabId,
},
gqlFields: WIDGET_OVERRIDE_GQL_FIELDS,
});
expect(data.updatePageLayoutWidget.pageLayoutTabId).toBe(
destinationTabId,
);
const { data: originalTabWidgetsData } = await findPageLayoutWidgets({
expectToFail: false,
input: { pageLayoutTabId: seededWidgetOriginalPageLayoutTabId },
gqlFields: WIDGET_OVERRIDE_GQL_FIELDS,
});
expect(
originalTabWidgetsData.getPageLayoutWidgets.map((widget) => widget.id),
).not.toContain(seededWidgetId);
const { data: destinationTabWidgetsData } = await findPageLayoutWidgets({
expectToFail: false,
input: { pageLayoutTabId: destinationTabId },
gqlFields: WIDGET_OVERRIDE_GQL_FIELDS,
});
expect(
destinationTabWidgetsData.getPageLayoutWidgets.map(
(widget) => widget.id,
),
).toContain(seededWidgetId);
} finally {
await updateOnePageLayoutWidget({
expectToFail: false,
input: {
id: seededWidgetId,
pageLayoutTabId: seededWidgetOriginalPageLayoutTabId,
},
gqlFields: WIDGET_OVERRIDE_GQL_FIELDS,
});
if (destinationTabId !== undefined) {
await destroyOnePageLayoutTab({
expectToFail: false,
input: { id: destinationTabId },
});
}
}
});
it('should return the overridden title when querying the widget', async () => {
const { data: tabsData } = await findPageLayoutTabs({
expectToFail: false,
@@ -42,12 +42,16 @@ export const updateOnePageLayoutWidgetQueryFactory = ({
variables: {
id: input.id,
input: {
pageLayoutTabId: input.pageLayoutTabId,
title: input.title,
type: input.type,
objectMetadataId: input.objectMetadataId,
gridPosition: input.gridPosition,
position: input.position,
configuration: input.configuration,
conditionalDisplay: input.conditionalDisplay,
conditionalAvailabilityExpression:
input.conditionalAvailabilityExpression,
},
},
});
@@ -229,4 +229,98 @@ describe('Page layout with tabs update should succeed', () => {
);
},
);
it('should move a widget to another tab when saving layout tabs and widgets', async () => {
const widgetId = v4();
await updateOnePageLayoutWithTabsAndWidgets({
expectToFail: false,
input: {
id: testPageLayoutId,
name: 'Layout Before Widget Move',
type: PageLayoutType.RECORD_PAGE,
objectMetadataId: null,
tabs: [
{
id: testTabId1,
title: 'Source Tab',
position: 0,
widgets: [
{
id: widgetId,
pageLayoutTabId: testTabId1,
title: 'Iframe Widget',
type: WidgetType.IFRAME,
objectMetadataId: null,
gridPosition: {
row: 0,
column: 0,
rowSpan: 1,
columnSpan: 1,
},
configuration: MOCK_IFRAME_CONFIGURATION,
},
],
},
{
id: testTabId2,
title: 'Destination Tab',
position: 1,
widgets: [],
},
],
},
});
const { data } = await updateOnePageLayoutWithTabsAndWidgets({
expectToFail: false,
input: {
id: testPageLayoutId,
name: 'Layout After Widget Move',
type: PageLayoutType.RECORD_PAGE,
objectMetadataId: null,
tabs: [
{
id: testTabId1,
title: 'Source Tab',
position: 0,
widgets: [],
},
{
id: testTabId2,
title: 'Destination Tab',
position: 1,
widgets: [
{
id: widgetId,
pageLayoutTabId: testTabId2,
title: 'Iframe Widget',
type: WidgetType.IFRAME,
objectMetadataId: null,
gridPosition: {
row: 0,
column: 0,
rowSpan: 1,
columnSpan: 1,
},
configuration: MOCK_IFRAME_CONFIGURATION,
},
],
},
],
},
});
const updatedTabs = data.updatePageLayoutWithTabsAndWidgets.tabs ?? [];
const sourceTab = updatedTabs.find((tab) => tab.id === testTabId1);
const destinationTab = updatedTabs.find((tab) => tab.id === testTabId2);
expect(sourceTab?.widgets).toHaveLength(0);
expect(destinationTab?.widgets).toHaveLength(1);
expect(destinationTab?.widgets?.[0]).toMatchObject({
id: widgetId,
pageLayoutTabId: testTabId2,
title: 'Iframe Widget',
});
});
});
+4 -1
View File
@@ -81,7 +81,10 @@ export type {
WorkflowRunStepInfos,
} from './types/WorkflowRunStateStepInfos';
export { StepStatus } from './types/WorkflowRunStateStepInfos';
export { canObjectBeManagedByWorkflow } from './utils/canObjectBeManagedByWorkflow';
export {
OBJECTS_BLOCKED_FROM_AUTOMATION,
canObjectBeManagedByAutomation,
} from './utils/canObjectBeManagedByAutomation';
export { extractRawVariableNamePart } from './utils/extractRawVariableNameParts';
export { getFunctionInputFromInputSchema } from './utils/getFunctionInputFromInputSchema';
export { getWorkflowRunContext } from './utils/getWorkflowRunContext';
@@ -0,0 +1,45 @@
import { canObjectBeManagedByAutomation } from '@/workflow/utils/canObjectBeManagedByAutomation';
describe('canObjectBeManagedByAutomation', () => {
it('should return true for a standard non-blocked object', () => {
expect(
canObjectBeManagedByAutomation({ nameSingular: 'company' }),
).toBe(true);
});
it('should return true for noteTarget and taskTarget', () => {
expect(
canObjectBeManagedByAutomation({ nameSingular: 'noteTarget' }),
).toBe(true);
expect(
canObjectBeManagedByAutomation({ nameSingular: 'taskTarget' }),
).toBe(true);
});
it('should return true for attachment and timelineActivity', () => {
expect(
canObjectBeManagedByAutomation({ nameSingular: 'attachment' }),
).toBe(true);
expect(
canObjectBeManagedByAutomation({ nameSingular: 'timelineActivity' }),
).toBe(true);
});
it.each([
'workflow',
'workflowVersion',
'workflowRun',
'workflowAutomatedTrigger',
'workspaceMember',
'dashboard',
'message',
'messageThread',
'messageChannelMessageAssociation',
'messageParticipant',
'calendarEvent',
'calendarEventParticipant',
'calendarChannelEventAssociation',
])('should return false for %s', (nameSingular) => {
expect(canObjectBeManagedByAutomation({ nameSingular })).toBe(false);
});
});
@@ -1,57 +0,0 @@
import { canObjectBeManagedByWorkflow } from '@/workflow/utils/canObjectBeManagedByWorkflow';
describe('canObjectBeManagedByWorkflow', () => {
it('should return true for non-system, non-excluded objects', () => {
expect(
canObjectBeManagedByWorkflow({
nameSingular: 'company',
isSystem: false,
}),
).toBe(true);
});
it('should return false for system objects', () => {
expect(
canObjectBeManagedByWorkflow({
nameSingular: 'company',
isSystem: true,
}),
).toBe(false);
});
it('should return false for workflow object', () => {
expect(
canObjectBeManagedByWorkflow({
nameSingular: 'workflow',
isSystem: false,
}),
).toBe(false);
});
it('should return false for workflowVersion object', () => {
expect(
canObjectBeManagedByWorkflow({
nameSingular: 'workflowVersion',
isSystem: false,
}),
).toBe(false);
});
it('should return false for workflowRun object', () => {
expect(
canObjectBeManagedByWorkflow({
nameSingular: 'workflowRun',
isSystem: false,
}),
).toBe(false);
});
it('should return false for dashboard object', () => {
expect(
canObjectBeManagedByWorkflow({
nameSingular: 'dashboard',
isSystem: false,
}),
).toBe(false);
});
});
@@ -0,0 +1,29 @@
// Objects whose records must not be created, updated, or deleted by
// automation callers (workflows and AI tools). Either they back the
// automation runtime itself (recursion risk), gate access/permissions,
// or are owned by background sync (writing to them corrupts state).
export const OBJECTS_BLOCKED_FROM_AUTOMATION = [
'workflow',
'workflowVersion',
'workflowRun',
'workflowAutomatedTrigger',
'workspaceMember',
'dashboard',
'message',
'messageThread',
'messageChannelMessageAssociation',
'messageParticipant',
'calendarEvent',
'calendarEventParticipant',
'calendarChannelEventAssociation',
] as const;
export const canObjectBeManagedByAutomation = ({
nameSingular,
}: {
nameSingular: string;
}): boolean => {
return !OBJECTS_BLOCKED_FROM_AUTOMATION.includes(
nameSingular as (typeof OBJECTS_BLOCKED_FROM_AUTOMATION)[number],
);
};
@@ -1,19 +0,0 @@
export const canObjectBeManagedByWorkflow = ({
nameSingular,
isSystem,
}: {
nameSingular: string;
isSystem: boolean;
}) => {
const excludedNonSystemObjectMetadataItemNames = [
'workflow',
'workflowVersion',
'workflowRun',
'dashboard',
];
return (
!excludedNonSystemObjectMetadataItemNames.includes(nameSingular) &&
!isSystem
);
};
-1
View File
@@ -24,7 +24,6 @@
"@lottiefiles/dotlottie-react": "^0.18.10",
"@tabler/icons-react": "^3.41.1",
"@wyw-in-js/babel-preset": "^0.8.1",
"axios": "^1.14.0",
"framer-motion": "^11.18.0",
"gray-matter": "^4.0.3",
"next": "16.1.7",
@@ -1,17 +1,18 @@
import axios from 'axios';
import { unstable_cache } from 'next/cache';
export const fetchDiscordMemberCount = unstable_cache(
async (): Promise<number | null> => {
try {
const response = await axios.get(
'https://discord.com/api/v10/invites/cx5n4Jzs57',
{
params: { with_counts: true },
},
const response = await fetch(
'https://discord.com/api/v10/invites/cx5n4Jzs57?with_counts=true',
);
return response.data.profile.member_count;
if (!response.ok) return null;
const data = (await response.json()) as {
profile?: { member_count?: number };
};
return data.profile?.member_count ?? null;
} catch {
return null;
}
@@ -1,4 +1,3 @@
import axios from 'axios';
import { unstable_cache } from 'next/cache';
export const fetchGithubStarCount = unstable_cache(
@@ -13,12 +12,15 @@ export const fetchGithubStarCount = unstable_cache(
headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
}
const response = await axios.get(
const response = await fetch(
'https://api.github.com/repos/twentyhq/twenty',
{ headers },
);
return response.data.stargazers_count;
if (!response.ok) return null;
const data = (await response.json()) as { stargazers_count?: number };
return data.stargazers_count ?? null;
} catch {
return null;
}
@@ -10,7 +10,12 @@ export const getStripeClient = (): Stripe => {
throw new Error('STRIPE_SECRET_KEY is not configured');
}
stripeInstance = new Stripe(secretKey, {});
// On Cloudflare Workers the Stripe SDK's default Node http transport hangs
// on outbound TLS — even with nodejs_compat — and aborts at the SDK's own
// 80s timeout. Forcing the fetch-based client uses workerd's native fetch.
stripeInstance = new Stripe(secretKey, {
httpClient: Stripe.createFetchHttpClient(),
});
}
return stripeInstance;
@@ -1,5 +1,3 @@
import axios from 'axios';
export async function fetchLatestGithubReleaseTag(): Promise<string | null> {
try {
const headers: Record<string, string> = {
@@ -11,12 +9,15 @@ export async function fetchLatestGithubReleaseTag(): Promise<string | null> {
headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
}
const response = await axios.get(
const response = await fetch(
'https://api.github.com/repos/twentyhq/twenty/releases/latest',
{ headers },
);
return response.data.tag_name;
if (!response.ok) return null;
const data = (await response.json()) as { tag_name?: string };
return data.tag_name ?? null;
} catch {
return null;
}
-19
View File
@@ -28661,17 +28661,6 @@ __metadata:
languageName: node
linkType: hard
"axios@npm:^1.14.0":
version: 1.14.0
resolution: "axios@npm:1.14.0"
dependencies:
follow-redirects: "npm:^1.15.11"
form-data: "npm:^4.0.5"
proxy-from-env: "npm:^2.1.0"
checksum: 10c0/2541f4aa215a7d1842429dad006fc682d82bc0e74bd14500823f7d8cce3bbae0e0a8c328c8538946718f366ab8ce5a4c12e9ad40e5a0f3482ff8bff0cd115d45
languageName: node
linkType: hard
"axios@npm:^1.9.0":
version: 1.13.6
resolution: "axios@npm:1.13.6"
@@ -49878,13 +49867,6 @@ __metadata:
languageName: node
linkType: hard
"proxy-from-env@npm:^2.1.0":
version: 2.1.0
resolution: "proxy-from-env@npm:2.1.0"
checksum: 10c0/ed01729fd4d094eab619cd7e17ce3698b3413b31eb102c4904f9875e677cd207392795d5b4adee9cec359dfd31c44d5ad7595a3a3ad51c40250e141512281c58
languageName: node
linkType: hard
"proxy-memoize@npm:3.0.1":
version: 3.0.1
resolution: "proxy-memoize@npm:3.0.1"
@@ -57101,7 +57083,6 @@ __metadata:
"@types/react-dom": "npm:^19"
"@types/three": "npm:^0.183.1"
"@wyw-in-js/babel-preset": "npm:^0.8.1"
axios: "npm:^1.14.0"
babel-plugin-react-compiler: "npm:1.0.0"
framer-motion: "npm:^11.18.0"
gray-matter: "npm:^4.0.3"