Replace Rose::CodeEditor with Hds::CodeEditor (#2679)

* chore: 🤖 replace Rose::CodeEditor with Hds::CodeEditor

- Remove CodeEditor 5 dependency

* chore: 🤖 Replace CodeMirror5 dep

remove CodeMirror5 and replace existing editors with HDS::CodeEditor

* chore: 🤖 deleting old codeeditor styles

* feat: 🎸 changing code editor

changing code editor

✅ Closes: 0

* removed unused export

* pr cleanup

* updating tests for new code editor

* responding to PR feedback

* added whitespace

* fixing linting violations

* chore: 🤖 self-review PR

self-review PR in the course of switching to new HDS code editor

* fix: 🐛 fix linting bug

* fix: 🐛 unneeded dependency

remove jslint dependency as this is handled in the new code editor from
HDS

* fix: 🐛 Fixing import bug

Fix import bug caused by importing scss modules that no longer exist

* Fix and refactor code editor acceptance test

* Fix and refactor code editor acceptance test

* chore: 🤖 remove codemirror dependency

* refactor: 💡 fix merge conflict issues

* test: 💍 update e2e test selectors

* test: 💍 fix target spec click event

* refactor: 💡 clean up test selectors for code editor

* refactor: 💡 clean up storage bucket test selectors

* chore: 🤖 remove code mirror modifiers

---------

Co-authored-by: calcaide <ccorvo@hashicorp.com>
Co-authored-by: cameronperera <cameron.perera@hashicorp.com>
This commit is contained in:
authored and Zhihe Li committed 2026-05-14 15:21:27 -04:00
1 parent fd1eba617b
commit d469f305de
41 files changed
+354 -744

No files matched your search

+1 -1
View File
@@ -30,7 +30,7 @@ module.exports = function (environment) {
},
mirage: {
enabled: true
enabled: true,
},
};
+1
View File
@@ -177,6 +177,7 @@ sync-interval:
tags:
label: Tags
secret-editor:
label: Secret Editor
details: This secret is saved but won't be displayed
edit: Click to replace this secret
created:
@@ -1,11 +0,0 @@
{{!
Copyright IBM Corp. 2021, 2026
SPDX-License-Identifier: BUSL-1.1
}}
<div
data-test-code-editor-field-editor
...attributes
class='rose-codemirror-container'
{{code-mirror value=@value onInput=@onInput options=@options}}
></div>
@@ -1,13 +0,0 @@
{{!
Copyright IBM Corp. 2021, 2026
SPDX-License-Identifier: BUSL-1.1
}}
<div data-test-code-editor>
{{yield
(hash
fieldEditor=(component 'rose/code-editor/field-editor' value=@codeValue)
toolbar=(component 'rose/code-editor/toolbar' copyText=@codeValue)
)
}}
</div>
@@ -1,31 +0,0 @@
{{!
Copyright IBM Corp. 2021, 2026
SPDX-License-Identifier: BUSL-1.1
}}
<div class='rose-code-editor-toolbar' data-test-code-editor-toolbar>
<div data-test-code-editor-toolbar-action class='code-editor-toolbar__action'>
{{yield to='action'}}
</div>
<div class='rose-code-editor-toolbar__right-aligned'>
{{#if (has-block 'menu')}}
<div
data-test-code-editor-toolbar-menu
class='rose-code-editor-toolbar__menu'
>
{{yield to='menu'}}
</div>
<div
class='rose-code-editor-toolbar__menu-divider'
data-test-code-editor-toolbar-menu-divider
></div>
{{/if}}
<Hds::Copy::Button
@text='Copy code'
@textToCopy={{@copyText}}
@onSuccess={{this.copied}}
data-test-code-editor-toolbar-copy-button
/>
</div>
</div>
@@ -1,14 +0,0 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
import { action } from '@ember/object';
export default class RoseCodeEditorToolbarComponent extends Component {
@action
copied() {
this.args.onCopy?.();
}
}
@@ -1,99 +0,0 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import Modifier from 'ember-modifier';
import codemirror from 'codemirror';
import '../utilities/register-codemirror-hcl';
import 'codemirror/mode/go/go';
import 'codemirror/mode/javascript/javascript';
import 'codemirror/mode/shell/shell';
import 'codemirror/addon/edit/matchbrackets';
import 'codemirror/addon/edit/closebrackets';
import 'codemirror/addon/selection/active-line';
import 'codemirror/addon/display/autorefresh';
import 'codemirror/addon/lint/lint';
import 'codemirror/addon/lint/json-lint';
// Here we define default options for the editor.
// These should follow the codemirror configuration types
//
const _PRESET_DEFAULTS = {
theme: 'monokai',
lineNumbers: true,
cursorBlinkRate: 500,
matchBrackets: true,
autoCloseBrackets: true,
styleActiveLine: true,
mode: 'hcl',
};
/**
*
* `CodeMirror` implements a modifier that creates a CodeMirror instance on the provided element.
*
* The supported modes for the editor are currently: hcl, shell, go, javascript
* The supported themes are: monokai
*
* Sample usage:
* ```
* <div
* {{code-mirror value=@value onInput=@onInput options=@options}}
* />
* ```
*
* @class CodeMirror
*
*/
export default class CodeMirrorModifier extends Modifier {
_editor;
didSetup = false;
modify(element, _, named) {
if (!this.didSetup) {
this._setup(element, named);
this.didSetup = true;
}
if (this._editor?.getValue() !== named.value) {
this._editor?.setValue(named.value);
}
}
/**
* Called when editor contents change.
* @method CodeMirror#_onChange
* See documentation for the CodeMirror editor events, specifically 'change' event here https://codemirror.net/doc/manual.html#events.
*/
_onChange(editor, named) {
const { onInput } = named;
const newVal = editor.getValue();
onInput && onInput(newVal);
}
/**
* Initializes the editor with default configuration merged with consumer-provided configuration.
* Also registers a 'change' event on the editor.
* @method CodeMirror#_setup
*/
_setup(element, named) {
if (!element) {
throw new Error('CodeMirror modifier has no element');
}
const editor = codemirror(element, {
..._PRESET_DEFAULTS,
...(named.options || {}),
value: named.value,
});
editor.on('change', (editor) => {
this._onChange(editor, named);
});
this._editor = editor;
}
}
@@ -1,43 +0,0 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import codemirror from 'codemirror';
import 'codemirror/addon/mode/simple';
// This utility defines 'hcl' mode for a codemirror editor and enables correct syntax highlighting.
// Importing this file in the codemirror modifier will allow 'hcl' to be selected as a mode:
// import 'cloud-ui-core/utils/register-codemirror-hcl'
codemirror.defineSimpleMode('hcl', {
start: [
{ regex: /"(?:[^\\]|\\.)*?(?:"|$)/, token: 'string' }, // strings
{ regex: /(\w+)(\s+)(=)/, token: ['keyword', 'null', 'null'] }, // assignment of variables
{
regex: /(build|deploy|release|hook|registry|type|runner|url)( )({)/,
token: ['keyword', 'null', 'null'],
}, // top level keywords
{ regex: /(variable)\b/, token: 'keyword' }, // input variable keyword
{ regex: /true|false|null|undefined/, token: 'atom' }, // bool keywords
{
regex: /0x[a-f\d]+|[-+]?(?:\.\d+|\d+\.?\d*)(?:e[-+]?\d+)?/i,
token: 'number',
}, // numbers
{ regex: /(#|\/\/)(\s*\S*)/, token: 'comment' }, // single line comments
{ regex: /(path)(.)/, token: ['variable-2', 'null'] }, // path variables
{ regex: /(workspace)(.)(\S*)/, token: ['string', 'null', 'string'] }, // workspace variables
{ regex: /\/\*/, token: 'comment', next: 'comment' }, // multi-line comment
{ regex: /[-+/*=<>!]+/, token: 'operator' }, // operators
{ regex: /[{[(]/, indent: true }, // for auto indent
{ regex: /[}\])]/, dedent: true },
],
// The multi-line comment state.
comment: [
{ regex: /.*?\*\//, token: 'comment', next: 'start' },
{ regex: /.*/, token: 'comment' },
],
meta: {
dontIndentStates: ['comment'],
lineComment: '#',
},
});
@@ -1,6 +0,0 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
export { default } from 'rose/components/rose/code-editor';
@@ -1,6 +0,0 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
export { default } from 'rose/components/rose/code-editor/field-editor';
@@ -1,6 +0,0 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
export { default } from 'rose/components/rose/code-editor/toolbar';
-6
View File
@@ -1,6 +0,0 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
export { default } from 'rose/modifiers/code-mirror';
@@ -5,7 +5,6 @@
// This list is sorted in ascending order to improve readability
@use 'code-editor';
@use 'form';
@use 'frame';
@use 'layout';
@@ -1,70 +0,0 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
/* stylelint-disable selector-class-pattern */
@use '../../variables/sizing';
// Codemirror overrides
.rose-codemirror-container {
width: 100%;
}
.CodeMirror {
max-width: 100%;
display: flex;
flex: 1;
font-family: var(--token-typography-font-stack-code);
}
.rose-autosize {
.CodeMirror {
height: auto;
}
}
.CodeMirror-scroll {
width: 100%;
}
.CodeMirror-sizer {
border-right-width: 0 !important;
}
// Toolbar
.rose-code-editor-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--ui-gray-subtler-3);
height: 2.75rem;
&__action {
margin-left: sizing.rems(s);
}
&__right-aligned {
display: flex;
height: 100%;
align-items: center;
}
&__menu {
margin-right: sizing.rems(s);
.pds-dropdownTrigger {
box-shadow: none;
&__layoutWrapper {
padding-left: sizing.rems(s);
padding-right: sizing.rems(s);
}
}
&-divider {
border-right: 1px solid var(--ui-gray-subtler-3);
height: sizing.rems(xl);
}
}
}
-5
View File
@@ -12,11 +12,6 @@ module.exports = {
included(app) {
this._super.included.apply(this, arguments);
this.import('node_modules/codemirror/lib/codemirror.css');
this.import('node_modules/codemirror/theme/monokai.css');
this.import('node_modules/codemirror/addon/lint/lint.css');
this.import('node_modules/jsonlint/lib/jsonlint.js');
this.includeHDSStyles(app);
this.includeFlightIcons(app);
this.includePublic(app);
-2
View File
@@ -34,14 +34,12 @@
"@hashicorp/design-system-tokens": "^2.3.0",
"@hashicorp/flight-icons": "^3.10.0",
"@nullvoxpopuli/ember-composable-helpers": "^5.2.10",
"codemirror": "5.65.7",
"ember-auto-import": "^2.10.0",
"ember-cli-babel": "^8.2.0",
"ember-cli-htmlbars": "^6.3.0",
"ember-cli-sass": "^11.0.1",
"ember-focus-trap": "^1.0.1",
"ember-named-blocks-polyfill": "^0.2.5",
"jsonlint": "^1.6.3",
"sass": "^1.69.5",
"uuid": "^14.0.0"
},
@@ -1,32 +0,0 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
module('Integration | Component | rose/code-editor', function (hooks) {
setupRenderingTest(hooks);
test('it renders', async function (assert) {
const codeValue = 'export HCP_CLIENT_ID = {place your client_id here}';
this.set('codeValue', codeValue);
await render(hbs`
<Rose::CodeEditor
@codeValue={{this.codeValue}}
as |c|
>
<c.toolbar />
<c.fieldEditor />
</Rose::CodeEditor>
`);
assert.dom('[data-test-code-editor]').isVisible();
assert.dom('[data-test-code-editor-toolbar]').isVisible();
assert.dom('[data-test-code-editor-field-editor]').isVisible();
assert.dom('[data-test-code-editor-field-editor]').includesText(codeValue);
});
});
@@ -1,74 +0,0 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { typeIn, render, waitUntil } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
module(
'Integration | Component | rose/code-editor/field-editor',
function (hooks) {
setupRenderingTest(hooks);
const editorSelector = '[data-test-code-editor-field-editor]';
test('it renders', async function (assert) {
const codeValue = `export HCP_CLIENT_ID = {place your client_id here}`;
this.set('codeValue', codeValue);
await render(hbs`
<Rose::CodeEditor::FieldEditor @value={{this.codeValue}} />
`);
assert.dom(editorSelector).isVisible();
assert.dom(editorSelector).includesText(codeValue);
const myNewCode = 'NEW CODE';
await typeIn(`${editorSelector} textarea`, myNewCode);
assert.dom(editorSelector).includesText(myNewCode);
});
test('it renders readonly', async function (assert) {
const codeValue = `export HCP_CLIENT_ID = {place your client_id here}`;
this.set('codeValue', codeValue);
this.set('options', {
readOnly: true,
});
await render(hbs`
<Rose::CodeEditor::FieldEditor @value={{this.codeValue}} @options={{this.options}} />
`);
assert.dom(editorSelector).isVisible();
assert.dom(editorSelector).includesText(codeValue);
const myNewCode = 'NEW CODE';
waitUntil(async () => {
try {
// This should fail if the textarea is readonly
await typeIn(`${editorSelector} textarea`, myNewCode);
} catch {
return true;
}
});
assert.dom(editorSelector).doesNotIncludeText(myNewCode);
});
test('it calls onInput on change', async function (assert) {
const codeValue = `export HCP_CLIENT_ID = {place your client_id here}`;
const onInput = () => {
this.set('called', true);
};
this.set('codeValue', codeValue);
this.set('onInput', onInput);
await render(hbs`
<Rose::CodeEditor::FieldEditor @value={{this.codeValue}} @onInput={{this.onInput}} />
`);
assert.dom(editorSelector).isVisible();
assert.dom(editorSelector).includesText(codeValue);
const myNewCode = 'NEW CODE';
await typeIn(`${editorSelector} textarea`, myNewCode);
assert.dom(editorSelector).includesText(myNewCode);
assert.true(this.called);
});
},
);
@@ -1,65 +0,0 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { click, render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import sinon from 'sinon';
module('Integration | Component | rose/code-editor/toolbar', function (hooks) {
setupRenderingTest(hooks);
const toolbarSelector = '[data-test-code-editor-toolbar]';
const copyButtonSelector = '[data-test-code-editor-toolbar-copy-button]';
const menuDividerSelector = '[data-test-code-editor-toolbar-menu-divider]';
hooks.beforeEach(function () {
const copyText = 'copyText';
this.set('copyText', copyText);
});
test('it renders', async function (assert) {
await render(hbs`
<Rose::CodeEditor::Toolbar @copyText={{this.copyText}} />
`);
assert.dom(toolbarSelector).isVisible();
assert.dom(copyButtonSelector).isVisible();
assert.dom(menuDividerSelector).doesNotExist();
});
test('it displays action', async function (assert) {
await render(hbs`
<Rose::CodeEditor::Toolbar @onCopy={{this.copyText}}>
<:action>
<div data-test-my-action>Hello</div>
</:action>
</Rose::CodeEditor::Toolbar>
`);
assert.dom(toolbarSelector).isVisible();
assert.dom(copyButtonSelector).isVisible();
assert.dom(menuDividerSelector).doesNotExist();
assert.dom('[data-test-my-action]').isVisible();
});
test('it calls onCopy callback', async function (assert) {
// `writeText` could fail if the document is not in focus which is common during test runs
sinon.stub(window.navigator.clipboard, 'writeText').resolves();
const onCopy = () => {
this.set('called', true);
};
this.set('onCopy', onCopy);
await render(hbs`
<Rose::CodeEditor::Toolbar @copyText={{this.copyText}} @onCopy={{this.onCopy}} />
`);
assert.dom(toolbarSelector).isVisible();
assert.dom(copyButtonSelector).isVisible();
assert.dom(menuDividerSelector).doesNotExist();
await click(copyButtonSelector);
assert.true(this.called);
sinon.restore();
});
});