mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 04:34:06 -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"
|
||||
@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">
|
||||
{{ $locale.baseText('ndv.pinData.pin.link') }}
|
||||
</n8n-link>
|
||||
</n8n-text>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else #content>
|
||||
<div :class="$style.tooltipContainer">
|
||||
{{ $locale.baseText('node.discovery.pinData.ndv') }}
|
||||
</div>
|
||||
</template>
|
||||
<n8n-icon-button
|
||||
:class="['ml-2xs', $style.pinDataButton]"
|
||||
type="tertiary"
|
||||
:active="pinnedData.hasData.value"
|
||||
icon="thumbtack"
|
||||
: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>
|
||||
<RunDataPinButton
|
||||
v-if="(canPinData || !!binaryData?.length) && rawInputData.length && !editMode.enabled"
|
||||
:disabled="
|
||||
(!rawInputData.length && !pinnedData.hasData.value) ||
|
||||
isReadOnlyRoute ||
|
||||
readOnlyEnv ||
|
||||
!!binaryData?.length
|
||||
"
|
||||
:tooltip-contents-visibility="{
|
||||
binaryDataTooltipContent: !!binaryData?.length,
|
||||
pinDataDiscoveryTooltipContent:
|
||||
isControlledPinDataTooltip && pinDataDiscoveryTooltipVisible,
|
||||
}"
|
||||
:data-pinning-docs-url="dataPinningDocsUrl"
|
||||
:pinned-data="pinnedData"
|
||||
@toggle-pin-data="onTogglePinData({ source: 'pin-icon-click' })"
|
||||
/>
|
||||
|
||||
<div v-show="editMode.enabled" :class="$style.editModeActions">
|
||||
<n8n-button
|
||||
|
@ -621,6 +597,7 @@ import { useToast } from '@/composables/useToast';
|
|||
import { isObject } from 'lodash-es';
|
||||
import { useExternalHooks } from '@/composables/useExternalHooks';
|
||||
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||
import RunDataPinButton from '@/components/RunDataPinButton.vue';
|
||||
|
||||
const RunDataTable = defineAsyncComponent(
|
||||
async () => await import('@/components/RunDataTable.vue'),
|
||||
|
@ -649,6 +626,7 @@ export default defineComponent({
|
|||
RunDataSchema,
|
||||
RunDataHtml,
|
||||
RunDataSearch,
|
||||
RunDataPinButton,
|
||||
},
|
||||
props: {
|
||||
node: {
|
||||
|
@ -1727,12 +1705,6 @@ export default defineComponent({
|
|||
max-width: 240px;
|
||||
}
|
||||
|
||||
.pinDataButton {
|
||||
svg {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.spinner {
|
||||
* {
|
||||
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.pinData.pin.title": "Pin data",
|
||||
"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.multipleRuns.title": "Run #{index} was pinned",
|
||||
"ndv.pinData.pin.multipleRuns.description": "This run will be outputted each time the node is run.",
|
||||
|
|
Loading…
Reference in a new issue