feat(editor): Help users discover expressions when using drag n drop (#8869)

This commit is contained in:
Elias Meire
2024-03-13 12:57:08 +01:00
committed by GitHub
parent 71f1b23771
commit e78cc2d8d2
28 changed files with 559 additions and 323 deletions

View File

@@ -58,7 +58,7 @@ const assignmentTypeToNodeProperty = (
const nameParameter = computed<INodeProperties>(() => ({
name: 'name',
displayName: '',
displayName: 'Name',
default: '',
requiresDataPath: 'single',
placeholder: 'name',
@@ -68,7 +68,7 @@ const nameParameter = computed<INodeProperties>(() => ({
const valueParameter = computed<INodeProperties>(() => {
return {
name: 'value',
displayName: '',
displayName: 'Value',
default: '',
placeholder: 'value',
...assignmentTypeToNodeProperty(assignment.value.type ?? 'string'),

View File

@@ -22,6 +22,7 @@
:rows="rows"
:additional-data="additionalExpressionData"
:path="path"
:event-bus="eventBus"
@focus="onFocus"
@blur="onBlur"
@change="onChange"
@@ -64,6 +65,7 @@ import type { Segment } from '@/types/expressions';
import type { TargetItem } from '@/Interface';
import type { IDataObject } from 'n8n-workflow';
import { useDebounce } from '@/composables/useDebounce';
import { type EventBus, createEventBus } from 'n8n-design-system/utils';
type InlineExpressionEditorInputRef = InstanceType<typeof InlineExpressionEditorInput>;
@@ -97,6 +99,10 @@ export default defineComponent({
type: Object as PropType<IDataObject>,
default: () => ({}),
},
eventBus: {
type: Object as PropType<EventBus>,
default: () => createEventBus(),
},
},
setup() {
const { callDebounced } = useDebounce();

View File

@@ -3,27 +3,30 @@
</template>
<script lang="ts">
import { completionStatus, startCompletion } from '@codemirror/autocomplete';
import { history } from '@codemirror/commands';
import { Compartment, EditorState, Prec } from '@codemirror/state';
import { EditorView, keymap } from '@codemirror/view';
import type { PropType } from 'vue';
import { defineComponent } from 'vue';
import { defineComponent, nextTick } from 'vue';
import { completionManager } from '@/mixins/completionManager';
import { expressionManager } from '@/mixins/expressionManager';
import { expressionInputHandler } from '@/plugins/codemirror/inputHandlers/expression.inputHandler';
import { n8nAutocompletion, n8nLang } from '@/plugins/codemirror/n8nLang';
import { highlighter } from '@/plugins/codemirror/resolvableHighlighter';
import { isEqual } from 'lodash-es';
import type { IDataObject } from 'n8n-workflow';
import { inputTheme } from './theme';
import {
autocompleteKeyMap,
enterKeyMap,
historyKeyMap,
tabKeyMap,
} from '@/plugins/codemirror/keymap';
import { completionStatus } from '@codemirror/autocomplete';
import { n8nAutocompletion, n8nLang } from '@/plugins/codemirror/n8nLang';
import { highlighter } from '@/plugins/codemirror/resolvableHighlighter';
import { isEqual } from 'lodash-es';
import { createEventBus, type EventBus } from 'n8n-design-system/utils';
import type { IDataObject } from 'n8n-workflow';
import { inputTheme } from './theme';
import { useNDVStore } from '@/stores/ndv.store';
import { mapStores } from 'pinia';
const editableConf = new Compartment();
@@ -51,6 +54,13 @@ export default defineComponent({
type: Object as PropType<IDataObject>,
default: () => ({}),
},
eventBus: {
type: Object as PropType<EventBus>,
default: () => createEventBus(),
},
},
computed: {
...mapStores(useNDVStore),
},
watch: {
isReadOnly(newValue: boolean) {
@@ -132,14 +142,34 @@ export default defineComponent({
this.editorState = this.editor.state;
highlighter.addColor(this.editor, this.resolvableSegments);
this.eventBus.on('drop', this.onDrop);
},
beforeUnmount() {
this.editor?.destroy();
this.eventBus.off('drop', this.onDrop);
},
methods: {
focus() {
this.editor?.focus();
},
setCursorPosition(pos: number) {
this.editor.dispatch({ selection: { anchor: pos, head: pos } });
},
async onDrop() {
await nextTick();
this.focus();
const END_OF_EXPRESSION = ' }}';
const value = this.editor.state.sliceDoc(0);
const cursorPosition = Math.max(value.lastIndexOf(END_OF_EXPRESSION), 0);
this.setCursorPosition(cursorPosition);
if (!this.ndvStore.isAutocompleteOnboarded) {
startCompletion(this.editor as EditorView);
}
},
},
});
</script>

View File

@@ -7,130 +7,111 @@
<div ref="root" data-test-id="inline-expression-editor-output"></div>
</n8n-text>
<div :class="$style.footer">
<n8n-text size="small" compact>
{{ i18n.baseText('parameterInput.anythingInside') }}
</n8n-text>
<div :class="$style['expression-syntax-example']" v-text="`{{ }}`"></div>
<n8n-text size="small" compact>
{{ i18n.baseText('parameterInput.isJavaScript') }}
</n8n-text>
{{ ' ' }}
<n8n-link
:class="$style['learn-more']"
size="small"
underline
theme="text"
:to="expressionsDocsUrl"
>
{{ i18n.baseText('parameterInput.learnMore') }}
</n8n-link>
<InlineExpressionTip />
</div>
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import { EditorView } from '@codemirror/view';
import { EditorState } from '@codemirror/state';
import { defineComponent } from 'vue';
import type { PropType } from 'vue';
import { highlighter } from '@/plugins/codemirror/resolvableHighlighter';
import { outputTheme } from './theme';
import type { Plaintext, Resolved, Segment } from '@/types/expressions';
import { EXPRESSIONS_DOCS_URL } from '@/constants';
import { useI18n } from '@/composables/useI18n';
import type { Plaintext, Resolved, Segment } from '@/types/expressions';
import { highlighter } from '@/plugins/codemirror/resolvableHighlighter';
import { computed, onMounted, onBeforeUnmount, ref, watch } from 'vue';
import { outputTheme } from './theme';
import InlineExpressionTip from './InlineExpressionTip.vue';
export default defineComponent({
name: 'InlineExpressionEditorOutput',
props: {
segments: {
type: Array as PropType<Segment[]>,
required: true,
},
isReadOnly: {
type: Boolean,
default: false,
},
visible: {
type: Boolean,
default: false,
},
noInputData: {
type: Boolean,
default: false,
},
hoveringItemNumber: {
type: Number,
required: true,
},
},
setup() {
const i18n = useI18n();
interface InlineExpressionEditorOutputProps {
segments: Segment[];
hoveringItemNumber: number;
isReadOnly?: boolean;
visible?: boolean;
noInputData?: boolean;
}
return {
i18n,
};
},
data() {
return {
editor: null as EditorView | null,
expressionsDocsUrl: EXPRESSIONS_DOCS_URL,
};
},
computed: {
resolvedExpression(): string {
return this.segments.reduce((acc, segment) => {
acc += segment.kind === 'resolvable' ? segment.resolved : segment.plaintext;
return acc;
}, '');
},
plaintextSegments(): Plaintext[] {
return this.segments.filter((s): s is Plaintext => s.kind === 'plaintext');
},
resolvedSegments(): Resolved[] {
let cursor = 0;
const props = withDefaults(defineProps<InlineExpressionEditorOutputProps>(), {
readOnly: false,
visible: false,
noInputData: false,
});
return this.segments
.map((segment) => {
segment.from = cursor;
cursor +=
segment.kind === 'plaintext'
? segment.plaintext.length
: segment.resolved
? segment.resolved.toString().length
: 0;
segment.to = cursor;
return segment;
})
.filter((segment): segment is Resolved => segment.kind === 'resolvable');
},
},
watch: {
segments() {
if (!this.editor) return;
const i18n = useI18n();
this.editor.dispatch({
changes: { from: 0, to: this.editor.state.doc.length, insert: this.resolvedExpression },
});
const editor = ref<EditorView | null>(null);
const root = ref<HTMLElement | null>(null);
highlighter.addColor(this.editor, this.resolvedSegments);
highlighter.removeColor(this.editor, this.plaintextSegments);
},
},
mounted() {
this.editor = new EditorView({
parent: this.$refs.root as HTMLDivElement,
state: EditorState.create({
doc: this.resolvedExpression,
extensions: [outputTheme(), EditorState.readOnly.of(true), EditorView.lineWrapping],
}),
const resolvedExpression = computed(() => {
if (props.segments.length === 0) {
return i18n.baseText('parameterInput.emptyString');
}
return props.segments.reduce((acc, segment) => {
acc += segment.kind === 'resolvable' ? (segment.resolved as string) : segment.plaintext;
return acc;
}, '');
});
const plaintextSegments = computed<Plaintext[]>(() => {
if (props.segments.length === 0) {
return [
{
from: 0,
to: resolvedExpression.value.length - 1,
plaintext: resolvedExpression.value,
kind: 'plaintext',
},
];
}
return props.segments.filter((s): s is Plaintext => s.kind === 'plaintext');
});
const resolvedSegments = computed<Resolved[]>(() => {
let cursor = 0;
return props.segments
.map((segment) => {
segment.from = cursor;
cursor +=
segment.kind === 'plaintext'
? segment.plaintext.length
: segment.resolved
? (segment.resolved as string | number | boolean).toString().length
: 0;
segment.to = cursor;
return segment;
})
.filter((segment): segment is Resolved => segment.kind === 'resolvable');
});
watch(
() => props.segments,
() => {
if (!editor.value) return;
editor.value.dispatch({
changes: { from: 0, to: editor.value.state.doc.length, insert: resolvedExpression.value },
});
highlighter.addColor(editor.value as EditorView, resolvedSegments.value);
highlighter.removeColor(editor.value as EditorView, plaintextSegments.value);
},
beforeUnmount() {
this.editor?.destroy();
},
);
onMounted(() => {
editor.value = new EditorView({
parent: root.value as HTMLElement,
state: EditorState.create({
doc: resolvedExpression.value,
extensions: [outputTheme(), EditorState.readOnly.of(true), EditorView.lineWrapping],
}),
});
});
onBeforeUnmount(() => {
editor.value?.destroy();
});
</script>
@@ -157,11 +138,14 @@ export default defineComponent({
}
.header,
.body,
.footer {
.body {
padding: var(--spacing-3xs);
}
.footer {
border-top: var(--border-base);
}
.header {
color: var(--color-text-dark);
font-weight: var(--font-weight-bold);
@@ -178,28 +162,5 @@ export default defineComponent({
padding-top: var(--spacing-2xs);
}
}
.footer {
border-top: var(--border-base);
padding: var(--spacing-4xs);
padding-left: var(--spacing-2xs);
padding-top: 0;
line-height: var(--font-line-height-regular);
color: var(--color-text-base);
.expression-syntax-example {
display: inline-block;
font-size: var(--font-size-2xs);
height: var(--font-size-m);
background-color: var(--color-expression-syntax-example);
margin-left: var(--spacing-5xs);
margin-right: var(--spacing-5xs);
}
.learn-more {
line-height: 1;
white-space: nowrap;
}
}
}
</style>

View File

@@ -0,0 +1,127 @@
<template>
<div v-if="tip === 'drag'" :class="$style.tip">
<n8n-text size="small" :class="$style.tipText"
>{{ $locale.baseText('parameterInput.tip') }}:
</n8n-text>
<n8n-text size="small" :class="$style.text">
{{ $locale.baseText('parameterInput.dragTipBeforePill') }}
</n8n-text>
<div :class="[$style.pill, { [$style.highlight]: !ndvStore.isMappingOnboarded }]">
{{ $locale.baseText('parameterInput.inputField') }}
</div>
<n8n-text size="small" :class="$style.text">
{{ $locale.baseText('parameterInput.dragTipAfterPill') }}
</n8n-text>
</div>
<div v-else-if="tip === 'executePrevious'" :class="$style.tip">
<n8n-text size="small" :class="$style.tipText"
>{{ $locale.baseText('parameterInput.tip') }}:
</n8n-text>
<n8n-text size="small" :class="$style.text"
>{{ $locale.baseText('expressionTip.noExecutionData') }}
</n8n-text>
</div>
<div v-else :class="$style.tip">
<n8n-text size="small" :class="$style.tipText"
>{{ $locale.baseText('parameterInput.tip') }}:
</n8n-text>
<n8n-text size="small" :class="$style.text">
{{ i18n.baseText('parameterInput.anythingInside') }}
</n8n-text>
<code v-text="`{{ }}`"></code>
<n8n-text size="small" :class="$style.text">
{{ i18n.baseText('parameterInput.isJavaScript') }}
</n8n-text>
<n8n-link
:class="$style['learn-more']"
size="small"
underline
theme="text"
:to="expressionsDocsUrl"
>
{{ i18n.baseText('parameterInput.learnMore') }}
</n8n-link>
</div>
</template>
<script setup lang="ts">
import { useI18n } from '@/composables/useI18n';
import { useNDVStore } from '@/stores/ndv.store';
import { computed } from 'vue';
import { EXPRESSIONS_DOCS_URL } from '@/constants';
const i18n = useI18n();
const ndvStore = useNDVStore();
const props = defineProps<{ tip?: 'drag' | 'default' }>();
const tip = computed(() => {
if (!ndvStore.hasInputData) {
return 'executePrevious';
}
if (props.tip) return props.tip;
if (ndvStore.focusedMappableInput) return 'drag';
return 'default';
});
const expressionsDocsUrl = EXPRESSIONS_DOCS_URL;
</script>
<style lang="scss" module>
.tip {
display: inline-flex;
align-items: center;
line-height: var(--font-line-height-regular);
color: var(--color-text-base);
font-size: var(--font-size-2xs);
padding: var(--spacing-2xs);
gap: var(--spacing-4xs);
.tipText {
color: var(--color-text-dark);
font-weight: var(--font-weight-bold);
}
.text {
flex-shrink: 0;
&:last-child {
flex-shrink: 1;
white-space: nowrap;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
}
code {
font-size: var(--font-size-3xs);
background: var(--color-background-base);
padding: var(--spacing-5xs);
border-radius: var(--border-radius-base);
}
.pill {
flex-shrink: 0;
display: flex;
align-items: center;
color: var(--color-text-dark);
border: var(--border-base);
border-color: var(--color-foreground-light);
background-color: var(--color-background-xlight);
padding: var(--spacing-5xs) var(--spacing-3xs);
border-radius: var(--border-radius-base);
}
.highlight {
color: var(--color-primary);
background-color: var(--color-primary-tint-3);
border-color: var(--color-primary-tint-1);
}
}
</style>

View File

@@ -51,6 +51,7 @@
:path="path"
:additional-expression-data="additionalExpressionData"
:class="{ 'ph-no-capture': shouldRedactValue }"
:event-bus="eventBus"
@update:model-value="expressionUpdated"
@modalOpenerClick="openExpressionEditorModal"
@focus="setFocus"

View File

@@ -29,46 +29,34 @@
@drop="onDrop"
>
<template #default="{ droppable, activeDrop }">
<n8n-tooltip
placement="left"
:visible="showMappingTooltip"
:buttons="dataMappingTooltipButtons"
>
<template #content>
<span
v-html="
i18n.baseText(`dataMapping.${displayMode}Hint`, {
interpolate: { name: parameter.displayName },
})
"
/>
</template>
<ParameterInputWrapper
ref="param"
:parameter="parameter"
:model-value="value"
:path="path"
:is-read-only="isReadOnly"
:is-assignment="isAssignment"
:rows="rows"
:droppable="droppable"
:active-drop="activeDrop"
:force-show-expression="forceShowExpression"
:hint="hint"
:hide-hint="hideHint"
:hide-issues="hideIssues"
:label="label"
:event-bus="eventBus"
input-size="small"
@update="valueChanged"
@textInput="onTextInput"
@focus="onFocus"
@blur="onBlur"
@drop="onDrop"
/>
</n8n-tooltip>
<ParameterInputWrapper
ref="param"
:parameter="parameter"
:model-value="value"
:path="path"
:is-read-only="isReadOnly"
:is-assignment="isAssignment"
:rows="rows"
:droppable="droppable"
:active-drop="activeDrop"
:force-show-expression="forceShowExpression"
:hint="hint"
:hide-hint="hideHint"
:hide-issues="hideIssues"
:label="label"
:event-bus="eventBus"
input-size="small"
@update="valueChanged"
@textInput="onTextInput"
@focus="onFocus"
@blur="onBlur"
@drop="onDrop"
/>
</template>
</DraggableTarget>
<div v-if="showDragnDropTip" :class="$style.tip">
<InlineExpressionTip tip="drag" />
</div>
<div
:class="{
[$style.options]: true,
@@ -94,7 +82,7 @@ import { defineComponent } from 'vue';
import type { PropType } from 'vue';
import { mapStores } from 'pinia';
import type { IN8nButton, INodeUi, IRunDataDisplayMode, IUpdateInformation } from '@/Interface';
import type { INodeUi, IRunDataDisplayMode, IUpdateInformation } from '@/Interface';
import ParameterOptions from '@/components/ParameterOptions.vue';
import DraggableTarget from '@/components/DraggableTarget.vue';
@@ -109,13 +97,11 @@ import type {
IParameterLabel,
NodeParameterValueType,
} from 'n8n-workflow';
import type { BaseTextKey } from '@/plugins/i18n';
import { useNDVStore } from '@/stores/ndv.store';
import { useSegment } from '@/stores/segment.store';
import { getMappedResult } from '@/utils/mappingUtils';
import { createEventBus } from 'n8n-design-system/utils';
const DISPLAY_MODES_WITH_DATA_MAPPING = ['table', 'json', 'schema'];
import InlineExpressionTip from './InlineExpressionEditor/InlineExpressionTip.vue';
export default defineComponent({
name: 'ParameterInputFull',
@@ -123,6 +109,7 @@ export default defineComponent({
ParameterOptions,
DraggableTarget,
ParameterInputWrapper,
InlineExpressionTip,
},
props: {
displayOptions: {
@@ -159,6 +146,7 @@ export default defineComponent({
},
parameter: {
type: Object as PropType<INodeProperties>,
required: true,
},
path: {
type: String,
@@ -192,24 +180,8 @@ export default defineComponent({
focused: false,
menuExpanded: false,
forceShowExpression: false,
dataMappingTooltipButtons: [] as IN8nButton[],
mappingTooltipEnabled: false,
};
},
mounted() {
const mappingTooltipDismissHandler = this.onMappingTooltipDismissed.bind(this);
this.dataMappingTooltipButtons = [
{
attrs: {
label: this.i18n.baseText('_reusableBaseText.dismiss' as BaseTextKey),
'data-test-id': 'dismiss-mapping-tooltip',
},
listeners: {
onClick: mappingTooltipDismissHandler,
},
},
];
},
computed: {
...mapStores(useNDVStore),
node(): INodeUi | null {
@@ -221,6 +193,9 @@ export default defineComponent({
isInputTypeString(): boolean {
return this.parameter.type === 'string';
},
isInputTypeNumber(): boolean {
return this.parameter.type === 'number';
},
isResourceLocator(): boolean {
return this.parameter.type === 'resourceLocator';
},
@@ -239,31 +214,29 @@ export default defineComponent({
displayMode(): IRunDataDisplayMode {
return this.ndvStore.inputPanelDisplayMode;
},
showMappingTooltip(): boolean {
showDragnDropTip(): boolean {
return (
this.mappingTooltipEnabled &&
!this.ndvStore.isMappingOnboarded &&
this.focused &&
this.isInputTypeString &&
!this.isInputDataEmpty &&
DISPLAY_MODES_WITH_DATA_MAPPING.includes(this.displayMode)
(this.isInputTypeString || this.isInputTypeNumber) &&
!this.isValueExpression &&
!this.isDropDisabled &&
!this.ndvStore.isMappingOnboarded
);
},
},
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) {
if (
!this.parameter.noDataExpression &&
this.ndvStore.focusedMappableInput === this.parameter.displayName
) {
this.ndvStore.setMappableNDVInputFocus('');
}
this.$emit('blur');
@@ -333,6 +306,7 @@ export default defineComponent({
}
this.valueChanged(parameterData);
this.eventBus.emit('drop', updatedValue);
if (!this.ndvStore.isMappingOnboarded) {
this.showMessage({
@@ -342,7 +316,7 @@ export default defineComponent({
dangerouslyUseHTMLString: true,
});
this.ndvStore.disableMappingHint();
this.ndvStore.setMappingOnboarded();
}
this.ndvStore.setMappingTelemetry({
@@ -364,21 +338,11 @@ export default defineComponent({
this.forceShowExpression = false;
}, 200);
},
onMappingTooltipDismissed() {
this.ndvStore.disableMappingHint(false);
},
},
watch: {
showMappingTooltip(newValue: boolean) {
if (!newValue) {
this.$telemetry.track('User viewed data mapping tooltip', { type: 'param focus' });
}
},
},
});
</script>
<style module>
<style lang="scss" module>
.wrapper {
position: relative;
@@ -388,6 +352,20 @@ export default defineComponent({
}
}
}
.tip {
position: absolute;
z-index: 2;
top: 100%;
background: var(--color-code-background);
border: var(--border-base);
border-top: none;
width: 100%;
box-shadow: 0 2px 6px 0 rgba(#441c17, 0.1);
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
.options {
position: absolute;
bottom: -22px;

View File

@@ -1,5 +1,5 @@
<template>
<div :class="$style.jsonDisplay">
<div :class="[$style.jsonDisplay, { [$style.highlight]: highlight }]">
<Suspense>
<RunDataJsonActions
v-if="!editMode.enabled"
@@ -157,6 +157,9 @@ export default defineComponent({
jsonData(): IDataObject[] {
return executionDataToJson(this.inputData);
},
highlight(): boolean {
return !this.ndvStore.isMappingOnboarded && Boolean(this.ndvStore.focusedMappableInput);
},
},
methods: {
getShortKey(el: HTMLElement): string {
@@ -200,7 +203,9 @@ export default defineComponent({
setTimeout(() => {
void this.externalHooks.run('runDataJson.onDragEnd', telemetryPayload);
this.$telemetry.track('User dragged data for mapping', telemetryPayload);
this.$telemetry.track('User dragged data for mapping', telemetryPayload, {
withPostHog: true,
});
}, 1000); // ensure dest data gets set if drop
},
getContent(value: unknown): string {
@@ -232,20 +237,22 @@ export default defineComponent({
opacity: 1;
}
}
}
.mappable {
cursor: grab;
.mappable {
cursor: grab;
&:hover {
background-color: var(--color-json-highlight);
&:hover {
background-color: var(--color-json-highlight);
}
}
}
.dragged {
&,
&:hover {
background-color: var(--color-primary-tint-2);
&.highlight .mappable,
.dragged {
&,
&:hover {
background-color: var(--color-primary-tint-2);
color: var(--color-primary);
}
}
}
</style>

View File

@@ -35,6 +35,10 @@ const schema = computed(() => getSchemaForExecutionData(props.data));
const isDataEmpty = computed(() => isEmpty(props.data));
const highlight = computed(() => {
return !ndvStore.isMappingOnboarded && Boolean(ndvStore.focusedMappableInput);
});
const onDragStart = (el: HTMLElement) => {
if (el?.dataset?.path) {
draggingPath.value = el.dataset.path;
@@ -62,13 +66,13 @@ const onDragEnd = (el: HTMLElement) => {
void useExternalHooks().run('runDataJson.onDragEnd', telemetryPayload);
telemetry.track('User dragged data for mapping', telemetryPayload);
telemetry.track('User dragged data for mapping', telemetryPayload, { withPostHog: true });
}, 1000); // ensure dest data gets set if drop
};
</script>
<template>
<div :class="$style.schemaWrapper">
<div :class="[$style.schemaWrapper, { highlightSchema: highlight }]">
<n8n-info-tip v-if="isDataEmpty">{{
i18n.baseText('dataMapping.schemaView.emptyData')
}}</n8n-info-tip>

View File

@@ -39,6 +39,8 @@ const text = computed(() =>
Array.isArray(props.schema.value) ? '' : shorten(props.schema.value, 600, 0),
);
const dragged = computed(() => props.draggingPath === props.schema.path);
const getJsonParameterPath = (path: string): string =>
getMappedExpression({
nodeName: props.node!.name,
@@ -83,7 +85,7 @@ const getIconBySchemaType = (type: Schema['type']): string => {
:class="{
[$style.pill]: true,
[$style.mappable]: mappingEnabled,
[$style.dragged]: draggingPath === schema.path,
[$style.highlight]: dragged,
}"
>
<span
@@ -203,6 +205,25 @@ const getIconBySchemaType = (type: Schema['type']): string => {
}
}
:global(.highlightSchema) {
.pill.mappable {
&,
&:hover,
span,
&:hover span span {
color: var(--color-primary);
border-color: var(--color-primary-tint-1);
background-color: var(--color-primary-tint-3);
svg {
path {
fill: var(--color-primary);
}
}
}
}
}
.pill {
float: left;
display: inline-flex;
@@ -237,22 +258,6 @@ const getIconBySchemaType = (type: Schema['type']): string => {
}
}
}
&.dragged {
&,
&:hover,
span {
color: var(--color-primary);
border-color: var(--color-primary-tint-1);
background-color: var(--color-primary-tint-3);
svg {
path {
fill: var(--color-primary);
}
}
}
}
}
.label {

View File

@@ -1,5 +1,5 @@
<template>
<div :class="$style.dataDisplay">
<div :class="[$style.dataDisplay, { [$style.highlight]: highlight }]">
<table v-if="tableData.columns && tableData.columns.length === 0" :class="$style.table">
<tr>
<th :class="$style.emptyCell"></th>
@@ -259,6 +259,9 @@ export default defineComponent({
focusedMappableInput(): string {
return this.ndvStore.focusedMappableInput;
},
highlight(): boolean {
return !this.ndvStore.isMappingOnboarded && Boolean(this.ndvStore.focusedMappableInput);
},
},
methods: {
shorten,
@@ -438,7 +441,9 @@ export default defineComponent({
void this.externalHooks.run('runDataTable.onDragEnd', telemetryPayload);
this.$telemetry.track('User dragged data for mapping', telemetryPayload);
this.$telemetry.track('User dragged data for mapping', telemetryPayload, {
withPostHog: true,
});
}, 1000); // ensure dest data gets set if drop
},
isSimple(data: unknown): boolean {
@@ -642,7 +647,12 @@ export default defineComponent({
}
}
.highlight .draggableHeader {
color: var(--color-primary);
}
.draggingHeader {
color: var(--color-primary);
background-color: var(--color-primary-tint-2);
}