fix(editor): Show pin button on binary output but disable it with tooltip (#8388)

This commit is contained in:
Csaba Tuncsik 2024-01-26 06:54:49 +01:00 committed by GitHub
parent dafacb90c6
commit caab97e667
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 212 additions and 47 deletions

View file

@ -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);

View 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>

View file

@ -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');
});
});

View file

@ -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.",