mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
fix(editor): Make expression edit modal read-only in executions view (#10806)
This commit is contained in:
parent
2f8c8448d3
commit
394ef88843
|
@ -0,0 +1,77 @@
|
||||||
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
|
import { cleanupAppModals, createAppModals } from '@/__tests__/utils';
|
||||||
|
import ExpressionEditModal from '@/components/ExpressionEditModal.vue';
|
||||||
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
|
import { waitFor, within } from '@testing-library/vue';
|
||||||
|
|
||||||
|
vi.mock('vue-router', () => {
|
||||||
|
const push = vi.fn();
|
||||||
|
return {
|
||||||
|
useRouter: () => ({
|
||||||
|
push,
|
||||||
|
}),
|
||||||
|
useRoute: () => ({}),
|
||||||
|
RouterLink: vi.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderModal = createComponentRenderer(ExpressionEditModal);
|
||||||
|
|
||||||
|
describe('ExpressionEditModal', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
createAppModals();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanupAppModals();
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders correctly', async () => {
|
||||||
|
const pinia = createTestingPinia();
|
||||||
|
|
||||||
|
const { getByTestId } = renderModal({
|
||||||
|
pinia,
|
||||||
|
props: {
|
||||||
|
parameter: { name: 'foo', type: 'string' },
|
||||||
|
path: '',
|
||||||
|
modelValue: 'test',
|
||||||
|
dialogVisible: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(getByTestId('expression-modal-input')).toBeInTheDocument();
|
||||||
|
expect(getByTestId('expression-modal-output')).toBeInTheDocument();
|
||||||
|
|
||||||
|
const editor = within(getByTestId('expression-modal-input')).getByRole('textbox');
|
||||||
|
expect(editor).toBeInTheDocument();
|
||||||
|
expect(editor).toHaveAttribute('contenteditable', 'true');
|
||||||
|
expect(editor).not.toHaveAttribute('aria-readonly');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is read only', async () => {
|
||||||
|
const pinia = createTestingPinia();
|
||||||
|
|
||||||
|
const { getByTestId } = renderModal({
|
||||||
|
pinia,
|
||||||
|
props: {
|
||||||
|
parameter: { name: 'foo', type: 'string' },
|
||||||
|
path: '',
|
||||||
|
modelValue: 'test',
|
||||||
|
dialogVisible: true,
|
||||||
|
isReadOnly: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(getByTestId('expression-modal-input')).toBeInTheDocument();
|
||||||
|
expect(getByTestId('expression-modal-output')).toBeInTheDocument();
|
||||||
|
|
||||||
|
const editor = within(getByTestId('expression-modal-input')).getByRole('textbox');
|
||||||
|
expect(editor).toBeInTheDocument();
|
||||||
|
expect(editor).toHaveAttribute('aria-readonly', 'true');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -151,9 +151,9 @@ async function onDrop(expression: string, event: MouseEvent) {
|
||||||
:class="$style.schema"
|
:class="$style.schema"
|
||||||
:search="appliedSearch"
|
:search="appliedSearch"
|
||||||
:nodes="parentNodes"
|
:nodes="parentNodes"
|
||||||
mapping-enabled
|
:mapping-enabled="!isReadOnly"
|
||||||
pane-type="input"
|
|
||||||
:connection-type="NodeConnectionType.Main"
|
:connection-type="NodeConnectionType.Main"
|
||||||
|
pane-type="input"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -472,6 +472,7 @@ function getParameterValue<T extends NodeParameterValueType = NodeParameterValue
|
||||||
:node="node"
|
:node="node"
|
||||||
:path="getPath(parameter.name)"
|
:path="getPath(parameter.name)"
|
||||||
:dependent-parameters-values="getDependentParametersValues(parameter)"
|
:dependent-parameters-values="getDependentParametersValues(parameter)"
|
||||||
|
:is-read-only="isReadOnly"
|
||||||
input-size="small"
|
input-size="small"
|
||||||
label-size="small"
|
label-size="small"
|
||||||
@value-changed="valueChanged"
|
@value-changed="valueChanged"
|
||||||
|
|
|
@ -21,6 +21,7 @@ import {
|
||||||
parseResourceMapperFieldName,
|
parseResourceMapperFieldName,
|
||||||
} from '@/utils/nodeTypesUtils';
|
} from '@/utils/nodeTypesUtils';
|
||||||
import { useNodeSpecificationValues } from '@/composables/useNodeSpecificationValues';
|
import { useNodeSpecificationValues } from '@/composables/useNodeSpecificationValues';
|
||||||
|
import { N8nIconButton, N8nInputLabel, N8nOption, N8nSelect, N8nTooltip } from 'n8n-design-system';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
parameter: INodeProperties;
|
parameter: INodeProperties;
|
||||||
|
@ -28,16 +29,18 @@ interface Props {
|
||||||
nodeValues: INodeParameters | undefined;
|
nodeValues: INodeParameters | undefined;
|
||||||
fieldsToMap: ResourceMapperField[];
|
fieldsToMap: ResourceMapperField[];
|
||||||
paramValue: ResourceMapperValue;
|
paramValue: ResourceMapperValue;
|
||||||
labelSize: string;
|
labelSize: 'small' | 'medium';
|
||||||
showMatchingColumnsSelector: boolean;
|
showMatchingColumnsSelector: boolean;
|
||||||
showMappingModeSelect: boolean;
|
showMappingModeSelect: boolean;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
refreshInProgress: boolean;
|
refreshInProgress: boolean;
|
||||||
teleported?: boolean;
|
teleported?: boolean;
|
||||||
|
isReadOnly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
teleported: true,
|
teleported: true,
|
||||||
|
isReadOnly: false,
|
||||||
});
|
});
|
||||||
const FORCE_TEXT_INPUT_FOR_TYPES: FieldType[] = ['time', 'object', 'array'];
|
const FORCE_TEXT_INPUT_FOR_TYPES: FieldType[] = ['time', 'object', 'array'];
|
||||||
|
|
||||||
|
@ -285,7 +288,7 @@ defineExpose({
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="mt-xs" data-test-id="mapping-fields-container">
|
<div class="mt-xs" data-test-id="mapping-fields-container">
|
||||||
<n8n-input-label
|
<N8nInputLabel
|
||||||
:label="valuesLabel"
|
:label="valuesLabel"
|
||||||
:underline="true"
|
:underline="true"
|
||||||
:size="labelSize"
|
:size="labelSize"
|
||||||
|
@ -300,14 +303,15 @@ defineExpose({
|
||||||
:custom-actions="parameterActions"
|
:custom-actions="parameterActions"
|
||||||
:loading="props.refreshInProgress"
|
:loading="props.refreshInProgress"
|
||||||
:loading-message="fetchingFieldsLabel"
|
:loading-message="fetchingFieldsLabel"
|
||||||
|
:is-read-only="isReadOnly"
|
||||||
@update:model-value="onParameterActionSelected"
|
@update:model-value="onParameterActionSelected"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</n8n-input-label>
|
</N8nInputLabel>
|
||||||
<div v-if="orderedFields.length === 0" class="mt-3xs mb-xs">
|
<div v-if="orderedFields.length === 0" class="mt-3xs mb-xs">
|
||||||
<n8n-text size="small">{{
|
<N8nText size="small">{{
|
||||||
$locale.baseText('fixedCollectionParameter.currentlyNoItemsExist')
|
$locale.baseText('fixedCollectionParameter.currentlyNoItemsExist')
|
||||||
}}</n8n-text>
|
}}</N8nText>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-for="field in orderedFields"
|
v-for="field in orderedFields"
|
||||||
|
@ -322,7 +326,7 @@ defineExpose({
|
||||||
v-if="resourceMapperMode === 'add' && field.required"
|
v-if="resourceMapperMode === 'add' && field.required"
|
||||||
:class="['delete-option', 'mt-5xs', $style.parameterTooltipIcon]"
|
:class="['delete-option', 'mt-5xs', $style.parameterTooltipIcon]"
|
||||||
>
|
>
|
||||||
<n8n-tooltip placement="top">
|
<N8nTooltip placement="top">
|
||||||
<template #content>
|
<template #content>
|
||||||
<span>{{
|
<span>{{
|
||||||
locale.baseText('resourceMapper.mandatoryField.title', {
|
locale.baseText('resourceMapper.mandatoryField.title', {
|
||||||
|
@ -331,7 +335,7 @@ defineExpose({
|
||||||
}}</span>
|
}}</span>
|
||||||
</template>
|
</template>
|
||||||
<font-awesome-icon icon="question-circle" />
|
<font-awesome-icon icon="question-circle" />
|
||||||
</n8n-tooltip>
|
</N8nTooltip>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="
|
v-else-if="
|
||||||
|
@ -343,7 +347,7 @@ defineExpose({
|
||||||
"
|
"
|
||||||
:class="['delete-option', 'mt-5xs']"
|
:class="['delete-option', 'mt-5xs']"
|
||||||
>
|
>
|
||||||
<n8n-icon-button
|
<N8nIconButton
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
text
|
text
|
||||||
size="mini"
|
size="mini"
|
||||||
|
@ -356,8 +360,9 @@ defineExpose({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
"
|
"
|
||||||
|
:disabled="isReadOnly"
|
||||||
@click="removeField(field.name)"
|
@click="removeField(field.name)"
|
||||||
></n8n-icon-button>
|
></N8nIconButton>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.parameterInput">
|
<div :class="$style.parameterInput">
|
||||||
<ParameterInputFull
|
<ParameterInputFull
|
||||||
|
@ -365,7 +370,7 @@ defineExpose({
|
||||||
:value="getParameterValue(field.name)"
|
:value="getParameterValue(field.name)"
|
||||||
:display-options="true"
|
:display-options="true"
|
||||||
:path="`${props.path}.${field.name}`"
|
:path="`${props.path}.${field.name}`"
|
||||||
:is-read-only="refreshInProgress || field.readOnly"
|
:is-read-only="refreshInProgress || field.readOnly || isReadOnly"
|
||||||
:hide-issues="true"
|
:hide-issues="true"
|
||||||
:node-values="nodeValues"
|
:node-values="nodeValues"
|
||||||
:class="$style.parameterInputFull"
|
:class="$style.parameterInputFull"
|
||||||
|
@ -379,7 +384,7 @@ defineExpose({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div :class="['add-option', $style.addOption]" data-test-id="add-fields-select">
|
<div :class="['add-option', $style.addOption]" data-test-id="add-fields-select">
|
||||||
<n8n-select
|
<N8nSelect
|
||||||
:placeholder="
|
:placeholder="
|
||||||
locale.baseText('resourceMapper.addFieldToSend', {
|
locale.baseText('resourceMapper.addFieldToSend', {
|
||||||
interpolate: { fieldWord: singularFieldWordCapitalized },
|
interpolate: { fieldWord: singularFieldWordCapitalized },
|
||||||
|
@ -387,18 +392,18 @@ defineExpose({
|
||||||
"
|
"
|
||||||
size="small"
|
size="small"
|
||||||
:teleported="teleported"
|
:teleported="teleported"
|
||||||
:disabled="addFieldOptions.length == 0"
|
:disabled="addFieldOptions.length == 0 || isReadOnly"
|
||||||
@update:model-value="addField"
|
@update:model-value="addField"
|
||||||
>
|
>
|
||||||
<n8n-option
|
<N8nOption
|
||||||
v-for="item in addFieldOptions"
|
v-for="item in addFieldOptions"
|
||||||
:key="item.value"
|
:key="item.value"
|
||||||
:label="item.name"
|
:label="item.name"
|
||||||
:value="item.value"
|
:value="item.value"
|
||||||
:disabled="item.disabled"
|
:disabled="item.disabled"
|
||||||
>
|
>
|
||||||
</n8n-option>
|
</N8nOption>
|
||||||
</n8n-select>
|
</N8nSelect>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -3,20 +3,22 @@ import type { INodePropertyTypeOptions, ResourceMapperFields } from 'n8n-workflo
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import { i18n as locale } from '@/plugins/i18n';
|
import { i18n as locale } from '@/plugins/i18n';
|
||||||
import { useNodeSpecificationValues } from '@/composables/useNodeSpecificationValues';
|
import { useNodeSpecificationValues } from '@/composables/useNodeSpecificationValues';
|
||||||
|
import { N8nInputLabel, N8nSelect, N8nText } from 'n8n-design-system';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
initialValue: string;
|
initialValue: string;
|
||||||
fieldsToMap: ResourceMapperFields['fields'];
|
fieldsToMap: ResourceMapperFields['fields'];
|
||||||
inputSize: string;
|
inputSize: 'small' | 'medium';
|
||||||
labelSize: string;
|
labelSize: 'small' | 'medium';
|
||||||
typeOptions: INodePropertyTypeOptions | undefined;
|
typeOptions: INodePropertyTypeOptions | undefined;
|
||||||
serviceName: string;
|
serviceName: string;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
loadingError: boolean;
|
loadingError: boolean;
|
||||||
teleported?: boolean;
|
teleported?: boolean;
|
||||||
|
isReadOnly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
const props = withDefaults(defineProps<Props>(), { isReadOnly: false });
|
||||||
const { resourceMapperTypeOptions, pluralFieldWord, singularFieldWord } =
|
const { resourceMapperTypeOptions, pluralFieldWord, singularFieldWord } =
|
||||||
useNodeSpecificationValues(props.typeOptions);
|
useNodeSpecificationValues(props.typeOptions);
|
||||||
|
|
||||||
|
@ -103,7 +105,7 @@ defineExpose({
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div data-test-id="mapping-mode-select">
|
<div data-test-id="mapping-mode-select">
|
||||||
<n8n-input-label
|
<N8nInputLabel
|
||||||
:label="locale.baseText('resourceMapper.mappingMode.label')"
|
:label="locale.baseText('resourceMapper.mappingMode.label')"
|
||||||
:bold="false"
|
:bold="false"
|
||||||
:required="false"
|
:required="false"
|
||||||
|
@ -111,13 +113,14 @@ defineExpose({
|
||||||
color="text-dark"
|
color="text-dark"
|
||||||
>
|
>
|
||||||
<div class="mt-5xs">
|
<div class="mt-5xs">
|
||||||
<n8n-select
|
<N8nSelect
|
||||||
:model-value="selected"
|
:model-value="selected"
|
||||||
:teleported="teleported"
|
:teleported="teleported"
|
||||||
:size="props.inputSize"
|
:size="props.inputSize"
|
||||||
|
:disabled="isReadOnly"
|
||||||
@update:model-value="onModeChanged"
|
@update:model-value="onModeChanged"
|
||||||
>
|
>
|
||||||
<n8n-option
|
<N8nOption
|
||||||
v-for="option in mappingModeOptions"
|
v-for="option in mappingModeOptions"
|
||||||
:key="option.value"
|
:key="option.value"
|
||||||
:value="option.value"
|
:value="option.value"
|
||||||
|
@ -130,12 +133,12 @@ defineExpose({
|
||||||
</div>
|
</div>
|
||||||
<div class="option-description" v-html="option.description" />
|
<div class="option-description" v-html="option.description" />
|
||||||
</div>
|
</div>
|
||||||
</n8n-option>
|
</N8nOption>
|
||||||
</n8n-select>
|
</N8nSelect>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-5xs">
|
<div class="mt-5xs">
|
||||||
<n8n-text v-if="loading" size="small">
|
<N8nText v-if="loading" size="small">
|
||||||
<n8n-icon icon="sync-alt" size="xsmall" :spin="true" />
|
<N8nIcon icon="sync-alt" size="xsmall" :spin="true" />
|
||||||
{{
|
{{
|
||||||
locale.baseText('resourceMapper.fetchingFields.message', {
|
locale.baseText('resourceMapper.fetchingFields.message', {
|
||||||
interpolate: {
|
interpolate: {
|
||||||
|
@ -143,15 +146,15 @@ defineExpose({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
</n8n-text>
|
</N8nText>
|
||||||
<n8n-text v-else-if="errorMessage !== ''" size="small" color="danger">
|
<N8nText v-else-if="errorMessage !== ''" size="small" color="danger">
|
||||||
<n8n-icon icon="exclamation-triangle" size="xsmall" />
|
<N8nIcon icon="exclamation-triangle" size="xsmall" />
|
||||||
{{ errorMessage }}
|
{{ errorMessage }}
|
||||||
<n8n-link size="small" theme="danger" :underline="true" @click="onRetryClick">
|
<N8nLink size="small" theme="danger" :underline="true" @click="onRetryClick">
|
||||||
{{ locale.baseText('generic.retry') }}
|
{{ locale.baseText('generic.retry') }}
|
||||||
</n8n-link>
|
</N8nLink>
|
||||||
</n8n-text>
|
</N8nText>
|
||||||
</div>
|
</div>
|
||||||
</n8n-input-label>
|
</N8nInputLabel>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -9,22 +9,25 @@ import { computed, reactive, watch } from 'vue';
|
||||||
import { i18n as locale } from '@/plugins/i18n';
|
import { i18n as locale } from '@/plugins/i18n';
|
||||||
import { useNodeSpecificationValues } from '@/composables/useNodeSpecificationValues';
|
import { useNodeSpecificationValues } from '@/composables/useNodeSpecificationValues';
|
||||||
import ParameterOptions from '@/components/ParameterOptions.vue';
|
import ParameterOptions from '@/components/ParameterOptions.vue';
|
||||||
|
import { N8nInputLabel, N8nNotice, N8nSelect } from 'n8n-design-system';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
parameter: INodeProperties;
|
parameter: INodeProperties;
|
||||||
initialValue: string[];
|
initialValue: string[];
|
||||||
fieldsToMap: ResourceMapperFields['fields'];
|
fieldsToMap: ResourceMapperFields['fields'];
|
||||||
typeOptions: INodePropertyTypeOptions | undefined;
|
typeOptions: INodePropertyTypeOptions | undefined;
|
||||||
labelSize: string;
|
labelSize: 'small' | 'medium';
|
||||||
inputSize: string;
|
inputSize: 'small' | 'medium';
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
serviceName: string;
|
serviceName: string;
|
||||||
teleported?: boolean;
|
|
||||||
refreshInProgress: boolean;
|
refreshInProgress: boolean;
|
||||||
|
teleported?: boolean;
|
||||||
|
isReadOnly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
teleported: true,
|
teleported: true,
|
||||||
|
isReadOnly: false,
|
||||||
});
|
});
|
||||||
const {
|
const {
|
||||||
resourceMapperTypeOptions,
|
resourceMapperTypeOptions,
|
||||||
|
@ -168,7 +171,7 @@ defineExpose({
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="mt-2xs" data-test-id="matching-column-select">
|
<div class="mt-2xs" data-test-id="matching-column-select">
|
||||||
<n8n-input-label
|
<N8nInputLabel
|
||||||
v-if="availableMatchingFields.length > 0"
|
v-if="availableMatchingFields.length > 0"
|
||||||
:label="fieldLabel"
|
:label="fieldLabel"
|
||||||
:tooltip-text="fieldTooltip"
|
:tooltip-text="fieldTooltip"
|
||||||
|
@ -183,14 +186,15 @@ defineExpose({
|
||||||
:custom-actions="parameterActions"
|
:custom-actions="parameterActions"
|
||||||
:loading="props.refreshInProgress"
|
:loading="props.refreshInProgress"
|
||||||
:loading-message="fetchingFieldsLabel"
|
:loading-message="fetchingFieldsLabel"
|
||||||
|
:is-read-only="isReadOnly"
|
||||||
@update:model-value="onParameterActionSelected"
|
@update:model-value="onParameterActionSelected"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<n8n-select
|
<N8nSelect
|
||||||
:multiple="resourceMapperTypeOptions?.multiKeyMatch === true"
|
:multiple="resourceMapperTypeOptions?.multiKeyMatch === true"
|
||||||
:model-value="state.selected"
|
:model-value="state.selected"
|
||||||
:size="props.inputSize"
|
:size="props.inputSize"
|
||||||
:disabled="loading"
|
:disabled="loading || isReadOnly"
|
||||||
:teleported="teleported"
|
:teleported="teleported"
|
||||||
@update:model-value="onSelectionChange"
|
@update:model-value="onSelectionChange"
|
||||||
>
|
>
|
||||||
|
@ -202,17 +206,17 @@ defineExpose({
|
||||||
>
|
>
|
||||||
{{ field.displayName }}
|
{{ field.displayName }}
|
||||||
</n8n-option>
|
</n8n-option>
|
||||||
</n8n-select>
|
</N8nSelect>
|
||||||
<n8n-text size="small">
|
<N8nText size="small">
|
||||||
{{ fieldDescription }}
|
{{ fieldDescription }}
|
||||||
</n8n-text>
|
</N8nText>
|
||||||
</n8n-input-label>
|
</N8nInputLabel>
|
||||||
<n8n-notice v-else>
|
<N8nNotice v-else>
|
||||||
{{
|
{{
|
||||||
locale.baseText('resourceMapper.columnsToMatchOn.noFieldsFound', {
|
locale.baseText('resourceMapper.columnsToMatchOn.noFieldsFound', {
|
||||||
interpolate: { fieldWord: singularFieldWord, serviceName: props.serviceName },
|
interpolate: { fieldWord: singularFieldWord, serviceName: props.serviceName },
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
</n8n-notice>
|
</N8nNotice>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -25,10 +25,11 @@ type Props = {
|
||||||
parameter: INodeProperties;
|
parameter: INodeProperties;
|
||||||
node: INode | null;
|
node: INode | null;
|
||||||
path: string;
|
path: string;
|
||||||
inputSize: string;
|
inputSize: 'small' | 'medium';
|
||||||
labelSize: string;
|
labelSize: 'small' | 'medium';
|
||||||
dependentParametersValues?: string | null;
|
|
||||||
teleported: boolean;
|
teleported: boolean;
|
||||||
|
dependentParametersValues?: string | null;
|
||||||
|
isReadOnly?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const nodeTypesStore = useNodeTypesStore();
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
|
@ -38,6 +39,7 @@ const workflowsStore = useWorkflowsStore();
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
teleported: true,
|
teleported: true,
|
||||||
dependentParametersValues: null,
|
dependentParametersValues: null,
|
||||||
|
isReadOnly: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
@ -485,6 +487,7 @@ defineExpose({
|
||||||
:loading-error="state.loadingError"
|
:loading-error="state.loadingError"
|
||||||
:fields-to-map="state.paramValue.schema"
|
:fields-to-map="state.paramValue.schema"
|
||||||
:teleported="teleported"
|
:teleported="teleported"
|
||||||
|
:is-read-only="isReadOnly"
|
||||||
@mode-changed="onModeChanged"
|
@mode-changed="onModeChanged"
|
||||||
@retry-fetch="initFetching"
|
@retry-fetch="initFetching"
|
||||||
/>
|
/>
|
||||||
|
@ -500,11 +503,12 @@ defineExpose({
|
||||||
:service-name="nodeType?.displayName || locale.baseText('generic.service')"
|
:service-name="nodeType?.displayName || locale.baseText('generic.service')"
|
||||||
:teleported="teleported"
|
:teleported="teleported"
|
||||||
:refresh-in-progress="state.refreshInProgress"
|
:refresh-in-progress="state.refreshInProgress"
|
||||||
|
:is-read-only="isReadOnly"
|
||||||
@matching-columns-changed="onMatchingColumnsChanged"
|
@matching-columns-changed="onMatchingColumnsChanged"
|
||||||
@refresh-field-list="initFetching(true)"
|
@refresh-field-list="initFetching(true)"
|
||||||
/>
|
/>
|
||||||
<n8n-text v-if="!showMappingModeSelect && state.loading" size="small">
|
<N8nText v-if="!showMappingModeSelect && state.loading" size="small">
|
||||||
<n8n-icon icon="sync-alt" size="xsmall" :spin="true" />
|
<N8nIcon icon="sync-alt" size="xsmall" :spin="true" />
|
||||||
{{
|
{{
|
||||||
locale.baseText('resourceMapper.fetchingFields.message', {
|
locale.baseText('resourceMapper.fetchingFields.message', {
|
||||||
interpolate: {
|
interpolate: {
|
||||||
|
@ -512,7 +516,7 @@ defineExpose({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
</n8n-text>
|
</N8nText>
|
||||||
<MappingFields
|
<MappingFields
|
||||||
v-if="showMappingFields"
|
v-if="showMappingFields"
|
||||||
:parameter="props.parameter"
|
:parameter="props.parameter"
|
||||||
|
@ -526,12 +530,13 @@ defineExpose({
|
||||||
:loading="state.loading"
|
:loading="state.loading"
|
||||||
:teleported="teleported"
|
:teleported="teleported"
|
||||||
:refresh-in-progress="state.refreshInProgress"
|
:refresh-in-progress="state.refreshInProgress"
|
||||||
|
:is-read-only="isReadOnly"
|
||||||
@field-value-changed="fieldValueChanged"
|
@field-value-changed="fieldValueChanged"
|
||||||
@remove-field="removeField"
|
@remove-field="removeField"
|
||||||
@add-field="addField"
|
@add-field="addField"
|
||||||
@refresh-field-list="initFetching(true)"
|
@refresh-field-list="initFetching(true)"
|
||||||
/>
|
/>
|
||||||
<n8n-notice
|
<N8nNotice
|
||||||
v-if="state.paramValue.mappingMode === 'autoMapInputData' && hasAvailableMatchingColumns"
|
v-if="state.paramValue.mappingMode === 'autoMapInputData' && hasAvailableMatchingColumns"
|
||||||
>
|
>
|
||||||
{{
|
{{
|
||||||
|
@ -542,6 +547,6 @@ defineExpose({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
</n8n-notice>
|
</N8nNotice>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -45,6 +45,14 @@ describe('ResourceMapper.vue', () => {
|
||||||
).toBe(MAPPING_COLUMNS_RESPONSE.fields.length);
|
).toBe(MAPPING_COLUMNS_RESPONSE.fields.length);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders correctly in read only mode', async () => {
|
||||||
|
const { getByTestId } = renderComponent({ props: { isReadOnly: true } });
|
||||||
|
await waitAllPromises();
|
||||||
|
expect(getByTestId('mapping-mode-select').querySelector('input')).toBeDisabled();
|
||||||
|
expect(getByTestId('matching-column-select').querySelector('input')).toBeDisabled();
|
||||||
|
expect(getByTestId('mapping-fields-container').querySelector('input')).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
it('renders add mode properly', async () => {
|
it('renders add mode properly', async () => {
|
||||||
const { getByTestId, queryByTestId } = renderComponent(
|
const { getByTestId, queryByTestId } = renderComponent(
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in a new issue