feat(editor): Allow to create new tags during evaluation edit

This commit is contained in:
Oleg Ivaniv 2024-12-05 12:00:14 +01:00
parent 0537524c3e
commit 2c4c457348
No known key found for this signature in database
3 changed files with 50 additions and 12 deletions

View file

@ -16,6 +16,8 @@ interface TagsDropdownProps {
allTags: ITag[]; allTags: ITag[];
isLoading: boolean; isLoading: boolean;
tagsById: Record<string, ITag>; tagsById: Record<string, ITag>;
createEnabled?: boolean;
manageEnabled?: boolean;
createTag?: (name: string) => Promise<ITag>; createTag?: (name: string) => Promise<ITag>;
} }
@ -27,6 +29,9 @@ const props = withDefaults(defineProps<TagsDropdownProps>(), {
placeholder: '', placeholder: '',
modelValue: () => [], modelValue: () => [],
eventBus: null, eventBus: null,
createEnabled: true,
manageEnabled: true,
createTag: undefined,
}); });
const emit = defineEmits<{ const emit = defineEmits<{
@ -59,6 +64,24 @@ const appliedTags = computed<string[]>(() => {
return props.modelValue.filter((id: string) => props.tagsById[id]); return props.modelValue.filter((id: string) => props.tagsById[id]);
}); });
const containerClasses = computed(() => {
return { 'tags-container': true, focused: focused.value };
});
const dropdownClasses = computed(() => {
const classes = ['tags-dropdown', 'tags-dropdown-' + dropdownId];
if (props.createEnabled) {
classes.push('tags-dropdown-create-enabled');
}
if (props.manageEnabled) {
classes.push('tags-dropdown-manage-enabled');
}
return classes.join(' ');
});
watch( watch(
() => props.allTags, () => props.allTags,
() => { () => {
@ -189,7 +212,7 @@ onClickOutside(
</script> </script>
<template> <template>
<div ref="container" :class="{ 'tags-container': true, focused }" @keydown.stop> <div ref="container" :class="containerClasses" @keydown.stop>
<N8nSelect <N8nSelect
ref="selectRef" ref="selectRef"
:teleported="true" :teleported="true"
@ -201,14 +224,14 @@ onClickOutside(
multiple multiple
:reserve-keyword="false" :reserve-keyword="false"
loading-text="..." loading-text="..."
:popper-class="['tags-dropdown', 'tags-dropdown-' + dropdownId].join(' ')" :popper-class="dropdownClasses"
data-test-id="tags-dropdown" data-test-id="tags-dropdown"
@update:model-value="onTagsUpdated" @update:model-value="onTagsUpdated"
@visible-change="onVisibleChange" @visible-change="onVisibleChange"
@remove-tag="onRemoveTag" @remove-tag="onRemoveTag"
> >
<N8nOption <N8nOption
v-if="options.length === 0 && filter" v-if="createEnabled && options.length === 0 && filter"
:key="CREATE_KEY" :key="CREATE_KEY"
ref="createRef" ref="createRef"
:value="CREATE_KEY" :value="CREATE_KEY"
@ -220,7 +243,7 @@ onClickOutside(
</span> </span>
</N8nOption> </N8nOption>
<N8nOption v-else-if="options.length === 0" value="message" disabled> <N8nOption v-else-if="options.length === 0" value="message" disabled>
<span>{{ i18n.baseText('tagsDropdown.typeToCreateATag') }}</span> <span v-if="createEnabled">{{ i18n.baseText('tagsDropdown.typeToCreateATag') }}</span>
<span v-if="allTags.length > 0">{{ <span v-if="allTags.length > 0">{{
i18n.baseText('tagsDropdown.noMatchingTagsExist') i18n.baseText('tagsDropdown.noMatchingTagsExist')
}}</span> }}</span>
@ -237,7 +260,7 @@ onClickOutside(
data-test-id="tag" data-test-id="tag"
/> />
<N8nOption :key="MANAGE_KEY" :value="MANAGE_KEY" class="ops manage-tags"> <N8nOption v-if="manageEnabled" :key="MANAGE_KEY" :value="MANAGE_KEY" class="ops manage-tags">
<font-awesome-icon icon="cog" /> <font-awesome-icon icon="cog" />
<span>{{ i18n.baseText('tagsDropdown.manageTags') }}</span> <span>{{ i18n.baseText('tagsDropdown.manageTags') }}</span>
</N8nOption> </N8nOption>
@ -313,7 +336,7 @@ onClickOutside(
} }
} }
&:after { .tags-dropdown-manage-enabled &:after {
content: ' '; content: ' ';
display: block; display: block;
min-height: $--item-height; min-height: $--item-height;

View file

@ -15,6 +15,7 @@ export interface TagsInputProps {
startEditing: (field: string) => void; startEditing: (field: string) => void;
saveChanges: (field: string) => void; saveChanges: (field: string) => void;
cancelEditing: (field: string) => void; cancelEditing: (field: string) => void;
createTag?: (name: string) => Promise<ITag>;
} }
const props = withDefaults(defineProps<TagsInputProps>(), { const props = withDefaults(defineProps<TagsInputProps>(), {
@ -22,6 +23,7 @@ const props = withDefaults(defineProps<TagsInputProps>(), {
isEditing: false, isEditing: false,
appliedTagIds: [], appliedTagIds: [],
}), }),
createTag: undefined,
}); });
const emit = defineEmits<{ 'update:modelValue': [value: TagsInputProps['modelValue']] }>(); const emit = defineEmits<{ 'update:modelValue': [value: TagsInputProps['modelValue']] }>();
@ -70,20 +72,19 @@ function updateTags(tags: string[]) {
v-else v-else
:model-value="modelValue.appliedTagIds" :model-value="modelValue.appliedTagIds"
:placeholder="locale.baseText('executionAnnotationView.chooseOrCreateATag')" :placeholder="locale.baseText('executionAnnotationView.chooseOrCreateATag')"
:create-enabled="false" :create-enabled="modelValue.appliedTagIds.length === 0"
:all-tags="allTags" :all-tags="allTags"
:is-loading="isLoading" :is-loading="isLoading"
:tags-by-id="tagsById" :tags-by-id="tagsById"
data-test-id="workflow-tags-dropdown" data-test-id="workflow-tags-dropdown"
:event-bus="tagsEventBus" :event-bus="tagsEventBus"
:create-tag="createTag"
:manage-enabled="false"
@update:model-value="updateTags" @update:model-value="updateTags"
@esc="cancelEditing('tags')" @esc="cancelEditing('tags')"
@blur="saveChanges('tags')" @blur="saveChanges('tags')"
/> />
</n8n-input-label> </n8n-input-label>
<n8n-text size="small" color="text-light">{{
locale.baseText('testDefinition.edit.tagsHelpText')
}}</n8n-text>
</div> </div>
</template> </template>

View file

@ -24,8 +24,11 @@ const route = useRoute();
const locale = useI18n(); const locale = useI18n();
const { debounce } = useDebounce(); const { debounce } = useDebounce();
const toast = useToast(); const toast = useToast();
const { isLoading, allTags, tagsById, fetchAll } = useAnnotationTagsStore(); const tagsStore = useAnnotationTagsStore();
const isLoading = computed(() => tagsStore.isLoading);
const allTags = computed(() => tagsStore.allTags);
const tagsById = computed(() => tagsStore.tagsById);
const testId = computed(() => props.testId ?? (route.params.testId as string)); const testId = computed(() => props.testId ?? (route.params.testId as string));
const currentWorkflowId = computed(() => route.params.name as string); const currentWorkflowId = computed(() => route.params.name as string);
const buttonLabel = computed(() => const buttonLabel = computed(() =>
@ -48,7 +51,7 @@ const {
} = useTestDefinitionForm(); } = useTestDefinitionForm();
onMounted(async () => { onMounted(async () => {
await fetchAll(); await tagsStore.fetchAll();
if (testId.value) { if (testId.value) {
await loadTestData(testId.value); await loadTestData(testId.value);
} else { } else {
@ -83,6 +86,16 @@ function hasIssues(key: string) {
return fieldsIssues.value.some((issue) => issue.field === key); return fieldsIssues.value.some((issue) => issue.field === key);
} }
async function handleCreateTag(tagName: string) {
try {
const newTag = await tagsStore.create(tagName);
return newTag;
} catch (error) {
toast.showError(error, 'Error', error.message);
throw error;
}
}
watch(() => state.value, debounce(onSaveTest, { debounceTime: 400 }), { deep: true }); watch(() => state.value, debounce(onSaveTest, { debounceTime: 400 }), { deep: true });
</script> </script>
@ -126,6 +139,7 @@ watch(() => state.value, debounce(onSaveTest, { debounceTime: 400 }), { deep: tr
:start-editing="startEditing" :start-editing="startEditing"
:save-changes="saveChanges" :save-changes="saveChanges"
:cancel-editing="cancelEditing" :cancel-editing="cancelEditing"
:create-tag="handleCreateTag"
/> />
</template> </template>
</EvaluationStep> </EvaluationStep>