Support define is tool logic function (#17926)

- supports isTool and timeout settings in defineLogicFunction in apps
and in setting tabs definition
- compute for all toolInputSchema for logic funciton, in settings and in
code steps

<img width="991" height="802" alt="image"
src="https://github.com/user-attachments/assets/05dc1221-cac9-45a3-87b0-3b13161446fd"
/>
This commit is contained in:
martmull
2026-02-16 10:43:29 +01:00
committed by GitHub
parent f694bb99b3
commit da064d5e88
138 changed files with 4839 additions and 367 deletions
@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`generateDepthRecordGqlFieldsFromObject should generate depth one record gql fields from object 1`] = `
{
@@ -0,0 +1,62 @@
import {
isFieldAddressValue,
addressSchema,
} from '@/object-record/record-field/ui/types/guards/isFieldAddressValue';
describe('isFieldAddressValue', () => {
it('should return true for valid address values', () => {
expect(
isFieldAddressValue({
addressStreet1: '123 Main St',
addressStreet2: null,
addressCity: 'Paris',
addressState: null,
addressPostcode: '75001',
addressCountry: 'France',
addressLat: 48.8566,
addressLng: 2.3522,
}),
).toBe(true);
});
it('should return true for minimal address with all nullable fields null', () => {
expect(
isFieldAddressValue({
addressStreet1: '',
addressStreet2: null,
addressCity: null,
addressState: null,
addressPostcode: null,
addressCountry: null,
addressLat: null,
addressLng: null,
}),
).toBe(true);
});
it('should return false for incomplete address', () => {
expect(isFieldAddressValue({ addressStreet1: '123 Main St' })).toBe(false);
});
it('should return false for non-object values', () => {
expect(isFieldAddressValue(null)).toBe(false);
expect(isFieldAddressValue('address')).toBe(false);
});
});
describe('addressSchema', () => {
it('should parse a valid address', () => {
const result = addressSchema.safeParse({
addressStreet1: '123 Main St',
addressStreet2: null,
addressCity: 'Paris',
addressState: null,
addressPostcode: '75001',
addressCountry: 'France',
addressLat: null,
addressLng: null,
});
expect(result.success).toBe(true);
});
});
@@ -0,0 +1,22 @@
import { isFieldArrayValue } from '@/object-record/record-field/ui/types/guards/isFieldArrayValue';
describe('isFieldArrayValue', () => {
it('should return true for string arrays', () => {
expect(isFieldArrayValue(['a', 'b', 'c'])).toBe(true);
expect(isFieldArrayValue([])).toBe(true);
});
it('should return true for null', () => {
expect(isFieldArrayValue(null)).toBe(true);
});
it('should return false for non-array values', () => {
expect(isFieldArrayValue('string')).toBe(false);
expect(isFieldArrayValue(42)).toBe(false);
expect(isFieldArrayValue(undefined)).toBe(false);
});
it('should return false for arrays of non-strings', () => {
expect(isFieldArrayValue([1, 2, 3])).toBe(false);
});
});
@@ -0,0 +1,15 @@
import { isFieldBooleanValue } from '@/object-record/record-field/ui/types/guards/isFieldBooleanValue';
describe('isFieldBooleanValue', () => {
it('should return true for boolean values', () => {
expect(isFieldBooleanValue(true)).toBe(true);
expect(isFieldBooleanValue(false)).toBe(true);
});
it('should return false for non-boolean values', () => {
expect(isFieldBooleanValue('true')).toBe(false);
expect(isFieldBooleanValue(1)).toBe(false);
expect(isFieldBooleanValue(null)).toBe(false);
expect(isFieldBooleanValue(undefined)).toBe(false);
});
});
@@ -0,0 +1,21 @@
import { isFieldCurrencyValue } from '@/object-record/record-field/ui/types/guards/isFieldCurrencyValue';
describe('isFieldCurrencyValue', () => {
it('should return true for valid currency values', () => {
expect(
isFieldCurrencyValue({ currencyCode: 'USD', amountMicros: 1000000 }),
).toBe(true);
expect(
isFieldCurrencyValue({ currencyCode: null, amountMicros: null }),
).toBe(true);
expect(isFieldCurrencyValue({ currencyCode: '', amountMicros: null })).toBe(
true,
);
});
it('should return false for invalid currency values', () => {
expect(isFieldCurrencyValue({ currencyCode: 'INVALID' })).toBe(false);
expect(isFieldCurrencyValue(null)).toBe(false);
expect(isFieldCurrencyValue('USD')).toBe(false);
});
});
@@ -0,0 +1,21 @@
import { isFieldDateValue } from '@/object-record/record-field/ui/types/guards/isFieldDateValue';
describe('isFieldDateValue', () => {
it('should return true for valid date strings', () => {
expect(isFieldDateValue('2024-01-15')).toBe(true);
expect(isFieldDateValue('2024-01-15T10:30:00Z')).toBe(true);
});
it('should return true for null', () => {
expect(isFieldDateValue(null)).toBe(true);
});
it('should return false for invalid date strings', () => {
expect(isFieldDateValue('not-a-date')).toBe(false);
});
it('should return false for non-string values', () => {
expect(isFieldDateValue(42)).toBe(false);
expect(isFieldDateValue(undefined)).toBe(false);
});
});
@@ -0,0 +1,21 @@
import { isFieldFullNameValue } from '@/object-record/record-field/ui/types/guards/isFieldFullNameValue';
describe('isFieldFullNameValue', () => {
it('should return true for valid full name objects', () => {
expect(isFieldFullNameValue({ firstName: 'John', lastName: 'Doe' })).toBe(
true,
);
expect(isFieldFullNameValue({ firstName: '', lastName: '' })).toBe(true);
});
it('should return false for incomplete objects', () => {
expect(isFieldFullNameValue({ firstName: 'John' })).toBe(false);
expect(isFieldFullNameValue({ lastName: 'Doe' })).toBe(false);
});
it('should return false for non-object values', () => {
expect(isFieldFullNameValue('John Doe')).toBe(false);
expect(isFieldFullNameValue(null)).toBe(false);
expect(isFieldFullNameValue(undefined)).toBe(false);
});
});
@@ -0,0 +1,19 @@
import { isFieldNumberValue } from '@/object-record/record-field/ui/types/guards/isFieldNumberValue';
describe('isFieldNumberValue', () => {
it('should return true for numbers', () => {
expect(isFieldNumberValue(42)).toBe(true);
expect(isFieldNumberValue(0)).toBe(true);
expect(isFieldNumberValue(-1.5)).toBe(true);
});
it('should return true for null', () => {
expect(isFieldNumberValue(null)).toBe(true);
});
it('should return false for non-number values', () => {
expect(isFieldNumberValue('42')).toBe(false);
expect(isFieldNumberValue(undefined)).toBe(false);
expect(isFieldNumberValue({})).toBe(false);
});
});
@@ -0,0 +1,23 @@
import { isFieldRawJsonValue } from '@/object-record/record-field/ui/types/guards/isFieldRawJsonValue';
describe('isFieldRawJsonValue', () => {
it('should return true for JSON objects', () => {
expect(isFieldRawJsonValue({ key: 'value' })).toBe(true);
expect(isFieldRawJsonValue({ nested: { deep: true } })).toBe(true);
});
it('should return true for JSON arrays', () => {
expect(isFieldRawJsonValue([1, 2, 3])).toBe(true);
expect(isFieldRawJsonValue([])).toBe(true);
});
it('should return true for null', () => {
expect(isFieldRawJsonValue(null)).toBe(true);
});
it('should return false for literal values other than null', () => {
expect(isFieldRawJsonValue('string')).toBe(false);
expect(isFieldRawJsonValue(42)).toBe(false);
expect(isFieldRawJsonValue(true)).toBe(false);
});
});
@@ -0,0 +1,15 @@
import { isFieldTextValue } from '@/object-record/record-field/ui/types/guards/isFieldTextValue';
describe('isFieldTextValue', () => {
it('should return true for strings', () => {
expect(isFieldTextValue('hello')).toBe(true);
expect(isFieldTextValue('')).toBe(true);
});
it('should return false for non-string values', () => {
expect(isFieldTextValue(42)).toBe(false);
expect(isFieldTextValue(null)).toBe(false);
expect(isFieldTextValue(undefined)).toBe(false);
expect(isFieldTextValue(true)).toBe(false);
});
});
@@ -0,0 +1,67 @@
import { RecordGroupDefinitionType } from '@/object-record/record-group/types/RecordGroupDefinition';
import { RecordGroupSort } from '@/object-record/record-group/types/RecordGroupSort';
import { sortRecordGroupDefinitions } from '@/object-record/record-group/utils/sortRecordGroupDefinitions';
const createGroup = (
overrides: Partial<{
id: string;
title: string;
position: number;
isVisible: boolean;
}>,
) => ({
id: overrides.id ?? '1',
type: RecordGroupDefinitionType.Value,
title: overrides.title ?? 'Group',
value: 'value',
color: 'green' as const,
position: overrides.position ?? 0,
isVisible: overrides.isVisible ?? true,
});
describe('sortRecordGroupDefinitions', () => {
const groups = [
createGroup({ id: '1', title: 'Charlie', position: 2 }),
createGroup({ id: '2', title: 'Alpha', position: 0 }),
createGroup({ id: '3', title: 'Bravo', position: 1 }),
];
it('should sort alphabetically', () => {
const result = sortRecordGroupDefinitions(
groups,
RecordGroupSort.Alphabetical,
);
expect(result.map((g) => g.title)).toEqual(['Alpha', 'Bravo', 'Charlie']);
});
it('should sort reverse alphabetically', () => {
const result = sortRecordGroupDefinitions(
groups,
RecordGroupSort.ReverseAlphabetical,
);
expect(result.map((g) => g.title)).toEqual(['Charlie', 'Bravo', 'Alpha']);
});
it('should sort by position for Manual sort', () => {
const result = sortRecordGroupDefinitions(groups, RecordGroupSort.Manual);
expect(result.map((g) => g.title)).toEqual(['Alpha', 'Bravo', 'Charlie']);
});
it('should filter out hidden groups', () => {
const groupsWithHidden = [
...groups,
createGroup({ id: '4', title: 'Delta', position: 3, isVisible: false }),
];
const result = sortRecordGroupDefinitions(
groupsWithHidden,
RecordGroupSort.Manual,
);
expect(result).toHaveLength(3);
expect(result.find((g) => g.title === 'Delta')).toBeUndefined();
});
});
@@ -0,0 +1,98 @@
import { computeNewPositionOfDraggedRecord } from '@/object-record/utils/computeNewPositionOfDraggedRecord';
describe('computeNewPositionOfDraggedRecord', () => {
const records = [
{ id: 'a', position: 1 },
{ id: 'b', position: 2 },
{ id: 'c', position: 3 },
{ id: 'd', position: 4 },
];
it('should return same position when dragging to itself', () => {
const result = computeNewPositionOfDraggedRecord({
arrayOfRecordsWithPosition: records,
idOfItemToMove: 'b',
idOfTargetItem: 'b',
isDroppedAfterList: false,
});
expect(result).toBe(2);
});
it('should return target position + 1 when dropped after list', () => {
const result = computeNewPositionOfDraggedRecord({
arrayOfRecordsWithPosition: records,
idOfItemToMove: 'a',
idOfTargetItem: 'd',
isDroppedAfterList: true,
});
expect(result).toBe(5);
});
it('should return target position - 1 when moving to first position', () => {
const result = computeNewPositionOfDraggedRecord({
arrayOfRecordsWithPosition: records,
idOfItemToMove: 'c',
idOfTargetItem: 'a',
isDroppedAfterList: false,
});
expect(result).toBe(0);
});
it('should compute intermediary position when moving after target', () => {
const result = computeNewPositionOfDraggedRecord({
arrayOfRecordsWithPosition: records,
idOfItemToMove: 'a',
idOfTargetItem: 'b',
isDroppedAfterList: false,
});
expect(result).toBe(2.5);
});
it('should compute intermediary position when moving before target', () => {
const result = computeNewPositionOfDraggedRecord({
arrayOfRecordsWithPosition: records,
idOfItemToMove: 'd',
idOfTargetItem: 'c',
isDroppedAfterList: false,
});
expect(result).toBe(2.5);
});
it('should throw when target item is not found', () => {
expect(() =>
computeNewPositionOfDraggedRecord({
arrayOfRecordsWithPosition: records,
idOfItemToMove: 'a',
idOfTargetItem: 'unknown',
isDroppedAfterList: false,
}),
).toThrow('Cannot find item to move for id : unknown');
});
it('should handle item not in table moved before target', () => {
const result = computeNewPositionOfDraggedRecord({
arrayOfRecordsWithPosition: records,
idOfItemToMove: 'new-item',
idOfTargetItem: 'c',
isDroppedAfterList: false,
});
expect(result).toBe(2.5);
});
it('should return target + 1 when moving after last item', () => {
const result = computeNewPositionOfDraggedRecord({
arrayOfRecordsWithPosition: records,
idOfItemToMove: 'a',
idOfTargetItem: 'd',
isDroppedAfterList: false,
});
expect(result).toBe(4 + 1);
});
});
@@ -0,0 +1,32 @@
import { filterAvailableTableColumns } from '@/object-record/utils/filterAvailableTableColumns';
describe('filterAvailableTableColumns', () => {
const createColumnDefinition = (fieldName: string) =>
({
metadata: { fieldName },
}) as any;
it('should return false for denied column names', () => {
expect(
filterAvailableTableColumns(createColumnDefinition('attachments')),
).toBe(false);
expect(
filterAvailableTableColumns(createColumnDefinition('activities')),
).toBe(false);
expect(
filterAvailableTableColumns(createColumnDefinition('timelineActivities')),
).toBe(false);
});
it('should return true for allowed column names', () => {
expect(filterAvailableTableColumns(createColumnDefinition('name'))).toBe(
true,
);
expect(filterAvailableTableColumns(createColumnDefinition('email'))).toBe(
true,
);
expect(filterAvailableTableColumns(createColumnDefinition('phone'))).toBe(
true,
);
});
});
@@ -0,0 +1,29 @@
import { getDropdownFocusIdForRecordField } from '@/object-record/utils/getDropdownFocusIdForRecordField';
describe('getDropdownFocusIdForRecordField', () => {
it('should generate correct dropdown focus id for table-cell', () => {
const result = getDropdownFocusIdForRecordField({
recordId: 'record-123',
fieldMetadataId: 'field-456',
componentType: 'table-cell',
instanceId: 'instance-789',
});
expect(result).toBe(
'dropdown-instance-789-table-cell-record-record-123-field-field-456',
);
});
it('should generate correct dropdown focus id for inline-cell', () => {
const result = getDropdownFocusIdForRecordField({
recordId: 'record-abc',
fieldMetadataId: 'field-def',
componentType: 'inline-cell',
instanceId: 'instance-ghi',
});
expect(result).toBe(
'dropdown-instance-ghi-inline-cell-record-record-abc-field-field-def',
);
});
});
@@ -0,0 +1,58 @@
import { getQueryIdentifier } from '@/object-record/utils/getQueryIdentifier';
describe('getQueryIdentifier', () => {
it('should create identifier from object name and variables', () => {
const result = getQueryIdentifier({
objectNameSingular: 'person',
filter: { name: { eq: 'Alice' } },
orderBy: [{ name: 'AscNullsFirst' }],
limit: 10,
});
expect(result).toContain('person');
expect(result).toContain('Alice');
expect(result).toContain('10');
});
it('should include cursor filter when present', () => {
const result = getQueryIdentifier({
objectNameSingular: 'company',
filter: {},
orderBy: [],
limit: 20,
cursorFilter: { cursor: 'cursor-123', cursorDirection: 'before' },
});
expect(result).toContain('cursor-123');
});
it('should include groupBy when present', () => {
const result = getQueryIdentifier({
objectNameSingular: 'task',
filter: {},
orderBy: [],
limit: 10,
groupBy: [{ status: 'Done' }],
});
expect(result).toContain('Done');
});
it('should produce different identifiers for different filters', () => {
const resultA = getQueryIdentifier({
objectNameSingular: 'person',
filter: { name: { eq: 'Alice' } },
orderBy: [],
limit: 10,
});
const resultB = getQueryIdentifier({
objectNameSingular: 'person',
filter: { name: { eq: 'Bob' } },
orderBy: [],
limit: 10,
});
expect(resultA).not.toBe(resultB);
});
});
@@ -0,0 +1,21 @@
import { getUpdateOneRecordMutationResponseField } from '@/object-record/utils/getUpdateOneRecordMutationResponseField';
describe('getUpdateOneRecordMutationResponseField', () => {
it('should capitalize and prefix with "update"', () => {
expect(getUpdateOneRecordMutationResponseField('person')).toBe(
'updatePerson',
);
});
it('should handle multi-word object names', () => {
expect(getUpdateOneRecordMutationResponseField('company')).toBe(
'updateCompany',
);
});
it('should handle already capitalized names', () => {
expect(getUpdateOneRecordMutationResponseField('Person')).toBe(
'updatePerson',
);
});
});
@@ -0,0 +1,33 @@
import { getUpdatedFieldsFromRecordInput } from '@/object-record/utils/getUpdatedFieldsFromRecordInput';
describe('getUpdatedFieldsFromRecordInput', () => {
it('should return field entries excluding id', () => {
const input = { id: '123', name: 'Alice', age: 30 };
const result = getUpdatedFieldsFromRecordInput(input);
expect(result).toEqual([{ name: 'Alice' }, { age: 30 }]);
});
it('should return empty array when only id is present', () => {
const input = { id: '123' };
const result = getUpdatedFieldsFromRecordInput(input);
expect(result).toEqual([]);
});
it('should handle inputs without id', () => {
const input = { name: 'Bob', email: 'bob@test.com' };
const result = getUpdatedFieldsFromRecordInput(input);
expect(result).toEqual([{ name: 'Bob' }, { email: 'bob@test.com' }]);
});
it('should handle empty input', () => {
const result = getUpdatedFieldsFromRecordInput({});
expect(result).toEqual([]);
});
});
@@ -0,0 +1,14 @@
import { isSystemSearchVectorField } from '@/object-record/utils/isSystemSearchVectorField';
describe('isSystemSearchVectorField', () => {
it('should return true for searchVector field', () => {
expect(isSystemSearchVectorField('searchVector')).toBe(true);
});
it('should return false for other field names', () => {
expect(isSystemSearchVectorField('name')).toBe(false);
expect(isSystemSearchVectorField('id')).toBe(false);
expect(isSystemSearchVectorField('position')).toBe(false);
expect(isSystemSearchVectorField('')).toBe(false);
});
});
@@ -0,0 +1,47 @@
import { sortRecordsByPosition } from '@/object-record/utils/sortRecordsByPosition';
describe('sortRecordsByPosition', () => {
it('should sort by numeric position', () => {
const record1 = { id: '1', __typename: 'Record', position: 1 };
const record2 = { id: '2', __typename: 'Record', position: 2 };
expect(sortRecordsByPosition(record1, record2)).toBeLessThan(0);
expect(sortRecordsByPosition(record2, record1)).toBeGreaterThan(0);
});
it('should return 0 for equal numeric positions', () => {
const record1 = { id: '1', __typename: 'Record', position: 5 };
const record2 = { id: '2', __typename: 'Record', position: 5 };
expect(sortRecordsByPosition(record1, record2)).toBe(0);
});
it('should place "first" before other positions', () => {
const record1 = { id: '1', __typename: 'Record', position: 'first' };
const record2 = { id: '2', __typename: 'Record', position: 'last' };
expect(sortRecordsByPosition(record1 as any, record2 as any)).toBe(-1);
});
it('should place "last" after other positions', () => {
const record1 = { id: '1', __typename: 'Record', position: 'last' };
const record2 = { id: '2', __typename: 'Record', position: 'first' };
expect(sortRecordsByPosition(record1 as any, record2 as any)).toBe(1);
});
it('should return 0 for unknown position types', () => {
const record1 = {
id: '1',
__typename: 'Record',
position: undefined as any,
};
const record2 = {
id: '2',
__typename: 'Record',
position: undefined as any,
};
expect(sortRecordsByPosition(record1, record2)).toBe(0);
});
});