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:
@ -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);
|
||||
};
|
||||
@ -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([]);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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([]);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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',
|
||||
},
|
||||
);
|
||||
@ -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([]);
|
||||
@ -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([]);
|
||||
Reference in New Issue
Block a user