diff --git a/packages/editor-ui/src/components/ExpressionEditModal.test.ts b/packages/editor-ui/src/components/ExpressionEditModal.test.ts new file mode 100644 index 0000000000..e0d59fe79c --- /dev/null +++ b/packages/editor-ui/src/components/ExpressionEditModal.test.ts @@ -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'); + }); + }); +}); diff --git a/packages/editor-ui/src/components/ExpressionEditModal.vue b/packages/editor-ui/src/components/ExpressionEditModal.vue index 9411fe3789..cea40fb431 100644 --- a/packages/editor-ui/src/components/ExpressionEditModal.vue +++ b/packages/editor-ui/src/components/ExpressionEditModal.vue @@ -151,9 +151,9 @@ async function onDrop(expression: string, event: MouseEvent) { :class="$style.schema" :search="appliedSearch" :nodes="parentNodes" - mapping-enabled - pane-type="input" + :mapping-enabled="!isReadOnly" :connection-type="NodeConnectionType.Main" + pane-type="input" /> diff --git a/packages/editor-ui/src/components/ParameterInputList.vue b/packages/editor-ui/src/components/ParameterInputList.vue index 08256fbfef..1d08dfa505 100644 --- a/packages/editor-ui/src/components/ParameterInputList.vue +++ b/packages/editor-ui/src/components/ParameterInputList.vue @@ -472,6 +472,7 @@ function getParameterValue(), { teleported: true, + isReadOnly: false, }); const FORCE_TEXT_INPUT_FOR_TYPES: FieldType[] = ['time', 'object', 'array']; @@ -285,7 +288,7 @@ defineExpose({ - +
- {{ + {{ $locale.baseText('fixedCollectionParameter.currentlyNoItemsExist') - }} + }}
- + - +
- + >
- - - - + +
diff --git a/packages/editor-ui/src/components/ResourceMapper/MappingModeSelect.vue b/packages/editor-ui/src/components/ResourceMapper/MappingModeSelect.vue index 492c74e3b7..2c69e15550 100644 --- a/packages/editor-ui/src/components/ResourceMapper/MappingModeSelect.vue +++ b/packages/editor-ui/src/components/ResourceMapper/MappingModeSelect.vue @@ -3,20 +3,22 @@ import type { INodePropertyTypeOptions, ResourceMapperFields } from 'n8n-workflo import { computed, ref, watch } from 'vue'; import { i18n as locale } from '@/plugins/i18n'; import { useNodeSpecificationValues } from '@/composables/useNodeSpecificationValues'; +import { N8nInputLabel, N8nSelect, N8nText } from 'n8n-design-system'; interface Props { initialValue: string; fieldsToMap: ResourceMapperFields['fields']; - inputSize: string; - labelSize: string; + inputSize: 'small' | 'medium'; + labelSize: 'small' | 'medium'; typeOptions: INodePropertyTypeOptions | undefined; serviceName: string; loading: boolean; loadingError: boolean; teleported?: boolean; + isReadOnly?: boolean; } -const props = defineProps(); +const props = withDefaults(defineProps(), { isReadOnly: false }); const { resourceMapperTypeOptions, pluralFieldWord, singularFieldWord } = useNodeSpecificationValues(props.typeOptions); @@ -103,7 +105,7 @@ defineExpose({ diff --git a/packages/editor-ui/src/components/ResourceMapper/MatchingColumnsSelect.vue b/packages/editor-ui/src/components/ResourceMapper/MatchingColumnsSelect.vue index 502d83af1a..7a40e24400 100644 --- a/packages/editor-ui/src/components/ResourceMapper/MatchingColumnsSelect.vue +++ b/packages/editor-ui/src/components/ResourceMapper/MatchingColumnsSelect.vue @@ -9,22 +9,25 @@ import { computed, reactive, watch } from 'vue'; import { i18n as locale } from '@/plugins/i18n'; import { useNodeSpecificationValues } from '@/composables/useNodeSpecificationValues'; import ParameterOptions from '@/components/ParameterOptions.vue'; +import { N8nInputLabel, N8nNotice, N8nSelect } from 'n8n-design-system'; interface Props { parameter: INodeProperties; initialValue: string[]; fieldsToMap: ResourceMapperFields['fields']; typeOptions: INodePropertyTypeOptions | undefined; - labelSize: string; - inputSize: string; + labelSize: 'small' | 'medium'; + inputSize: 'small' | 'medium'; loading: boolean; serviceName: string; - teleported?: boolean; refreshInProgress: boolean; + teleported?: boolean; + isReadOnly?: boolean; } const props = withDefaults(defineProps(), { teleported: true, + isReadOnly: false, }); const { resourceMapperTypeOptions, @@ -168,7 +171,7 @@ defineExpose({ - @@ -202,17 +206,17 @@ defineExpose({ > {{ field.displayName }} - - + + {{ fieldDescription }} - - - + + + {{ locale.baseText('resourceMapper.columnsToMatchOn.noFieldsFound', { interpolate: { fieldWord: singularFieldWord, serviceName: props.serviceName }, }) }} - + diff --git a/packages/editor-ui/src/components/ResourceMapper/ResourceMapper.vue b/packages/editor-ui/src/components/ResourceMapper/ResourceMapper.vue index 3ba28af42d..26b356580a 100644 --- a/packages/editor-ui/src/components/ResourceMapper/ResourceMapper.vue +++ b/packages/editor-ui/src/components/ResourceMapper/ResourceMapper.vue @@ -25,10 +25,11 @@ type Props = { parameter: INodeProperties; node: INode | null; path: string; - inputSize: string; - labelSize: string; - dependentParametersValues?: string | null; + inputSize: 'small' | 'medium'; + labelSize: 'small' | 'medium'; teleported: boolean; + dependentParametersValues?: string | null; + isReadOnly?: boolean; }; const nodeTypesStore = useNodeTypesStore(); @@ -38,6 +39,7 @@ const workflowsStore = useWorkflowsStore(); const props = withDefaults(defineProps(), { teleported: true, dependentParametersValues: null, + isReadOnly: false, }); const emit = defineEmits<{ @@ -485,6 +487,7 @@ defineExpose({ :loading-error="state.loadingError" :fields-to-map="state.paramValue.schema" :teleported="teleported" + :is-read-only="isReadOnly" @mode-changed="onModeChanged" @retry-fetch="initFetching" /> @@ -500,11 +503,12 @@ defineExpose({ :service-name="nodeType?.displayName || locale.baseText('generic.service')" :teleported="teleported" :refresh-in-progress="state.refreshInProgress" + :is-read-only="isReadOnly" @matching-columns-changed="onMatchingColumnsChanged" @refresh-field-list="initFetching(true)" /> - - + + {{ locale.baseText('resourceMapper.fetchingFields.message', { interpolate: { @@ -512,7 +516,7 @@ defineExpose({ }, }) }} - + - {{ @@ -542,6 +547,6 @@ defineExpose({ }, }) }} - + diff --git a/packages/editor-ui/src/components/__tests__/ResourceMapper.test.ts b/packages/editor-ui/src/components/__tests__/ResourceMapper.test.ts index 9aef3a739f..a9e42dddfd 100644 --- a/packages/editor-ui/src/components/__tests__/ResourceMapper.test.ts +++ b/packages/editor-ui/src/components/__tests__/ResourceMapper.test.ts @@ -45,6 +45,14 @@ describe('ResourceMapper.vue', () => { ).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 () => { const { getByTestId, queryByTestId } = renderComponent( {