Search action - Add variables to select and relations + other fixes (#12604)

- Variables can now be handled for select/multiselect/relations
- Hide field not supported in forms (source, rating)
- Add tests for schemas

Remaning issues:
- country/currency pickers not working
- stories for components
- variable picker hidden for dates
This commit is contained in:
Thomas Trompette
2025-06-16 13:45:28 +02:00
committed by GitHub
parent e0cb53af48
commit ae57e67c77
33 changed files with 621 additions and 234 deletions

View File

@ -1,19 +0,0 @@
import { ViewFilter } from '@/views/types/ViewFilter';
import { z } from 'zod';
const selectViewFilterValueSchema = z
.string()
.transform((val) => (val === '' ? [] : JSON.parse(val)))
.refine(
(parsed) =>
Array.isArray(parsed) && parsed.every((item) => typeof item === 'string'),
{
message: 'Expected an array of strings',
},
);
export const resolveSelectViewFilterValue = (
viewFilter: Pick<ViewFilter, 'value'>,
) => {
return selectViewFilterValueSchema.parse(viewFilter.value);
};

View File

@ -0,0 +1,91 @@
import { arrayOfStringsOrVariablesSchema } from '../arrayOfStringsOrVariablesSchema';
describe('arrayOfStringsOrVariablesSchema', () => {
describe('Empty value handling', () => {
it('should return empty array for empty string', () => {
const result = arrayOfStringsOrVariablesSchema.safeParse('');
expect(result.success).toBe(true);
if (result.success) {
expect(result.data).toEqual([]);
}
});
});
describe('Variable syntax validation', () => {
it('should accept valid variable syntax', () => {
const validVariables = [
'{{variable}}',
'{{user.id}}',
'{{company.name}}',
];
validVariables.forEach((variable) => {
const result = arrayOfStringsOrVariablesSchema.safeParse(variable);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data).toEqual([variable]);
}
});
});
});
describe('JSON array handling', () => {
it('should accept valid JSON array of strings', () => {
const validArrays = [
JSON.stringify(['value1', 'value2']),
JSON.stringify(['{{variable1}}', '{{variable2}}']),
JSON.stringify(['value1', '{{variable2}}']),
];
validArrays.forEach((array) => {
const result = arrayOfStringsOrVariablesSchema.safeParse(array);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data).toEqual(JSON.parse(array));
}
});
});
it('should reject JSON array with non-string values', () => {
const invalidArrays = [
JSON.stringify([1, 2, 3]),
JSON.stringify([true, false]),
JSON.stringify([null]),
JSON.stringify([{}]),
JSON.stringify([[]]),
];
invalidArrays.forEach((array) => {
const result = arrayOfStringsOrVariablesSchema.safeParse(array);
expect(result.success).toBe(false);
});
});
});
describe('Edge cases', () => {
it('should handle whitespace in variable syntax', () => {
const result =
arrayOfStringsOrVariablesSchema.safeParse('{{ variable }}');
expect(result.success).toBe(true);
if (result.success) {
expect(result.data).toEqual(['{{ variable }}']);
}
});
it('should handle nested variables in JSON array', () => {
const input = JSON.stringify(['{{outer.{{inner}}}}']);
const result = arrayOfStringsOrVariablesSchema.safeParse(input);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data).toEqual(['{{outer.{{inner}}}}']);
}
});
it('should handle empty array in JSON', () => {
const result = arrayOfStringsOrVariablesSchema.safeParse('[]');
expect(result.success).toBe(true);
if (result.success) {
expect(result.data).toEqual([]);
}
});
});
});

View File

