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
@@ -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);
});
});