Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code 17694bccdd Missing null check on workflow step settings causes crash when data is incomplete
https://sonarly.com/issue/13613?type=bug

WorkflowEditActionCreateRecord crashes with TypeError when a CREATE_RECORD workflow step has undefined `settings` or `settings.input`, due to unguarded property access in useState initializer.

Fix: Added defensive optional chaining (`?.`) when accessing `action.settings.input` across all workflow action editor components that handle record CRUD operations and email actions. The fix extracts `action.settings?.input` into a local `actionInput` variable, then uses optional chaining (`actionInput?.fieldName`) with nullish coalescing (`?? ''` or `?? []`) to provide safe defaults when the data is undefined.

**Why this happens:** Workflow step data is stored as JSON in the database and loaded via GraphQL without runtime schema validation on the frontend. TypeScript types (derived from Zod schemas) guarantee `settings.input` exists at compile time, but at runtime the data can be malformed — e.g., after a 502 error during editing, cache corruption, or incomplete data from the API.

**What changed:**
- `WorkflowEditActionCreateRecord.tsx` — the component from the Sentry stack trace
- `WorkflowEditActionUpdateRecord.tsx` — same vulnerable pattern
- `WorkflowEditActionDeleteRecord.tsx` — same vulnerable pattern
- `WorkflowEditActionUpsertRecord.tsx` — same vulnerable pattern
- `WorkflowEditActionEmailBase.tsx` — same vulnerable pattern
- `useEmailForm.ts` — hook used by EmailBase with same pattern

Each file follows the same transformation: `action.settings.input.X` → `actionInput?.X ?? defaultValue`, where `actionInput = action.settings?.input`. The defaults (empty string, empty array) match the initial values used by the server-side step creation code.
2026-03-12 09:19:54 +00:00
Sonarly Claude Code d9daf7e579 chore: additional changes for Google reCAPTCHA fails for users in China due to b 2026-03-12 09:10:08 +00:00
Sonarly Claude Code dc97343e07 chore: improve monitoring for Google reCAPTCHA fails for users in China due to b
Added `ignoreErrors` configuration to the Sentry `init()` call in `SentryInitEffect.tsx` to filter out reCAPTCHA "Invalid site key or not loaded in api.js" errors. These errors originate from Google's third-party reCAPTCHA script and are not actionable — they occur when Google services are blocked (e.g., users in China) and generate monitoring noise. With the code fix adding `.catch()`, these errors should no longer reach Sentry at all, but the `ignoreErrors` filter serves as a defense-in-depth measure for any edge cases where the error might still propagate (e.g., from the reCAPTCHA script's own internal error handling before our `.catch()` fires).
2026-03-12 09:10:06 +00:00
Sonarly Claude Code f1ddb926c7 Google reCAPTCHA fails for users in China due to blocked Google services
https://sonarly.com/issue/13611?type=bug

Google reCAPTCHA's `execute()` throws "Invalid site key or not loaded in api.js" because the reCAPTCHA script cannot fully initialize when Google services are blocked (user is in China). The error is unhandled due to a missing `.catch()` on the promise chain.

Fix: Added a `.catch()` handler to `window.grecaptcha.execute()` in `useRequestFreshCaptchaToken.ts`. When Google reCAPTCHA fails (e.g., due to blocked Google services in China, or an invalid site key), the handler resets `isRequestingCaptchaToken` to `false` so the app doesn't get stuck in a perpetual "requesting token" state. This prevents the unhandled error from bubbling up through React's effect lifecycle.

The fix is minimal and matches the team's existing error handling pattern — the same `setIsRequestingCaptchaToken(false)` reset that happens in the success path. The error is intentionally not re-thrown because reCAPTCHA unavailability is an expected condition in certain network environments and should degrade gracefully.

Added a corresponding test that verifies `isRequestingCaptchaToken` is reset to `false` when `grecaptcha.execute()` rejects.
2026-03-12 09:10:06 +00:00
Sonarly Claude Code 6460e344a2 Missing null check on action.settings.outputSchema crashes HTTP Request workflow editor
https://sonarly.com/issue/13598?type=bug

`useHttpRequestOutputSchema` hook calls `Object.keys(action.settings.outputSchema)` without guarding against `undefined`, crashing when a workflow's HTTP Request step has no `outputSchema` in its persisted settings JSON.

Fix: Added an `isDefined(action.settings.outputSchema)` guard before calling `Object.keys(action.settings.outputSchema)` in `useHttpRequestOutputSchema.ts`.

