mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
fix(editor): Show pin button on binary output but disable it with tooltip (#8388)
This commit is contained in:
parent
dafacb90c6
commit
caab97e667
|
@ -70,48 +70,24 @@
|
||||||
data-test-id="ndv-edit-pinned-data"
|
data-test-id="ndv-edit-pinned-data"
|
||||||
@click="enterEditMode({ origin: 'editIconButton' })"
|
@click="enterEditMode({ origin: 'editIconButton' })"
|
||||||
/>
|
/>
|
||||||
<n8n-tooltip
|
|
||||||
v-if="canPinData && rawInputData.length"
|
|
||||||
v-show="!editMode.enabled"
|
|
||||||
placement="bottom-end"
|
|
||||||
:visible="
|
|
||||||
isControlledPinDataTooltip
|
|
||||||
? isControlledPinDataTooltip && pinDataDiscoveryTooltipVisible
|
|
||||||
: undefined
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<template v-if="!isControlledPinDataTooltip" #content>
|
|
||||||
<div :class="$style.tooltipContainer">
|
|
||||||
<strong>{{ $locale.baseText('ndv.pinData.pin.title') }}</strong>
|
|
||||||
<n8n-text size="small" tag="p">
|
|
||||||
{{ $locale.baseText('ndv.pinData.pin.description') }}
|
|
||||||
|
|
||||||
<n8n-link :to="dataPinningDocsUrl" size="small">
|
<RunDataPinButton
|
||||||
{{ $locale.baseText('ndv.pinData.pin.link') }}
|
v-if="(canPinData || !!binaryData?.length) && rawInputData.length && !editMode.enabled"
|
||||||
</n8n-link>
|
:disabled="
|
||||||
</n8n-text>
|
(!rawInputData.length && !pinnedData.hasData.value) ||
|
||||||
</div>
|
isReadOnlyRoute ||
|
||||||
</template>
|
readOnlyEnv ||
|
||||||
<template v-else #content>
|
!!binaryData?.length
|
||||||
<div :class="$style.tooltipContainer">
|
"
|
||||||
{{ $locale.baseText('node.discovery.pinData.ndv') }}
|
:tooltip-contents-visibility="{
|
||||||
</div>
|
binaryDataTooltipContent: !!binaryData?.length,
|
||||||
</template>
|
pinDataDiscoveryTooltipContent:
|
||||||
<n8n-icon-button
|
isControlledPinDataTooltip && pinDataDiscoveryTooltipVisible,
|
||||||
:class="['ml-2xs', $style.pinDataButton]"
|
}"
|
||||||
type="tertiary"
|
:data-pinning-docs-url="dataPinningDocsUrl"
|
||||||
:active="pinnedData.hasData.value"
|
:pinned-data="pinnedData"
|
||||||
icon="thumbtack"
|
@toggle-pin-data="onTogglePinData({ source: 'pin-icon-click' })"
|
||||||
:disabled="
|
/>
|
||||||
editMode.enabled ||
|
|
||||||
(rawInputData.length === 0 && !pinnedData.hasData.value) ||
|
|
||||||
isReadOnlyRoute ||
|
|
||||||
readOnlyEnv
|
|
||||||
"
|
|
||||||
data-test-id="ndv-pin-data"
|
|
||||||
@click="onTogglePinData({ source: 'pin-icon-click' })"
|
|
||||||
/>
|
|
||||||
</n8n-tooltip>
|
|
||||||
|
|
||||||
<div v-show="editMode.enabled" :class="$style.editModeActions">
|
<div v-show="editMode.enabled" :class="$style.editModeActions">
|
||||||
<n8n-button
|
<n8n-button
|
||||||
|
@ -621,6 +597,7 @@ import { useToast } from '@/composables/useToast';
|
||||||
import { isObject } from 'lodash-es';
|
import { isObject } from 'lodash-es';
|
||||||
import { useExternalHooks } from '@/composables/useExternalHooks';
|
import { useExternalHooks } from '@/composables/useExternalHooks';
|
||||||
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||||
|
import RunDataPinButton from '@/components/RunDataPinButton.vue';
|
||||||
|
|
||||||
const RunDataTable = defineAsyncComponent(
|
const RunDataTable = defineAsyncComponent(
|
||||||
async () => await import('@/components/RunDataTable.vue'),
|
async () => await import('@/components/RunDataTable.vue'),
|
||||||
|
@ -649,6 +626,7 @@ export default defineComponent({
|
||||||
RunDataSchema,
|
RunDataSchema,
|
||||||
RunDataHtml,
|
RunDataHtml,
|
||||||
RunDataSearch,
|
RunDataSearch,
|
||||||
|
RunDataPinButton,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
node: {
|
node: {
|
||||||
|
@ -1727,12 +1705,6 @@ export default defineComponent({
|
||||||
max-width: 240px;
|
max-width: 240px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pinDataButton {
|
|
||||||
svg {
|
|
||||||
transition: transform 0.3s ease;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.spinner {
|
.spinner {
|
||||||
* {
|
* {
|
||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
|
|
68
packages/editor-ui/src/components/RunDataPinButton.vue
Normal file
68
packages/editor-ui/src/components/RunDataPinButton.vue
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
import type { usePinnedData } from '@/composables/usePinnedData';
|
||||||
|
|
||||||
|
const locale = useI18n();
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
tooltipContentsVisibility: {
|
||||||
|
binaryDataTooltipContent: boolean;
|
||||||
|
pinDataDiscoveryTooltipContent: boolean;
|
||||||
|
};
|
||||||
|
dataPinningDocsUrl: string;
|
||||||
|
pinnedData: ReturnType<typeof usePinnedData>;
|
||||||
|
disabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'togglePinData'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const visible = computed(() =>
|
||||||
|
props.tooltipContentsVisibility.pinDataDiscoveryTooltipContent ? true : undefined,
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n8n-tooltip placement="bottom-end" :visible="visible">
|
||||||
|
<template #content>
|
||||||
|
<div v-if="props.tooltipContentsVisibility.binaryDataTooltipContent">
|
||||||
|
{{ locale.baseText('ndv.pinData.pin.binary') }}
|
||||||
|
</div>
|
||||||
|
<div v-else-if="props.tooltipContentsVisibility.pinDataDiscoveryTooltipContent">
|
||||||
|
{{ locale.baseText('node.discovery.pinData.ndv') }}
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<strong>{{ locale.baseText('ndv.pinData.pin.title') }}</strong>
|
||||||
|
<n8n-text size="small" tag="p">
|
||||||
|
{{ locale.baseText('ndv.pinData.pin.description') }}
|
||||||
|
|
||||||
|
<n8n-link :to="props.dataPinningDocsUrl" size="small">
|
||||||
|
{{ locale.baseText('ndv.pinData.pin.link') }}
|
||||||
|
</n8n-link>
|
||||||
|
</n8n-text>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<n8n-icon-button
|
||||||
|
:class="$style.pinDataButton"
|
||||||
|
type="tertiary"
|
||||||
|
:active="props.pinnedData.hasData.value"
|
||||||
|
icon="thumbtack"
|
||||||
|
:disabled="props.disabled"
|
||||||
|
data-test-id="ndv-pin-data"
|
||||||
|
@click="emit('togglePinData')"
|
||||||
|
/>
|
||||||
|
</n8n-tooltip>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.pinDataButton {
|
||||||
|
margin-left: var(--spacing-2xs);
|
||||||
|
svg {
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,124 @@
|
||||||
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
|
import { cleanup, waitFor } from '@testing-library/vue';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
|
import RunDataPinButton from '@/components/RunDataPinButton.vue';
|
||||||
|
import { STORES } from '@/constants';
|
||||||
|
|
||||||
|
const renderComponent = createComponentRenderer(RunDataPinButton, {
|
||||||
|
global: {
|
||||||
|
stubs: ['font-awesome-icon'],
|
||||||
|
plugins: [
|
||||||
|
createTestingPinia({
|
||||||
|
initialState: {
|
||||||
|
[STORES.SETTINGS]: {
|
||||||
|
settings: {
|
||||||
|
templates: {
|
||||||
|
enabled: true,
|
||||||
|
host: 'https://api.n8n.io/api/',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
tooltipContentsVisibility: {
|
||||||
|
binaryDataTooltipContent: false,
|
||||||
|
pinDataDiscoveryTooltipContent: false,
|
||||||
|
},
|
||||||
|
dataPinningDocsUrl: '',
|
||||||
|
pinnedData: {
|
||||||
|
hasData: false,
|
||||||
|
},
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('RunDataPinButton.vue', () => {
|
||||||
|
beforeEach(cleanup);
|
||||||
|
|
||||||
|
it('shows default tooltip content only on button hover', async () => {
|
||||||
|
const { getByRole, queryByRole, emitted } = renderComponent();
|
||||||
|
|
||||||
|
expect(queryByRole('tooltip')).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(getByRole('button')).toBeEnabled();
|
||||||
|
await userEvent.hover(getByRole('button'));
|
||||||
|
|
||||||
|
expect(getByRole('tooltip')).toBeVisible();
|
||||||
|
expect(getByRole('tooltip')).toHaveTextContent('More info');
|
||||||
|
|
||||||
|
await userEvent.click(getByRole('button'));
|
||||||
|
expect(emitted().togglePinData).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows binary data tooltip content only on disabled button hover', async () => {
|
||||||
|
const { getByRole, queryByRole, emitted } = renderComponent({
|
||||||
|
props: {
|
||||||
|
tooltipContentsVisibility: {
|
||||||
|
binaryDataTooltipContent: true,
|
||||||
|
pinDataDiscoveryTooltipContent: false,
|
||||||
|
},
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(queryByRole('tooltip')).not.toBeInTheDocument();
|
||||||
|
expect(getByRole('button')).toBeDisabled();
|
||||||
|
|
||||||
|
await userEvent.hover(getByRole('button'));
|
||||||
|
|
||||||
|
expect(getByRole('tooltip')).toBeVisible();
|
||||||
|
expect(getByRole('tooltip')).toHaveTextContent('disabled');
|
||||||
|
|
||||||
|
await userEvent.click(getByRole('button'));
|
||||||
|
expect(emitted().togglePinData).not.toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows pin data discoverability tooltip immediately (not on hover)', async () => {
|
||||||
|
const { getByRole } = renderComponent({
|
||||||
|
props: {
|
||||||
|
tooltipContentsVisibility: {
|
||||||
|
binaryDataTooltipContent: false,
|
||||||
|
pinDataDiscoveryTooltipContent: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(getByRole('tooltip')).toBeVisible();
|
||||||
|
expect(getByRole('tooltip')).toHaveTextContent('instead of waiting');
|
||||||
|
});
|
||||||
|
expect(getByRole('button')).toBeEnabled();
|
||||||
|
|
||||||
|
await userEvent.hover(getByRole('button'));
|
||||||
|
|
||||||
|
expect(getByRole('tooltip')).toBeVisible();
|
||||||
|
expect(getByRole('tooltip')).toHaveTextContent('instead of waiting');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows binary data tooltip content even if discoverability tooltip enabled', async () => {
|
||||||
|
const { getByRole } = renderComponent({
|
||||||
|
props: {
|
||||||
|
tooltipContentsVisibility: {
|
||||||
|
binaryDataTooltipContent: true,
|
||||||
|
pinDataDiscoveryTooltipContent: true,
|
||||||
|
},
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(getByRole('tooltip')).toBeVisible();
|
||||||
|
expect(getByRole('tooltip')).toHaveTextContent('disabled');
|
||||||
|
});
|
||||||
|
expect(getByRole('button')).toBeDisabled();
|
||||||
|
|
||||||
|
await userEvent.hover(getByRole('button'));
|
||||||
|
|
||||||
|
expect(getByRole('tooltip')).toBeVisible();
|
||||||
|
expect(getByRole('tooltip')).toHaveTextContent('disabled');
|
||||||
|
});
|
||||||
|
});
|
|
@ -820,6 +820,7 @@
|
||||||
"ndv.title.renameNode": "Rename node",
|
"ndv.title.renameNode": "Rename node",
|
||||||
"ndv.pinData.pin.title": "Pin data",
|
"ndv.pinData.pin.title": "Pin data",
|
||||||
"ndv.pinData.pin.description": "Node will always output this data instead of executing.",
|
"ndv.pinData.pin.description": "Node will always output this data instead of executing.",
|
||||||
|
"ndv.pinData.pin.binary": "Pin Data is disabled as this node's output contains binary data.",
|
||||||
"ndv.pinData.pin.link": "More info",
|
"ndv.pinData.pin.link": "More info",
|
||||||
"ndv.pinData.pin.multipleRuns.title": "Run #{index} was pinned",
|
"ndv.pinData.pin.multipleRuns.title": "Run #{index} was pinned",
|
||||||
"ndv.pinData.pin.multipleRuns.description": "This run will be outputted each time the node is run.",
|
"ndv.pinData.pin.multipleRuns.description": "This run will be outputted each time the node is run.",
|
||||||
|
|
Loading…
Reference in a new issue