mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-17 05:29:44 +02:00
fix(editor): Restrict assignment fields in AI Assistant workflow setup (#33968)
This commit is contained in:
parent
008852c608
commit
678b9e00a0
|
|
@ -21,6 +21,7 @@ import type { INodeUi, INodeUpdatePropertiesInformation, IUpdateInformation } fr
|
|||
import type { WorkflowSetupSection } from '../workflowSetup.types';
|
||||
import { useWorkflowSetupContext } from '../composables/useWorkflowSetupContext';
|
||||
import { AI_GATEWAY_MANAGED_TAG } from '../../constants';
|
||||
import { findPlaceholderDetails } from '@n8n/utils/placeholder';
|
||||
|
||||
const props = defineProps<{
|
||||
section: WorkflowSetupSection;
|
||||
|
|
@ -75,6 +76,25 @@ const parameterDefinitions = computed<INodeProperties[]>(() => {
|
|||
return nodeType.value.properties.filter((property) => names.has(property.name));
|
||||
});
|
||||
|
||||
const assignmentCollectionEditableValueIndices = computed<Record<string, number[]>>(() => {
|
||||
const result: Record<string, number[]> = {};
|
||||
for (const parameter of parameterDefinitions.value) {
|
||||
if (parameter.type !== 'assignmentCollection') continue;
|
||||
|
||||
const indices = new Set<number>();
|
||||
const placeholderDetails = findPlaceholderDetails(
|
||||
props.section.node.parameters[parameter.name],
|
||||
);
|
||||
for (const detail of placeholderDetails) {
|
||||
if (detail.path[0] !== 'assignments' || detail.path[2] !== 'value') continue;
|
||||
const match = /^\[(\d+)\]$/.exec(detail.path[1] ?? '');
|
||||
if (match?.[1]) indices.add(Number.parseInt(match[1], 10));
|
||||
}
|
||||
result[parameter.name] = [...indices];
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
const revealedIssues = ref(new Set<string>());
|
||||
|
||||
watch(
|
||||
|
|
@ -212,6 +232,7 @@ function onParameterValueChanged(update: IUpdateInformation) {
|
|||
:remove-first-parameter-margin="true"
|
||||
:remove-last-parameter-margin="true"
|
||||
:options-overrides="{ hideExpressionSelector: true, hideFocusPanelButton: true }"
|
||||
:assignment-collection-editable-value-indices="assignmentCollectionEditableValueIndices"
|
||||
@value-changed="onParameterValueChanged"
|
||||
@parameter-blur="revealParameterIssues"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { propertyNameFromExpression } from '@/app/utils/mappingUtils';
|
|||
import { N8nIconButton, N8nTooltip } from '@n8n/design-system';
|
||||
import { typeFromExpression } from '../../utils/assignmentCollection.utils';
|
||||
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
import type { ParameterOptionsOverrides } from '@/features/ndv/shared/ndv.utils';
|
||||
|
||||
interface Props {
|
||||
path: string;
|
||||
|
|
@ -27,6 +28,8 @@ interface Props {
|
|||
disableType?: boolean;
|
||||
isReadOnly?: boolean;
|
||||
index?: number;
|
||||
valueOnly?: boolean;
|
||||
optionsOverrides?: ParameterOptionsOverrides;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
|
@ -91,7 +94,7 @@ const valueParameter = computed<INodeProperties>(() => {
|
|||
: 'value';
|
||||
return {
|
||||
name: 'value',
|
||||
displayName: 'Value',
|
||||
displayName: props.valueOnly && assignment.value.name ? assignment.value.name : 'Value',
|
||||
default: '',
|
||||
placeholder,
|
||||
...assignmentTypeToNodeProperty(assignment.value.type ?? 'string'),
|
||||
|
|
@ -165,14 +168,14 @@ const onValueDrop = async (droppedExpression: string) => {
|
|||
>
|
||||
<N8nIconButton
|
||||
variant="ghost"
|
||||
v-if="!isReadOnly"
|
||||
v-if="!isReadOnly && !valueOnly"
|
||||
size="small"
|
||||
icon="grip-vertical"
|
||||
:class="[$style.iconButton, $style.defaultTopPadding, 'drag-handle']"
|
||||
/>
|
||||
<N8nIconButton
|
||||
variant="ghost"
|
||||
v-if="!isReadOnly"
|
||||
v-if="!isReadOnly && !valueOnly"
|
||||
size="small"
|
||||
icon="trash-2"
|
||||
data-test-id="assignment-remove"
|
||||
|
|
@ -181,7 +184,32 @@ const onValueDrop = async (droppedExpression: string) => {
|
|||
/>
|
||||
|
||||
<div :class="$style.inputs">
|
||||
<InputTriple middle-width="100px">
|
||||
<div v-if="valueOnly" :class="$style.value">
|
||||
<ParameterInputFull
|
||||
display-options
|
||||
hide-issues
|
||||
hide-hint
|
||||
is-assignment
|
||||
:is-read-only="isReadOnly"
|
||||
:options-overrides="optionsOverrides"
|
||||
:parameter="valueParameter"
|
||||
:value="assignment.value"
|
||||
:path="`${path}.value`"
|
||||
data-test-id="assignment-value"
|
||||
@update="onAssignmentValueChange"
|
||||
@blur="onBlur"
|
||||
@drop="onValueDrop"
|
||||
/>
|
||||
<ParameterInputHint
|
||||
v-if="resolvedExpressionString"
|
||||
data-test-id="parameter-expression-preview-value"
|
||||
:class="[$style.hint]"
|
||||
:highlight="highlightHint"
|
||||
:hint="hint"
|
||||
single-line
|
||||
/>
|
||||
</div>
|
||||
<InputTriple v-else middle-width="100px">
|
||||
<template #left>
|
||||
<ParameterInputFull
|
||||
display-options
|
||||
|
|
|
|||
|
|
@ -133,6 +133,42 @@ describe('AssignmentCollection.vue', () => {
|
|||
expect(getInput(within(assignments[1]).getByTestId('assignment-value'))).toHaveValue('value2');
|
||||
});
|
||||
|
||||
it('keeps source assignments unchanged while editing restricted values', async () => {
|
||||
const sourceValue: AssignmentCollectionValue = {
|
||||
assignments: [
|
||||
{
|
||||
name: 'teamId',
|
||||
value: '<__PLACEHOLDER_VALUE__Linear team ID__>',
|
||||
type: 'string',
|
||||
} as AssignmentValue,
|
||||
],
|
||||
};
|
||||
|
||||
const { findByTestId, queryByTestId } = renderComponent({
|
||||
props: {
|
||||
value: sourceValue,
|
||||
editableValueIndices: [0],
|
||||
},
|
||||
});
|
||||
|
||||
const assignment = await findByTestId('assignment');
|
||||
const valueInput = getInput(within(assignment).getByTestId('assignment-value'));
|
||||
await userEvent.type(valueInput, 'team-1');
|
||||
await fireEvent.blur(valueInput);
|
||||
|
||||
expect(sourceValue).toEqual({
|
||||
assignments: [
|
||||
{
|
||||
name: 'teamId',
|
||||
value: '<__PLACEHOLDER_VALUE__Linear team ID__>',
|
||||
type: 'string',
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(within(assignment).queryByTestId('assignment-remove')).not.toBeInTheDocument();
|
||||
expect(queryByTestId('assignment-collection-drop-area')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders empty state properly', async () => {
|
||||
const { getByTestId, queryByTestId } = renderComponent();
|
||||
expect(getByTestId('assignment-collection-fields')).toBeInTheDocument();
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import Draggable from 'vuedraggable';
|
|||
import ExperimentalEmbeddedNdvMapper from '@/features/workflows/canvas/experimental/components/ExperimentalEmbeddedNdvMapper.vue';
|
||||
import { ExpressionLocalResolveContextSymbol } from '@/app/constants';
|
||||
import { useExperimentalNdvStore } from '@/features/workflows/canvas/experimental/experimentalNdv.store';
|
||||
import type { ParameterOptionsOverrides } from '@/features/ndv/shared/ndv.utils';
|
||||
|
||||
import { N8nInputLabel } from '@n8n/design-system';
|
||||
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
|
|
@ -31,6 +32,8 @@ interface Props {
|
|||
disableType?: boolean;
|
||||
node: INode | null;
|
||||
isReadOnly?: boolean;
|
||||
editableValueIndices?: number[];
|
||||
optionsOverrides?: ParameterOptionsOverrides;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
|
|
@ -50,10 +53,10 @@ const dropAreaContainer = useTemplateRef('dropArea');
|
|||
function createParamValue(value: AssignmentCollectionValue): AssignmentCollectionValue {
|
||||
return {
|
||||
assignments:
|
||||
value.assignments?.map((assignment) => {
|
||||
if (!assignment.id) assignment.id = crypto.randomUUID();
|
||||
return assignment;
|
||||
}) ?? [],
|
||||
value.assignments?.map((assignment) => ({
|
||||
...assignment,
|
||||
id: assignment.id ?? crypto.randomUUID(),
|
||||
})) ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -72,6 +75,18 @@ const issues = computed(() => {
|
|||
});
|
||||
|
||||
const empty = computed(() => state.paramValue.assignments.length === 0);
|
||||
const hasRestrictedAssignments = computed(() => props.editableValueIndices !== undefined);
|
||||
const visibleAssignments = computed(() => {
|
||||
const editableIndices = props.editableValueIndices;
|
||||
if (editableIndices === undefined) {
|
||||
return state.paramValue.assignments.map((assignment, index) => ({ assignment, index }));
|
||||
}
|
||||
|
||||
return editableIndices.flatMap((index) => {
|
||||
const assignment = state.paramValue.assignments[index];
|
||||
return assignment ? [{ assignment, index }] : [];
|
||||
});
|
||||
});
|
||||
const activeDragField = computed(() => propertyNameFromExpression(ndvStore.value.draggableData));
|
||||
const inputData = computed(() => ndvStore.value.ndvInputData?.[0]?.json);
|
||||
const actions = computed(() => {
|
||||
|
|
@ -166,7 +181,7 @@ function optionSelected(action: string) {
|
|||
underline
|
||||
color="text-dark"
|
||||
>
|
||||
<template #options>
|
||||
<template v-if="!hasRestrictedAssignments" #options>
|
||||
<ParameterOptions
|
||||
:parameter="parameter"
|
||||
:value="value"
|
||||
|
|
@ -180,6 +195,7 @@ function optionSelected(action: string) {
|
|||
|
||||
<ExperimentalEmbeddedNdvMapper
|
||||
v-if="
|
||||
!hasRestrictedAssignments &&
|
||||
experimentalNdvStore.isNdvInFocusPanelEnabled &&
|
||||
dropAreaContainer?.$el &&
|
||||
node &&
|
||||
|
|
@ -194,6 +210,7 @@ function optionSelected(action: string) {
|
|||
<div :class="$style.content">
|
||||
<div :class="$style.assignments">
|
||||
<Draggable
|
||||
v-if="!hasRestrictedAssignments"
|
||||
v-model="state.paramValue.assignments"
|
||||
item-key="id"
|
||||
handle=".drag-handle"
|
||||
|
|
@ -215,9 +232,23 @@ function optionSelected(action: string) {
|
|||
</Assignment>
|
||||
</template>
|
||||
</Draggable>
|
||||
<Assignment
|
||||
v-for="{ assignment, index } in visibleAssignments"
|
||||
v-else
|
||||
:key="assignment.id ?? index"
|
||||
:model-value="assignment"
|
||||
:index="index"
|
||||
:path="`${path}.assignments.${index}`"
|
||||
:issues="getIssues(index)"
|
||||
:is-read-only="isReadOnly"
|
||||
disable-type
|
||||
:options-overrides="optionsOverrides"
|
||||
value-only
|
||||
@update:model-value="(value) => onAssignmentUpdate(index, value)"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="!isReadOnly"
|
||||
v-if="!isReadOnly && !hasRestrictedAssignments"
|
||||
:class="$style.dropAreaWrapper"
|
||||
data-test-id="assignment-collection-drop-area"
|
||||
@click="addAssignment"
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ type Props = {
|
|||
removeLastParameterMargin?: boolean;
|
||||
newlyAddedParameters?: Set<string>;
|
||||
optionsOverrides?: ParameterOptionsOverrides;
|
||||
assignmentCollectionEditableValueIndices?: Record<string, number[]>;
|
||||
layout?: 'inline';
|
||||
};
|
||||
|
||||
|
|
@ -922,6 +923,8 @@ watch(
|
|||
:is-read-only="isReadOnly"
|
||||
:default-type="item.parameter.typeOptions?.assignment?.defaultType"
|
||||
:disable-type="item.parameter.typeOptions?.assignment?.disableType"
|
||||
:options-overrides="optionsOverrides"
|
||||
:editable-value-indices="assignmentCollectionEditableValueIndices?.[item.parameter.name]"
|
||||
@value-changed="valueChanged"
|
||||
/>
|
||||
<div v-else-if="credentialsParameterIndex !== index" class="parameter-item">
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user