When a workflow's HTTP Request step has stored settings JSON that is missing the `outputSchema` field (e.g., from direct DB manipulation or a code path that didn't include it), the hook now gracefully initializes the `outputSchema` state to `null` instead of crashing with `TypeError: undefined is not an object`.

This matches the team's established pattern of using `isDefined()` from `twenty-shared/utils` for null guards, as seen in `filterOutputSchema.ts` and `useAvailableVariablesInWorkflowStep.ts`.

The fix is minimal and safe: when `outputSchema` is missing, the user sees an empty output schema field they can populate — the same experience as a newly created HTTP Request step.
2026-03-12 08:44:36 +00:00
6 changed files with 31 additions and 19 deletions
@@ -84,9 +84,11 @@ export const WorkflowEditActionCreateRecord = ({
value: item.nameSingular,
}));
const actionInput = action.settings?.input;
const [formData, setFormData] = useState<CreateRecordFormData>({
objectName: action.settings.input.objectName,
...action.settings.input.objectRecord,
objectName: actionInput?.objectName ?? '',
...actionInput?.objectRecord,
});
const isFormDisabled = actionOptions.readonly === true;
@@ -56,9 +56,11 @@ export const WorkflowEditActionDeleteRecord = ({
value: item.nameSingular,
}));
const actionInput = action.settings?.input;
const [formData, setFormData] = useState<DeleteRecordFormData>({
objectNameSingular: action.settings.input.objectName,
objectRecordId: action.settings.input.objectRecordId,
objectNameSingular: actionInput?.objectName ?? '',
objectRecordId: actionInput?.objectRecordId ?? '',
});
const isFormDisabled = actionOptions.readonly;
@@ -72,11 +72,13 @@ export const WorkflowEditActionEmailBase = ({
readonly: actionOptions.readonly === true,
});
const actionInput = action.settings?.input;
const [visibleAdvancedFields, setVisibleAdvancedFields] = useState<{
cc: boolean;
bcc: boolean;
}>(() => {
const inputRecipients = action.settings.input.recipients;
const inputRecipients = actionInput?.recipients;
return {
cc: Boolean(inputRecipients?.cc),
@@ -117,12 +119,12 @@ export const WorkflowEditActionEmailBase = ({
};
if (
isDefined(action.settings.input.connectedAccountId) &&
action.settings.input.connectedAccountId !== ''
isDefined(actionInput?.connectedAccountId) &&
actionInput.connectedAccountId !== ''
) {
filter.or.push({
id: {
eq: action.settings.input.connectedAccountId,
eq: actionInput.connectedAccountId,
},
});
}
@@ -57,11 +57,13 @@ export const WorkflowEditActionUpdateRecord = ({
value: item.nameSingular,
}));
const actionInput = action.settings?.input;
const [formData, setFormData] = useState<UpdateRecordFormData>({
objectNameSingular: action.settings.input.objectName,
objectRecordId: action.settings.input.objectRecordId,
fieldsToUpdate: action.settings.input.fieldsToUpdate ?? [],
...action.settings.input.objectRecord,
objectNameSingular: actionInput?.objectName ?? '',
objectRecordId: actionInput?.objectRecordId ?? '',
fieldsToUpdate: actionInput?.fieldsToUpdate ?? [],
...actionInput?.objectRecord,
});
const isFormDisabled = actionOptions.readonly === true;
@@ -94,9 +94,11 @@ export const WorkflowEditActionUpsertRecord = ({
value: item.nameSingular,
}));
const actionInput = action.settings?.input;
const [formData, setFormData] = useState<UpsertRecordFormData>({
objectName: action.settings.input.objectName,
...action.settings.input.objectRecord,
objectName: actionInput?.objectName ?? '',
...actionInput?.objectRecord,
});
const isFormDisabled = actionOptions.readonly === true;
@@ -15,19 +15,21 @@ export const useEmailForm = ({
onActionUpdate,
readonly,
}: UseEmailFormParams) => {
const actionInput = action.settings?.input;
const [formData, setFormData] = useState<EmailFormData>(() => {
const inputRecipients = action.settings.input.recipients;
const inputRecipients = actionInput?.recipients;
return {
connectedAccountId: action.settings.input.connectedAccountId,
connectedAccountId: actionInput?.connectedAccountId ?? '',
recipients: {
to: inputRecipients?.to ?? '',
cc: inputRecipients?.cc ?? '',
bcc: inputRecipients?.bcc ?? '',
},
subject: action.settings.input.subject ?? '',
body: action.settings.input.body ?? '',
files: action.settings.input.files ?? [],
subject: actionInput?.subject ?? '',
body: actionInput?.body ?? '',
files: actionInput?.files ?? [],
};
});