mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-12 05:17:28 -08:00
feat(editor): Schema view (#4615)
* feat(editor): Generate custom schema from data (#4562) * feat(core): adding a type package to n8n * feat(editor): adding custom schema generator * fix: add new types package to lock file * fix: remove n8n_io/types package * fix: adding path to generated schema * fix: handling nested lists in schema generation * fix: add date support to schema generation * fix: define dates in ISO format * fix: using test instead of it in repeated tests * fix(editor): JSON schema treat nested lists as object to allow mapping each level * fix(editor): rename JSON schema type * fix(editor): make JSON schema path required * fix(editor): using JSON schema bracket notation for object props to handle exceptional keys * fix(editor): reorder JSON schema generator function args * feat(editor): Add date recognizer util function (#4620) * ✨ Implemented date recogniser fuction * ✅ Added unit tests for date recogniser * ✔️ Fixing linting errors * 👌 Updating test cases * feat(editor): Implement JSON Schema view UI functionalities (#4601) * feat(core): adding a type package to n8n * feat(editor): adding custom schema generator * fix: add new types package to lock file * fix: remove n8n_io/types package * fix: adding path to generated schema * fix: handling nested lists in schema generation * fix: add date support to schema generation * fix: define dates in ISO format * fix: using test instead of it in repeated tests * fix(editor): JSON schema treat nested lists as object to allow mapping each level * fix(editor): rename JSON schema type * fix(editor): make JSON schema path required * fix(editor): using JSON schema bracket notation for object props to handle exceptional keys * fix(editor): reorder JSON schema generator function args * fix(editor): WIP json schema view * fix(editor): formatting fix * fix(editor): WIP json schema viewer * fix(editor): fix schema generator and add deep merge * fix(editor): WIP update json schema view components * fix(editor): extend valid date checking * fix(editor): WIP improving JSON schema view * chore(editor): code formatting * feat(editor): WIP Json schema view mapping + animations * feat(editor): WIP update mergeDeep * feat(editor): adding first item of json data to the end once more to get sample data from the first item * feat(editor): adding first item of json data to the end once more to get sample data from the first item * fix(editor): improving draggable design * fix(editor): move util functions to their correct place after merge conflict * fix(editor): move some type guards * fix(editor): move some type guards * fix(editor): change import path in unit test * fix(editor): import missing interface * fix(editor): remove unused functions and parts from json schema generation * feat(editor): Add telemetry calls to JSON schema mapping (#4695) * feat(editor): WIP JSON schema telemetry call * feat(editor): make telemetry usable outside of Vue component context * chore(editor): remove unused variable * Merge branch 'feature/json-schema-view' of github.com:n8n-io/n8n into n8n-5410-add-telemetry-calls # Conflicts: # packages/editor-ui/src/components/RunDataJsonSchema.vue * fix(editor): VUE typing for telemetry * fix(editor): enable PostHog feature flag * fix(editor): Schema design review (#4740) * refactor(editor): rename JsonSchema to Schema * fix(editor): schema component name * fix(editor): schema pill style * fix(editor): schema type date as string * fix(editor): schema styles (support long text + firefox) * fix(editor): schema truncate text if it's too long * fix(editor): schema types * fix(editor): droppable styles * fix(editor): schema component props * fix(editor): fix draggable pill styles * fix(editor): schema view styles * fix(editor): schema mapping tooltip * fix(editor): schema mapping styles * fix(editor): mapping styles * fix(editor): empty schema case * fix(editor): delay mapping tooltip * test(editor): add schema view snapshot test * fix(editor): schema empty string * fix(editor): schema string without space * fix(editor): update schema test snapshot * fix(editor): applying review comments * fix(editor): make n8nExternalHooks optional * fix(editor): remove TODO comment Co-authored-by: Milorad FIlipović <milorad@n8n.io>
This commit is contained in:
parent
9485e2f12a
commit
4528f34462
BIN
packages/editor-ui/public/static/schema-mapping-gif.gif
Normal file
BIN
packages/editor-ui/public/static/schema-mapping-gif.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 464 KiB |
|
@ -1026,8 +1026,8 @@ export interface NestedRecord<T> {
|
|||
[key: string]: T | NestedRecord<T>;
|
||||
}
|
||||
|
||||
export type IRunDataDisplayMode = 'table' | 'json' | 'binary';
|
||||
export type nodePanelType = 'input' | 'output';
|
||||
export type IRunDataDisplayMode = 'table' | 'json' | 'binary' | 'schema';
|
||||
export type NodePanelType = 'input' | 'output';
|
||||
|
||||
export interface TargetItem {
|
||||
nodeName: string;
|
||||
|
@ -1290,3 +1290,21 @@ export interface CurlToJSONResponse {
|
|||
"parameters.sendQuery": boolean;
|
||||
"parameters.sendBody": boolean;
|
||||
}
|
||||
|
||||
export type Basic = string | number | boolean;
|
||||
export type Primitives = Basic | bigint | symbol;
|
||||
|
||||
export type Optional<T> = T | undefined | null;
|
||||
|
||||
export type SchemaType =
|
||||
| 'string'
|
||||
| 'number'
|
||||
| 'boolean'
|
||||
| 'bigint'
|
||||
| 'symbol'
|
||||
| 'array'
|
||||
| 'object'
|
||||
| 'function'
|
||||
| 'null'
|
||||
| 'undefined';
|
||||
export type Schema = { type: SchemaType, key?: string, value: string | Schema[], path: string };
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import '@testing-library/jest-dom';
|
||||
import Vue from 'vue';
|
||||
import '../plugins';
|
||||
|
||||
Vue.config.productionTip = false;
|
||||
Vue.config.devtools = false;
|
||||
|
|
|
@ -90,7 +90,11 @@ export default Vue.extend({
|
|||
}
|
||||
|
||||
this.draggingEl = e.target as HTMLElement;
|
||||
if (this.targetDataKey && this.draggingEl && this.draggingEl.dataset.target !== this.targetDataKey) {
|
||||
if (this.targetDataKey && this.draggingEl.dataset?.target !== this.targetDataKey) {
|
||||
this.draggingEl = this.draggingEl.closest(`[data-target="${this.targetDataKey}"]`) as HTMLElement;
|
||||
}
|
||||
|
||||
if (this.targetDataKey && this.draggingEl?.dataset?.target !== this.targetDataKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -51,7 +51,7 @@ import mixins from 'vue-typed-mixins';
|
|||
import { debounceHelper } from '@/mixins/debounce';
|
||||
import { mapStores } from 'pinia';
|
||||
import { useNDVStore } from '@/stores/ndv';
|
||||
import { nodePanelType } from '@/Interface';
|
||||
import { NodePanelType } from '@/Interface';
|
||||
|
||||
|
||||
const SIDE_MARGIN = 24;
|
||||
|
@ -127,7 +127,7 @@ export default mixins(debounceHelper).extend({
|
|||
relativeLeft: number,
|
||||
relativeRight: number
|
||||
} {
|
||||
return this.ndvStore.getMainPanelDimensions(this.currentNodePaneType as nodePanelType);
|
||||
return this.ndvStore.getMainPanelDimensions(this.currentNodePaneType);
|
||||
},
|
||||
supportedResizeDirections(): string[] {
|
||||
const supportedDirections = ['right'];
|
||||
|
@ -278,7 +278,7 @@ export default mixins(debounceHelper).extend({
|
|||
|
||||
if(isMaxRight) {
|
||||
this.ndvStore.setMainPanelDimensions({
|
||||
panelType: this.currentNodePaneType as nodePanelType,
|
||||
panelType: this.currentNodePaneType,
|
||||
dimensions: {
|
||||
relativeLeft: 1 - this.mainPanelDimensions.relativeWidth - this.maximumRightPosition,
|
||||
relativeRight: this.maximumRightPosition as number,
|
||||
|
@ -288,7 +288,7 @@ export default mixins(debounceHelper).extend({
|
|||
}
|
||||
|
||||
this.ndvStore.setMainPanelDimensions({
|
||||
panelType: this.currentNodePaneType as nodePanelType,
|
||||
panelType: this.currentNodePaneType,
|
||||
dimensions: {
|
||||
relativeLeft: isInputless ? this.minimumLeftPosition : mainPanelRelativeLeft,
|
||||
relativeRight: mainPanelRelativeRight,
|
||||
|
|
|
@ -1165,18 +1165,23 @@ export default mixins(
|
|||
|
||||
|
||||
.droppable {
|
||||
--input-border-color: var(--color-secondary-tint-1);
|
||||
--input-background-color: var(--color-secondary-tint-3);
|
||||
--input-border-color: var(--color-secondary);
|
||||
--input-background-color: var(--color-foreground-xlight);
|
||||
--input-border-style: dashed;
|
||||
|
||||
textarea, input {
|
||||
border-width: 1.5px;
|
||||
}
|
||||
}
|
||||
|
||||
.activeDrop {
|
||||
--input-border-color: var(--color-success);
|
||||
--input-background-color: var(--color-success-tint-2);
|
||||
--input-background-color: var(--color-foreground-xlight);
|
||||
--input-border-style: solid;
|
||||
|
||||
textarea, input {
|
||||
cursor: grabbing !important;
|
||||
border-width: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
type="mapping"
|
||||
:disabled="isDropDisabled"
|
||||
:sticky="true"
|
||||
:stickyOffset="4"
|
||||
:stickyOffset="3"
|
||||
@drop="onDrop"
|
||||
>
|
||||
<template #default="{ droppable, activeDrop }">
|
||||
|
@ -98,6 +98,7 @@ export default mixins(
|
|||
menuExpanded: false,
|
||||
forceShowExpression: false,
|
||||
dataMappingTooltipButtons: [] as IN8nButton[],
|
||||
mappingTooltipEnabled: false,
|
||||
};
|
||||
},
|
||||
props: {
|
||||
|
@ -165,18 +166,22 @@ export default mixins(
|
|||
return this.ndvStore.inputPanelDisplayMode;
|
||||
},
|
||||
showMappingTooltip (): boolean {
|
||||
return this.focused && this.isInputTypeString && !this.isInputDataEmpty && window.localStorage.getItem(LOCAL_STORAGE_MAPPING_FLAG) !== 'true';
|
||||
return this.mappingTooltipEnabled && this.focused && this.isInputTypeString && !this.isInputDataEmpty && window.localStorage.getItem(LOCAL_STORAGE_MAPPING_FLAG) !== 'true';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onFocus() {
|
||||
this.focused = true;
|
||||
setTimeout(() => {
|
||||
this.mappingTooltipEnabled = true;
|
||||
}, 500);
|
||||
if (!this.parameter.noDataExpression) {
|
||||
this.ndvStore.setMappableNDVInputFocus(this.parameter.displayName);
|
||||
}
|
||||
},
|
||||
onBlur() {
|
||||
this.focused = false;
|
||||
this.mappingTooltipEnabled = false;
|
||||
if (!this.parameter.noDataExpression) {
|
||||
this.ndvStore.setMappableNDVInputFocus('');
|
||||
}
|
||||
|
|
|
@ -121,7 +121,7 @@
|
|||
<slot name="run-info"></slot>
|
||||
</div>
|
||||
|
||||
<div v-if="maxOutputIndex > 0 && branches.length > 1" :class="{[$style.tabs]: displayMode === 'table'}">
|
||||
<div v-if="maxOutputIndex > 0 && branches.length > 1" :class="$style.tabs">
|
||||
<n8n-tabs :value="currentOutputIndex" @input="onBranchChange" :options="branches" />
|
||||
</div>
|
||||
|
||||
|
@ -249,6 +249,16 @@
|
|||
:totalRuns="maxRunIndex"
|
||||
/>
|
||||
|
||||
<run-data-schema
|
||||
v-else-if="hasNodeRun && displayMode === 'schema' && jsonData?.length > 0"
|
||||
:data="jsonData"
|
||||
:mappingEnabled="mappingEnabled"
|
||||
:distanceFromActive="distanceFromActive"
|
||||
:node="node"
|
||||
:runIndex="runIndex"
|
||||
:totalRuns="maxRunIndex"
|
||||
/>
|
||||
|
||||
<div v-else-if="displayMode === 'binary' && binaryData.length === 0" :class="$style.center">
|
||||
<n8n-text align="center" tag="div">{{ $locale.baseText('runData.noBinaryDataFound') }}</n8n-text>
|
||||
</div>
|
||||
|
@ -351,6 +361,7 @@ import {
|
|||
INodeUpdatePropertiesInformation,
|
||||
IRunDataDisplayMode,
|
||||
ITab,
|
||||
NodePanelType,
|
||||
} from '@/Interface';
|
||||
|
||||
import {
|
||||
|
@ -382,6 +393,7 @@ import { useNodeTypesStore } from "@/stores/nodeTypes";
|
|||
|
||||
const RunDataTable = () => import('@/components/RunDataTable.vue');
|
||||
const RunDataJson = () => import('@/components/RunDataJson.vue');
|
||||
const RunDataSchema = () => import('@/components/RunDataSchema.vue');
|
||||
|
||||
export type EnterEditModeArgs = {
|
||||
origin: 'editIconButton' | 'insertTestDataLink',
|
||||
|
@ -402,6 +414,7 @@ export default mixins(
|
|||
CodeEditor,
|
||||
RunDataTable,
|
||||
RunDataJson,
|
||||
RunDataSchema,
|
||||
},
|
||||
props: {
|
||||
nodeUi: {
|
||||
|
@ -432,7 +445,7 @@ export default mixins(
|
|||
type: String,
|
||||
},
|
||||
paneType: {
|
||||
type: String,
|
||||
type: String as PropType<NodePanelType>,
|
||||
},
|
||||
overrideOutputs: {
|
||||
type: Array as PropType<number[]>,
|
||||
|
@ -513,7 +526,7 @@ export default mixins(
|
|||
return DATA_EDITING_DOCS_URL;
|
||||
},
|
||||
displayMode(): IRunDataDisplayMode {
|
||||
return this.ndvStore.getPanelDisplayMode(this.paneType as "input" | "output");
|
||||
return this.ndvStore.getPanelDisplayMode(this.paneType);
|
||||
},
|
||||
node(): INodeUi | null {
|
||||
return (this.nodeUi as INodeUi | null) || null;
|
||||
|
@ -537,10 +550,13 @@ export default mixins(
|
|||
{ label: this.$locale.baseText('runData.table'), value: 'table'},
|
||||
{ label: this.$locale.baseText('runData.json'), value: 'json'},
|
||||
];
|
||||
|
||||
if (this.binaryData.length) {
|
||||
return [ ...defaults,
|
||||
{ label: this.$locale.baseText('runData.binary'), value: 'binary'},
|
||||
];
|
||||
defaults.push({ label: this.$locale.baseText('runData.binary'), value: 'binary'});
|
||||
}
|
||||
|
||||
if (this.isPaneTypeInput && window.posthog?.isFeatureEnabled?.('schema-view')) {
|
||||
defaults.unshift({ label: this.$locale.baseText('runData.schema'), value: 'schema'});
|
||||
}
|
||||
|
||||
return defaults;
|
||||
|
@ -957,7 +973,7 @@ export default mixins(
|
|||
},
|
||||
onDisplayModeChange(displayMode: IRunDataDisplayMode) {
|
||||
const previous = this.displayMode;
|
||||
this.ndvStore.setPanelDisplayMode({pane: this.paneType as "input" | "output", mode: displayMode});
|
||||
this.ndvStore.setPanelDisplayMode({pane: this.paneType, mode: displayMode});
|
||||
|
||||
const dataContainer = this.$refs.dataContainer;
|
||||
if (dataContainer) {
|
||||
|
|
|
@ -225,7 +225,6 @@ export default mixins(externalHooks).extend({
|
|||
height: 100%;
|
||||
padding-bottom: var(--spacing-3xl);
|
||||
background-color: var(--color-background-base);
|
||||
padding-top: var(--spacing-s);
|
||||
|
||||
&:hover {
|
||||
/* Shows .actionsGroup element from <run-data-json-actions /> child component */
|
||||
|
@ -251,7 +250,10 @@ export default mixins(externalHooks).extend({
|
|||
}
|
||||
|
||||
.dragPill {
|
||||
padding: var(--spacing-4xs) var(--spacing-4xs) var(--spacing-3xs) var(--spacing-4xs);
|
||||
display: flex;
|
||||
height: 24px;
|
||||
align-items: center;
|
||||
padding: 0 var(--spacing-4xs);
|
||||
color: var(--color-text-xlight);
|
||||
font-weight: var(--font-weight-bold);
|
||||
font-size: var(--font-size-2xs);
|
||||
|
|
61
packages/editor-ui/src/components/RunDataSchema.test.ts
Normal file
61
packages/editor-ui/src/components/RunDataSchema.test.ts
Normal file
|
@ -0,0 +1,61 @@
|
|||
import { PiniaVuePlugin } from 'pinia';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { render } from '@testing-library/vue';
|
||||
import RunDataJsonSchema from '@/components/RunDataSchema.vue';
|
||||
import { STORES } from "@/constants";
|
||||
|
||||
describe('RunDataJsonSchema.vue', () => {
|
||||
it('renders json schema properly', () => {
|
||||
const {container} = render(RunDataJsonSchema, {
|
||||
pinia: createTestingPinia({
|
||||
initialState: {
|
||||
[STORES.SETTINGS]: {
|
||||
settings: {
|
||||
templates: {
|
||||
enabled: true,
|
||||
host: 'https://api.n8n.io/api/',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
props: {
|
||||
mappingEnabled: true,
|
||||
distanceFromActive: 1,
|
||||
runIndex: 1,
|
||||
totalRuns: 2,
|
||||
node: {
|
||||
parameters: {
|
||||
keepOnlySet: false,
|
||||
values: {},
|
||||
options: {},
|
||||
},
|
||||
id: '820ea733-d8a6-4379-8e73-88a2347ea003',
|
||||
name: 'Set',
|
||||
type: 'n8n-nodes-base.set',
|
||||
typeVersion: 1,
|
||||
position: [
|
||||
380,
|
||||
1060,
|
||||
],
|
||||
disabled: false,
|
||||
},
|
||||
data: [{ name: 'John', age: 22, hobbies: ['surfing', 'traveling'] }, { name: 'Joe', age: 33, hobbies: ['skateboarding', 'gaming'] }],
|
||||
},
|
||||
mocks: {
|
||||
$locale: {
|
||||
baseText() {
|
||||
return '';
|
||||
},
|
||||
},
|
||||
$store: {
|
||||
getters: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
vue => {
|
||||
vue.use(PiniaVuePlugin);
|
||||
});
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
157
packages/editor-ui/src/components/RunDataSchema.vue
Normal file
157
packages/editor-ui/src/components/RunDataSchema.vue
Normal file
|
@ -0,0 +1,157 @@
|
|||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { INodeUi, Schema } from "@/Interface";
|
||||
import RunDataSchemaItem from "@/components/RunDataSchemaItem.vue";
|
||||
import Draggable from '@/components/Draggable.vue';
|
||||
import { useNDVStore } from "@/stores/ndv";
|
||||
import { useWebhooksStore } from "@/stores/webhooks";
|
||||
import { runExternalHook } from "@/mixins/externalHooks";
|
||||
import { telemetry } from "@/plugins/telemetry";
|
||||
import { IDataObject } from "n8n-workflow";
|
||||
import { getSchema, mergeDeep } from "@/utils";
|
||||
|
||||
type Props = {
|
||||
data: IDataObject[]
|
||||
mappingEnabled: boolean
|
||||
distanceFromActive: number
|
||||
runIndex: number
|
||||
totalRuns: number
|
||||
node: INodeUi | null
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
distanceFromActive: 0,
|
||||
});
|
||||
|
||||
const draggingPath = ref<string>('');
|
||||
const ndvStore = useNDVStore();
|
||||
const webhooksStore = useWebhooksStore();
|
||||
|
||||
const schema = computed<Schema>(() => {
|
||||
const [head, ...tail] = props.data;
|
||||
return getSchema(mergeDeep([head, ...tail, head]));
|
||||
});
|
||||
|
||||
const onDragStart = (el: HTMLElement) => {
|
||||
if (el && el.dataset?.path) {
|
||||
draggingPath.value = el.dataset.path;
|
||||
}
|
||||
|
||||
ndvStore.resetMappingTelemetry();
|
||||
};
|
||||
const onDragEnd = (el: HTMLElement) => {
|
||||
draggingPath.value = '';
|
||||
|
||||
setTimeout(() => {
|
||||
const mappingTelemetry = ndvStore.mappingTelemetry;
|
||||
const telemetryPayload = {
|
||||
src_node_type: props.node?.type,
|
||||
src_field_name: el.dataset.name || '',
|
||||
src_nodes_back: props.distanceFromActive,
|
||||
src_run_index: props.runIndex,
|
||||
src_runs_total: props.totalRuns,
|
||||
src_field_nest_level: el.dataset.depth || 0,
|
||||
src_view: 'schema',
|
||||
src_element: el,
|
||||
success: false,
|
||||
...mappingTelemetry,
|
||||
};
|
||||
|
||||
runExternalHook('runDataJson.onDragEnd', webhooksStore, telemetryPayload);
|
||||
|
||||
telemetry.track('User dragged data for mapping', telemetryPayload);
|
||||
}, 1000); // ensure dest data gets set if drop
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="$style.schemaWrapper">
|
||||
<draggable
|
||||
type="mapping"
|
||||
targetDataKey="mappable"
|
||||
:disabled="!mappingEnabled"
|
||||
@dragstart="onDragStart"
|
||||
@dragend="onDragEnd"
|
||||
>
|
||||
<template #preview="{ canDrop, el }">
|
||||
<div v-if="el" :class="[$style.dragPill, canDrop ? $style.droppablePill : $style.defaultPill]" v-html="el.outerHTML" />
|
||||
</template>
|
||||
<template>
|
||||
<div :class="$style.schema">
|
||||
<run-data-schema-item
|
||||
:schema="schema"
|
||||
:level="0"
|
||||
:parent="null"
|
||||
:subKey="`${schema.type}-0-0`"
|
||||
:mappingEnabled="mappingEnabled"
|
||||
:draggingPath="draggingPath"
|
||||
:distanceFromActive="distanceFromActive"
|
||||
:node="node"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.schemaWrapper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
overflow: auto;
|
||||
line-height: 1.5;
|
||||
word-break: normal;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: var(--color-background-base);
|
||||
}
|
||||
|
||||
.schema {
|
||||
display: inline-block;
|
||||
padding: 0 var(--spacing-s) var(--spacing-s);
|
||||
}
|
||||
|
||||
.dragPill {
|
||||
display: inline-flex;
|
||||
height: 24px;
|
||||
padding: 0 var(--spacing-3xs);
|
||||
border: 1px solid var(--color-foreground-light);
|
||||
border-radius: 4px;
|
||||
background: var(--color-background-xlight);
|
||||
font-size: var(--font-size-2xs);
|
||||
color: var(--color-text-base);
|
||||
white-space: nowrap;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.droppablePill {
|
||||
&,
|
||||
span span {
|
||||
color: var(--color-success);
|
||||
border-color: var(--color-success-light);
|
||||
background: var(--color-success-tint-3);
|
||||
}
|
||||
}
|
||||
|
||||
.defaultPill {
|
||||
transform: translate(-50%, -100%);
|
||||
box-shadow: 0 2px 6px rgba(68, 28, 23, 0.2);
|
||||
|
||||
&,
|
||||
span span {
|
||||
color: var(--color-primary);
|
||||
border-color: var(--color-primary-tint-1);
|
||||
background: var(--color-primary-tint-3);
|
||||
}
|
||||
}
|
||||
</style>
|
267
packages/editor-ui/src/components/RunDataSchemaItem.vue
Normal file
267
packages/editor-ui/src/components/RunDataSchemaItem.vue
Normal file
|
@ -0,0 +1,267 @@
|
|||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { INodeUi, Schema } from "@/Interface";
|
||||
import { checkExhaustive, shorten } from "@/utils";
|
||||
|
||||
type Props = {
|
||||
schema: Schema
|
||||
level: number
|
||||
parent: Schema | null
|
||||
subKey: string
|
||||
mappingEnabled: boolean
|
||||
draggingPath: string
|
||||
distanceFromActive: number
|
||||
node: INodeUi | null
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const isSchemaValueArray = computed(() => Array.isArray(props.schema.value));
|
||||
const isSchemaParentTypeArray = computed(() => props.parent?.type === 'array');
|
||||
const isFlat = computed(() => props.level === 0 && Array.isArray(props.schema.value) && props.schema.value.every(v => !Array.isArray(v.value)));
|
||||
const key = computed((): string | undefined => isSchemaParentTypeArray.value ? `[${props.schema.key}]` : props.schema.key);
|
||||
const schemaName = computed(() => isSchemaParentTypeArray.value ? `${props.schema.type}[${props.schema.key}]` : props.schema.key);
|
||||
const text = computed(() => Array.isArray(props.schema.value) ? '' : shorten(props.schema.value, 600, 0));
|
||||
|
||||
const getJsonParameterPath = (path: string): string => `{{ ${props.distanceFromActive === 1 ? '$json' : `$node["${ props.node!.name }"].json`}${path} }}`;
|
||||
const transitionDelay = (i:number) => `${i * 0.033}s`;
|
||||
|
||||
const getIconBySchemaType = (type: Schema['type']): string => {
|
||||
switch (type) {
|
||||
case 'object':
|
||||
return 'cube';
|
||||
case 'array':
|
||||
return 'list';
|
||||
case 'string':
|
||||
case 'null':
|
||||
return 'font';
|
||||
case 'number':
|
||||
return 'hashtag';
|
||||
case 'boolean':
|
||||
return 'check-square';
|
||||
case 'function':
|
||||
return 'code';
|
||||
case 'bigint':
|
||||
return 'calculator';
|
||||
case 'symbol':
|
||||
return 'sun';
|
||||
case 'undefined':
|
||||
return 'ban';
|
||||
}
|
||||
|
||||
checkExhaustive(type);
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="$style.item">
|
||||
<div
|
||||
v-if="level > 0 || level === 0 && !isSchemaValueArray"
|
||||
:title="schema.type"
|
||||
:class="{
|
||||
[$style.pill]: true,
|
||||
[$style.mappable]: mappingEnabled,
|
||||
[$style.dragged]: draggingPath === schema.path,
|
||||
}"
|
||||
>
|
||||
<span
|
||||
:class="$style.label"
|
||||
:data-value="getJsonParameterPath(schema.path)"
|
||||
:data-name="schemaName"
|
||||
:data-path="schema.path"
|
||||
:data-depth="level"
|
||||
data-target="mappable"
|
||||
>
|
||||
<font-awesome-icon :icon="getIconBySchemaType(schema.type)" size="sm"/>
|
||||
<span v-if="isSchemaParentTypeArray">{{ parent.key }}</span>
|
||||
<span v-if="key" :class="{[$style.arrayIndex]: isSchemaParentTypeArray}">{{ key }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<span v-if="text" :class="$style.text">{{ text }}</span>
|
||||
<input :id="subKey" type="checkbox" checked />
|
||||
<label v-if="level > 0 && isSchemaValueArray" :class="$style.toggle" :for="subKey">
|
||||
<font-awesome-icon icon="angle-up" />
|
||||
</label>
|
||||
<div v-if="isSchemaValueArray" :class="{[$style.sub]: true, [$style.flat]: isFlat}">
|
||||
<run-data-schema-item v-for="(s, i) in schema.value"
|
||||
:key="`${s.type}-${level}-${i}`"
|
||||
:schema="s"
|
||||
:level="level + 1"
|
||||
:parent="schema"
|
||||
:subKey="`${s.type}-${level}-${i}`"
|
||||
:mappingEnabled="mappingEnabled"
|
||||
:draggingPath="draggingPath"
|
||||
:distanceFromActive="distanceFromActive"
|
||||
:node="node"
|
||||
:style="{transitionDelay: transitionDelay(i)}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
@import '@/styles/css-animation-helpers.scss';
|
||||
|
||||
.item {
|
||||
display: block;
|
||||
position: relative;
|
||||
transition: all 0.3s $ease-out-expo;
|
||||
|
||||
.item {
|
||||
padding-top: var(--spacing-2xs);
|
||||
padding-left: var(--spacing-l);
|
||||
}
|
||||
|
||||
input {
|
||||
position: absolute;
|
||||
left: -100%;
|
||||
|
||||
~ .sub {
|
||||
height: 0;
|
||||
|
||||
> .item {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
}
|
||||
|
||||
&:checked {
|
||||
~ .toggle svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
~ .sub {
|
||||
height: auto;
|
||||
|
||||
> .item {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
clear: both;
|
||||
}
|
||||
}
|
||||
|
||||
.sub {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
transition: all 0.2s $ease-out-expo;
|
||||
clear: both;
|
||||
|
||||
&.flat {
|
||||
> .item {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-of-type(1) {
|
||||
> .item:nth-of-type(1) {
|
||||
padding-top: 0;
|
||||
|
||||
.toggle {
|
||||
top: -2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pill {
|
||||
float: left;
|
||||
display: inline-flex;
|
||||
height: 24px;
|
||||
padding: 0 var(--spacing-3xs);
|
||||
border: 1px solid var(--color-foreground-light);
|
||||
border-radius: 4px;
|
||||
background: var(--color-background-xlight);
|
||||
font-size: var(--font-size-2xs);
|
||||
color: var(--color-text-dark);
|
||||
|
||||
span {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
|
||||
svg {
|
||||
path {
|
||||
fill: var(--color-text-light);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.mappable {
|
||||
cursor: grab;
|
||||
|
||||
&:hover {
|
||||
&,
|
||||
span span {
|
||||
background-color: var(--color-background-light);
|
||||
border-color: var(--color-foreground-base);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.dragged {
|
||||
&,
|
||||
&:hover,
|
||||
span {
|
||||
color: var(--color-primary);
|
||||
border-color: var(--color-primary-tint-1);
|
||||
background: var(--color-primary-tint-3);
|
||||
|
||||
svg {
|
||||
path {
|
||||
fill: var(--color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
> span {
|
||||
margin-left: var(--spacing-3xs);
|
||||
padding-left: var(--spacing-3xs);
|
||||
border-left: 1px solid var(--color-foreground-light);
|
||||
|
||||
&.arrayIndex {
|
||||
border: 0;
|
||||
padding-left: 0;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
display: block;
|
||||
padding-top: var(--spacing-4xs);
|
||||
padding-left: var(--spacing-2xs);
|
||||
font-weight: var(--font-weight-normal);
|
||||
font-size: var(--font-size-2xs);
|
||||
overflow: hidden;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.toggle {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
padding: var(--spacing-2xs);
|
||||
left: 0;
|
||||
top: 5px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
font-weight: normal;
|
||||
font-size: var(--font-size-s);
|
||||
overflow: hidden;
|
||||
|
||||
svg {
|
||||
transition: all 0.3s $ease-out-expo;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -634,7 +634,10 @@ export default mixins(externalHooks).extend({
|
|||
}
|
||||
|
||||
.dragPill {
|
||||
padding: var(--spacing-4xs) var(--spacing-4xs) var(--spacing-3xs) var(--spacing-4xs);
|
||||
display: flex;
|
||||
height: 24px;
|
||||
align-items: center;
|
||||
padding: 0 var(--spacing-4xs);
|
||||
color: var(--color-text-xlight);
|
||||
font-weight: var(--font-weight-bold);
|
||||
font-size: var(--font-size-2xs);
|
||||
|
|
|
@ -0,0 +1,326 @@
|
|||
// Vitest Snapshot v1
|
||||
|
||||
exports[`RunDataJsonSchema.vue > renders json schema properly 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="_schemaWrapper_1w572_1"
|
||||
>
|
||||
<div
|
||||
class=""
|
||||
>
|
||||
<div
|
||||
class="_schema_1w572_1"
|
||||
>
|
||||
<div
|
||||
class="_item_eg159_1"
|
||||
>
|
||||
<!---->
|
||||
<!---->
|
||||
<input
|
||||
checked="checked"
|
||||
id="object-0-0"
|
||||
type="checkbox"
|
||||
/>
|
||||
<!---->
|
||||
<div
|
||||
class="_sub_eg159_14"
|
||||
>
|
||||
<div
|
||||
class="_item_eg159_1"
|
||||
style="transition-delay: 0s;"
|
||||
>
|
||||
<div
|
||||
class="_pill_eg159_51 _mappable_eg159_70"
|
||||
title="string"
|
||||
>
|
||||
<span
|
||||
class="_label_eg159_89"
|
||||
data-depth="1"
|
||||
data-name="name"
|
||||
data-path="[\\"name\\"]"
|
||||
data-target="mappable"
|
||||
data-value="{{ $json[\\"name\\"] }}"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="svg-inline--fa fa-font fa-w-14 fa-sm"
|
||||
data-icon="font"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
viewBox="0 0 448 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
class=""
|
||||
d="M432 416h-23.41L277.88 53.69A32 32 0 0 0 247.58 32h-47.16a32 32 0 0 0-30.3 21.69L39.41 416H16a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h128a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16h-19.58l23.3-64h152.56l23.3 64H304a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h128a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zM176.85 272L224 142.51 271.15 272z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<!---->
|
||||
<span
|
||||
class=""
|
||||
>
|
||||
name
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
class="_text_eg159_100"
|
||||
>
|
||||
John
|
||||
</span>
|
||||
<input
|
||||
checked="checked"
|
||||
id="string-0-0"
|
||||
type="checkbox"
|
||||
/>
|
||||
<!---->
|
||||
<!---->
|
||||
</div>
|
||||
<div
|
||||
class="_item_eg159_1"
|
||||
style="transition-delay: 0.033s;"
|
||||
>
|
||||
<div
|
||||
class="_pill_eg159_51 _mappable_eg159_70"
|
||||
title="number"
|
||||
>
|
||||
<span
|
||||
class="_label_eg159_89"
|
||||
data-depth="1"
|
||||
data-name="age"
|
||||
data-path="[\\"age\\"]"
|
||||
data-target="mappable"
|
||||
data-value="{{ $json[\\"age\\"] }}"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="svg-inline--fa fa-hashtag fa-w-14 fa-sm"
|
||||
data-icon="hashtag"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
viewBox="0 0 448 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
class=""
|
||||
d="M440.667 182.109l7.143-40c1.313-7.355-4.342-14.109-11.813-14.109h-74.81l14.623-81.891C377.123 38.754 371.468 32 363.997 32h-40.632a12 12 0 0 0-11.813 9.891L296.175 128H197.54l14.623-81.891C213.477 38.754 207.822 32 200.35 32h-40.632a12 12 0 0 0-11.813 9.891L132.528 128H53.432a12 12 0 0 0-11.813 9.891l-7.143 40C33.163 185.246 38.818 192 46.289 192h74.81L98.242 320H19.146a12 12 0 0 0-11.813 9.891l-7.143 40C-1.123 377.246 4.532 384 12.003 384h74.81L72.19 465.891C70.877 473.246 76.532 480 84.003 480h40.632a12 12 0 0 0 11.813-9.891L151.826 384h98.634l-14.623 81.891C234.523 473.246 240.178 480 247.65 480h40.632a12 12 0 0 0 11.813-9.891L315.472 384h79.096a12 12 0 0 0 11.813-9.891l7.143-40c1.313-7.355-4.342-14.109-11.813-14.109h-74.81l22.857-128h79.096a12 12 0 0 0 11.813-9.891zM261.889 320h-98.634l22.857-128h98.634l-22.857 128z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<!---->
|
||||
<span
|
||||
class=""
|
||||
>
|
||||
age
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
class="_text_eg159_100"
|
||||
>
|
||||
22
|
||||
</span>
|
||||
<input
|
||||
checked="checked"
|
||||
id="number-0-1"
|
||||
type="checkbox"
|
||||
/>
|
||||
<!---->
|
||||
<!---->
|
||||
</div>
|
||||
<div
|
||||
class="_item_eg159_1"
|
||||
style="transition-delay: 0.066s;"
|
||||
>
|
||||
<div
|
||||
class="_pill_eg159_51 _mappable_eg159_70"
|
||||
title="array"
|
||||
>
|
||||
<span
|
||||
class="_label_eg159_89"
|
||||
data-depth="1"
|
||||
data-name="hobbies"
|
||||
data-path="[\\"hobbies\\"]"
|
||||
data-target="mappable"
|
||||
data-value="{{ $json[\\"hobbies\\"] }}"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="svg-inline--fa fa-list fa-w-16 fa-sm"
|
||||
data-icon="list"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
class=""
|
||||
d="M80 368H16a16 16 0 0 0-16 16v64a16 16 0 0 0 16 16h64a16 16 0 0 0 16-16v-64a16 16 0 0 0-16-16zm0-320H16A16 16 0 0 0 0 64v64a16 16 0 0 0 16 16h64a16 16 0 0 0 16-16V64a16 16 0 0 0-16-16zm0 160H16a16 16 0 0 0-16 16v64a16 16 0 0 0 16 16h64a16 16 0 0 0 16-16v-64a16 16 0 0 0-16-16zm416 176H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zm0-320H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16V80a16 16 0 0 0-16-16zm0 160H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<!---->
|
||||
<span
|
||||
class=""
|
||||
>
|
||||
hobbies
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<!---->
|
||||
<input
|
||||
checked="checked"
|
||||
id="array-0-2"
|
||||
type="checkbox"
|
||||
/>
|
||||
<label
|
||||
class="_toggle_eg159_20"
|
||||
for="array-0-2"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="svg-inline--fa fa-angle-up fa-w-10"
|
||||
data-icon="angle-up"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
viewBox="0 0 320 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
class=""
|
||||
d="M177 159.7l136 136c9.4 9.4 9.4 24.6 0 33.9l-22.6 22.6c-9.4 9.4-24.6 9.4-33.9 0L160 255.9l-96.4 96.4c-9.4 9.4-24.6 9.4-33.9 0L7 329.7c-9.4-9.4-9.4-24.6 0-33.9l136-136c9.4-9.5 24.6-9.5 34-.1z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</label>
|
||||
<div
|
||||
class="_sub_eg159_14"
|
||||
>
|
||||
<div
|
||||
class="_item_eg159_1"
|
||||
style="transition-delay: 0s;"
|
||||
>
|
||||
<div
|
||||
class="_pill_eg159_51 _mappable_eg159_70"
|
||||
title="string"
|
||||
>
|
||||
<span
|
||||
class="_label_eg159_89"
|
||||
data-depth="2"
|
||||
data-name="string[0]"
|
||||
data-path="[\\"hobbies\\"][0]"
|
||||
data-target="mappable"
|
||||
data-value="{{ $json[\\"hobbies\\"][0] }}"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="svg-inline--fa fa-font fa-w-14 fa-sm"
|
||||
data-icon="font"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
viewBox="0 0 448 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
class=""
|
||||
d="M432 416h-23.41L277.88 53.69A32 32 0 0 0 247.58 32h-47.16a32 32 0 0 0-30.3 21.69L39.41 416H16a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h128a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16h-19.58l23.3-64h152.56l23.3 64H304a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h128a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zM176.85 272L224 142.51 271.15 272z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
hobbies
|
||||
</span>
|
||||
<span
|
||||
class="_arrayIndex_eg159_94"
|
||||
>
|
||||
[0]
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
class="_text_eg159_100"
|
||||
>
|
||||
surfing
|
||||
</span>
|
||||
<input
|
||||
checked="checked"
|
||||
id="string-1-0"
|
||||
type="checkbox"
|
||||
/>
|
||||
<!---->
|
||||
<!---->
|
||||
</div>
|
||||
<div
|
||||
class="_item_eg159_1"
|
||||
style="transition-delay: 0.033s;"
|
||||
>
|
||||
<div
|
||||
class="_pill_eg159_51 _mappable_eg159_70"
|
||||
title="string"
|
||||
>
|
||||
<span
|
||||
class="_label_eg159_89"
|
||||
data-depth="2"
|
||||
data-name="string[1]"
|
||||
data-path="[\\"hobbies\\"][1]"
|
||||
data-target="mappable"
|
||||
data-value="{{ $json[\\"hobbies\\"][1] }}"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="svg-inline--fa fa-font fa-w-14 fa-sm"
|
||||
data-icon="font"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
viewBox="0 0 448 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
class=""
|
||||
d="M432 416h-23.41L277.88 53.69A32 32 0 0 0 247.58 32h-47.16a32 32 0 0 0-30.3 21.69L39.41 416H16a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h128a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16h-19.58l23.3-64h152.56l23.3 64H304a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h128a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zM176.85 272L224 142.51 271.15 272z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
hobbies
|
||||
</span>
|
||||
<span
|
||||
class="_arrayIndex_eg159_94"
|
||||
>
|
||||
[1]
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
class="_text_eg159_100"
|
||||
>
|
||||
traveling
|
||||
</span>
|
||||
<input
|
||||
checked="checked"
|
||||
id="string-1-1"
|
||||
type="checkbox"
|
||||
/>
|
||||
<!---->
|
||||
<!---->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="teleporter hidden"
|
||||
data-v-d4e6e290=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
|
@ -1,29 +1,31 @@
|
|||
import { IExternalHooks, IRootState } from '@/Interface';
|
||||
import { store } from '@/store';
|
||||
import { IExternalHooks } from '@/Interface';
|
||||
import { useWebhooksStore } from '@/stores/webhooks';
|
||||
import { IDataObject } from 'n8n-workflow';
|
||||
import { Store } from 'pinia';
|
||||
import Vue from 'vue';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
n8nExternalHooks?: Record<string, Record<string, Array<(store: Store, metadata?: IDataObject) => Promise<void>>>>;
|
||||
}
|
||||
}
|
||||
|
||||
export async function runExternalHook(
|
||||
eventName: string,
|
||||
store: Store,
|
||||
metadata?: IDataObject,
|
||||
) {
|
||||
// @ts-ignore
|
||||
if (!window.n8nExternalHooks) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [resource, operator] = eventName.split('.');
|
||||
|
||||
// @ts-ignore
|
||||
if (window.n8nExternalHooks[resource] && window.n8nExternalHooks[resource][operator]) {
|
||||
// @ts-ignore
|
||||
if (window.n8nExternalHooks[resource]?.[operator]) {
|
||||
const hookMethods = window.n8nExternalHooks[resource][operator];
|
||||
|
||||
for (const hookmethod of hookMethods) {
|
||||
await hookmethod(store, metadata);
|
||||
for (const hookMethod of hookMethods) {
|
||||
await hookMethod(store, metadata);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -362,6 +362,7 @@
|
|||
"dataMapping.success.moreInfo": "Check out our <a href=\"https://docs.n8n.io/data/data-mapping\" target=\"_blank\">docs</a> for more details on mapping data in n8n",
|
||||
"dataMapping.tableHint": "<img src='/static/data-mapping-gif.gif'/> Drag a column onto <b>{name}</b> to map it",
|
||||
"dataMapping.jsonHint": "<img src='/static/json-mapping-gif.gif'/> Drag a JSON key onto <b>{name}</b> to map data",
|
||||
"dataMapping.schemaHint": "<img src='/static/schema-mapping-gif.gif'/> Drag a datapill onto <b>{name}</b> to map data",
|
||||
"dataMapping.mapKeyToField": "Map '{name}' to a field",
|
||||
"dataMapping.mapAllKeysToField": "Map every '{name}' to a field",
|
||||
"dataMapping.tableView.tableColumnsExceeded": "Some columns are hidden",
|
||||
|
@ -983,6 +984,7 @@
|
|||
"runData.invalidPinnedData": "Invalid pinned data",
|
||||
"runData.items": "Items",
|
||||
"runData.json": "JSON",
|
||||
"runData.schema": "Schema",
|
||||
"runData.mimeType": "Mime Type",
|
||||
"runData.fileSize": "File Size",
|
||||
"runData.ms": "ms",
|
||||
|
|
|
@ -10,13 +10,15 @@ import {
|
|||
faArrowLeft,
|
||||
faArrowRight,
|
||||
faAt,
|
||||
faBan,
|
||||
faBook,
|
||||
faBoxOpen,
|
||||
faBug,
|
||||
faBan,
|
||||
faCalculator,
|
||||
faCalendar,
|
||||
faCheck,
|
||||
faCheckCircle,
|
||||
faCheckSquare,
|
||||
faChevronDown,
|
||||
faChevronUp,
|
||||
faChevronLeft,
|
||||
|
@ -52,12 +54,14 @@ import {
|
|||
faFilter,
|
||||
faFlask,
|
||||
faFolderOpen,
|
||||
faFont,
|
||||
faGlobeAmericas,
|
||||
faGift,
|
||||
faGlobe,
|
||||
faGraduationCap,
|
||||
faGripVertical,
|
||||
faHandPointLeft,
|
||||
faHashtag,
|
||||
faHdd,
|
||||
faHome,
|
||||
faHourglass,
|
||||
|
@ -67,6 +71,7 @@ import {
|
|||
faInfoCircle,
|
||||
faKey,
|
||||
faLink,
|
||||
faList,
|
||||
faLightbulb,
|
||||
faMapSigns,
|
||||
faMousePointer,
|
||||
|
@ -137,9 +142,11 @@ addIcon(faBan);
|
|||
addIcon(faBook);
|
||||
addIcon(faBoxOpen);
|
||||
addIcon(faBug);
|
||||
addIcon(faCalculator);
|
||||
addIcon(faCalendar);
|
||||
addIcon(faCheck);
|
||||
addIcon(faCheckCircle);
|
||||
addIcon(faCheckSquare);
|
||||
addIcon(faChevronLeft);
|
||||
addIcon(faChevronRight);
|
||||
addIcon(faChevronDown);
|
||||
|
@ -176,11 +183,13 @@ addIcon(faFilePdf);
|
|||
addIcon(faFilter);
|
||||
addIcon(faFlask);
|
||||
addIcon(faFolderOpen);
|
||||
addIcon(faFont);
|
||||
addIcon(faGift);
|
||||
addIcon(faGlobe);
|
||||
addIcon(faGlobeAmericas);
|
||||
addIcon(faGraduationCap);
|
||||
addIcon(faHandPointLeft);
|
||||
addIcon(faHashtag);
|
||||
addIcon(faHdd);
|
||||
addIcon(faHome);
|
||||
addIcon(faHourglass);
|
||||
|
@ -190,6 +199,7 @@ addIcon(faInfo);
|
|||
addIcon(faInfoCircle);
|
||||
addIcon(faKey);
|
||||
addIcon(faLink);
|
||||
addIcon(faList);
|
||||
addIcon(faLightbulb);
|
||||
addIcon(faMapSigns);
|
||||
addIcon(faMousePointer);
|
||||
|
|
|
@ -6,22 +6,11 @@ import {
|
|||
} from 'n8n-workflow';
|
||||
import { Route } from "vue-router";
|
||||
|
||||
import type { INodeCreateElement, IRootState } from "@/Interface";
|
||||
import type { INodeCreateElement } from "@/Interface";
|
||||
import type { IUserNodesPanelSession } from "./telemetry.types";
|
||||
import { useSettingsStore } from "@/stores/settings";
|
||||
import { useRootStore } from "@/stores/n8nRootStore";
|
||||
|
||||
export function TelemetryPlugin(vue: typeof _Vue): void {
|
||||
const telemetry = new Telemetry();
|
||||
|
||||
Object.defineProperty(vue, '$telemetry', {
|
||||
get() { return telemetry; },
|
||||
});
|
||||
Object.defineProperty(vue.prototype, '$telemetry', {
|
||||
get() { return telemetry; },
|
||||
});
|
||||
}
|
||||
|
||||
export class Telemetry {
|
||||
|
||||
private pageEventQueue: Array<{route: Route}>;
|
||||
|
@ -51,7 +40,7 @@ export class Telemetry {
|
|||
instanceId: string;
|
||||
userId?: string;
|
||||
versionCli: string
|
||||
},
|
||||
},
|
||||
) {
|
||||
if (!telemetrySettings.enabled || !telemetrySettings.config || this.rudderStack) return;
|
||||
|
||||
|
@ -252,3 +241,14 @@ export class Telemetry {
|
|||
this.rudderStack.load(key, url, options);
|
||||
}
|
||||
}
|
||||
|
||||
export const telemetry = new Telemetry();
|
||||
|
||||
export function TelemetryPlugin(vue: typeof _Vue): void {
|
||||
Object.defineProperty(vue, '$telemetry', {
|
||||
get() { return telemetry; },
|
||||
});
|
||||
Object.defineProperty(vue.prototype, '$telemetry', {
|
||||
get() { return telemetry; },
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { STORES } from "@/constants";
|
||||
import { INodeUi, IRunDataDisplayMode, NDVState, XYPosition } from "@/Interface";
|
||||
import { INodeUi, IRunDataDisplayMode, NDVState, NodePanelType, XYPosition } from "@/Interface";
|
||||
import { IRunData } from "n8n-workflow";
|
||||
import { defineStore } from "pinia";
|
||||
import Vue from "vue";
|
||||
|
@ -11,7 +11,7 @@ export const useNDVStore = defineStore(STORES.NDV, {
|
|||
mainPanelDimensions: {},
|
||||
sessionId: '',
|
||||
input: {
|
||||
displayMode: 'table',
|
||||
displayMode: window.posthog?.isFeatureEnabled?.('schema-view') ? 'schema': 'table',
|
||||
nodeName: undefined,
|
||||
run: undefined,
|
||||
branch: undefined,
|
||||
|
@ -60,7 +60,7 @@ export const useNDVStore = defineStore(STORES.NDV, {
|
|||
return executionData.data?.resultData?.runData?.[inputNodeName]?.[inputRunIndex]?.data?.main?.[inputBranchIndex];
|
||||
},
|
||||
getPanelDisplayMode() {
|
||||
return (panel: 'input' | 'output') => this[panel].displayMode;
|
||||
return (panel: NodePanelType) => this[panel].displayMode;
|
||||
},
|
||||
inputPanelDisplayMode(): IRunDataDisplayMode {
|
||||
return this.input.displayMode;
|
||||
|
@ -125,7 +125,7 @@ export const useNDVStore = defineStore(STORES.NDV, {
|
|||
resetNDVSessionId(): void {
|
||||
Vue.set(this, 'sessionId', '');
|
||||
},
|
||||
setPanelDisplayMode(params: {pane: 'input' | 'output', mode: IRunDataDisplayMode}): void {
|
||||
setPanelDisplayMode(params: {pane: NodePanelType, mode: IRunDataDisplayMode}): void {
|
||||
Vue.set(this[params.pane], 'displayMode', params.mode);
|
||||
},
|
||||
setOutputPanelEditModeEnabled(isEnabled: boolean): void {
|
||||
|
|
1
packages/editor-ui/src/styles/css-animation-helpers.scss
Normal file
1
packages/editor-ui/src/styles/css-animation-helpers.scss
Normal file
|
@ -0,0 +1 @@
|
|||
$ease-out-expo: cubic-bezier(0.19, 1, 0.22, 1);
|
|
@ -1,8 +1,10 @@
|
|||
import { isEmpty, intersection } from "@/utils";
|
||||
import jp from "jsonpath";
|
||||
import { isEmpty, intersection, mergeDeep, getSchema, isValidDate } from "@/utils";
|
||||
import { Schema } from "@/Interface";
|
||||
|
||||
describe("Utils", () => {
|
||||
describe("isEmpty", () => {
|
||||
it.each([
|
||||
test.each([
|
||||
[undefined, true],
|
||||
[null, true],
|
||||
[{}, true],
|
||||
|
@ -38,4 +40,287 @@ describe("Utils", () => {
|
|||
expect(intersection([1, 2, 2, 3, 4], [2, 3, 3, 4], [2, 1, 5, 4, 4, 1], [2, 4, 5, 5, 6, 7])).toEqual([2, 4]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("mergeDeep", () => {
|
||||
test.each([
|
||||
[
|
||||
[[1, 2], [3, 4]],
|
||||
{},
|
||||
[3, 4],
|
||||
],
|
||||
[
|
||||
[[1, 2], [3, 4]],
|
||||
{ concatArrays: true },
|
||||
[1, 2, 3, 4],
|
||||
],
|
||||
[
|
||||
[[1, 2], [3, 4]],
|
||||
{ overwriteArrays: true },
|
||||
[3, 4],
|
||||
],
|
||||
[
|
||||
[[1, 2, 3], [4, 5]],
|
||||
{},
|
||||
[4, 5, 3],
|
||||
],
|
||||
[
|
||||
[[1, 2, 3], [4, 5]],
|
||||
{ concatArrays: true },
|
||||
[1, 2, 3, 4, 5],
|
||||
],
|
||||
[
|
||||
[[1, 2, 3], [4, 5]],
|
||||
{ overwriteArrays: true },
|
||||
[4, 5],
|
||||
],
|
||||
[
|
||||
[[1, 2], [3, 4, 5]],
|
||||
{},
|
||||
[3, 4, 5],
|
||||
],
|
||||
[
|
||||
[[1, 2], [3, 4, 5]],
|
||||
{ concatArrays: true },
|
||||
[1, 2, 3, 4, 5],
|
||||
],
|
||||
[
|
||||
[[1, 2], [3, 4, 5]],
|
||||
{ overwriteArrays: true },
|
||||
[3, 4, 5],
|
||||
],
|
||||
[
|
||||
[{a:1, b: [1, 2, {d: 2}]},{}],
|
||||
{},
|
||||
{a:1, b: [1, 2, {d:2}]},
|
||||
],
|
||||
[
|
||||
[{a:1, b: [1, 2, {d: 2}]},{}],
|
||||
{ concatArrays: true },
|
||||
{a:1, b: [1, 2, {d:2}]},
|
||||
],
|
||||
[
|
||||
[{a:1, b: [1, 2, {d: 2}]},{}],
|
||||
{ overwriteArrays: true },
|
||||
{a:1, b: [1, 2, {d:2}]},
|
||||
],
|
||||
[
|
||||
[[{a:1, b: [1, 2, {d: 2}]}],[]],
|
||||
{},
|
||||
[{a:1, b: [1, 2, {d:2}]}],
|
||||
],
|
||||
[
|
||||
[[{a:1, b: [1, 2, {d: 2}]}],[]],
|
||||
{ concatArrays: true },
|
||||
[{a:1, b: [1, 2, {d:2}]}],
|
||||
],
|
||||
[
|
||||
[[{a:1, b: [1, 2, {d: 2}]}],[]],
|
||||
{ overwriteArrays: true },
|
||||
[],
|
||||
],
|
||||
[
|
||||
[{a: 1, b: [1, 2, 3]}, {a: 2, b: [4, 5, 6, 7], c: "2"}, {a: 3, b: [8, 9], d: "3"}],
|
||||
{},
|
||||
{a: 3, b: [8, 9, 6, 7], c: "2", d: "3"},
|
||||
],
|
||||
[
|
||||
[{a: 1, b: [1, 2, 3]}, {a: 2, b: [4, 5, 6, 7], c: "2"}, {a: 3, b: [8, 9], d: "3"}],
|
||||
{ concatArrays: true },
|
||||
{a: 3, b: [1, 2, 3, 4, 5, 6, 7, 8, 9], c: "2", d: "3"},
|
||||
],
|
||||
[
|
||||
[{a: 1, b: [1, 2, 3]}, {a: 2, b: [4, 5, 6, 7], c: "2"}, {a: 3, b: [8, 9], d: "3"}],
|
||||
{ overwriteArrays: true },
|
||||
{a: 3, b: [8, 9], c: "2", d: "3"},
|
||||
],
|
||||
[
|
||||
[{a: 1, b: [{x: 'a'}]}, {a: 2, b: [{y: 'b'}], c: "2"}, {a: 3, b: [{z: 'c'}], d: "3"}],
|
||||
{},
|
||||
{a: 3, b: [{x: 'a', y: 'b', z: 'c'}], c: "2", d: "3"},
|
||||
],
|
||||
[
|
||||
[{a: 1, b: [{x: 'a'}]}, {a: 2, b: [{y: 'b'}], c: "2"}, {a: 3, b: [{z: 'c'}], d: "3"}],
|
||||
{ concatArrays: true },
|
||||
{a: 3, b: [{x: 'a'}, {y: 'b'}, {z: 'c'}], c: "2", d: "3"},
|
||||
],
|
||||
[
|
||||
[{a: 1, b: [{x: 'a'}]}, {a: 2, b: [{y: 'b'}], c: "2"}, {a: 3, b: [{z: 'c'}], d: "3"}],
|
||||
{ overwriteArrays: true },
|
||||
{a: 3, b: [{z: 'c'}], c: "2", d: "3"},
|
||||
],
|
||||
[
|
||||
[{a: 1, b: [{x: 'a'}, {w: 'd'}]}, {a: 2, b: [{y: 'b'}], c: "2"}, {a: 3, b: [{z: 'c'}], d: "3"}],
|
||||
{},
|
||||
{a: 3, b: [{z: 'c'}, {w: 'd'}], c: "2", d: "3"},
|
||||
],
|
||||
[
|
||||
[{a: 1, b: [{x: 'a'}, {w: 'd'}]}, {a: 2, b: [{y: 'b'}], c: "2"}, {a: 3, b: [{z: 'c'}], d: "3"}],
|
||||
{ concatArrays: true },
|
||||
{a: 3, b: [{x: 'a'}, {w: 'd'}, {y: 'b'}, {z: 'c'}], c: "2", d: "3"},
|
||||
],
|
||||
[
|
||||
[{a: 1, b: [{x: 'a'}, {w: 'd'}]}, {a: 2, b: [{y: 'b'}], c: "2"}, {a: 3, b: [{z: 'c'}], d: "3"}],
|
||||
{ overwriteArrays: true },
|
||||
{a: 3, b: [{z: 'c'}], c: "2", d: "3"},
|
||||
],
|
||||
])(`case %#. input %j, options %j should return %j`, (sources, options, expected) => {
|
||||
expect(mergeDeep([...sources], options)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getSchema", () => {
|
||||
test.each([
|
||||
[
|
||||
,
|
||||
{ type: 'undefined', value: 'undefined', path: '' },
|
||||
],
|
||||
[
|
||||
undefined,
|
||||
{ type: 'undefined', value: 'undefined', path: '' },
|
||||
],
|
||||
[
|
||||
null,
|
||||
{ type: 'null', value: '[null]', path: '' },
|
||||
],
|
||||
[
|
||||
'John',
|
||||
{ type: 'string', value: 'John', path: '' },
|
||||
],
|
||||
[
|
||||
'123',
|
||||
{ type: 'string', value: '123', path: '' },
|
||||
],
|
||||
[
|
||||
123,
|
||||
{ type: 'number', value: '123', path: '' },
|
||||
],
|
||||
[
|
||||
true,
|
||||
{ type: 'boolean', value: 'true', path: '' },
|
||||
],
|
||||
[
|
||||
false,
|
||||
{ type: 'boolean', value: 'false', path: '' },
|
||||
],
|
||||
[
|
||||
() => {},
|
||||
{ type: 'function', value: '', path: '' },
|
||||
],
|
||||
[
|
||||
{},
|
||||
{ type: 'object', value: [], path: '' },
|
||||
],
|
||||
[
|
||||
[],
|
||||
{ type: 'array', value: [], path: '' },
|
||||
],
|
||||
[
|
||||
new Date('2022-11-22T00:00:00.000Z'),
|
||||
{ type: 'string', value: '2022-11-22T00:00:00.000Z', path: '' },
|
||||
],
|
||||
[
|
||||
Symbol('x'),
|
||||
{ type: 'symbol', value: 'Symbol(x)', path: '' },
|
||||
],
|
||||
[
|
||||
1n,
|
||||
{ type: 'bigint', value: '1', path: '' },
|
||||
],
|
||||
[
|
||||
['John', 1, true],
|
||||
{ type: 'array', value: [{ type: 'string', value: 'John', key: '0', path: '[0]' }, { type: 'number', value: '1', key: '1', path: '[1]' }, { type: 'boolean', value: 'true', key: '2', path: '[2]' }], path: '' },
|
||||
],
|
||||
[
|
||||
{ people: ['Joe', 'John']},
|
||||
{ type: 'object', value: [{ type: 'array', key: 'people', value: [{ type: 'string', value: 'Joe', key: '0', path: '["people"][0]' }, { type: 'string', value: 'John', key: '1', path: '["people"][1]' }], path: '["people"]' }], path: '' },
|
||||
],
|
||||
[
|
||||
[{ name: 'John', age: 22 }, { name: 'Joe', age: 33 }],
|
||||
{ type: 'array', value: [{ type: 'object', key: '0', value: [{ type: 'string', key: 'name', value: 'John', path: '[0]["name"]'}, { type: 'number', key: 'age', value: '22', path: '[0]["age"]' }], path: '[0]'}, { type: 'object', key: '1', value: [{ type: 'string', key: 'name', value: 'Joe', path: '[1]["name"]'}, { type: 'number', key: 'age', value: '33', path: '[1]["age"]' }], path: '[1]'}], path: '' },
|
||||
],
|
||||
[
|
||||
[{ name: 'John', age: 22, hobbies: ['surfing', 'traveling'] }, { name: 'Joe', age: 33, hobbies: ['skateboarding', 'gaming'] }],
|
||||
{ type: 'array', value: [{ type: 'object', key: '0', value: [{ type: 'string', key: 'name', value: 'John', path: '[0]["name"]'}, { type: 'number', key: 'age', value: '22', path: '[0]["age"]' }, {type: 'array', key: 'hobbies', value: [{type: 'string', key: '0', value: 'surfing', path: '[0]["hobbies"][0]'},{type: 'string', key: '1', value: 'traveling', path: '[0]["hobbies"][1]'}], path: '[0]["hobbies"]'}], path: '[0]'}, { type: 'object', key: '1', value: [{ type: 'string', key: 'name', value: 'Joe', path: '[1]["name"]'}, { type: 'number', key: 'age', value: '33', path: '[1]["age"]' }, {type: 'array', key: 'hobbies', value: [{type: 'string', key: '0', value: 'skateboarding', path: '[1]["hobbies"][0]'},{type: 'string', key: '1', value: 'gaming', path: '[1]["hobbies"][1]'}], path: '[1]["hobbies"]'}], path: '[1]'}], path: '' },
|
||||
],
|
||||
[
|
||||
[],
|
||||
{ type: 'array', value: [], path: '' },
|
||||
],
|
||||
[
|
||||
[[1,2]],
|
||||
{ type: 'array', value: [{ type: 'array', key: '0', value: [{type: 'number', key: '0', value: '1', path: '[0][0]'}, {type: 'number', key: '1', value: '2', path: '[0][1]'}], path: '[0]' }], path: '' },
|
||||
],
|
||||
[
|
||||
[[{ name: 'John', age: 22 }, { name: 'Joe', age: 33 }]],
|
||||
{ type: 'array', value: [{type: 'array', key: '0', value:[{ type: 'object', key: '0', value: [{ type: 'string', key: 'name', value: 'John', path: '[0][0]["name"]'}, { type: 'number', key: 'age', value: '22', path: '[0][0]["age"]' }], path: '[0][0]'}, { type: 'object', key: '1', value: [{ type: 'string', key: 'name', value: 'Joe', path: '[0][1]["name"]'}, { type: 'number', key: 'age', value: '33', path: '[0][1]["age"]' }], path: '[0][1]'}], path: '[0]'}], path: '' },
|
||||
],
|
||||
[
|
||||
[{ dates: [[new Date('2022-11-22T00:00:00.000Z'), new Date('2022-11-23T00:00:00.000Z')], [new Date('2022-12-22T00:00:00.000Z'), new Date('2022-12-23T00:00:00.000Z')]] }],
|
||||
{ type: 'array', value: [{ type: 'object', key: '0', value: [{ type: 'array', key: 'dates', value: [{ type: 'array', key: '0', value: [{type: 'string', key: '0', value: '2022-11-22T00:00:00.000Z', path: '[0]["dates"][0][0]'}, {type: 'string', key: '1', value: '2022-11-23T00:00:00.000Z', path: '[0]["dates"][0][1]'}], path: '[0]["dates"][0]' }, { type: 'array', key: '1', value: [{type: 'string', key: '0', value: '2022-12-22T00:00:00.000Z', path: '[0]["dates"][1][0]'}, {type: 'string', key: '1', value: '2022-12-23T00:00:00.000Z', path: '[0]["dates"][1][1]'}], path: '[0]["dates"][1]' }], path: '[0]["dates"]' }], path: '[0]' }], path: '' },
|
||||
],
|
||||
])('should return the correct json schema for %s', (input, schema) => {
|
||||
expect(getSchema(input)).toEqual(schema);
|
||||
});
|
||||
|
||||
it('should return the correct data when using the generated json path on an object', () => {
|
||||
const input = { people: ['Joe', 'John']};
|
||||
const schema = getSchema(input) as Schema;
|
||||
const pathData = jp.query(input, `$${ ((schema.value as Schema[])[0].value as Schema[])[0].path }`);
|
||||
expect(pathData).toEqual(['Joe']);
|
||||
});
|
||||
|
||||
it('should return the correct data when using the generated json path on a list', () => {
|
||||
const input = [{ name: 'John', age: 22, hobbies: ['surfing', 'traveling'] }, { name: 'Joe', age: 33, hobbies: ['skateboarding', 'gaming'] }];
|
||||
const schema = getSchema(input) as Schema;
|
||||
const pathData = jp.query(input, `$${ (((schema.value as Schema[])[0].value as Schema[])[2].value as Schema[])[1].path }`);
|
||||
expect(pathData).toEqual(['traveling']);
|
||||
});
|
||||
|
||||
it('should return the correct data when using the generated json path on a list of list', () => {
|
||||
const input = [[1,2]];
|
||||
const schema = getSchema(input) as Schema;
|
||||
const pathData = jp.query(input, `$${ (((schema.value as Schema[])[0]).value as Schema[])[1].path }`);
|
||||
expect(pathData).toEqual([2]);
|
||||
});
|
||||
|
||||
it('should return the correct data when using the generated json path on a list of list of objects', () => {
|
||||
const input = [[{ name: 'John', age: 22 }, { name: 'Joe', age: 33 }]];
|
||||
const schema = getSchema(input) as Schema;
|
||||
const pathData = jp.query(input, `$${ (((schema.value as Schema[])[0].value as Schema[])[1].value as Schema[])[1].path}`);
|
||||
expect(pathData).toEqual([33]);
|
||||
});
|
||||
|
||||
it('should return the correct data when using the generated json path on a list of objects with a list of date tuples', () => {
|
||||
const input = [{ dates: [[new Date('2022-11-22T00:00:00.000Z'), new Date('2022-11-23T00:00:00.000Z')], [new Date('2022-12-22T00:00:00.000Z'), new Date('2022-12-23T00:00:00.000Z')]] }];
|
||||
const schema = getSchema(input) as Schema;
|
||||
const pathData = jp.query(input, `$${ ((((schema.value as Schema[])[0].value as Schema[])[0].value as Schema[])[0].value as Schema[])[0].path}`);
|
||||
expect(pathData).toEqual([new Date('2022-11-22T00:00:00.000Z')]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("dateTests", () => {
|
||||
test.each([
|
||||
'04-08-2021',
|
||||
'15.11.2022 12:34h',
|
||||
'15.11.2022. 12:34h',
|
||||
'21-03-1988 12:34h',
|
||||
'2022-11-15',
|
||||
'11/11/2022',
|
||||
1668470400000,
|
||||
'2021-1-01',
|
||||
'2021-01-1',
|
||||
'2021/11/24',
|
||||
'2021/04/08',
|
||||
'Mar 25 2015',
|
||||
'25 Mar 2015',
|
||||
'2019-06-11T00:00',
|
||||
'2022-11-15T19:21:13.932Z',
|
||||
'Tue Jan 01 2019 02:07:00 GMT+0530',
|
||||
new Date(),
|
||||
'4/08/2021',
|
||||
'2021/04/04',
|
||||
])('should correctly recognize dates', (input) => {
|
||||
expect(isValidDate(input)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
import { INodeParameterResourceLocator } from "n8n-workflow";
|
||||
import { ICredentialsResponse } from "../Interface";
|
||||
import {INodeParameterResourceLocator} from "n8n-workflow";
|
||||
import {ICredentialsResponse} from "@/Interface";
|
||||
|
||||
/*
|
||||
Type guards used in editor-ui project
|
||||
*/
|
||||
|
||||
export const checkExhaustive = (value: never): never => {
|
||||
throw new Error(`Unhandled value: ${value}`);
|
||||
};
|
||||
|
||||
export function isResourceLocatorValue(value: unknown): value is INodeParameterResourceLocator {
|
||||
return Boolean(typeof value === 'object' && value && 'mode' in value && 'value' in value);
|
||||
}
|
||||
|
@ -16,3 +20,13 @@ export function isNotNull<T>(value: T | null): value is T {
|
|||
export function isValidCredentialResponse(value: unknown): value is ICredentialsResponse {
|
||||
return typeof value === 'object' && value !== null && 'id' in value;
|
||||
}
|
||||
|
||||
export const isObj = (obj: unknown): obj is object => !!obj && Object.getPrototypeOf(obj) === Object.prototype;
|
||||
|
||||
export function isString(value: unknown): value is string {
|
||||
return typeof value === 'string';
|
||||
}
|
||||
|
||||
export function isNumber(value: unknown): value is number {
|
||||
return typeof value === 'number';
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import dateformat from 'dateformat';
|
||||
import { IDataObject, jsonParse } from 'n8n-workflow';
|
||||
import { Schema, Optional, Primitives } from "@/Interface";
|
||||
import { isObj } from "@/utils/typeGuards";
|
||||
|
||||
/*
|
||||
Constants and utility functions than can be used to manipulate different data types and objects
|
||||
|
@ -40,7 +42,6 @@ export const intersection = <T>(...arrays: T[][]): T[] => {
|
|||
return [...new Set(rest.length ? intersection(ab, ...rest) : ab)];
|
||||
};
|
||||
|
||||
|
||||
export function abbreviateNumber(num: number) {
|
||||
const tier = (Math.log10(Math.abs(num)) / 3) | 0;
|
||||
|
||||
|
@ -61,18 +62,6 @@ export function convertToHumanReadableDate (epochTime: number) {
|
|||
return dateformat(epochTime, 'd mmmm, yyyy @ HH:MM Z');
|
||||
}
|
||||
|
||||
export function isString(value: unknown): value is string {
|
||||
return typeof value === 'string';
|
||||
}
|
||||
|
||||
export function isStringNumber(value: unknown): value is string {
|
||||
return !isNaN(Number(value));
|
||||
}
|
||||
|
||||
export function isNumber(value: unknown): value is number {
|
||||
return typeof value === 'number';
|
||||
}
|
||||
|
||||
export function stringSizeInBytes(input: string | IDataObject | IDataObject[] | undefined): number {
|
||||
if (input === undefined) return 0;
|
||||
|
||||
|
@ -135,3 +124,113 @@ export const clearJsonKey = (userInput: string | object) => {
|
|||
|
||||
return parsedUserInput.map(item => isJsonKeyObject(item) ? item.json : item);
|
||||
};
|
||||
|
||||
|
||||
// Holds weird date formats that we encounter when working with strings
|
||||
// Should be extended as new cases are found
|
||||
const CUSTOM_DATE_FORMATS = [
|
||||
/\d{1,2}-\d{1,2}-\d{4}/, // Should handle dash separated dates with year at the end
|
||||
/\d{1,2}\.\d{1,2}\.\d{4}/, // Should handle comma separated dates
|
||||
];
|
||||
|
||||
export const isValidDate = (input: string | number | Date): boolean => {
|
||||
try {
|
||||
// Try to construct date object using input
|
||||
const date = new Date(input);
|
||||
// This will not fail for wrong dates so have to check like this:
|
||||
if (date.getTime() < 0) {
|
||||
return false;
|
||||
} else if (date.toString() !== 'Invalid Date') {
|
||||
return true;
|
||||
} else if (typeof input === 'string') {
|
||||
// Try to cover edge cases with regex
|
||||
for (const regex of CUSTOM_DATE_FORMATS) {
|
||||
if (input.match(regex)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const getObjectKeys = <T extends object, K extends keyof T>(o: T): K[] => Object.keys(o) as K[];
|
||||
|
||||
export const mergeDeep = <T extends object | Primitives>(sources: T[], options?: Partial<Record<'overwriteArrays' | 'concatArrays', boolean>>): T => sources.reduce((target, source) => {
|
||||
if(Array.isArray(target) && Array.isArray(source)){
|
||||
const tLength = target.length;
|
||||
const sLength = source.length;
|
||||
|
||||
if(tLength === 0 || options?.overwriteArrays) {
|
||||
return source;
|
||||
}
|
||||
|
||||
if(sLength === 0) {
|
||||
return target;
|
||||
}
|
||||
|
||||
if(options?.concatArrays) {
|
||||
return [...target, ...source];
|
||||
}
|
||||
|
||||
if(tLength === sLength) {
|
||||
return target.map((item, index) => mergeDeep([item, source[index]], options));
|
||||
} else if(tLength < sLength) {
|
||||
return source.map((item, index) => mergeDeep([target[index], item], options));
|
||||
} else {
|
||||
return [...source, ...target.slice(sLength)];
|
||||
}
|
||||
} else if(isObj(target) && isObj(source)) {
|
||||
const targetKeys = getObjectKeys(target);
|
||||
const sourceKeys = getObjectKeys(source);
|
||||
const allKeys = [...new Set([...targetKeys, ...sourceKeys])];
|
||||
const mergedObject = Object.create(Object.prototype);
|
||||
for (const key of allKeys) {
|
||||
if (targetKeys.includes(key) && sourceKeys.includes(key)) {
|
||||
mergedObject[key] = mergeDeep([target[key] as T, source[key] as T], options);
|
||||
} else if (targetKeys.includes(key)) {
|
||||
mergedObject[key] = target[key];
|
||||
} else {
|
||||
mergedObject[key] = source[key];
|
||||
}
|
||||
}
|
||||
return mergedObject;
|
||||
} else {
|
||||
return source;
|
||||
}
|
||||
}, (Array.isArray(sources[0]) ? [] : {}) as T);
|
||||
|
||||
export const getSchema = (input: Optional<Primitives | object>, path = ''): Schema => {
|
||||
let schema:Schema = { type: 'undefined', value: 'undefined', path };
|
||||
switch (typeof input) {
|
||||
case 'object':
|
||||
if (input === null) {
|
||||
schema = { type: 'null', value: '[null]', path };
|
||||
} else if (input instanceof Date) {
|
||||
schema = { type: 'string', value: input.toISOString(), path };
|
||||
} else if (Array.isArray(input)) {
|
||||
schema = {
|
||||
type: 'array',
|
||||
value: input.map((item, index) => ({key: index.toString(), ...getSchema(item,`${path}[${index}]`)})),
|
||||
path,
|
||||
};
|
||||
} else if (isObj(input)) {
|
||||
schema = {
|
||||
type: 'object',
|
||||
value: Object.entries(input).map(([k, v]) => ({ key: k, ...getSchema(v, path + `["${ k }"]`)})),
|
||||
path,
|
||||
};
|
||||
}
|
||||
break;
|
||||
case 'function':
|
||||
schema = { type: 'function', value: ``, path };
|
||||
break;
|
||||
default:
|
||||
schema = { type: typeof input, value: String(input), path };
|
||||
}
|
||||
|
||||
return schema;
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue