diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/run-workflow-actions/hooks/useRunWorkflowRecordActions.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/run-workflow-actions/hooks/useRunWorkflowRecordActions.tsx index 3b229db619..7d8e68bfb6 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/run-workflow-actions/hooks/useRunWorkflowRecordActions.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/run-workflow-actions/hooks/useRunWorkflowRecordActions.tsx @@ -5,13 +5,16 @@ import { ActionType } from '@/action-menu/actions/types/ActionType'; import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { useActiveWorkflowVersionsWithManualTrigger } from '@/workflow/hooks/useActiveWorkflowVersionsWithManualTrigger'; import { useRunWorkflowVersion } from '@/workflow/hooks/useRunWorkflowVersion'; import { type WorkflowVersion } from '@/workflow/types/Workflow'; import { COMMAND_MENU_DEFAULT_ICON } from '@/workflow/workflow-trigger/constants/CommandMenuDefaultIcon'; +import { t } from '@lingui/core/macro'; import { useRecoilCallback } from 'recoil'; +import { QUERY_MAX_RECORDS } from 'twenty-shared/constants'; import { capitalize, isDefined } from 'twenty-shared/utils'; import { useIcons } from 'twenty-ui/display'; @@ -23,6 +26,7 @@ export const useRunWorkflowRecordActions = ({ skip?: boolean; }) => { const { getIcon } = useIcons(); + const { enqueueWarningSnackBar } = useSnackBar(); const contextStoreTargetedRecordsRule = useRecoilComponentValue( contextStoreTargetedRecordsRuleComponentState, ); @@ -49,12 +53,30 @@ export const useRunWorkflowRecordActions = ({ 'id' | 'workflowId' | 'trigger' >, ) => { + if (selectedRecordIds.length > QUERY_MAX_RECORDS) { + const selectedCountFormatted = + selectedRecordIds.length.toLocaleString(); + const limitFormatted = QUERY_MAX_RECORDS.toLocaleString(); + + enqueueWarningSnackBar({ + message: t`You selected ${selectedCountFormatted} records but manual triggers can run on at most ${limitFormatted} records at once. Only the first ${limitFormatted} records will be processed.`, + options: { + dedupeKey: 'workflow-manual-trigger-selection-limit', + }, + }); + } + + const limitedSelectedRecordIds = selectedRecordIds.slice( + 0, + QUERY_MAX_RECORDS, + ); + if ( isDefined(activeWorkflowVersion?.trigger) && isBulkRecordsManualTrigger(activeWorkflowVersion.trigger) ) { const objectNamePlural = objectMetadataItem.namePlural; - const selectedRecords = selectedRecordIds + const selectedRecords = limitedSelectedRecordIds .map((recordId) => snapshot.getLoadable(recordStoreFamilyState(recordId)).getValue(), ) @@ -68,7 +90,7 @@ export const useRunWorkflowRecordActions = ({ }, }); } else { - for (const selectedRecordId of selectedRecordIds) { + for (const selectedRecordId of limitedSelectedRecordIds) { const selectedRecord = snapshot .getLoadable(recordStoreFamilyState(selectedRecordId)) .getValue(); @@ -85,7 +107,7 @@ export const useRunWorkflowRecordActions = ({ } } }, - [runWorkflowVersion, objectMetadataItem], + [runWorkflowVersion, objectMetadataItem, enqueueWarningSnackBar], ); return activeWorkflowVersions diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowEditActionFindRecords.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowEditActionFindRecords.tsx index 093849417a..3347f91c17 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowEditActionFindRecords.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowEditActionFindRecords.tsx @@ -24,6 +24,7 @@ import { WorkflowFindRecordsSorts } from '@/workflow/workflow-steps/workflow-act import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader'; import { useLingui } from '@lingui/react/macro'; import { isNumber } from '@sniptt/guards'; +import { QUERY_MAX_RECORDS } from 'twenty-shared/constants'; import { isDefined } from 'twenty-shared/utils'; import { HorizontalSeparator, useIcons } from 'twenty-ui/display'; import { type SelectOption } from 'twenty-ui/input'; @@ -66,6 +67,7 @@ export const WorkflowEditActionFindRecords = ({ }: WorkflowEditActionFindRecordsProps) => { const { getIcon } = useIcons(); const { t } = useLingui(); + const maxRecordsFormatted = QUERY_MAX_RECORDS.toLocaleString(); const { activeNonSystemObjectMetadataItems } = useFilteredObjectMetadataItems(); @@ -77,12 +79,17 @@ export const WorkflowEditActionFindRecords = ({ value: item.nameSingular, })); - const [formData, setFormData] = useState({ + const [formData, setFormData] = useState(() => ({ objectNameSingular: action.settings.input.objectName, - limit: action.settings.input.limit, + limit: + isNumber(action.settings.input.limit) && + action.settings.input.limit > QUERY_MAX_RECORDS + ? QUERY_MAX_RECORDS + : (action.settings.input.limit ?? 1), filter: action.settings.input.filter as FindRecordsActionFilter, orderBy: action.settings.input.orderBy as FindRecordsActionOrderBy, - }); + })); + const [limitError, setLimitError] = useState(undefined); const isFormDisabled = actionOptions.readonly ?? false; const instanceId = `workflow-edit-action-record-find-records-${action.id}-${formData.objectNameSingular}`; @@ -277,18 +284,35 @@ export const WorkflowEditActionFindRecords = ({ )} { if (isFormDisabled === true || !isNumber(limit)) { return; } + const normalizedLimit = Math.floor(limit); + + if (normalizedLimit <= 0) { + setLimitError(t`Limit must be greater than 0.`); + return; + } + + const cappedLimit = Math.min(normalizedLimit, QUERY_MAX_RECORDS); + + setLimitError( + normalizedLimit > QUERY_MAX_RECORDS + ? t`Limit cannot exceed ${maxRecordsFormatted} records.` + : undefined, + ); + const newFormData: FindRecordsFormData = { ...formData, - limit, + limit: cappedLimit, }; setFormData(newFormData); diff --git a/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerManual.tsx b/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerManual.tsx index 58b9fc28cb..3e65bea520 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerManual.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerManual.tsx @@ -17,6 +17,7 @@ import { getTriggerIconColor } from '@/workflow/workflow-trigger/utils/getTrigge import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { useLingui } from '@lingui/react/macro'; +import { QUERY_MAX_RECORDS } from 'twenty-shared/constants'; import { TRIGGER_STEP_ID } from 'twenty-shared/workflow'; import { useIcons } from 'twenty-ui/display'; import { type SelectOption } from 'twenty-ui/input'; @@ -61,6 +62,7 @@ export const WorkflowEditTriggerManual = ({ const { t } = useLingui(); const { getIcon } = useIcons(); + const maxRecordsFormatted = QUERY_MAX_RECORDS.toLocaleString(); const { activeNonSystemObjectMetadataItems } = useFilteredObjectMetadataItems(); @@ -82,7 +84,7 @@ export const WorkflowEditTriggerManual = ({ const availabilityDescriptions = { SINGLE_RECORD: t`The selected record will be passed to your workflow`, - BULK_RECORDS: t`The selected records will be passed to your workflow`, + BULK_RECORDS: t`The selected records (up to ${maxRecordsFormatted}) will be passed to your workflow`, GLOBAL: t`No record is required to trigger this workflow`, };