mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
fix(editor): Improve handling of trailing 'Trigger' in NodeCreator search (#14612)
This commit is contained in:
@@ -2,15 +2,14 @@ import topLevel from './snapshots/toplevel.snapshot.json';
|
|||||||
import { sublimeSearch } from './sublimeSearch';
|
import { sublimeSearch } from './sublimeSearch';
|
||||||
|
|
||||||
describe('sublimeSearch', () => {
|
describe('sublimeSearch', () => {
|
||||||
const testCases = [{ filter: 'agent', expectedOrder: ['Magento 2', 'AI Agent'] }];
|
describe('search finds at least one match', () => {
|
||||||
|
const testCases: Array<[string, string[]]> = [['agent', ['Magento 2', 'AI Agent']]];
|
||||||
|
|
||||||
test.each(testCases)(
|
test.each(testCases)(
|
||||||
'should return results in the correct order for filter "$filter"',
|
'should return at least "$expectedOrder" for filter "$filter"',
|
||||||
({ filter, expectedOrder }) => {
|
(filter, expectedOrder) => {
|
||||||
const results = sublimeSearch(filter, topLevel, [
|
// These match the weights in the production use case
|
||||||
{ key: 'properties.displayName', weight: 1.3 },
|
const results = sublimeSearch(filter, topLevel);
|
||||||
{ key: 'properties.codex.alias', weight: 1 },
|
|
||||||
]);
|
|
||||||
|
|
||||||
const resultNames = results.map((result) => result.item.properties.displayName);
|
const resultNames = results.map((result) => result.item.properties.displayName);
|
||||||
expectedOrder.forEach((expectedName, index) => {
|
expectedOrder.forEach((expectedName, index) => {
|
||||||
@@ -18,4 +17,5 @@ describe('sublimeSearch', () => {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ const LEADING_LETTER_PENALTY = -20; // penalty applied for every letter in str b
|
|||||||
const MAX_LEADING_LETTER_PENALTY = -200; // maximum penalty for leading letters
|
const MAX_LEADING_LETTER_PENALTY = -200; // maximum penalty for leading letters
|
||||||
const UNMATCHED_LETTER_PENALTY = -5;
|
const UNMATCHED_LETTER_PENALTY = -5;
|
||||||
|
|
||||||
|
export const DEFAULT_KEYS = [
|
||||||
|
{ key: 'properties.displayName', weight: 1.3 },
|
||||||
|
{ key: 'properties.codex.alias', weight: 1 },
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if each character in pattern is found sequentially within target
|
* Returns true if each character in pattern is found sequentially within target
|
||||||
* @param {*} pattern string
|
* @param {*} pattern string
|
||||||
@@ -217,7 +222,7 @@ function getValue<T extends object>(obj: T, prop: string): unknown {
|
|||||||
export function sublimeSearch<T extends object>(
|
export function sublimeSearch<T extends object>(
|
||||||
filter: string,
|
filter: string,
|
||||||
data: Readonly<T[]>,
|
data: Readonly<T[]>,
|
||||||
keys: Array<{ key: string; weight: number }>,
|
keys: Array<{ key: string; weight: number }> = DEFAULT_KEYS,
|
||||||
): Array<{ score: number; item: T }> {
|
): Array<{ score: number; item: T }> {
|
||||||
const results = data.reduce((accu: Array<{ score: number; item: T }>, item: T) => {
|
const results = data.reduce((accu: Array<{ score: number; item: T }>, item: T) => {
|
||||||
let values: Array<{ value: string; weight: number }> = [];
|
let values: Array<{ value: string; weight: number }> = [];
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import type { SectionCreateElement } from '@/Interface';
|
import type { SectionCreateElement } from '@/Interface';
|
||||||
import { formatTriggerActionName, groupItemsInSections, sortNodeCreateElements } from './utils';
|
import {
|
||||||
|
formatTriggerActionName,
|
||||||
|
groupItemsInSections,
|
||||||
|
removeTrailingTrigger,
|
||||||
|
sortNodeCreateElements,
|
||||||
|
} from './utils';
|
||||||
import {
|
import {
|
||||||
mockActionCreateElement,
|
mockActionCreateElement,
|
||||||
mockNodeCreateElement,
|
mockNodeCreateElement,
|
||||||
@@ -77,4 +82,25 @@ describe('NodeCreator - utils', () => {
|
|||||||
expect(formatTriggerActionName(actionName)).toEqual(expected);
|
expect(formatTriggerActionName(actionName)).toEqual(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('removeTrailingTrigger', () => {
|
||||||
|
test.each([
|
||||||
|
['Telegram Trigger', 'Telegram'],
|
||||||
|
['Trigger Telegram', 'Trigger Telegram'],
|
||||||
|
['Telegram Tri', 'Telegram'],
|
||||||
|
['Telegram Bot', 'Telegram Bot'],
|
||||||
|
['Tri', 'Tri'],
|
||||||
|
['Trigger', 'Trigger'],
|
||||||
|
['Telegram', 'Telegram'],
|
||||||
|
['Telegram Trigger Bot', 'Telegram Trigger Bot'],
|
||||||
|
['Telegram Trig', 'Telegram'],
|
||||||
|
['Telegram Bot trigger', 'Telegram Bot'],
|
||||||
|
['Telegram TRIGGER', 'Telegram'],
|
||||||
|
['', ''],
|
||||||
|
['Telegram Trigger', 'Telegram Trigger'], // full-width space,
|
||||||
|
['Telegram Trigger ', 'Telegram Trigger'],
|
||||||
|
['Telegram Trigger', 'Telegram'],
|
||||||
|
])('should transform "%s" to "%s"', (input, expected) => {
|
||||||
|
expect(removeTrailingTrigger(input)).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -90,22 +90,34 @@ export function sortNodeCreateElements(nodes: INodeCreateElement[]) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We remove `Trigger` from e.g. `Telegram Trigger` to show it as part of the `Telegram` group,
|
||||||
|
// but still want to show matching results when the user types `Telegram Tri` or `Telegram Trigger`
|
||||||
|
// Ideally this would be handled via metadata, but that is a larger refactor.
|
||||||
|
export function removeTrailingTrigger(searchFilter: string) {
|
||||||
|
const parts = searchFilter.split(' ');
|
||||||
|
if (parts.length > 1 && 'trigger'.startsWith(parts.slice(-1)[0].toLowerCase())) {
|
||||||
|
return parts
|
||||||
|
.slice(0, -1)
|
||||||
|
.filter((x) => x)
|
||||||
|
.join(' ')
|
||||||
|
.trimEnd();
|
||||||
|
}
|
||||||
|
return searchFilter;
|
||||||
|
}
|
||||||
|
|
||||||
export function searchNodes(searchFilter: string, items: INodeCreateElement[]) {
|
export function searchNodes(searchFilter: string, items: INodeCreateElement[]) {
|
||||||
const askAiEnabled = useSettingsStore().isAskAiEnabled;
|
const askAiEnabled = useSettingsStore().isAskAiEnabled;
|
||||||
if (!askAiEnabled) {
|
if (!askAiEnabled) {
|
||||||
items = items.filter((item) => item.key !== AI_TRANSFORM_NODE_TYPE);
|
items = items.filter((item) => item.key !== AI_TRANSFORM_NODE_TYPE);
|
||||||
}
|
}
|
||||||
|
|
||||||
// In order to support the old search we need to remove the 'trigger' part
|
const trimmedFilter = removeTrailingTrigger(searchFilter).toLowerCase();
|
||||||
const trimmedFilter = searchFilter.toLowerCase().replace('trigger', '').trimEnd();
|
|
||||||
// We have a snapshot of this call in sublimeSearch.test.ts to assert practical order for some cases
|
// We have a snapshot of this call in sublimeSearch.test.ts to assert practical order for some cases
|
||||||
// Please kindly update the test call if you modify the weights here, and ideally regenerate the snapshot as described in its file.
|
// Please update the snapshots per the README next to the the snapshots if you modify items significantly.
|
||||||
const result = (
|
const result = (sublimeSearch<INodeCreateElement>(trimmedFilter, items) || []).map(
|
||||||
sublimeSearch<INodeCreateElement>(trimmedFilter, items, [
|
({ item }) => item,
|
||||||
{ key: 'properties.displayName', weight: 1.3 },
|
);
|
||||||
{ key: 'properties.codex.alias', weight: 1 },
|
|
||||||
]) || []
|
|
||||||
).map(({ item }) => item);
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user