@ -0,0 +1,169 @@
import { arrayOfUuidOrVariableSchema } from '../arrayOfUuidsOrVariablesSchema';
describe('arrayOfUuidOrVariableSchema', () => {
describe('UUID validation', () => {
it('should accept valid UUIDs', () => {
const validUuids = [
'123e4567-e89b-12d3-a456-426614174000',
'550e8400-e29b-41d4-a716-446655440000',
];
validUuids.forEach((uuid) => {
// Test as single value
const singleResult = arrayOfUuidOrVariableSchema.safeParse(uuid);
expect(singleResult.success).toBe(true);
if (singleResult.success) {
expect(singleResult.data).toEqual([uuid]);
}
// Test as array
const arrayResult = arrayOfUuidOrVariableSchema.safeParse([uuid]);
expect(arrayResult.success).toBe(true);
if (arrayResult.success) {
expect(arrayResult.data).toEqual([uuid]);
}
});
});
it('should return empty array for invalid UUIDs', () => {
const invalidUuids = [
'invalid-uuid',
'12345',
'550e8400e29b41d4a716446655440000',
'',
'123e4567-e89b-12d3-a456-42661417400-',
];
invalidUuids.forEach((uuid) => {
// Test as single value
const singleResult = arrayOfUuidOrVariableSchema.safeParse(uuid);
expect(singleResult.success).toBe(true);
if (singleResult.success) {
expect(singleResult.data).toEqual([]);
}
// Test as array
const arrayResult = arrayOfUuidOrVariableSchema.safeParse([uuid]);
expect(arrayResult.success).toBe(true);
if (arrayResult.success) {
expect(arrayResult.data).toEqual([]);
}
});
});
});
describe('Variable syntax validation', () => {
it('should accept valid variable syntax', () => {
const validVariables = [
'{{variable}}',
'{{user.id}}',
'{{company.name}}',
];
validVariables.forEach((variable) => {
// Test as single value
const singleResult = arrayOfUuidOrVariableSchema.safeParse(variable);
expect(singleResult.success).toBe(true);
if (singleResult.success) {
expect(singleResult.data).toEqual([variable]);
}
// Test as array
const arrayResult = arrayOfUuidOrVariableSchema.safeParse([variable]);
expect(arrayResult.success).toBe(true);
if (arrayResult.success) {
expect(arrayResult.data).toEqual([variable]);
}
});
});
it('should return empty array for invalid variable syntax', () => {
const invalidVariables = ['{{variable', 'variable}}', '{{}}', '{{', '}}'];
invalidVariables.forEach((variable) => {
// Test as single value
const singleResult = arrayOfUuidOrVariableSchema.safeParse(variable);
expect(singleResult.success).toBe(true);
if (singleResult.success) {
expect(singleResult.data).toEqual([]);
}
// Test as array
const arrayResult = arrayOfUuidOrVariableSchema.safeParse([variable]);
expect(arrayResult.success).toBe(true);
if (arrayResult.success) {
expect(arrayResult.data).toEqual([]);
}
});
});
});
describe('Input type handling', () => {
it('should handle string input with valid JSON', () => {
const input = JSON.stringify(['123e4567-e89b-12d3-a456-426614174000']);
const result = arrayOfUuidOrVariableSchema.safeParse(input);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data).toEqual(['123e4567-e89b-12d3-a456-426614174000']);
}
});
it('should handle string input with variables', () => {
const input = '{{variable}}';
const result = arrayOfUuidOrVariableSchema.safeParse(input);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data).toEqual(['{{variable}}']);
}
});
it('should handle array input directly', () => {
const input = ['123e4567-e89b-12d3-a456-426614174000', '{{variable}}'];
const result = arrayOfUuidOrVariableSchema.safeParse(input);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data).toEqual(input);
}
});
it('should handle single value input', () => {
const input = '20202020-0687-4c41-b707-ed1bfca972a7';
const result = arrayOfUuidOrVariableSchema.safeParse(input);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data).toEqual([input]);
}
});
});
describe('Error handling', () => {
it('should return empty array for invalid JSON string', () => {
const input = 'invalid-json';
const result = arrayOfUuidOrVariableSchema.safeParse(input);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data).toEqual([]);
}
});
it('should return empty array for non-string, non-array input', () => {
const inputs = [null, undefined, 123, true, {}];
inputs.forEach((input) => {
const result = arrayOfUuidOrVariableSchema.safeParse(input);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data).toEqual([]);
}
});
});
it('should return empty array for array with invalid values', () => {
const input = ['invalid-uuid', 'not-a-variable'];
const result = arrayOfUuidOrVariableSchema.safeParse(input);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data).toEqual([]);
}
});
});
});

View File

@ -0,0 +1,19 @@
import { isValidVariable } from 'twenty-shared/utils';
import { z } from 'zod';
export const arrayOfStringsOrVariablesSchema = z
.string()
.transform((val) => {
if (val === '') return [];
if (isValidVariable(val) as boolean) {
return [val];
}
return JSON.parse(val);
})
.refine(
(parsed) =>
Array.isArray(parsed) && parsed.every((item) => typeof item === 'string'),
{
message: 'Expected an array of strings',
},
);

View File

@ -0,0 +1,30 @@
import { isValidUuid, isValidVariable } from 'twenty-shared/utils';
import { z } from 'zod';
export const arrayOfUuidOrVariableSchema = z
.preprocess(
(value) => {
try {
if (typeof value === 'string') {
if (isValidVariable(value) as boolean) {
return [value];
}
try {
const parsed = JSON.parse(value);
return Array.isArray(parsed) ? parsed : [parsed];
} catch {
return [value];
}
}
return Array.isArray(value) ? value : [value];
} catch {
return [];
}
},
z.array(
z.string().refine((val) => {
return isValidUuid(val) || isValidVariable(val);
}, 'Must be a valid UUID or a variable with {{ }} syntax'),
),
)
.catch([]);

View File

@ -1,11 +0,0 @@
import { z } from 'zod';
export const simpleRelationFilterValueSchema = z
.preprocess((value) => {
try {
return typeof value === 'string' ? JSON.parse(value) : [];
} catch {
return [];
}
}, z.array(z.string().uuid()))
.catch([]);