Files
n8n-enterprise-unlocked/packages/editor-ui/src/components/TestDefinition/composables/useTestDefinitionForm.ts

235 lines
6.1 KiB
TypeScript

import { ref, computed } from 'vue';
import type { ComponentPublicInstance, ComputedRef } from 'vue';
import { useTestDefinitionStore } from '@/stores/testDefinition.store.ee';
import type AnnotationTagsDropdownEe from '@/components/AnnotationTagsDropdown.ee.vue';
import type { N8nInput } from 'n8n-design-system';
import type { UpdateTestDefinitionParams } from '@/api/testDefinition.ee';
import type { EditableField, EditableFormState, EvaluationFormState } from '../types';
type FormRefs = {
nameInput: ComponentPublicInstance<typeof N8nInput>;
tagsInput: ComponentPublicInstance<typeof AnnotationTagsDropdownEe>;
};
export function useTestDefinitionForm() {
const evaluationsStore = useTestDefinitionStore();
// State initialization
const state = ref<EvaluationFormState>({
name: {
value: `My Test ${evaluationsStore.allTestDefinitions.length + 1}`,
tempValue: '',
isEditing: false,
},
tags: {
value: [],
tempValue: [],
isEditing: false,
},
description: '',
evaluationWorkflow: {
mode: 'list',
value: '',
__rl: true,
},
metrics: [],
mockedNodes: [],
});
const isSaving = ref(false);
const fieldsIssues = ref<Array<{ field: string; message: string }>>([]);
const fields = ref<FormRefs>({} as FormRefs);
// A computed mapping of editable fields to their states
// This ensures TS knows the exact type of each field.
const editableFields: ComputedRef<{
name: EditableField<string>;
tags: EditableField<string[]>;
}> = computed(() => ({
name: state.value.name,
tags: state.value.tags,
}));
/**
* Load test data including metrics.
*/
const loadTestData = async (testId: string) => {
try {
await evaluationsStore.fetchAll({ force: true });
const testDefinition = evaluationsStore.testDefinitionsById[testId];
if (testDefinition) {
const metrics = await evaluationsStore.fetchMetrics(testId);
state.value.description = testDefinition.description ?? '';
state.value.name = {
value: testDefinition.name ?? '',
isEditing: false,
tempValue: '',
};
state.value.tags = {
isEditing: false,
value: testDefinition.annotationTagId ? [testDefinition.annotationTagId] : [],
tempValue: [],
};
state.value.evaluationWorkflow = {
mode: 'list',
value: testDefinition.evaluationWorkflowId ?? '',
__rl: true,
};
state.value.metrics = metrics;
state.value.mockedNodes = testDefinition.mockedNodes ?? [];
}
} catch (error) {
console.error('Failed to load test data', error);
}
};
const createTest = async (workflowId: string) => {
if (isSaving.value) return;
isSaving.value = true;
fieldsIssues.value = [];
try {
const params = {
name: state.value.name.value,
workflowId,
description: state.value.description,
};
return await evaluationsStore.create(params);
} finally {
isSaving.value = false;
}
};
const deleteMetric = async (metricId: string, testId: string) => {
await evaluationsStore.deleteMetric({ id: metricId, testDefinitionId: testId });
state.value.metrics = state.value.metrics.filter((metric) => metric.id !== metricId);
};
const updateMetrics = async (testId: string) => {
const promises = state.value.metrics.map(async (metric) => {
if (!metric.name) return;
if (!metric.id) {
const createdMetric = await evaluationsStore.createMetric({
name: metric.name,
testDefinitionId: testId,
});
metric.id = createdMetric.id;
} else {
await evaluationsStore.updateMetric({
name: metric.name,
id: metric.id,
testDefinitionId: testId,
});
}
});
await Promise.all(promises);
};
const updateTest = async (testId: string) => {
if (isSaving.value) return;
isSaving.value = true;
fieldsIssues.value = [];
try {
if (!testId) {
throw new Error('Test ID is required for updating a test');
}
const params: UpdateTestDefinitionParams = {
name: state.value.name.value,
description: state.value.description,
};
if (state.value.evaluationWorkflow.value) {
params.evaluationWorkflowId = state.value.evaluationWorkflow.value.toString();
}
const annotationTagId = state.value.tags.value[0];
if (annotationTagId) {
params.annotationTagId = annotationTagId;
}
if (state.value.mockedNodes.length > 0) {
params.mockedNodes = state.value.mockedNodes;
}
return await evaluationsStore.update({ ...params, id: testId });
} finally {
isSaving.value = false;
}
};
/**
* Start editing an editable field by copying `value` to `tempValue`.
*/
function startEditing<T extends keyof EditableFormState>(field: T) {
const fieldObj = editableFields.value[field];
if (fieldObj.isEditing) {
// Already editing, do nothing
return;
}
if (Array.isArray(fieldObj.value)) {
fieldObj.tempValue = [...fieldObj.value];
} else {
fieldObj.tempValue = fieldObj.value;
}
fieldObj.isEditing = true;
}
/**
* Save changes by copying `tempValue` back into `value`.
*/
function saveChanges<T extends keyof EditableFormState>(field: T) {
const fieldObj = editableFields.value[field];
fieldObj.value = Array.isArray(fieldObj.tempValue)
? [...fieldObj.tempValue]
: fieldObj.tempValue;
fieldObj.isEditing = false;
}
/**
* Cancel editing and revert `tempValue` from `value`.
*/
function cancelEditing<T extends keyof EditableFormState>(field: T) {
const fieldObj = editableFields.value[field];
if (Array.isArray(fieldObj.value)) {
fieldObj.tempValue = [...fieldObj.value];
} else {
fieldObj.tempValue = fieldObj.value;
}
fieldObj.isEditing = false;
}
/**
* Handle keyboard events during editing.
*/
function handleKeydown<T extends keyof EditableFormState>(event: KeyboardEvent, field: T) {
if (event.key === 'Escape') {
cancelEditing(field);
} else if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
saveChanges(field);
}
}
return {
state,
fields,
isSaving: computed(() => isSaving.value),
fieldsIssues: computed(() => fieldsIssues.value),
deleteMetric,
updateMetrics,
loadTestData,
createTest,
updateTest,
startEditing,
saveChanges,
cancelEditing,
handleKeydown,
};
}