mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
feat(editor): Collapse button on table view (#16993)
This commit is contained in:
@@ -44,6 +44,7 @@ import IconLucideChevronDown from '~icons/lucide/chevron-down';
|
|||||||
import IconLucideChevronLeft from '~icons/lucide/chevron-left';
|
import IconLucideChevronLeft from '~icons/lucide/chevron-left';
|
||||||
import IconLucideChevronRight from '~icons/lucide/chevron-right';
|
import IconLucideChevronRight from '~icons/lucide/chevron-right';
|
||||||
import IconLucideChevronUp from '~icons/lucide/chevron-up';
|
import IconLucideChevronUp from '~icons/lucide/chevron-up';
|
||||||
|
import IconLucideChevronsDownUp from '~icons/lucide/chevrons-down-up';
|
||||||
import IconLucideChevronsLeft from '~icons/lucide/chevrons-left';
|
import IconLucideChevronsLeft from '~icons/lucide/chevrons-left';
|
||||||
import IconLucideChevronsUpDown from '~icons/lucide/chevrons-up-down';
|
import IconLucideChevronsUpDown from '~icons/lucide/chevrons-up-down';
|
||||||
import IconLucideCircle from '~icons/lucide/circle';
|
import IconLucideCircle from '~icons/lucide/circle';
|
||||||
@@ -438,6 +439,8 @@ export const updatedIconSet = {
|
|||||||
'chevron-right': IconLucideChevronRight,
|
'chevron-right': IconLucideChevronRight,
|
||||||
'chevron-up': IconLucideChevronUp,
|
'chevron-up': IconLucideChevronUp,
|
||||||
'chevrons-left': IconLucideChevronsLeft,
|
'chevrons-left': IconLucideChevronsLeft,
|
||||||
|
'chevrons-down-up': IconLucideChevronsDownUp,
|
||||||
|
'chevrons-up-down': IconLucideChevronsUpDown,
|
||||||
circle: IconLucideCircle,
|
circle: IconLucideCircle,
|
||||||
'circle-alert': IconLucideCircleAlert,
|
'circle-alert': IconLucideCircleAlert,
|
||||||
'circle-check': IconLucideCircleCheck,
|
'circle-check': IconLucideCircleCheck,
|
||||||
|
|||||||
@@ -683,6 +683,8 @@
|
|||||||
"dataMapping.tableView.tableColumnsExceeded": "Some columns are hidden",
|
"dataMapping.tableView.tableColumnsExceeded": "Some columns are hidden",
|
||||||
"dataMapping.tableView.tableColumnsExceeded.tooltip": "Your data has more than {columnLimit} columns so some are hidden. Switch to {link} to see all data.",
|
"dataMapping.tableView.tableColumnsExceeded.tooltip": "Your data has more than {columnLimit} columns so some are hidden. Switch to {link} to see all data.",
|
||||||
"dataMapping.tableView.tableColumnsExceeded.tooltip.link": "JSON view",
|
"dataMapping.tableView.tableColumnsExceeded.tooltip.link": "JSON view",
|
||||||
|
"dataMapping.tableView.columnCollapsing": "Collapse rows",
|
||||||
|
"dataMapping.tableView.columnCollapsing.tooltip": "Collapse rows (to compare values in this column)",
|
||||||
"dataMapping.schemaView.emptyData": "No fields - node executed, but no items were sent on this branch",
|
"dataMapping.schemaView.emptyData": "No fields - node executed, but no items were sent on this branch",
|
||||||
"dataMapping.schemaView.emptySchema": "No fields - item(s) exist, but they're empty",
|
"dataMapping.schemaView.emptySchema": "No fields - item(s) exist, but they're empty",
|
||||||
"dataMapping.schemaView.emptySchemaWithBinary": "Only binary data exists. View it using the 'Binary' tab",
|
"dataMapping.schemaView.emptySchemaWithBinary": "Only binary data exists. View it using the 'Binary' tab",
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ const showDraggableHintWithDelay = ref(false);
|
|||||||
const draggableHintShown = ref(false);
|
const draggableHintShown = ref(false);
|
||||||
|
|
||||||
const mappedNode = ref<string | null>(null);
|
const mappedNode = ref<string | null>(null);
|
||||||
|
const collapsingColumnName = ref<string | null>(null);
|
||||||
const inputModes = [
|
const inputModes = [
|
||||||
{ value: 'mapping', label: i18n.baseText('ndv.input.mapping') },
|
{ value: 'mapping', label: i18n.baseText('ndv.input.mapping') },
|
||||||
{ value: 'debugging', label: i18n.baseText('ndv.input.fromAI') },
|
{ value: 'debugging', label: i18n.baseText('ndv.input.fromAI') },
|
||||||
@@ -365,6 +366,10 @@ function onConnectionHelpClick() {
|
|||||||
function activatePane() {
|
function activatePane() {
|
||||||
emit('activatePane');
|
emit('activatePane');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleChangeCollapsingColumn(columnName: string | null) {
|
||||||
|
collapsingColumnName.value = columnName;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -390,6 +395,7 @@ function activatePane() {
|
|||||||
pane-type="input"
|
pane-type="input"
|
||||||
data-test-id="ndv-input-panel"
|
data-test-id="ndv-input-panel"
|
||||||
:disable-ai-content="true"
|
:disable-ai-content="true"
|
||||||
|
:collapsing-table-column-name="collapsingColumnName"
|
||||||
@activate-pane="activatePane"
|
@activate-pane="activatePane"
|
||||||
@item-hover="onItemHover"
|
@item-hover="onItemHover"
|
||||||
@link-run="onLinkRun"
|
@link-run="onLinkRun"
|
||||||
@@ -398,6 +404,7 @@ function activatePane() {
|
|||||||
@table-mounted="onTableMounted"
|
@table-mounted="onTableMounted"
|
||||||
@search="onSearch"
|
@search="onSearch"
|
||||||
@display-mode-change="emit('displayModeChange', $event)"
|
@display-mode-change="emit('displayModeChange', $event)"
|
||||||
|
@collapsing-table-column-changed="handleChangeCollapsingColumn"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<div :class="[$style.titleSection, { [$style.titleSectionV2]: isNDVV2 }]">
|
<div :class="[$style.titleSection, { [$style.titleSectionV2]: isNDVV2 }]">
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ const outputTypes = ref([
|
|||||||
{ label: i18n.baseText('ndv.output.outType.logs'), value: OUTPUT_TYPE.LOGS },
|
{ label: i18n.baseText('ndv.output.outType.logs'), value: OUTPUT_TYPE.LOGS },
|
||||||
]);
|
]);
|
||||||
const runDataRef = ref<RunDataRef>();
|
const runDataRef = ref<RunDataRef>();
|
||||||
|
const collapsingColumnName = ref<string | null>(null);
|
||||||
|
|
||||||
// Computed
|
// Computed
|
||||||
|
|
||||||
@@ -321,6 +322,10 @@ watch(defaultOutputMode, (newValue: OutputType, oldValue: OutputType) => {
|
|||||||
const activatePane = () => {
|
const activatePane = () => {
|
||||||
emit('activatePane');
|
emit('activatePane');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function handleChangeCollapsingColumn(columnName: string | null) {
|
||||||
|
collapsingColumnName.value = columnName;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -346,6 +351,7 @@ const activatePane = () => {
|
|||||||
:callout-message="allToolsWereUnusedNotice"
|
:callout-message="allToolsWereUnusedNotice"
|
||||||
:display-mode="displayMode"
|
:display-mode="displayMode"
|
||||||
:disable-ai-content="true"
|
:disable-ai-content="true"
|
||||||
|
:collapsing-table-column-name="collapsingColumnName"
|
||||||
data-test-id="ndv-output-panel"
|
data-test-id="ndv-output-panel"
|
||||||
@activate-pane="activatePane"
|
@activate-pane="activatePane"
|
||||||
@run-change="onRunIndexChange"
|
@run-change="onRunIndexChange"
|
||||||
@@ -355,6 +361,7 @@ const activatePane = () => {
|
|||||||
@item-hover="emit('itemHover', $event)"
|
@item-hover="emit('itemHover', $event)"
|
||||||
@search="emit('search', $event)"
|
@search="emit('search', $event)"
|
||||||
@display-mode-change="emit('displayModeChange', $event)"
|
@display-mode-change="emit('displayModeChange', $event)"
|
||||||
|
@collapsing-table-column-changed="handleChangeCollapsingColumn"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<div :class="[$style.titleSection, { [$style.titleSectionV2]: isNDVV2 }]">
|
<div :class="[$style.titleSection, { [$style.titleSectionV2]: isNDVV2 }]">
|
||||||
|
|||||||
@@ -152,6 +152,7 @@ type Props = {
|
|||||||
tableHeaderBgColor?: 'base' | 'light';
|
tableHeaderBgColor?: 'base' | 'light';
|
||||||
disableHoverHighlight?: boolean;
|
disableHoverHighlight?: boolean;
|
||||||
disableAiContent?: boolean;
|
disableAiContent?: boolean;
|
||||||
|
collapsingTableColumnName: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
@@ -207,6 +208,7 @@ const emit = defineEmits<{
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
displayModeChange: [IRunDataDisplayMode];
|
displayModeChange: [IRunDataDisplayMode];
|
||||||
|
collapsingTableColumnChanged: [columnName: string | null];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const connectionType = ref<NodeConnectionType>(NodeConnectionTypes.Main);
|
const connectionType = ref<NodeConnectionType>(NodeConnectionTypes.Main);
|
||||||
@@ -1436,6 +1438,16 @@ defineExpose({ enterEditMode });
|
|||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|
||||||
|
<N8nIconButton
|
||||||
|
v-if="displayMode === 'table' && collapsingTableColumnName !== null"
|
||||||
|
:class="$style.resetCollapseButton"
|
||||||
|
text
|
||||||
|
icon="chevrons-up-down"
|
||||||
|
size="xmini"
|
||||||
|
type="tertiary"
|
||||||
|
@click="emit('collapsingTableColumnChanged', null)"
|
||||||
|
/>
|
||||||
|
|
||||||
<RunDataDisplayModeSelect
|
<RunDataDisplayModeSelect
|
||||||
v-show="
|
v-show="
|
||||||
hasPreviewSchema ||
|
hasPreviewSchema ||
|
||||||
@@ -1844,9 +1856,11 @@ defineExpose({ enterEditMode });
|
|||||||
:header-bg-color="tableHeaderBgColor"
|
:header-bg-color="tableHeaderBgColor"
|
||||||
:compact="props.compact"
|
:compact="props.compact"
|
||||||
:disable-hover-highlight="props.disableHoverHighlight"
|
:disable-hover-highlight="props.disableHoverHighlight"
|
||||||
|
:collapsing-column-name="collapsingTableColumnName"
|
||||||
@mounted="emit('tableMounted', $event)"
|
@mounted="emit('tableMounted', $event)"
|
||||||
@active-row-changed="onItemHover"
|
@active-row-changed="onItemHover"
|
||||||
@display-mode-change="onDisplayModeChange"
|
@display-mode-change="onDisplayModeChange"
|
||||||
|
@collapsing-column-changed="emit('collapsingTableColumnChanged', $event)"
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|
||||||
@@ -2335,6 +2349,10 @@ defineExpose({ enterEditMode });
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.resetCollapseButton {
|
||||||
|
color: var(--color-foreground-xdark);
|
||||||
|
}
|
||||||
|
|
||||||
@container (max-width: 240px) {
|
@container (max-width: 240px) {
|
||||||
/* Hide title when the panel is too narrow */
|
/* Hide title when the panel is too narrow */
|
||||||
.compact:hover .title {
|
.compact:hover .title {
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ watch(
|
|||||||
.ioSearchIcon {
|
.ioSearchIcon {
|
||||||
color: var(--color-foreground-xdark);
|
color: var(--color-foreground-xdark);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.el-input__prefix) {
|
:global(.el-input__prefix) {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { createComponentRenderer } from '@/__tests__/render';
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
import RunDataTable from '@/components/RunDataTable.vue';
|
import RunDataTable from '@/components/RunDataTable.vue';
|
||||||
import { createTestingPinia } from '@pinia/testing';
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
import { cleanup } from '@testing-library/vue';
|
import { cleanup, fireEvent, waitFor } from '@testing-library/vue';
|
||||||
|
import { nextTick } from 'vue';
|
||||||
|
|
||||||
vi.mock('vue-router', () => {
|
vi.mock('vue-router', () => {
|
||||||
const push = vi.fn();
|
const push = vi.fn();
|
||||||
@@ -107,4 +108,48 @@ describe('RunDataTable.vue', () => {
|
|||||||
expect(getAllByText(value)).not.toHaveLength(0);
|
expect(getAllByText(value)).not.toHaveLength(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('inserts col elements in DOM to specify column widths when collapsing column name is specified', async () => {
|
||||||
|
const inputData = { json: { firstName: 'John', lastName: 'Doe' } };
|
||||||
|
const rendered = renderComponent({
|
||||||
|
props: {
|
||||||
|
inputData: [inputData],
|
||||||
|
collapsingColumnName: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(rendered.container.querySelectorAll('col')).toHaveLength(0);
|
||||||
|
|
||||||
|
await rendered.rerender({
|
||||||
|
inputData: [inputData],
|
||||||
|
collapsingColumnName: 'firstName',
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => expect(rendered.container.querySelectorAll('col')).toHaveLength(3)); // two data columns + one right margin column
|
||||||
|
|
||||||
|
await rendered.rerender({
|
||||||
|
inputData: [inputData],
|
||||||
|
collapsingColumnName: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => expect(rendered.container.querySelectorAll('col')).toHaveLength(0));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows the button for column collapsing in the column header', async () => {
|
||||||
|
const inputData = { json: { firstName: 'John', lastName: 'Doe' } };
|
||||||
|
const rendered = renderComponent({
|
||||||
|
props: {
|
||||||
|
inputData: [inputData],
|
||||||
|
collapsingColumnName: 'firstName',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(rendered.getAllByLabelText('Collapse rows')).toHaveLength(2);
|
||||||
|
|
||||||
|
await fireEvent.click(rendered.getAllByLabelText('Collapse rows')[0]);
|
||||||
|
await fireEvent.click(rendered.getAllByLabelText('Collapse rows')[1]);
|
||||||
|
expect(rendered.emitted('collapsingColumnChanged')).toEqual([[null], ['lastName']]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { getMappedExpression } from '@/utils/mappingUtils';
|
|||||||
import { getPairedItemId } from '@/utils/pairedItemUtils';
|
import { getPairedItemId } from '@/utils/pairedItemUtils';
|
||||||
import { shorten } from '@/utils/typesUtils';
|
import { shorten } from '@/utils/typesUtils';
|
||||||
import type { GenericValue, IDataObject, INodeExecutionData } from 'n8n-workflow';
|
import type { GenericValue, IDataObject, INodeExecutionData } from 'n8n-workflow';
|
||||||
import { computed, onMounted, ref, watch } from 'vue';
|
import { useTemplateRef, computed, onMounted, ref, watch } from 'vue';
|
||||||
import Draggable from '@/components/Draggable.vue';
|
import Draggable from '@/components/Draggable.vue';
|
||||||
import MappingPill from './MappingPill.vue';
|
import MappingPill from './MappingPill.vue';
|
||||||
import TextWithHighlights from './TextWithHighlights.vue';
|
import TextWithHighlights from './TextWithHighlights.vue';
|
||||||
@@ -35,6 +35,7 @@ type Props = {
|
|||||||
headerBgColor?: 'base' | 'light';
|
headerBgColor?: 'base' | 'light';
|
||||||
compact?: boolean;
|
compact?: boolean;
|
||||||
disableHoverHighlight?: boolean;
|
disableHoverHighlight?: boolean;
|
||||||
|
collapsingColumnName: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
@@ -52,9 +53,13 @@ const emit = defineEmits<{
|
|||||||
activeRowChanged: [row: number | null];
|
activeRowChanged: [row: number | null];
|
||||||
displayModeChange: [mode: IRunDataDisplayMode];
|
displayModeChange: [mode: IRunDataDisplayMode];
|
||||||
mounted: [data: { avgRowHeight: number }];
|
mounted: [data: { avgRowHeight: number }];
|
||||||
|
collapsingColumnChanged: [columnName: string | null];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const externalHooks = useExternalHooks();
|
const externalHooks = useExternalHooks();
|
||||||
|
|
||||||
|
const tableRef = useTemplateRef('tableRef');
|
||||||
|
|
||||||
const activeColumn = ref(-1);
|
const activeColumn = ref(-1);
|
||||||
const forceShowGrip = ref(false);
|
const forceShowGrip = ref(false);
|
||||||
const draggedColumn = ref(false);
|
const draggedColumn = ref(false);
|
||||||
@@ -64,6 +69,7 @@ const activeRow = ref<number | null>(null);
|
|||||||
const columnLimit = ref(MAX_COLUMNS_LIMIT);
|
const columnLimit = ref(MAX_COLUMNS_LIMIT);
|
||||||
const columnLimitExceeded = ref(false);
|
const columnLimitExceeded = ref(false);
|
||||||
const draggableRef = ref<DraggableRef>();
|
const draggableRef = ref<DraggableRef>();
|
||||||
|
const fixedColumnWidths = ref<number[] | undefined>();
|
||||||
|
|
||||||
const ndvStore = useNDVStore();
|
const ndvStore = useNDVStore();
|
||||||
const workflowsStore = useWorkflowsStore();
|
const workflowsStore = useWorkflowsStore();
|
||||||
@@ -82,6 +88,13 @@ const canDraggableDrop = computed(() => ndvStore.canDraggableDrop);
|
|||||||
const draggableStickyPosition = computed(() => ndvStore.draggableStickyPos);
|
const draggableStickyPosition = computed(() => ndvStore.draggableStickyPos);
|
||||||
const pairedItemMappings = computed(() => workflowsStore.workflowExecutionPairedItemMappings);
|
const pairedItemMappings = computed(() => workflowsStore.workflowExecutionPairedItemMappings);
|
||||||
const tableData = computed(() => convertToTable(props.inputData));
|
const tableData = computed(() => convertToTable(props.inputData));
|
||||||
|
const collapsingColumnIndex = computed(() => {
|
||||||
|
if (!props.collapsingColumnName) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tableData.value.columns.indexOf(props.collapsingColumnName);
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (tableData.value?.columns && draggableRef.value) {
|
if (tableData.value?.columns && draggableRef.value) {
|
||||||
@@ -419,6 +432,15 @@ function switchToJsonView() {
|
|||||||
emit('displayModeChange', 'json');
|
emit('displayModeChange', 'json');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleSetCollapsingColumn(columnIndex: number) {
|
||||||
|
emit(
|
||||||
|
'collapsingColumnChanged',
|
||||||
|
collapsingColumnIndex.value === columnIndex
|
||||||
|
? null
|
||||||
|
: (tableData.value.columns[columnIndex] ?? null),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
watch(focusedMappableInput, (curr) => {
|
watch(focusedMappableInput, (curr) => {
|
||||||
setTimeout(
|
setTimeout(
|
||||||
() => {
|
() => {
|
||||||
@@ -427,6 +449,27 @@ watch(focusedMappableInput, (curr) => {
|
|||||||
curr ? 300 : 150,
|
curr ? 300 : 150,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
[collapsingColumnIndex, tableRef],
|
||||||
|
([index, table]) => {
|
||||||
|
if (index === -1) {
|
||||||
|
fixedColumnWidths.value = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (table === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fixedColumnWidths.value = [...table.querySelectorAll('thead tr th')].map((el) =>
|
||||||
|
el instanceof HTMLElement
|
||||||
|
? el.getBoundingClientRect().width // using getBoundingClientRect for decimal accuracy
|
||||||
|
: 0,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{ immediate: true, flush: 'post' },
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -437,6 +480,7 @@ watch(focusedMappableInput, (curr) => {
|
|||||||
[$style.highlight]: highlight,
|
[$style.highlight]: highlight,
|
||||||
[$style.lightHeader]: headerBgColor === 'light',
|
[$style.lightHeader]: headerBgColor === 'light',
|
||||||
[$style.compact]: props.compact,
|
[$style.compact]: props.compact,
|
||||||
|
[$style.hasCollapsingColumn]: fixedColumnWidths !== undefined,
|
||||||
},
|
},
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
@@ -500,13 +544,20 @@ watch(focusedMappableInput, (curr) => {
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<table v-else :class="$style.table">
|
<table v-else ref="tableRef" :class="$style.table">
|
||||||
|
<colgroup v-if="fixedColumnWidths">
|
||||||
|
<col v-for="(width, i) in fixedColumnWidths" :key="i" :width="width" />
|
||||||
|
</colgroup>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th v-if="tableData.metadata.hasExecutionIds" :class="$style.executionLinkRowHeader">
|
<th v-if="tableData.metadata.hasExecutionIds" :class="$style.executionLinkRowHeader">
|
||||||
<!-- column for execution link -->
|
<!-- column for execution link -->
|
||||||
</th>
|
</th>
|
||||||
<th v-for="(column, i) in tableData.columns || []" :key="column">
|
<th
|
||||||
|
v-for="(column, i) in tableData.columns || []"
|
||||||
|
:key="column"
|
||||||
|
:class="collapsingColumnIndex === i ? $style.isCollapsingColumn : ''"
|
||||||
|
>
|
||||||
<N8nTooltip placement="bottom-start" :disabled="!mappingEnabled" :show-after="1000">
|
<N8nTooltip placement="bottom-start" :disabled="!mappingEnabled" :show-after="1000">
|
||||||
<template #content>
|
<template #content>
|
||||||
<div>
|
<div>
|
||||||
@@ -540,7 +591,23 @@ watch(focusedMappableInput, (curr) => {
|
|||||||
:content="getValueToRender(column || '')"
|
:content="getValueToRender(column || '')"
|
||||||
:search="search"
|
:search="search"
|
||||||
/>
|
/>
|
||||||
<div :class="$style.dragButton">
|
<N8nTooltip
|
||||||
|
:content="i18n.baseText('dataMapping.tableView.columnCollapsing.tooltip')"
|
||||||
|
:disabled="mappingEnabled || collapsingColumnIndex === i"
|
||||||
|
>
|
||||||
|
<N8nIconButton
|
||||||
|
:class="$style.collapseColumnButton"
|
||||||
|
type="tertiary"
|
||||||
|
size="xmini"
|
||||||
|
text
|
||||||
|
:icon="
|
||||||
|
collapsingColumnIndex === i ? 'chevrons-up-down' : 'chevrons-down-up'
|
||||||
|
"
|
||||||
|
:aria-label="i18n.baseText('dataMapping.tableView.columnCollapsing')"
|
||||||
|
@click="handleSetCollapsingColumn(i)"
|
||||||
|
/>
|
||||||
|
</N8nTooltip>
|
||||||
|
<div v-if="mappingEnabled" :class="$style.dragButton">
|
||||||
<n8n-icon icon="grip-vertical" />
|
<n8n-icon icon="grip-vertical" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -629,7 +696,10 @@ watch(focusedMappableInput, (curr) => {
|
|||||||
:key="index2"
|
:key="index2"
|
||||||
:data-row="index1"
|
:data-row="index1"
|
||||||
:data-col="index2"
|
:data-col="index2"
|
||||||
:class="hasJsonInColumn(index2) ? $style.minColWidth : $style.limitColWidth"
|
:class="[
|
||||||
|
hasJsonInColumn(index2) ? $style.minColWidth : $style.limitColWidth,
|
||||||
|
collapsingColumnIndex === index2 ? $style.isCollapsingColumn : '',
|
||||||
|
]"
|
||||||
@mouseenter="onMouseEnterCell"
|
@mouseenter="onMouseEnterCell"
|
||||||
@mouseleave="onMouseLeaveCell"
|
@mouseleave="onMouseLeaveCell"
|
||||||
>
|
>
|
||||||
@@ -757,6 +827,41 @@ watch(focusedMappableInput, (curr) => {
|
|||||||
td:last-child {
|
td:last-child {
|
||||||
border-right: var(--border-base);
|
border-right: var(--border-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hasCollapsingColumn & {
|
||||||
|
table-layout: fixed;
|
||||||
|
|
||||||
|
td:not(.isCollapsingColumn) {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
& :global(.n8n-tree) {
|
||||||
|
height: 1.5em;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
th.isCollapsingColumn {
|
||||||
|
border-top-color: var(--color-foreground-xdark);
|
||||||
|
border-left-color: var(--color-foreground-xdark);
|
||||||
|
border-right-color: var(--color-foreground-xdark);
|
||||||
|
}
|
||||||
|
|
||||||
|
td.isCollapsingColumn {
|
||||||
|
border-left-color: var(--color-foreground-xdark);
|
||||||
|
border-right-color: var(--color-foreground-xdark);
|
||||||
|
|
||||||
|
tr:last-child & {
|
||||||
|
border-bottom-color: var(--color-foreground-xdark);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
td.isCollapsingColumn + td,
|
||||||
|
th.isCollapsingColumn + th {
|
||||||
|
border-left-color: var(--color-foreground-xdark);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodeClass {
|
.nodeClass {
|
||||||
@@ -808,7 +913,10 @@ watch(focusedMappableInput, (curr) => {
|
|||||||
|
|
||||||
.dragButton {
|
.dragButton {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
margin-left: var(--spacing-2xs);
|
|
||||||
|
& > svg {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dataKey {
|
.dataKey {
|
||||||
@@ -885,4 +993,18 @@ watch(focusedMappableInput, (curr) => {
|
|||||||
.executionLinkRowHeader {
|
.executionLinkRowHeader {
|
||||||
width: var(--spacing-m);
|
width: var(--spacing-m);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.collapseColumnButton {
|
||||||
|
span {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
visibility: hidden;
|
||||||
|
margin-block: calc(-2 * var(--spacing-2xs));
|
||||||
|
|
||||||
|
.isCollapsingColumn &,
|
||||||
|
th:hover & {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ exports[`InputPanel > should render 1`] = `
|
|||||||
data-v-2e5cd75c=""
|
data-v-2e5cd75c=""
|
||||||
>
|
>
|
||||||
<!---->
|
<!---->
|
||||||
|
<!--v-if-->
|
||||||
<div
|
<div
|
||||||
class="n8n-radio-buttons radioGroup displayModeSelect"
|
class="n8n-radio-buttons radioGroup displayModeSelect"
|
||||||
data-test-id="ndv-run-data-display-mode"
|
data-test-id="ndv-run-data-display-mode"
|
||||||
|
|||||||
@@ -88,6 +88,8 @@ describe('LogDetailsPanel', () => {
|
|||||||
isOpen: true,
|
isOpen: true,
|
||||||
logEntry: createLogEntry({ node: aiNode, runIndex: 0, runData: aiNodeRunData }),
|
logEntry: createLogEntry({ node: aiNode, runIndex: 0, runData: aiNodeRunData }),
|
||||||
panels: LOG_DETAILS_PANEL_STATE.BOTH,
|
panels: LOG_DETAILS_PANEL_STATE.BOTH,
|
||||||
|
collapsingInputTableColumnName: null,
|
||||||
|
collapsingOutputTableColumnName: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const header = within(rendered.getByTestId('log-details-header'));
|
const header = within(rendered.getByTestId('log-details-header'));
|
||||||
@@ -109,6 +111,8 @@ describe('LogDetailsPanel', () => {
|
|||||||
runData: { ...aiNodeRunData, executionStatus: 'running' },
|
runData: { ...aiNodeRunData, executionStatus: 'running' },
|
||||||
}),
|
}),
|
||||||
panels: LOG_DETAILS_PANEL_STATE.BOTH,
|
panels: LOG_DETAILS_PANEL_STATE.BOTH,
|
||||||
|
collapsingInputTableColumnName: null,
|
||||||
|
collapsingOutputTableColumnName: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const inputPanel = within(rendered.getByTestId('log-details-input'));
|
const inputPanel = within(rendered.getByTestId('log-details-input'));
|
||||||
@@ -123,6 +127,8 @@ describe('LogDetailsPanel', () => {
|
|||||||
isOpen: true,
|
isOpen: true,
|
||||||
logEntry: createLogEntry({ node: aiNode, runIndex: 0, runData: aiNodeRunData }),
|
logEntry: createLogEntry({ node: aiNode, runIndex: 0, runData: aiNodeRunData }),
|
||||||
panels: LOG_DETAILS_PANEL_STATE.BOTH,
|
panels: LOG_DETAILS_PANEL_STATE.BOTH,
|
||||||
|
collapsingInputTableColumnName: null,
|
||||||
|
collapsingOutputTableColumnName: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
await fireEvent.mouseDown(rendered.getByTestId('resize-handle'));
|
await fireEvent.mouseDown(rendered.getByTestId('resize-handle'));
|
||||||
@@ -138,6 +144,8 @@ describe('LogDetailsPanel', () => {
|
|||||||
isOpen: true,
|
isOpen: true,
|
||||||
logEntry: createLogEntry({ node: aiNode, runIndex: 0, runData: aiNodeRunData }),
|
logEntry: createLogEntry({ node: aiNode, runIndex: 0, runData: aiNodeRunData }),
|
||||||
panels: LOG_DETAILS_PANEL_STATE.BOTH,
|
panels: LOG_DETAILS_PANEL_STATE.BOTH,
|
||||||
|
collapsingInputTableColumnName: null,
|
||||||
|
collapsingOutputTableColumnName: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
await fireEvent.mouseDown(rendered.getByTestId('resize-handle'));
|
await fireEvent.mouseDown(rendered.getByTestId('resize-handle'));
|
||||||
@@ -164,6 +172,8 @@ describe('LogDetailsPanel', () => {
|
|||||||
execution: { resultData: { runData: { A: [runDataA], B: [runDataB] } } },
|
execution: { resultData: { runData: { A: [runDataA], B: [runDataB] } } },
|
||||||
}),
|
}),
|
||||||
panels: LOG_DETAILS_PANEL_STATE.BOTH,
|
panels: LOG_DETAILS_PANEL_STATE.BOTH,
|
||||||
|
collapsingInputTableColumnName: null,
|
||||||
|
collapsingOutputTableColumnName: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
|
|||||||
@@ -20,18 +20,30 @@ import { LOG_DETAILS_PANEL_STATE } from '@/features/logs/logs.constants';
|
|||||||
|
|
||||||
const MIN_IO_PANEL_WIDTH = 200;
|
const MIN_IO_PANEL_WIDTH = 200;
|
||||||
|
|
||||||
const { isOpen, logEntry, window, latestInfo, panels } = defineProps<{
|
const {
|
||||||
|
isOpen,
|
||||||
|
logEntry,
|
||||||
|
window,
|
||||||
|
latestInfo,
|
||||||
|
panels,
|
||||||
|
collapsingInputTableColumnName,
|
||||||
|
collapsingOutputTableColumnName,
|
||||||
|
} = defineProps<{
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
logEntry: LogEntry;
|
logEntry: LogEntry;
|
||||||
window?: Window;
|
window?: Window;
|
||||||
latestInfo?: LatestNodeInfo;
|
latestInfo?: LatestNodeInfo;
|
||||||
panels: LogDetailsPanelState;
|
panels: LogDetailsPanelState;
|
||||||
|
collapsingInputTableColumnName: string | null;
|
||||||
|
collapsingOutputTableColumnName: string | null;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
clickHeader: [];
|
clickHeader: [];
|
||||||
toggleInputOpen: [] | [boolean];
|
toggleInputOpen: [] | [boolean];
|
||||||
toggleOutputOpen: [] | [boolean];
|
toggleOutputOpen: [] | [boolean];
|
||||||
|
collapsingInputTableColumnChanged: [columnName: string | null];
|
||||||
|
collapsingOutputTableColumnChanged: [columnName: string | null];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
defineSlots<{ actions: {} }>();
|
defineSlots<{ actions: {} }>();
|
||||||
@@ -149,6 +161,8 @@ function handleResizeEnd() {
|
|||||||
pane-type="input"
|
pane-type="input"
|
||||||
:title="locale.baseText('logs.details.header.actions.input')"
|
:title="locale.baseText('logs.details.header.actions.input')"
|
||||||
:log-entry="logEntry"
|
:log-entry="logEntry"
|
||||||
|
:collapsing-table-column-name="collapsingInputTableColumnName"
|
||||||
|
@collapsing-table-column-changed="emit('collapsingInputTableColumnChanged', $event)"
|
||||||
/>
|
/>
|
||||||
</N8nResizeWrapper>
|
</N8nResizeWrapper>
|
||||||
<LogsViewRunData
|
<LogsViewRunData
|
||||||
@@ -158,6 +172,8 @@ function handleResizeEnd() {
|
|||||||
:class="$style.outputPanel"
|
:class="$style.outputPanel"
|
||||||
:title="locale.baseText('logs.details.header.actions.output')"
|
:title="locale.baseText('logs.details.header.actions.output')"
|
||||||
:log-entry="logEntry"
|
:log-entry="logEntry"
|
||||||
|
:collapsing-table-column-name="collapsingOutputTableColumnName"
|
||||||
|
@collapsing-table-column-changed="emit('collapsingOutputTableColumnChanged', $event)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { nextTick, computed, useTemplateRef } from 'vue';
|
import { nextTick, computed, useTemplateRef, ref } from 'vue';
|
||||||
import { N8nResizeWrapper } from '@n8n/design-system';
|
import { N8nResizeWrapper } from '@n8n/design-system';
|
||||||
import { useChatState } from '@/features/logs/composables/useChatState';
|
import { useChatState } from '@/features/logs/composables/useChatState';
|
||||||
import LogsOverviewPanel from '@/features/logs/components/LogsOverviewPanel.vue';
|
import LogsOverviewPanel from '@/features/logs/components/LogsOverviewPanel.vue';
|
||||||
@@ -66,6 +66,9 @@ const { selected, select, selectNext, selectPrev } = useLogsSelection(
|
|||||||
toggleExpanded,
|
toggleExpanded,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const inputTableColumnCollapsing = ref<{ nodeName: string; columnName: string }>();
|
||||||
|
const outputTableColumnCollapsing = ref<{ nodeName: string; columnName: string }>();
|
||||||
|
|
||||||
const isLogDetailsOpen = computed(() => isOpen.value && selected.value !== undefined);
|
const isLogDetailsOpen = computed(() => isOpen.value && selected.value !== undefined);
|
||||||
const isLogDetailsVisuallyOpen = computed(
|
const isLogDetailsVisuallyOpen = computed(
|
||||||
() => isLogDetailsOpen.value && !isCollapsingDetailsPanel.value,
|
() => isLogDetailsOpen.value && !isCollapsingDetailsPanel.value,
|
||||||
@@ -79,6 +82,16 @@ const logsPanelActionsProps = computed<InstanceType<typeof LogsPanelActions>['$p
|
|||||||
onToggleOpen,
|
onToggleOpen,
|
||||||
onToggleSyncSelection: logsStore.toggleLogSelectionSync,
|
onToggleSyncSelection: logsStore.toggleLogSelectionSync,
|
||||||
}));
|
}));
|
||||||
|
const inputCollapsingColumnName = computed(() =>
|
||||||
|
inputTableColumnCollapsing.value?.nodeName === selected.value?.node.name
|
||||||
|
? (inputTableColumnCollapsing.value?.columnName ?? null)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
const outputCollapsingColumnName = computed(() =>
|
||||||
|
outputTableColumnCollapsing.value?.nodeName === selected.value?.node.name
|
||||||
|
? (outputTableColumnCollapsing.value?.columnName ?? null)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
|
||||||
const keyMap = computed<KeyMap>(() => ({
|
const keyMap = computed<KeyMap>(() => ({
|
||||||
j: selectNext,
|
j: selectNext,
|
||||||
@@ -110,6 +123,16 @@ function handleOpenNdv(treeNode: LogEntry) {
|
|||||||
ndvStore.setOutputRunIndex(treeNode.runIndex);
|
ndvStore.setOutputRunIndex(treeNode.runIndex);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleChangeInputTableColumnCollapsing(columnName: string | null) {
|
||||||
|
inputTableColumnCollapsing.value =
|
||||||
|
columnName && selected.value ? { nodeName: selected.value.node.name, columnName } : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleChangeOutputTableColumnCollapsing(columnName: string | null) {
|
||||||
|
outputTableColumnCollapsing.value =
|
||||||
|
columnName && selected.value ? { nodeName: selected.value.node.name, columnName } : undefined;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -202,9 +225,13 @@ function handleOpenNdv(treeNode: LogEntry) {
|
|||||||
:window="pipWindow"
|
:window="pipWindow"
|
||||||
:latest-info="latestNodeNameById[selected.node.id]"
|
:latest-info="latestNodeNameById[selected.node.id]"
|
||||||
:panels="logsStore.detailsState"
|
:panels="logsStore.detailsState"
|
||||||
|
:collapsing-input-table-column-name="inputCollapsingColumnName"
|
||||||
|
:collapsing-output-table-column-name="outputCollapsingColumnName"
|
||||||
@click-header="onToggleOpen(true)"
|
@click-header="onToggleOpen(true)"
|
||||||
@toggle-input-open="logsStore.toggleInputOpen"
|
@toggle-input-open="logsStore.toggleInputOpen"
|
||||||
@toggle-output-open="logsStore.toggleOutputOpen"
|
@toggle-output-open="logsStore.toggleOutputOpen"
|
||||||
|
@collapsing-input-table-column-changed="handleChangeInputTableColumnCollapsing"
|
||||||
|
@collapsing-output-table-column-changed="handleChangeOutputTableColumnCollapsing"
|
||||||
>
|
>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<LogsPanelActions v-if="isLogDetailsVisuallyOpen" v-bind="logsPanelActionsProps" />
|
<LogsPanelActions v-if="isLogDetailsVisuallyOpen" v-bind="logsPanelActionsProps" />
|
||||||
|
|||||||
@@ -11,10 +11,15 @@ import { I18nT } from 'vue-i18n';
|
|||||||
import { PiPWindowSymbol } from '@/constants';
|
import { PiPWindowSymbol } from '@/constants';
|
||||||
import { isSubNodeLog } from '../logs.utils';
|
import { isSubNodeLog } from '../logs.utils';
|
||||||
|
|
||||||
const { title, logEntry, paneType } = defineProps<{
|
const { title, logEntry, paneType, collapsingTableColumnName } = defineProps<{
|
||||||
title: string;
|
title: string;
|
||||||
paneType: NodePanelType;
|
paneType: NodePanelType;
|
||||||
logEntry: LogEntry;
|
logEntry: LogEntry;
|
||||||
|
collapsingTableColumnName: string | null;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
collapsingTableColumnChanged: [columnName: string | null];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const locale = useI18n();
|
const locale = useI18n();
|
||||||
@@ -85,7 +90,9 @@ function handleChangeDisplayMode(value: IRunDataDisplayMode) {
|
|||||||
:disable-ai-content="!isSubNodeLog(logEntry)"
|
:disable-ai-content="!isSubNodeLog(logEntry)"
|
||||||
:is-executing="isExecuting"
|
:is-executing="isExecuting"
|
||||||
table-header-bg-color="light"
|
table-header-bg-color="light"
|
||||||
|
:collapsing-table-column-name="collapsingTableColumnName"
|
||||||
@display-mode-change="handleChangeDisplayMode"
|
@display-mode-change="handleChangeDisplayMode"
|
||||||
|
@collapsing-table-column-changed="emit('collapsingTableColumnChanged', $event)"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<N8nText :class="$style.title" :bold="true" color="text-light" size="small">
|
<N8nText :class="$style.title" :bold="true" color="text-light" size="small">
|
||||||
|
|||||||
Reference in New Issue
Block a user