diff --git a/packages/editor-ui/src/mixins/nodeHelpers.ts b/packages/editor-ui/src/mixins/nodeHelpers.ts index da2bacd563..413cdd1ab0 100644 --- a/packages/editor-ui/src/mixins/nodeHelpers.ts +++ b/packages/editor-ui/src/mixins/nodeHelpers.ts @@ -34,7 +34,7 @@ import type { import { get } from 'lodash-es'; -import { isObjectLiteral } from '@/utils'; +import { isObject } from '@/utils'; import { getCredentialPermissions } from '@/permissions'; import { mapStores } from 'pinia'; import { useSettingsStore } from '@/stores/settings.store'; @@ -65,7 +65,7 @@ export const nodeHelpers = defineComponent({ isCustomApiCallSelected(nodeValues: INodeParameters): boolean { const { parameters } = nodeValues; - if (!isObjectLiteral(parameters)) return false; + if (!isObject(parameters)) return false; return ( (parameters.resource !== undefined && parameters.resource.includes(CUSTOM_API_CALL_KEY)) || diff --git a/packages/editor-ui/src/stores/workflows.store.ts b/packages/editor-ui/src/stores/workflows.store.ts index 2ac885d7f4..2e2195482d 100644 --- a/packages/editor-ui/src/stores/workflows.store.ts +++ b/packages/editor-ui/src/stores/workflows.store.ts @@ -77,7 +77,7 @@ import { isJsonKeyObject, getPairedItemsMapping, stringSizeInBytes, - isObjectLiteral, + isObject, isEmpty, makeRestApiRequest, unflattenExecutionData, @@ -1065,7 +1065,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { const uiStore = useUIStore(); uiStore.stateIsDirty = true; const newParameters = - !!append && isObjectLiteral(updateInformation.value) + !!append && isObject(updateInformation.value) ? { ...node.parameters, ...updateInformation.value } : updateInformation.value; diff --git a/packages/editor-ui/src/utils/__tests__/objectUtils.test.ts b/packages/editor-ui/src/utils/__tests__/objectUtils.test.ts new file mode 100644 index 0000000000..db1e45741d --- /dev/null +++ b/packages/editor-ui/src/utils/__tests__/objectUtils.test.ts @@ -0,0 +1,98 @@ +import { isObjectOrArray, isObject, searchInObject } from '@/utils'; + +const testData = [1, '', true, null, undefined, new Date(), () => {}].map((value) => [ + value, + typeof value, +]); + +describe('objectUtils', () => { + describe('isObjectOrArray', () => { + it('should return true for objects', () => { + assert(isObjectOrArray({})); + }); + + it('should return true for arrays', () => { + assert(isObjectOrArray([])); + }); + + test.each(testData)('should return false for %j (type %s)', (value) => { + assert(!isObjectOrArray(value)); + }); + }); + + describe('isObject', () => { + it('should return true for objects', () => { + assert(isObject({})); + }); + + it('should return false for arrays', () => { + assert(!isObject([])); + }); + + test.each(testData)('should return false for %j (type %s)', (value) => { + assert(!isObject(value)); + }); + }); + + describe('searchInObject', () => { + it('should return true if the search string is found in the object', () => { + assert(searchInObject({ a: 'b' }, 'b')); + }); + + it('should return false if the search string is not found in the object', () => { + assert(!searchInObject({ a: 'b' }, 'c')); + }); + + it('should return true if the search string is not found in the object as a key', () => { + assert(searchInObject({ a: 'b' }, 'a')); + }); + + it('should return true if the search string is found in a nested object', () => { + assert(searchInObject({ a: { b: 'c' } }, 'c')); + }); + + it('should return true if the search string is found in a nested object as a key', () => { + assert(searchInObject({ a: { b: 'c' } }, 'b')); + }); + + it('should return true if the search string is found in an array', () => { + assert(searchInObject(['a', 'b'], 'a')); + }); + + it('should return true if the search string is found in a nested array', () => { + assert(searchInObject(['a', ['b', 'c']], 'c')); + }); + + it('should return false if the search string is not found in an array', () => { + assert(!searchInObject(['a', 'b'], 'c')); + }); + + it('should return false if the search string is not found in a nested array', () => { + assert(!searchInObject(['a', ['b', 'c']], 'd')); + }); + + it('should return true if the search string is found in a nested object in an array', () => { + assert(searchInObject([{ a: 'b' }], 'b')); + }); + + it('should return true if the search string is found in a nested array in an object', () => { + assert(searchInObject({ a: ['b', 'c'] }, 'c')); + }); + + it('should return false if the search string is not found in a nested object in an array', () => { + assert(!searchInObject([{ a: 'b' }], 'c')); + }); + + it('should return false if the search string is not found in a nested array in an object', () => { + assert(!searchInObject({ a: ['b', 'c'] }, 'd')); + }); + + it('should return true if the search string is found in an object as a key in a nested array', () => { + assert(searchInObject({ a: ['b', { c: 'd' }] }, 'c')); + }); + + it('should return true if the search string is found in an object in a nested array', () => { + assert(searchInObject({ a: ['b', { c: 'd' }] }, 'd')); + }); + }); +}); diff --git a/packages/editor-ui/src/utils/index.ts b/packages/editor-ui/src/utils/index.ts index 168b54ee24..a1d66c0b23 100644 --- a/packages/editor-ui/src/utils/index.ts +++ b/packages/editor-ui/src/utils/index.ts @@ -10,3 +10,4 @@ export * from './typesUtils'; export * from './userUtils'; export * from './sourceControlUtils'; export * from './expressions'; +export * from './objectUtils'; diff --git a/packages/editor-ui/src/utils/objectUtils.ts b/packages/editor-ui/src/utils/objectUtils.ts new file mode 100644 index 0000000000..081f796876 --- /dev/null +++ b/packages/editor-ui/src/utils/objectUtils.ts @@ -0,0 +1,20 @@ +type ObjectOrArray = Record | unknown[]; + +export function isDateObject(maybeDate: unknown): maybeDate is Date { + return maybeDate instanceof Date; +} + +export function isObjectOrArray(maybeObject: unknown): maybeObject is ObjectOrArray { + return typeof maybeObject === 'object' && maybeObject !== null && !isDateObject(maybeObject); +} + +export function isObject(maybeObject: unknown): maybeObject is Record { + return isObjectOrArray(maybeObject) && !Array.isArray(maybeObject); +} + +export const searchInObject = (obj: ObjectOrArray, searchString: string): boolean => + (Array.isArray(obj) ? obj : Object.entries(obj)).some((entry) => + isObjectOrArray(entry) + ? searchInObject(entry, searchString) + : entry?.toString().includes(searchString), + ); diff --git a/packages/editor-ui/src/utils/typesUtils.ts b/packages/editor-ui/src/utils/typesUtils.ts index dbf3369d59..82b5e1d016 100644 --- a/packages/editor-ui/src/utils/typesUtils.ts +++ b/packages/editor-ui/src/utils/typesUtils.ts @@ -1,6 +1,7 @@ import dateformat from 'dateformat'; import type { IDataObject } from 'n8n-workflow'; import { jsonParse } from 'n8n-workflow'; +import { isObject } from '@/utils/objectUtils'; /* Constants and utility functions than can be used to manipulate different data types and objects @@ -10,15 +11,11 @@ const SI_SYMBOL = ['', 'k', 'M', 'G', 'T', 'P', 'E']; export const omit = (keyToOmit: string, { [keyToOmit]: _, ...remainder }) => remainder; -export function isObjectLiteral(maybeObject: unknown): maybeObject is { [key: string]: string } { - return typeof maybeObject === 'object' && maybeObject !== null && !Array.isArray(maybeObject); -} - export function isJsonKeyObject(item: unknown): item is { json: unknown; [otherKeys: string]: unknown; } { - if (!isObjectLiteral(item)) return false; + if (!isObject(item)) return false; return Object.keys(item).includes('json'); }