mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-11 12:57:29 -08:00
feat(editor): Allow to create new tags during evaluation edit
This commit is contained in:
parent
0537524c3e
commit
2c4c457348
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue