feat(editor): Migrate debounce mixin to useDebounce composable (no-changelog) (#8244)

This commit is contained in:
Alex Grozav 2024-01-08 14:00:49 +02:00 committed by GitHub
parent 8affdf680d
commit 8c8caac4e8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 136 additions and 106 deletions

View file

@ -17,14 +17,17 @@ import { BREAKPOINT_SM, BREAKPOINT_MD, BREAKPOINT_LG, BREAKPOINT_XL } from '@/co
* xl >= 1920 * xl >= 1920
*/ */
import { debounceHelper } from '@/mixins/debounce';
import { useUIStore } from '@/stores/ui.store'; import { useUIStore } from '@/stores/ui.store';
import { getBannerRowHeight } from '@/utils/htmlUtils'; import { getBannerRowHeight } from '@/utils/htmlUtils';
import { useDebounce } from '@/composables/useDebounce';
export default defineComponent({ export default defineComponent({
name: 'BreakpointsObserver', name: 'BreakpointsObserver',
mixins: [debounceHelper],
props: ['valueXS', 'valueXL', 'valueLG', 'valueMD', 'valueSM', 'valueDefault'], props: ['valueXS', 'valueXL', 'valueLG', 'valueMD', 'valueSM', 'valueDefault'],
setup() {
const { callDebounced } = useDebounce();
return { callDebounced };
},
data() { data() {
return { return {
width: window.innerWidth, width: window.innerWidth,
@ -83,7 +86,7 @@ export default defineComponent({
}, },
methods: { methods: {
onResize() { onResize() {
void this.callDebounced('onResizeEnd', { debounceTime: 50 }); void this.callDebounced(this.onResizeEnd, { debounceTime: 50 });
}, },
async onResizeEnd() { async onResizeEnd() {
this.width = window.innerWidth; this.width = window.innerWidth;

View file

@ -1,6 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, reactive, onBeforeMount, ref } from 'vue'; import { computed, reactive, onBeforeMount, ref } from 'vue';
import debounce from 'lodash/debounce';
import type { import type {
ExecutionFilterType, ExecutionFilterType,
ExecutionFilterMetadata, ExecutionFilterMetadata,
@ -11,10 +10,10 @@ import TagsDropdown from '@/components/TagsDropdown.vue';
import { getObjectKeys, isEmpty } from '@/utils/typesUtils'; import { getObjectKeys, isEmpty } from '@/utils/typesUtils';
import { EnterpriseEditionFeature } from '@/constants'; import { EnterpriseEditionFeature } from '@/constants';
import { useSettingsStore } from '@/stores/settings.store'; import { useSettingsStore } from '@/stores/settings.store';
import { useUsageStore } from '@/stores/usage.store';
import { useUIStore } from '@/stores/ui.store'; import { useUIStore } from '@/stores/ui.store';
import { useTelemetry } from '@/composables/useTelemetry'; import { useTelemetry } from '@/composables/useTelemetry';
import type { Placement } from '@floating-ui/core'; import type { Placement } from '@floating-ui/core';
import { useDebounce } from '@/composables/useDebounce';
export type ExecutionFilterProps = { export type ExecutionFilterProps = {
workflows?: IWorkflowShortResponse[]; workflows?: IWorkflowShortResponse[];
@ -25,8 +24,8 @@ export type ExecutionFilterProps = {
const DATE_TIME_MASK = 'YYYY-MM-DD HH:mm'; const DATE_TIME_MASK = 'YYYY-MM-DD HH:mm';
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
const usageStore = useUsageStore();
const uiStore = useUIStore(); const uiStore = useUIStore();
const { debounce } = useDebounce();
const telemetry = useTelemetry(); const telemetry = useTelemetry();
@ -37,7 +36,9 @@ const props = withDefaults(defineProps<ExecutionFilterProps>(), {
const emit = defineEmits<{ const emit = defineEmits<{
(event: 'filterChanged', value: ExecutionFilterType): void; (event: 'filterChanged', value: ExecutionFilterType): void;
}>(); }>();
const debouncedEmit = debounce(emit, 500); const debouncedEmit = debounce(emit, {
debounceTime: 500,
});
const isCustomDataFilterTracked = ref(false); const isCustomDataFilterTracked = ref(false);
const isAdvancedExecutionFilterEnabled = computed(() => const isAdvancedExecutionFilterEnabled = computed(() =>

View file

@ -58,7 +58,6 @@ import { v4 as uuid } from 'uuid';
import type { Route } from 'vue-router'; import type { Route } from 'vue-router';
import { executionHelpers } from '@/mixins/executionsHelpers'; import { executionHelpers } from '@/mixins/executionsHelpers';
import { range as _range } from 'lodash-es'; import { range as _range } from 'lodash-es';
import { debounceHelper } from '@/mixins/debounce';
import { NO_NETWORK_ERROR_CODE } from '@/utils/apiUtils'; import { NO_NETWORK_ERROR_CODE } from '@/utils/apiUtils';
import { getNodeViewTab } from '@/utils/canvasUtils'; import { getNodeViewTab } from '@/utils/canvasUtils';
import { workflowHelpers } from '@/mixins/workflowHelpers'; import { workflowHelpers } from '@/mixins/workflowHelpers';
@ -69,6 +68,7 @@ import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useTagsStore } from '@/stores/tags.store'; import { useTagsStore } from '@/stores/tags.store';
import { executionFilterToQueryFilter } from '@/utils/executionUtils'; import { executionFilterToQueryFilter } from '@/utils/executionUtils';
import { useExternalHooks } from '@/composables/useExternalHooks'; import { useExternalHooks } from '@/composables/useExternalHooks';
import { useDebounce } from '@/composables/useDebounce';
// Number of execution pages that are fetched before temporary execution card is shown // Number of execution pages that are fetched before temporary execution card is shown
const MAX_LOADING_ATTEMPTS = 5; const MAX_LOADING_ATTEMPTS = 5;
@ -80,12 +80,14 @@ export default defineComponent({
components: { components: {
ExecutionsSidebar, ExecutionsSidebar,
}, },
mixins: [executionHelpers, debounceHelper, workflowHelpers], mixins: [executionHelpers, workflowHelpers],
setup() { setup() {
const externalHooks = useExternalHooks(); const externalHooks = useExternalHooks();
const { callDebounced } = useDebounce();
return { return {
externalHooks, externalHooks,
callDebounced,
...useToast(), ...useToast(),
...useMessage(), ...useMessage(),
}; };
@ -231,7 +233,7 @@ export default defineComponent({
}, },
async onLoadMore(): Promise<void> { async onLoadMore(): Promise<void> {
if (!this.loadingMore) { if (!this.loadingMore) {
await this.callDebounced('loadMore', { debounceTime: 1000 }); await this.callDebounced(this.loadMore, { debounceTime: 1000 });
} }
}, },
async loadMore(limit = 20): Promise<void> { async loadMore(limit = 20): Promise<void> {

View file

@ -89,11 +89,11 @@ import type { IVariableItemSelected } from '@/Interface';
import { EXPRESSIONS_DOCS_URL } from '@/constants'; import { EXPRESSIONS_DOCS_URL } from '@/constants';
import { debounceHelper } from '@/mixins/debounce';
import { useWorkflowsStore } from '@/stores/workflows.store'; import { useWorkflowsStore } from '@/stores/workflows.store';
import { useNDVStore } from '@/stores/ndv.store'; import { useNDVStore } from '@/stores/ndv.store';
import { useExternalHooks } from '@/composables/useExternalHooks'; import { useExternalHooks } from '@/composables/useExternalHooks';
import { createExpressionTelemetryPayload } from '@/utils/telemetryUtils'; import { createExpressionTelemetryPayload } from '@/utils/telemetryUtils';
import { useDebounce } from '@/composables/useDebounce';
import type { Segment } from '@/types/expressions'; import type { Segment } from '@/types/expressions';
@ -104,7 +104,6 @@ export default defineComponent({
ExpressionEditorModalOutput, ExpressionEditorModalOutput,
VariableSelector, VariableSelector,
}, },
mixins: [debounceHelper],
props: { props: {
dialogVisible: { dialogVisible: {
type: Boolean, type: Boolean,
@ -137,8 +136,10 @@ export default defineComponent({
}, },
setup() { setup() {
const externalHooks = useExternalHooks(); const externalHooks = useExternalHooks();
const { callDebounced } = useDebounce();
return { return {
callDebounced,
externalHooks, externalHooks,
}; };
}, },
@ -194,7 +195,9 @@ export default defineComponent({
this.updateDisplayValue(); this.updateDisplayValue();
this.$emit('update:modelValue', this.latestValue); this.$emit('update:modelValue', this.latestValue);
} else { } else {
void this.callDebounced('updateDisplayValue', { debounceTime: 500 }); void this.callDebounced(this.updateDisplayValue, {
debounceTime: 500,
});
} }
}, },

View file

@ -18,7 +18,7 @@ import {
DEFAULT_OPERATOR_VALUE, DEFAULT_OPERATOR_VALUE,
} from './constants'; } from './constants';
import { useI18n } from '@/composables/useI18n'; import { useI18n } from '@/composables/useI18n';
import { useDebounceHelper } from '@/composables/useDebounce'; import { useDebounce } from '@/composables/useDebounce';
import Condition from './Condition.vue'; import Condition from './Condition.vue';
import CombinatorSelect from './CombinatorSelect.vue'; import CombinatorSelect from './CombinatorSelect.vue';
import { resolveParameter } from '@/mixins/workflowHelpers'; import { resolveParameter } from '@/mixins/workflowHelpers';
@ -39,7 +39,7 @@ const emit = defineEmits<{
const i18n = useI18n(); const i18n = useI18n();
const ndvStore = useNDVStore(); const ndvStore = useNDVStore();
const { callDebounced } = useDebounceHelper(); const { callDebounced } = useDebounce();
function createCondition(): FilterConditionValue { function createCondition(): FilterConditionValue {
return { id: uuid(), leftValue: '', rightValue: '', operator: DEFAULT_OPERATOR_VALUE }; return { id: uuid(), leftValue: '', rightValue: '', operator: DEFAULT_OPERATOR_VALUE };

View file

@ -107,7 +107,6 @@ import GiftNotificationIcon from './GiftNotificationIcon.vue';
import { useMessage } from '@/composables/useMessage'; import { useMessage } from '@/composables/useMessage';
import { ABOUT_MODAL_KEY, VERSIONS_MODAL_KEY, VIEWS } from '@/constants'; import { ABOUT_MODAL_KEY, VERSIONS_MODAL_KEY, VIEWS } from '@/constants';
import { userHelpers } from '@/mixins/userHelpers'; import { userHelpers } from '@/mixins/userHelpers';
import { debounceHelper } from '@/mixins/debounce';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { mapStores } from 'pinia'; import { mapStores } from 'pinia';
import { useCloudPlanStore } from '@/stores/cloudPlan.store'; import { useCloudPlanStore } from '@/stores/cloudPlan.store';
@ -123,6 +122,7 @@ import ExecutionsUsage from '@/components/ExecutionsUsage.vue';
import MainSidebarSourceControl from '@/components/MainSidebarSourceControl.vue'; import MainSidebarSourceControl from '@/components/MainSidebarSourceControl.vue';
import { hasPermission } from '@/rbac/permissions'; import { hasPermission } from '@/rbac/permissions';
import { useExternalHooks } from '@/composables/useExternalHooks'; import { useExternalHooks } from '@/composables/useExternalHooks';
import { useDebounce } from '@/composables/useDebounce';
export default defineComponent({ export default defineComponent({
name: 'MainSidebar', name: 'MainSidebar',
@ -131,12 +131,14 @@ export default defineComponent({
ExecutionsUsage, ExecutionsUsage,
MainSidebarSourceControl, MainSidebarSourceControl,
}, },
mixins: [userHelpers, debounceHelper], mixins: [userHelpers],
setup(props, ctx) { setup(props, ctx) {
const externalHooks = useExternalHooks(); const externalHooks = useExternalHooks();
const { callDebounced } = useDebounce();
return { return {
externalHooks, externalHooks,
callDebounced,
...useMessage(), ...useMessage(),
}; };
}, },
@ -497,7 +499,7 @@ export default defineComponent({
return defaultSettingsRoute; return defaultSettingsRoute;
}, },
onResize(event: UIEvent) { onResize(event: UIEvent) {
void this.callDebounced('onResizeEnd', { debounceTime: 100 }, event); void this.callDebounced(this.onResizeEnd, { debounceTime: 100 }, event);
}, },
async onResizeEnd(event: UIEvent) { async onResizeEnd(event: UIEvent) {
const browserWidth = (event.target as Window).outerWidth; const browserWidth = (event.target as Window).outerWidth;

View file

@ -53,10 +53,10 @@ import type { INodeTypeDescription } from 'n8n-workflow';
import PanelDragButton from './PanelDragButton.vue'; import PanelDragButton from './PanelDragButton.vue';
import { LOCAL_STORAGE_MAIN_PANEL_RELATIVE_WIDTH, MAIN_NODE_PANEL_WIDTH } from '@/constants'; import { LOCAL_STORAGE_MAIN_PANEL_RELATIVE_WIDTH, MAIN_NODE_PANEL_WIDTH } from '@/constants';
import { debounceHelper } from '@/mixins/debounce';
import { useNDVStore } from '@/stores/ndv.store'; import { useNDVStore } from '@/stores/ndv.store';
import { ndvEventBus } from '@/event-bus'; import { ndvEventBus } from '@/event-bus';
import NDVFloatingNodes from '@/components/NDVFloatingNodes.vue'; import NDVFloatingNodes from '@/components/NDVFloatingNodes.vue';
import { useDebounce } from '@/composables/useDebounce';
const SIDE_MARGIN = 24; const SIDE_MARGIN = 24;
const SIDE_PANELS_MARGIN = 80; const SIDE_PANELS_MARGIN = 80;
@ -78,7 +78,6 @@ export default defineComponent({
PanelDragButton, PanelDragButton,
NDVFloatingNodes, NDVFloatingNodes,
}, },
mixins: [debounceHelper],
props: { props: {
isDraggable: { isDraggable: {
type: Boolean, type: Boolean,
@ -94,6 +93,11 @@ export default defineComponent({
default: () => ({}), default: () => ({}),
}, },
}, },
setup() {
const { callDebounced } = useDebounce();
return { callDebounced };
},
data(): { data(): {
windowWidth: number; windowWidth: number;
isDragging: boolean; isDragging: boolean;
@ -343,7 +347,7 @@ export default defineComponent({
}, },
onResizeDebounced(data: { direction: string; x: number; width: number }) { onResizeDebounced(data: { direction: string; x: number; width: number }) {
if (this.initialized) { if (this.initialized) {
void this.callDebounced('onResize', { debounceTime: 10, trailing: true }, data); void this.callDebounced(this.onResize, { debounceTime: 10, trailing: true }, data);
} }
}, },
onResize({ direction, x, width }: { direction: string; x: number; width: number }) { onResize({ direction, x, width }: { direction: string; x: number; width: number }) {

View file

@ -176,7 +176,6 @@ import TitledList from '@/components/TitledList.vue';
import { get } from 'lodash-es'; import { get } from 'lodash-es';
import { getTriggerNodeServiceName } from '@/utils/nodeTypesUtils'; import { getTriggerNodeServiceName } from '@/utils/nodeTypesUtils';
import type { INodeUi, XYPosition } from '@/Interface'; import type { INodeUi, XYPosition } from '@/Interface';
import { debounceHelper } from '@/mixins/debounce';
import { useUIStore } from '@/stores/ui.store'; import { useUIStore } from '@/stores/ui.store';
import { useWorkflowsStore } from '@/stores/workflows.store'; import { useWorkflowsStore } from '@/stores/workflows.store';
import { useNDVStore } from '@/stores/ndv.store'; import { useNDVStore } from '@/stores/ndv.store';
@ -187,6 +186,7 @@ import { type ContextMenuTarget, useContextMenu } from '@/composables/useContext
import { useNodeHelpers } from '@/composables/useNodeHelpers'; import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { useExternalHooks } from '@/composables/useExternalHooks'; import { useExternalHooks } from '@/composables/useExternalHooks';
import { usePinnedData } from '@/composables/usePinnedData'; import { usePinnedData } from '@/composables/usePinnedData';
import { useDebounce } from '@/composables/useDebounce';
export default defineComponent({ export default defineComponent({
name: 'Node', name: 'Node',
@ -195,7 +195,7 @@ export default defineComponent({
FontAwesomeIcon, FontAwesomeIcon,
NodeIcon, NodeIcon,
}, },
mixins: [nodeBase, workflowHelpers, debounceHelper], mixins: [nodeBase, workflowHelpers],
props: { props: {
isProductionExecutionPreview: { isProductionExecutionPreview: {
type: Boolean, type: Boolean,
@ -217,8 +217,9 @@ export default defineComponent({
const nodeHelpers = useNodeHelpers(); const nodeHelpers = useNodeHelpers();
const node = workflowsStore.getNodeByName(props.name); const node = workflowsStore.getNodeByName(props.name);
const pinnedData = usePinnedData(node); const pinnedData = usePinnedData(node);
const { callDebounced } = useDebounce();
return { contextMenu, externalHooks, nodeHelpers, pinnedData }; return { contextMenu, externalHooks, nodeHelpers, pinnedData, callDebounced };
}, },
computed: { computed: {
...mapStores(useNodeTypesStore, useNDVStore, useUIStore, useWorkflowsStore), ...mapStores(useNodeTypesStore, useNDVStore, useUIStore, useWorkflowsStore),
@ -679,7 +680,7 @@ export default defineComponent({
}, },
onClick(event: MouseEvent) { onClick(event: MouseEvent) {
void this.callDebounced('onClickDebounced', { debounceTime: 50, trailing: true }, event); void this.callDebounced(this.onClickDebounced, { debounceTime: 50, trailing: true }, event);
}, },
onClickDebounced(event: MouseEvent) { onClickDebounced(event: MouseEvent) {

View file

@ -508,7 +508,6 @@ import { isResourceLocatorValue } from '@/utils/typeGuards';
import { CUSTOM_API_CALL_KEY, HTML_NODE_TYPE, NODES_USING_CODE_NODE_EDITOR } from '@/constants'; import { CUSTOM_API_CALL_KEY, HTML_NODE_TYPE, NODES_USING_CODE_NODE_EDITOR } from '@/constants';
import type { PropType } from 'vue'; import type { PropType } from 'vue';
import { debounceHelper } from '@/mixins/debounce';
import { useWorkflowsStore } from '@/stores/workflows.store'; import { useWorkflowsStore } from '@/stores/workflows.store';
import { useNDVStore } from '@/stores/ndv.store'; import { useNDVStore } from '@/stores/ndv.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import { useNodeTypesStore } from '@/stores/nodeTypes.store';
@ -522,6 +521,7 @@ import { useI18n } from '@/composables/useI18n';
import type { N8nInput } from 'n8n-design-system'; import type { N8nInput } from 'n8n-design-system';
import { isCredentialOnlyNodeType } from '@/utils/credentialOnlyNodes'; import { isCredentialOnlyNodeType } from '@/utils/credentialOnlyNodes';
import { useExternalHooks } from '@/composables/useExternalHooks'; import { useExternalHooks } from '@/composables/useExternalHooks';
import { useDebounce } from '@/composables/useDebounce';
type Picker = { $emit: (arg0: string, arg1: Date) => void }; type Picker = { $emit: (arg0: string, arg1: Date) => void };
@ -540,7 +540,7 @@ export default defineComponent({
ResourceLocator, ResourceLocator,
TextEdit, TextEdit,
}, },
mixins: [workflowHelpers, debounceHelper], mixins: [workflowHelpers],
props: { props: {
additionalExpressionData: { additionalExpressionData: {
type: Object as PropType<IDataObject>, type: Object as PropType<IDataObject>,
@ -612,11 +612,13 @@ export default defineComponent({
const externalHooks = useExternalHooks(); const externalHooks = useExternalHooks();
const i18n = useI18n(); const i18n = useI18n();
const nodeHelpers = useNodeHelpers(); const nodeHelpers = useNodeHelpers();
const { callDebounced } = useDebounce();
return { return {
externalHooks, externalHooks,
i18n, i18n,
nodeHelpers, nodeHelpers,
callDebounced,
}; };
}, },
data() { data() {
@ -1243,7 +1245,7 @@ export default defineComponent({
this.$emit('textInput', parameterData); this.$emit('textInput', parameterData);
}, },
valueChangedDebounced(value: NodeParameterValueType | {} | Date) { valueChangedDebounced(value: NodeParameterValueType | {} | Date) {
void this.callDebounced('valueChanged', { debounceTime: 100 }, value); void this.callDebounced(this.valueChanged, { debounceTime: 100 }, value);
}, },
onUpdateTextInput(value: string) { onUpdateTextInput(value: string) {
this.valueChanged(value); this.valueChanged(value);

View file

@ -148,7 +148,6 @@ import type { IResourceLocatorReqParams, IResourceLocatorResultExpanded } from '
import DraggableTarget from '@/components/DraggableTarget.vue'; import DraggableTarget from '@/components/DraggableTarget.vue';
import ExpressionParameterInput from '@/components/ExpressionParameterInput.vue'; import ExpressionParameterInput from '@/components/ExpressionParameterInput.vue';
import ParameterIssues from '@/components/ParameterIssues.vue'; import ParameterIssues from '@/components/ParameterIssues.vue';
import { debounceHelper } from '@/mixins/debounce';
import { workflowHelpers } from '@/mixins/workflowHelpers'; import { workflowHelpers } from '@/mixins/workflowHelpers';
import { useRootStore } from '@/stores/n8nRoot.store'; import { useRootStore } from '@/stores/n8nRoot.store';
import { useNDVStore } from '@/stores/ndv.store'; import { useNDVStore } from '@/stores/ndv.store';
@ -174,6 +173,7 @@ import { mapStores } from 'pinia';
import type { PropType } from 'vue'; import type { PropType } from 'vue';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import ResourceLocatorDropdown from './ResourceLocatorDropdown.vue'; import ResourceLocatorDropdown from './ResourceLocatorDropdown.vue';
import { useDebounce } from '@/composables/useDebounce';
interface IResourceLocatorQuery { interface IResourceLocatorQuery {
results: INodeListSearchItems[]; results: INodeListSearchItems[];
@ -190,7 +190,7 @@ export default defineComponent({
ParameterIssues, ParameterIssues,
ResourceLocatorDropdown, ResourceLocatorDropdown,
}, },
mixins: [debounceHelper, workflowHelpers], mixins: [workflowHelpers],
props: { props: {
parameter: { parameter: {
type: Object as PropType<INodeProperties>, type: Object as PropType<INodeProperties>,
@ -267,6 +267,11 @@ export default defineComponent({
width: 0, width: 0,
}; };
}, },
setup() {
const { callDebounced } = useDebounce();
return { callDebounced };
},
computed: { computed: {
...mapStores(useNodeTypesStore, useNDVStore, useRootStore, useUIStore, useWorkflowsStore), ...mapStores(useNodeTypesStore, useNDVStore, useRootStore, useUIStore, useWorkflowsStore),
appName(): string { appName(): string {
@ -636,7 +641,10 @@ export default defineComponent({
} }
}, },
loadResourcesDebounced() { loadResourcesDebounced() {
void this.callDebounced('loadResources', { debounceTime: 1000, trailing: true }); void this.callDebounced(this.loadResources, {
debounceTime: 1000,
trailing: true,
});
}, },
setResponse(paramsKey: string, props: Partial<IResourceLocatorQuery>) { setResponse(paramsKey: string, props: Partial<IResourceLocatorQuery>) {
this.cachedResponses = { this.cachedResponses = {

View file

@ -191,13 +191,13 @@ import type { IUser } from '@/Interface';
import PageViewLayout from '@/components/layouts/PageViewLayout.vue'; import PageViewLayout from '@/components/layouts/PageViewLayout.vue';
import PageViewLayoutList from '@/components/layouts/PageViewLayoutList.vue'; import PageViewLayoutList from '@/components/layouts/PageViewLayoutList.vue';
import { EnterpriseEditionFeature } from '@/constants'; import { EnterpriseEditionFeature } from '@/constants';
import { debounceHelper } from '@/mixins/debounce';
import ResourceOwnershipSelect from '@/components/forms/ResourceOwnershipSelect.ee.vue'; import ResourceOwnershipSelect from '@/components/forms/ResourceOwnershipSelect.ee.vue';
import ResourceFiltersDropdown from '@/components/forms/ResourceFiltersDropdown.vue'; import ResourceFiltersDropdown from '@/components/forms/ResourceFiltersDropdown.vue';
import { useSettingsStore } from '@/stores/settings.store'; import { useSettingsStore } from '@/stores/settings.store';
import { useUsersStore } from '@/stores/users.store'; import { useUsersStore } from '@/stores/users.store';
import type { N8nInput, DatatableColumn } from 'n8n-design-system'; import type { N8nInput, DatatableColumn } from 'n8n-design-system';
import { useI18n } from '@/composables/useI18n'; import { useI18n } from '@/composables/useI18n';
import { useDebounce } from '@/composables/useDebounce';
export interface IResource { export interface IResource {
id: string; id: string;
@ -227,7 +227,6 @@ export default defineComponent({
ResourceOwnershipSelect, ResourceOwnershipSelect,
ResourceFiltersDropdown, ResourceFiltersDropdown,
}, },
mixins: [debounceHelper],
props: { props: {
resourceKey: { resourceKey: {
type: String, type: String,
@ -289,9 +288,11 @@ export default defineComponent({
}, },
setup() { setup() {
const i18n = useI18n(); const i18n = useI18n();
const { callDebounced } = useDebounce();
return { return {
i18n, i18n,
callDebounced,
}; };
}, },
data() { data() {
@ -406,7 +407,7 @@ export default defineComponent({
}, },
'filtersModel.search'() { 'filtersModel.search'() {
void this.callDebounced( void this.callDebounced(
'sendFiltersTelemetry', this.sendFiltersTelemetry,
{ debounceTime: 1000, trailing: true }, { debounceTime: 1000, trailing: true },
'search', 'search',
); );

View file

@ -1,8 +1,8 @@
import { vi, describe, it, expect } from 'vitest'; import { vi, describe, it, expect } from 'vitest';
import { useDebounceHelper } from '../useDebounce'; import { useDebounce } from '../useDebounce';
import { render, screen } from '@testing-library/vue'; import { render, screen } from '@testing-library/vue';
describe('useDebounceHelper', () => { describe('useDebounce()', () => {
const debounceTime = 500; const debounceTime = 500;
const TestComponent = { const TestComponent = {
@ -24,7 +24,7 @@ describe('useDebounceHelper', () => {
}, },
setup() { setup() {
vitest.useFakeTimers(); vitest.useFakeTimers();
const { callDebounced } = useDebounceHelper(); const { callDebounced } = useDebounce();
return { return {
callDebounced, callDebounced,
debounceTime, debounceTime,

View file

@ -1,6 +1,6 @@
import { onBeforeUnmount, onMounted, ref } from 'vue'; import { onBeforeUnmount, onMounted, ref } from 'vue';
import { debounce } from 'lodash-es';
import { useClipboard as useClipboardCore } from '@vueuse/core'; import { useClipboard as useClipboardCore } from '@vueuse/core';
import { useDebounce } from '@/composables/useDebounce';
type ClipboardEventFn = (data: string, event?: ClipboardEvent) => void; type ClipboardEventFn = (data: string, event?: ClipboardEvent) => void;
@ -11,6 +11,7 @@ export function useClipboard(
onPaste() {}, onPaste() {},
}, },
) { ) {
const { debounce } = useDebounce();
const { copy, copied, isSupported, text } = useClipboardCore(); const { copy, copied, isSupported, text } = useClipboardCore();
const ignoreClasses = ['el-messsage-box', 'ignore-key-press']; const ignoreClasses = ['el-messsage-box', 'ignore-key-press'];
@ -46,7 +47,9 @@ export function useClipboard(
} }
} }
const debouncedOnPaste = debounce(onPaste, 1000, { leading: true }); const debouncedOnPaste = debounce(onPaste, {
debounceTime: 1000,
});
/** /**
* Initialize copy/paste elements and events * Initialize copy/paste elements and events

View file

@ -1,38 +1,54 @@
import { ref } from 'vue'; import { ref } from 'vue';
import { debounce } from 'lodash-es'; import { debounce as _debounce } from 'lodash-es';
type DebouncedFunction = (...args: unknown[]) => Promise<void> | void; export interface DebounceOptions {
debounceTime: number;
trailing?: boolean;
}
export function useDebounceHelper() { export type DebouncedFunction<R = void> = (...args: unknown[]) => R;
export function useDebounce() {
// Create a ref for the WeakMap to store debounced functions. // Create a ref for the WeakMap to store debounced functions.
const debouncedFunctions = ref(new WeakMap<DebouncedFunction, DebouncedFunction>()); const debounceCache = ref(new WeakMap<DebouncedFunction<unknown>, DebouncedFunction<unknown>>());
const callDebounced = async ( const debounce = <T extends DebouncedFunction<ReturnType<T>>>(
func: DebouncedFunction, fn: T,
options: { debounceTime: number; trailing?: boolean }, options: DebounceOptions,
...inputParameters: unknown[] ): T => {
): Promise<void> => {
const { trailing, debounceTime } = options; const { trailing, debounceTime } = options;
// Check if a debounced version of the function is already stored in the WeakMap. // Check if a debounced version of the function is already stored in the WeakMap.
let debouncedFunc = debouncedFunctions.value.get(func); let debouncedFn = debounceCache.value.get(fn);
// If a debounced version is not found, create one and store it in the WeakMap. // If a debounced version is not found, create one and store it in the WeakMap.
if (debouncedFunc === undefined) { if (debouncedFn === undefined) {
debouncedFunc = debounce( debouncedFn = _debounce(
async (...args: unknown[]) => { async (...args: unknown[]) => {
await func(...args); return fn(...args);
}, },
debounceTime, debounceTime,
trailing ? { trailing } : { leading: true }, trailing ? { trailing } : { leading: true },
); );
debouncedFunctions.value.set(func, debouncedFunc);
debounceCache.value.set(fn, debouncedFn);
} }
await debouncedFunc(...inputParameters); return debouncedFn as T;
};
const callDebounced = <T extends DebouncedFunction<ReturnType<T>>>(
fn: T,
options: DebounceOptions,
...inputParameters: Parameters<T>
): ReturnType<T> => {
const debouncedFn = debounce(fn, options);
return debouncedFn(...inputParameters) as ReturnType<T>;
}; };
return { return {
debounce,
callDebounced, callDebounced,
}; };
} }

View file

@ -6,11 +6,11 @@ import { useHistoryStore } from '@/stores/history.store';
import { useUIStore } from '@/stores/ui.store'; import { useUIStore } from '@/stores/ui.store';
import { onMounted, onUnmounted, nextTick } from 'vue'; import { onMounted, onUnmounted, nextTick } from 'vue';
import { useDebounceHelper } from './useDebounce';
import { useDeviceSupport } from 'n8n-design-system/composables/useDeviceSupport'; import { useDeviceSupport } from 'n8n-design-system/composables/useDeviceSupport';
import { getNodeViewTab } from '@/utils/canvasUtils'; import { getNodeViewTab } from '@/utils/canvasUtils';
import type { Route } from 'vue-router'; import type { Route } from 'vue-router';
import { useTelemetry } from './useTelemetry'; import { useTelemetry } from './useTelemetry';
import { useDebounce } from '@/composables/useDebounce';
const UNDO_REDO_DEBOUNCE_INTERVAL = 100; const UNDO_REDO_DEBOUNCE_INTERVAL = 100;
const ELEMENT_UI_OVERLAY_SELECTOR = '.el-overlay'; const ELEMENT_UI_OVERLAY_SELECTOR = '.el-overlay';
@ -22,7 +22,7 @@ export function useHistoryHelper(activeRoute: Route) {
const historyStore = useHistoryStore(); const historyStore = useHistoryStore();
const uiStore = useUIStore(); const uiStore = useUIStore();
const { callDebounced } = useDebounceHelper(); const { callDebounced } = useDebounce();
const { isCtrlKeyPressed } = useDeviceSupport(); const { isCtrlKeyPressed } = useDeviceSupport();
const undo = async () => const undo = async () =>

View file

@ -1,30 +0,0 @@
import { debounce } from 'lodash-es';
import { defineComponent } from 'vue';
export const debounceHelper = defineComponent({
data() {
return {
debouncedFunctions: {} as Record<string, (...args: unknown[]) => Promise<void> | void>,
};
},
methods: {
async callDebounced(
functionName: string,
options: { debounceTime: number; trailing?: boolean },
...inputParameters: unknown[]
): Promise<void> {
const { trailing, debounceTime } = options;
if (this.debouncedFunctions[functionName] === undefined) {
this.debouncedFunctions[functionName] = debounce(
async (...args: unknown[]) => {
// @ts-ignore
await this[functionName](...args);
},
debounceTime,
trailing ? { trailing } : { leading: true },
);
}
await this.debouncedFunctions[functionName](...inputParameters);
},
},
});

View file

@ -8,7 +8,7 @@ import { useSettingsStore } from '@/stores/settings.store';
import type { FeatureFlags, IDataObject } from 'n8n-workflow'; import type { FeatureFlags, IDataObject } from 'n8n-workflow';
import { EXPERIMENTS_TO_TRACK, LOCAL_STORAGE_EXPERIMENT_OVERRIDES } from '@/constants'; import { EXPERIMENTS_TO_TRACK, LOCAL_STORAGE_EXPERIMENT_OVERRIDES } from '@/constants';
import { useTelemetryStore } from './telemetry.store'; import { useTelemetryStore } from './telemetry.store';
import { debounce } from 'lodash-es'; import { useDebounce } from '@/composables/useDebounce';
const EVENTS = { const EVENTS = {
IS_PART_OF_EXPERIMENT: 'User is part of experiment', IS_PART_OF_EXPERIMENT: 'User is part of experiment',
@ -21,6 +21,7 @@ export const usePostHog = defineStore('posthog', () => {
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
const telemetryStore = useTelemetryStore(); const telemetryStore = useTelemetryStore();
const rootStore = useRootStore(); const rootStore = useRootStore();
const { debounce } = useDebounce();
const featureFlags: Ref<FeatureFlags | null> = ref(null); const featureFlags: Ref<FeatureFlags | null> = ref(null);
const trackedDemoExp: Ref<FeatureFlags> = ref({}); const trackedDemoExp: Ref<FeatureFlags> = ref({});
@ -116,7 +117,9 @@ export const usePostHog = defineStore('posthog', () => {
const trackExperiments = (featFlags: FeatureFlags) => { const trackExperiments = (featFlags: FeatureFlags) => {
EXPERIMENTS_TO_TRACK.forEach((name) => trackExperiment(featFlags, name)); EXPERIMENTS_TO_TRACK.forEach((name) => trackExperiment(featFlags, name));
}; };
const trackExperimentsDebounced = debounce(trackExperiments, 2000); const trackExperimentsDebounced = debounce(trackExperiments, {
debounceTime: 2000,
});
const init = (evaluatedFeatureFlags?: FeatureFlags) => { const init = (evaluatedFeatureFlags?: FeatureFlags) => {
if (!window.posthog) { if (!window.posthog) {

View file

@ -315,7 +315,6 @@ import type {
ToggleNodeCreatorOptions, ToggleNodeCreatorOptions,
} from '@/Interface'; } from '@/Interface';
import { debounceHelper } from '@/mixins/debounce';
import type { Route, RawLocation } from 'vue-router'; import type { Route, RawLocation } from 'vue-router';
import { dataPinningEventBus, nodeViewEventBus } from '@/event-bus'; import { dataPinningEventBus, nodeViewEventBus } from '@/event-bus';
import { useCanvasStore } from '@/stores/canvas.store'; import { useCanvasStore } from '@/stores/canvas.store';
@ -377,6 +376,7 @@ import { useExternalHooks } from '@/composables/useExternalHooks';
import { useClipboard } from '@/composables/useClipboard'; import { useClipboard } from '@/composables/useClipboard';
import { usePinnedData } from '@/composables/usePinnedData'; import { usePinnedData } from '@/composables/usePinnedData';
import { useSourceControlStore } from '@/stores/sourceControl.store'; import { useSourceControlStore } from '@/stores/sourceControl.store';
import { useDebounce } from '@/composables/useDebounce';
interface AddNodeOptions { interface AddNodeOptions {
position?: XYPosition; position?: XYPosition;
@ -404,7 +404,7 @@ export default defineComponent({
ContextMenu, ContextMenu,
SetupWorkflowCredentialsButton, SetupWorkflowCredentialsButton,
}, },
mixins: [moveNodeWorkflow, workflowHelpers, workflowRun, debounceHelper], mixins: [moveNodeWorkflow, workflowHelpers, workflowRun],
async beforeRouteLeave(to, from, next) { async beforeRouteLeave(to, from, next) {
if ( if (
getNodeViewTab(to) === MAIN_HEADER_TABS.EXECUTIONS || getNodeViewTab(to) === MAIN_HEADER_TABS.EXECUTIONS ||
@ -475,6 +475,7 @@ export default defineComponent({
const clipboard = useClipboard(); const clipboard = useClipboard();
const { activeNode } = storeToRefs(ndvStore); const { activeNode } = storeToRefs(ndvStore);
const pinnedData = usePinnedData(activeNode); const pinnedData = usePinnedData(activeNode);
const { callDebounced } = useDebounce();
return { return {
locale, locale,
@ -484,6 +485,7 @@ export default defineComponent({
externalHooks, externalHooks,
clipboard, clipboard,
pinnedData, pinnedData,
callDebounced,
...useCanvasMouseSelect(), ...useCanvasMouseSelect(),
...useGlobalLinkActions(), ...useGlobalLinkActions(),
...useTitleChange(), ...useTitleChange(),
@ -1422,7 +1424,7 @@ export default defineComponent({
return; return;
} }
void this.callDebounced('onSaveKeyboardShortcut', { debounceTime: 1000 }, e); void this.callDebounced(this.onSaveKeyboardShortcut, { debounceTime: 1000 }, e);
return; return;
} }
@ -1467,7 +1469,7 @@ export default defineComponent({
.filter((node) => !!node) as INode[]; .filter((node) => !!node) as INode[];
if (e.key === 'd' && noModifierKeys && !readOnly) { if (e.key === 'd' && noModifierKeys && !readOnly) {
void this.callDebounced('toggleActivationNodes', { debounceTime: 350 }, selectedNodes); void this.callDebounced(this.toggleActivationNodes, { debounceTime: 350 }, selectedNodes);
} else if (e.key === 'd' && ctrlModifier && !readOnly) { } else if (e.key === 'd' && ctrlModifier && !readOnly) {
if (selectedNodes.length > 0) { if (selectedNodes.length > 0) {
e.preventDefault(); e.preventDefault();
@ -1482,7 +1484,7 @@ export default defineComponent({
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
void this.callDebounced('deleteNodes', { debounceTime: 500 }, selectedNodes); void this.callDebounced(this.deleteNodes, { debounceTime: 500 }, selectedNodes);
} else if (e.key === 'Tab' && noModifierKeys && !readOnly) { } else if (e.key === 'Tab' && noModifierKeys && !readOnly) {
this.onToggleNodeCreator({ this.onToggleNodeCreator({
source: NODE_CREATOR_OPEN_SOURCES.TAB, source: NODE_CREATOR_OPEN_SOURCES.TAB,
@ -1500,7 +1502,7 @@ export default defineComponent({
const lastSelectedNode = this.lastSelectedNode; const lastSelectedNode = this.lastSelectedNode;
if (lastSelectedNode !== null && lastSelectedNode.type !== STICKY_NODE_TYPE) { if (lastSelectedNode !== null && lastSelectedNode.type !== STICKY_NODE_TYPE) {
void this.callDebounced( void this.callDebounced(
'renameNodePrompt', this.renameNodePrompt,
{ debounceTime: 1500 }, { debounceTime: 1500 },
lastSelectedNode.name, lastSelectedNode.name,
); );
@ -1510,15 +1512,15 @@ export default defineComponent({
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
void this.callDebounced('selectAllNodes', { debounceTime: 1000 }); void this.callDebounced(this.selectAllNodes, { debounceTime: 1000 });
} else if (e.key === 'c' && ctrlModifier) { } else if (e.key === 'c' && ctrlModifier) {
void this.callDebounced('copyNodes', { debounceTime: 1000 }, selectedNodes); void this.callDebounced(this.copyNodes, { debounceTime: 1000 }, selectedNodes);
} else if (e.key === 'x' && ctrlModifier && !readOnly) { } else if (e.key === 'x' && ctrlModifier && !readOnly) {
// Cut nodes // Cut nodes
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
void this.callDebounced('cutNodes', { debounceTime: 1000 }, selectedNodes); void this.callDebounced(this.cutNodes, { debounceTime: 1000 }, selectedNodes);
} else if (e.key === 'n' && ctrlAltModifier) { } else if (e.key === 'n' && ctrlAltModifier) {
// Create a new workflow // Create a new workflow
e.stopPropagation(); e.stopPropagation();
@ -1555,7 +1557,9 @@ export default defineComponent({
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
void this.callDebounced('selectDownstreamNodes', { debounceTime: 1000 }); void this.callDebounced(this.selectDownstreamNodes, {
debounceTime: 1000,
});
} else if (e.key === 'ArrowRight' && noModifierKeys) { } else if (e.key === 'ArrowRight' && noModifierKeys) {
// Set child node active // Set child node active
const lastSelectedNode = this.lastSelectedNode; const lastSelectedNode = this.lastSelectedNode;
@ -1572,7 +1576,7 @@ export default defineComponent({
} }
void this.callDebounced( void this.callDebounced(
'nodeSelectedByName', this.nodeSelectedByName,
{ debounceTime: 100 }, { debounceTime: 100 },
connections.main[0][0].node, connections.main[0][0].node,
false, false,
@ -1583,7 +1587,9 @@ export default defineComponent({
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
void this.callDebounced('selectUpstreamNodes', { debounceTime: 1000 }); void this.callDebounced(this.selectUpstreamNodes, {
debounceTime: 1000,
});
} else if (e.key === 'ArrowLeft' && noModifierKeys) { } else if (e.key === 'ArrowLeft' && noModifierKeys) {
// Set parent node active // Set parent node active
const lastSelectedNode = this.lastSelectedNode; const lastSelectedNode = this.lastSelectedNode;
@ -1604,7 +1610,7 @@ export default defineComponent({
} }
void this.callDebounced( void this.callDebounced(
'nodeSelectedByName', this.nodeSelectedByName,
{ debounceTime: 100 }, { debounceTime: 100 },
connections.main[0][0].node, connections.main[0][0].node,
false, false,
@ -1676,7 +1682,7 @@ export default defineComponent({
if (nextSelectNode !== null) { if (nextSelectNode !== null) {
void this.callDebounced( void this.callDebounced(
'nodeSelectedByName', this.nodeSelectedByName,
{ debounceTime: 100 }, { debounceTime: 100 },
nextSelectNode, nextSelectNode,
false, false,
@ -3033,7 +3039,7 @@ export default defineComponent({
}, },
onDragMove() { onDragMove() {
const totalNodes = this.nodes.length; const totalNodes = this.nodes.length;
void this.callDebounced('updateConnectionsOverlays', { void this.callDebounced(this.updateConnectionsOverlays, {
debounceTime: totalNodes > 20 ? 200 : 0, debounceTime: totalNodes > 20 ? 200 : 0,
}); });
}, },

View file

@ -93,13 +93,13 @@ import type {
import type { IDataObject } from 'n8n-workflow'; import type { IDataObject } from 'n8n-workflow';
import { setPageTitle } from '@/utils/htmlUtils'; import { setPageTitle } from '@/utils/htmlUtils';
import { CREATOR_HUB_URL, VIEWS } from '@/constants'; import { CREATOR_HUB_URL, VIEWS } from '@/constants';
import { debounceHelper } from '@/mixins/debounce';
import { useSettingsStore } from '@/stores/settings.store'; import { useSettingsStore } from '@/stores/settings.store';
import { useUsersStore } from '@/stores/users.store'; import { useUsersStore } from '@/stores/users.store';
import { useTemplatesStore } from '@/stores/templates.store'; import { useTemplatesStore } from '@/stores/templates.store';
import { useUIStore } from '@/stores/ui.store'; import { useUIStore } from '@/stores/ui.store';
import { useToast } from '@/composables/useToast'; import { useToast } from '@/composables/useToast';
import { usePostHog } from '@/stores/posthog.store'; import { usePostHog } from '@/stores/posthog.store';
import { useDebounce } from '@/composables/useDebounce';
interface ISearchEvent { interface ISearchEvent {
search_string: string; search_string: string;
@ -117,9 +117,11 @@ export default defineComponent({
TemplateList, TemplateList,
TemplatesView, TemplatesView,
}, },
mixins: [debounceHelper],
setup() { setup() {
const { callDebounced } = useDebounce();
return { return {
callDebounced,
...useToast(), ...useToast(),
}; };
}, },
@ -263,7 +265,10 @@ export default defineComponent({
this.loadingWorkflows = true; this.loadingWorkflows = true;
this.loadingCollections = true; this.loadingCollections = true;
this.search = search; this.search = search;
void this.callDebounced('updateSearch', { debounceTime: 500, trailing: true }); void this.callDebounced(this.updateSearch, {
debounceTime: 500,
trailing: true,
});
if (search.length === 0) { if (search.length === 0) {
this.trackSearch(); this.trackSearch();