mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
* introduce analytics * add user survey backend * add user survey backend * set answers on survey submit Co-authored-by: Mutasem Aldmour <4711238+mutdmour@users.noreply.github.com> * change name to personalization * lint Co-authored-by: Mutasem Aldmour <4711238+mutdmour@users.noreply.github.com> * N8n 2495 add personalization modal (#2280) * update modals * add onboarding modal * implement questions * introduce analytics * simplify impl * implement survey handling * add personalized cateogry * update modal behavior * add thank you view * handle empty cases * rename modal * standarize modal names * update image, add tags to headings * remove unused file * remove unused interfaces * clean up footer spacing * introduce analytics * refactor to fix bug * update endpoint * set min height * update stories * update naming from questions to survey * remove spacing after core categories * fix bug in logic * sort nodes * rename types * merge with be * rename userSurvey * clean up rest api * use constants for keys * use survey keys * clean up types * move personalization to its own file Co-authored-by: ahsan-virani <ahsan.virani@gmail.com> * Survey new options (#2300) * split up options * fix quotes * remove unused import * add user created workflow event (#2301) * simplify env vars * fix versionCli on FE * update personalization env * fix event User opened Credentials panel * fix select modal spacing * fix nodes panel event * fix workflow id in workflow execute event * improve telemetry error logging * fix config and stop process events * add flush call on n8n stop * ready for release * improve telemetry process exit * fix merge * improve n8n stop events Co-authored-by: Mutasem Aldmour <4711238+mutdmour@users.noreply.github.com> Co-authored-by: Mutasem <mutdmour@gmail.com> Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com>
170 lines
5.3 KiB
TypeScript
170 lines
5.3 KiB
TypeScript
import { CORE_NODES_CATEGORY, CUSTOM_NODES_CATEGORY, SUBCATEGORY_DESCRIPTIONS, UNCATEGORIZED_CATEGORY, UNCATEGORIZED_SUBCATEGORY, REGULAR_NODE_FILTER, TRIGGER_NODE_FILTER, ALL_NODE_FILTER, PERSONALIZED_CATEGORY } from '@/constants';
|
|
import { INodeCreateElement, ICategoriesWithNodes, INodeItemProps } from '@/Interface';
|
|
import { INodeTypeDescription } from 'n8n-workflow';
|
|
|
|
const addNodeToCategory = (accu: ICategoriesWithNodes, nodeType: INodeTypeDescription, category: string, subcategory: string) => {
|
|
if (!accu[category]) {
|
|
accu[category] = {};
|
|
}
|
|
if (!accu[category][subcategory]) {
|
|
accu[category][subcategory] = {
|
|
triggerCount: 0,
|
|
regularCount: 0,
|
|
nodes: [],
|
|
};
|
|
}
|
|
const isTrigger = nodeType.group.includes('trigger');
|
|
if (isTrigger) {
|
|
accu[category][subcategory].triggerCount++;
|
|
}
|
|
if (!isTrigger) {
|
|
accu[category][subcategory].regularCount++;
|
|
}
|
|
accu[category][subcategory].nodes.push({
|
|
type: 'node',
|
|
key: `${category}_${nodeType.name}`,
|
|
category,
|
|
properties: {
|
|
nodeType,
|
|
subcategory,
|
|
},
|
|
includedByTrigger: isTrigger,
|
|
includedByRegular: !isTrigger,
|
|
});
|
|
};
|
|
|
|
export const getCategoriesWithNodes = (nodeTypes: INodeTypeDescription[], personalizedNodeTypes: string[]): ICategoriesWithNodes => {
|
|
const sorted = [...nodeTypes].sort((a: INodeTypeDescription, b: INodeTypeDescription) => a.displayName > b.displayName? 1 : -1);
|
|
return sorted.reduce(
|
|
(accu: ICategoriesWithNodes, nodeType: INodeTypeDescription) => {
|
|
if (personalizedNodeTypes.includes(nodeType.name)) {
|
|
addNodeToCategory(accu, nodeType, PERSONALIZED_CATEGORY, UNCATEGORIZED_SUBCATEGORY);
|
|
}
|
|
|
|
if (!nodeType.codex || !nodeType.codex.categories) {
|
|
addNodeToCategory(accu, nodeType, UNCATEGORIZED_CATEGORY, UNCATEGORIZED_SUBCATEGORY);
|
|
return accu;
|
|
}
|
|
|
|
nodeType.codex.categories.forEach((_category: string) => {
|
|
const category = _category.trim();
|
|
const subcategory =
|
|
nodeType.codex &&
|
|
nodeType.codex.subcategories &&
|
|
nodeType.codex.subcategories[category]
|
|
? nodeType.codex.subcategories[category][0]
|
|
: UNCATEGORIZED_SUBCATEGORY;
|
|
|
|
addNodeToCategory(accu, nodeType, category, subcategory);
|
|
});
|
|
return accu;
|
|
},
|
|
{},
|
|
);
|
|
};
|
|
|
|
const getCategories = (categoriesWithNodes: ICategoriesWithNodes): string[] => {
|
|
const excludeFromSort = [CORE_NODES_CATEGORY, CUSTOM_NODES_CATEGORY, UNCATEGORIZED_CATEGORY, PERSONALIZED_CATEGORY];
|
|
const categories = Object.keys(categoriesWithNodes);
|
|
const sorted = categories.filter(
|
|
(category: string) =>
|
|
!excludeFromSort.includes(category),
|
|
);
|
|
sorted.sort();
|
|
|
|
return [CORE_NODES_CATEGORY, CUSTOM_NODES_CATEGORY, PERSONALIZED_CATEGORY, ...sorted, UNCATEGORIZED_CATEGORY];
|
|
};
|
|
|
|
export const getCategorizedList = (categoriesWithNodes: ICategoriesWithNodes): INodeCreateElement[] => {
|
|
const categories = getCategories(categoriesWithNodes);
|
|
|
|
return categories.reduce(
|
|
(accu: INodeCreateElement[], category: string) => {
|
|
if (!categoriesWithNodes[category]) {
|
|
return accu;
|
|
}
|
|
|
|
const categoryEl: INodeCreateElement = {
|
|
type: 'category',
|
|
key: category,
|
|
category,
|
|
properties: {
|
|
expanded: false,
|
|
},
|
|
};
|
|
|
|
const subcategories = Object.keys(categoriesWithNodes[category]);
|
|
if (subcategories.length === 1) {
|
|
const subcategory = categoriesWithNodes[category][
|
|
subcategories[0]
|
|
];
|
|
if (subcategory.triggerCount > 0) {
|
|
categoryEl.includedByTrigger = subcategory.triggerCount > 0;
|
|
}
|
|
if (subcategory.regularCount > 0) {
|
|
categoryEl.includedByRegular = subcategory.regularCount > 0;
|
|
}
|
|
return [...accu, categoryEl, ...subcategory.nodes];
|
|
}
|
|
|
|
subcategories.sort();
|
|
const subcategorized = subcategories.reduce(
|
|
(accu: INodeCreateElement[], subcategory: string) => {
|
|
const subcategoryEl: INodeCreateElement = {
|
|
type: 'subcategory',
|
|
key: `${category}_${subcategory}`,
|
|
category,
|
|
properties: {
|
|
subcategory,
|
|
description: SUBCATEGORY_DESCRIPTIONS[category][subcategory],
|
|
},
|
|
includedByTrigger: categoriesWithNodes[category][subcategory].triggerCount > 0,
|
|
includedByRegular: categoriesWithNodes[category][subcategory].regularCount > 0,
|
|
};
|
|
|
|
if (subcategoryEl.includedByTrigger) {
|
|
categoryEl.includedByTrigger = true;
|
|
}
|
|
if (subcategoryEl.includedByRegular) {
|
|
categoryEl.includedByRegular = true;
|
|
}
|
|
|
|
accu.push(subcategoryEl);
|
|
return accu;
|
|
},
|
|
[],
|
|
);
|
|
|
|
return [...accu, categoryEl, ...subcategorized];
|
|
},
|
|
[],
|
|
);
|
|
};
|
|
|
|
export const matchesSelectType = (el: INodeCreateElement, selectedType: string) => {
|
|
if (selectedType === REGULAR_NODE_FILTER && el.includedByRegular) {
|
|
return true;
|
|
}
|
|
if (selectedType === TRIGGER_NODE_FILTER && el.includedByTrigger) {
|
|
return true;
|
|
}
|
|
|
|
return selectedType === ALL_NODE_FILTER;
|
|
};
|
|
|
|
const matchesAlias = (nodeType: INodeTypeDescription, filter: string): boolean => {
|
|
if (!nodeType.codex || !nodeType.codex.alias) {
|
|
return false;
|
|
}
|
|
|
|
return nodeType.codex.alias.reduce((accu: boolean, alias: string) => {
|
|
return accu || alias.toLowerCase().indexOf(filter) > -1;
|
|
}, false);
|
|
};
|
|
|
|
export const matchesNodeType = (el: INodeCreateElement, filter: string) => {
|
|
const nodeType = (el.properties as INodeItemProps).nodeType;
|
|
|
|
return nodeType.displayName.toLowerCase().indexOf(filter) !== -1 || matchesAlias(nodeType, filter);
|
|
};
|