diff --git a/changelog/27569.txt b/changelog/27569.txt new file mode 100644 index 000000000000..cb81aa23df73 --- /dev/null +++ b/changelog/27569.txt @@ -0,0 +1,3 @@ +```release-note:bug +ui: Fix cursor jump on KVv2 json editor that would occur after pressing ENTER. +``` diff --git a/ui/lib/core/addon/modifiers/code-mirror.js b/ui/lib/core/addon/modifiers/code-mirror.js index 79fe0064aa4e..b9b46900dcc8 100644 --- a/ui/lib/core/addon/modifiers/code-mirror.js +++ b/ui/lib/core/addon/modifiers/code-mirror.js @@ -7,6 +7,7 @@ import { action } from '@ember/object'; import { bind } from '@ember/runloop'; import codemirror from 'codemirror'; import Modifier from 'ember-modifier'; +import { stringify } from 'core/helpers/stringify'; import 'codemirror/addon/edit/matchbrackets'; import 'codemirror/addon/selection/active-line'; @@ -26,7 +27,19 @@ export default class CodeMirrorModifier extends Modifier { } else { // this hook also fires any time there is a change to tracked state this._editor.setOption('readOnly', namedArgs.readOnly); - if (namedArgs.content && this._editor.getValue() !== namedArgs.content) { + let value = this._editor.getValue(); + let content = namedArgs.content; + if (!content) return; + try { + // First parse json to make white space and line breaks consistent between the two items, + // then stringify so they can be compared. + // We use the stringify helper so we do not flatten the json object + value = stringify([JSON.parse(value)], {}); + content = stringify([JSON.parse(content)], {}); + } catch { + // this catch will occur for non-json content when the mode is not javascript (e.g. ruby). + } + if (value !== content) { this._editor.setValue(namedArgs.content); } } diff --git a/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-edge-cases-test.js b/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-edge-cases-test.js index 142925cedb00..a62722fa0cf8 100644 --- a/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-edge-cases-test.js +++ b/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-edge-cases-test.js @@ -5,7 +5,16 @@ /* eslint-disable no-useless-escape */ import { module, test } from 'qunit'; import { v4 as uuidv4 } from 'uuid'; -import { click, currentURL, fillIn, findAll, setupOnerror, typeIn, visit } from '@ember/test-helpers'; +import { + click, + currentURL, + fillIn, + findAll, + setupOnerror, + typeIn, + visit, + triggerKeyEvent, +} from '@ember/test-helpers'; import { setupApplicationTest } from 'vault/tests/helpers'; import authPage from 'vault/tests/pages/auth'; import { @@ -24,6 +33,7 @@ import { } from 'vault/tests/helpers/kv/policy-generator'; import { clearRecords, writeSecret, writeVersionedSecret } from 'vault/tests/helpers/kv/kv-run-commands'; import { FORM, PAGE } from 'vault/tests/helpers/kv/kv-selectors'; +import { GENERAL } from 'vault/tests/helpers/general-selectors'; import codemirror from 'vault/tests/helpers/codemirror'; /** @@ -309,6 +319,19 @@ module('Acceptance | kv-v2 workflow | edge cases', function (hooks) { assert.false(codemirror().getValue().includes('*'), 'Values are not obscured on edit view'); }); + test('on enter the JSON editor cursor goes to the next line', async function (assert) { + // see issue here: https://github.com/hashicorp/vault/issues/27524 + const predictedCursorPosition = JSON.stringify({ line: 3, ch: 0, sticky: null }); + await visit(`/vault/secrets/${this.backend}/kv/create`); + await fillIn(FORM.inputByAttr('path'), 'json jump'); + + await click(FORM.toggleJson); + codemirror().setCursor({ line: 2, ch: 1 }); + await triggerKeyEvent(GENERAL.codemirrorTextarea, 'keydown', 'Enter'); + const actualCursorPosition = JSON.stringify(codemirror().getCursor()); + assert.strictEqual(actualCursorPosition, predictedCursorPosition, 'the cursor stayed on the next line'); + }); + test('viewing advanced secret data versions displays the correct version data', async function (assert) { assert.expect(2); const obscuredDataV1 = `{ diff --git a/ui/tests/helpers/general-selectors.ts b/ui/tests/helpers/general-selectors.ts index 44294db7adbe..042da9f88cbf 100644 --- a/ui/tests/helpers/general-selectors.ts +++ b/ui/tests/helpers/general-selectors.ts @@ -81,4 +81,6 @@ export const GENERAL = { cancelButton: '[data-test-cancel]', saveButton: '[data-test-save]', maskedInput: (name: string) => `[data-test-textarea="${name}"]`, + codemirror: `[data-test-component="code-mirror-modifier"]`, + codemirrorTextarea: `[data-test-component="code-mirror-modifier"] textarea`, }; diff --git a/ui/tests/integration/components/json-editor-test.js b/ui/tests/integration/components/json-editor-test.js index 35a0ab78565b..72153802f1e2 100644 --- a/ui/tests/integration/components/json-editor-test.js +++ b/ui/tests/integration/components/json-editor-test.js @@ -6,7 +6,7 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; import { create } from 'ember-cli-page-object'; -import { render, fillIn, find, waitUntil, click } from '@ember/test-helpers'; +import { render, fillIn, find, waitUntil, click, triggerKeyEvent } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import jsonEditor from '../../pages/components/json-editor'; import sinon from 'sinon'; @@ -122,12 +122,33 @@ module('Integration | Component | json-editor', function (hooks) { assert.dom('.CodeMirror-code').hasText(`{ "test": "********"}`, 'shows data with obscured values'); assert.dom('[data-test-toggle-input="revealValues"]').isNotChecked('reveal values toggle is unchecked'); await click('[data-test-toggle-input="revealValues"]'); - assert.dom('.CodeMirror-code').hasText(JSON_BLOB, 'shows data with real values'); + // we are hardcoding the hasText comparison instead of using the JSON_BLOB because we no longer match passed content (ex: @value) to the tracked codemirror instance (ex: this._editor.getVale()) if there are line-breaks or whitespace differences. + assert.dom('.CodeMirror-code').hasText(`{ "test": "test" }`, 'shows data with real values'); assert.dom('[data-test-toggle-input="revealValues"]').isChecked('reveal values toggle is checked'); // turn obscure back on to ensure readonly overrides reveal setting await click('[data-test-toggle-input="revealValues"]'); this.set('readOnly', false); assert.dom('[data-test-toggle-input="revealValues"]').doesNotExist('reveal values toggle is hidden'); - assert.dom('.CodeMirror-code').hasText(JSON_BLOB, 'shows data with real values on edit mode'); + assert.dom('.CodeMirror-code').hasText(`{ "test": "test" }`, 'shows data with real values on edit mode'); + }); + + test('code-mirror modifier sets value correctly on non json object', async function (assert) { + // this.value is a tracked property, so anytime it changes the modifier is called. We're testing non-json content by setting the mode to ruby and adding a comment + this.value = null; + await render(hbs` + + `); + await fillIn('textarea', '#A comment'); + assert.strictEqual(this.value, '#A comment', 'value is set correctly'); + await triggerKeyEvent('textarea', 'keydown', 'Enter'); + assert.strictEqual( + this.value, + `#A comment\n`, + 'even after hitting enter the value is still set correctly' + ); }); });