diff --git a/addons/rose/addon/styles/hds/overrides.scss b/addons/rose/addon/styles/hds/overrides.scss
index f21fb9c7ce..535de8dde5 100644
--- a/addons/rose/addon/styles/hds/overrides.scss
+++ b/addons/rose/addon/styles/hds/overrides.scss
@@ -13,6 +13,7 @@
form {
[class*='hds-form-field--layout'],
[class*='hds-form-group--layout'],
+ [class='hds-form-key-value-inputs'],
.rose-form-actions.hds-button-set {
margin-bottom: 1.5rem;
}
diff --git a/addons/rose/package.json b/addons/rose/package.json
index 6314579664..a6740bf9dd 100644
--- a/addons/rose/package.json
+++ b/addons/rose/package.json
@@ -30,7 +30,7 @@
},
"dependencies": {
"@babel/core": "^7.26.10",
- "@hashicorp/design-system-components": "^4.20.2",
+ "@hashicorp/design-system-components": "^4.24.0",
"@hashicorp/design-system-tokens": "^2.3.0",
"@hashicorp/flight-icons": "^3.10.0",
"@nullvoxpopuli/ember-composable-helpers": "^5.2.10",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 50299daa2f..e255e1c7c3 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -535,8 +535,8 @@ importers:
specifier: ^7.26.10
version: 7.27.1
'@hashicorp/design-system-components':
- specifier: ^4.20.2
- version: 4.20.2(@babel/core@7.27.1)(@ember/test-helpers@5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2))(@glimmer/component@2.0.0)(@glint/template@1.5.2)(ember-basic-dropdown@8.6.1(@babel/core@7.27.1)(@ember/string@3.1.1)(@ember/test-helpers@5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2))(@glimmer/component@2.0.0)(@glint/template@1.5.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8)))(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8))
+ specifier: ^4.24.0
+ version: 4.24.0(@babel/core@7.27.1)(@ember/string@4.0.1)(@ember/test-helpers@5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2))(@glimmer/component@2.0.0)(@glint/template@1.5.2)(ember-basic-dropdown@8.6.1(@babel/core@7.27.1)(@ember/string@4.0.1)(@ember/test-helpers@5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2))(@glimmer/component@2.0.0)(@glint/template@1.5.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8)))(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8))
'@hashicorp/design-system-tokens':
specifier: ^2.3.0
version: 2.3.0
@@ -2044,10 +2044,6 @@ packages:
'@glint/template':
optional: true
- '@ember/string@3.1.1':
- resolution: {integrity: sha512-UbXJ+k3QOrYN4SRPHgXCqYIJ+yWWUg1+vr0H4DhdQPTy8LJfyqwZ2tc5uqpSSnEXE+/1KopHBE5J8GDagAg5cg==}
- engines: {node: 12.* || 14.* || >= 16}
-
'@ember/string@4.0.1':
resolution: {integrity: sha512-VWeng8BSWrIsdPfffOQt/bKwNKJL7+37gPFh/6iZZ9bke+S83kKqkS30poo4bTGfRcMnvAE0ie7txom+iDu81Q==}
@@ -2065,8 +2061,8 @@ packages:
resolution: {integrity: sha512-gcJuHiXgnrzaU8NyU+2bMbtS6PNOr5v5B8OXBqaBvTCsMpXLvKo8OBOQFCoUN0rPX2J6VaFqrbi/371sMvzZug==}
engines: {node: 12.* || 14.* || >= 16}
- '@embroider/macros@1.16.13':
- resolution: {integrity: sha512-2oGZh0m1byBYQFWEa8b2cvHJB2LzaF3DdMCLCqcRAccABMROt1G3sultnNCT30NhfdGWMEsJOT3Jm4nFxXmTRw==}
+ '@embroider/macros@1.18.0':
+ resolution: {integrity: sha512-KanP80XxNK4bmQ1HKTcUjy/cdCt9n7knPMLK1vzHdOFymACHo+GbhgUjXjYdOCuBTv+ZwcjL2P2XDmBcYS9r8g==}
engines: {node: 12.* || 14.* || >= 16}
peerDependencies:
'@glint/template': ^1.0.0
@@ -2074,8 +2070,8 @@ packages:
'@glint/template':
optional: true
- '@embroider/macros@1.18.0':
- resolution: {integrity: sha512-KanP80XxNK4bmQ1HKTcUjy/cdCt9n7knPMLK1vzHdOFymACHo+GbhgUjXjYdOCuBTv+ZwcjL2P2XDmBcYS9r8g==}
+ '@embroider/macros@1.18.2':
+ resolution: {integrity: sha512-mkgk0yjcYgujZQv9IGLD3yPb4a+d6EDKm22GK6TUyBCF8veTeg6HBXwHfu6K2DCnG0LEwnb2MJ0WCFGmTiatPw==}
engines: {node: 12.* || 14.* || >= 16}
peerDependencies:
'@glint/template': ^1.0.0
@@ -2091,6 +2087,10 @@ packages:
resolution: {integrity: sha512-5J5ipUMCAinQS38WW7wedruq5Z4VnHvNo+ZgOduw0PtI9w0CQWx7/HE+98PBDW8jclikeF+aHwF317vc1hwuzg==}
engines: {node: 12.* || 14.* || >= 16}
+ '@embroider/shared-internals@3.0.1':
+ resolution: {integrity: sha512-d7RQwDwqqHo7YvjE9t1rtIrCCYtbSoO0uRq2ikVhRh4hGS5OojZNu2ZtS0Wqrg+V72CRtMFr/hibTvHNsRM2Lg==}
+ engines: {node: 12.* || 14.* || >= 16}
+
'@embroider/test-setup@4.0.0':
resolution: {integrity: sha512-1S3Ebk0CEh3XDqD93AWSwQZBCk+oGv03gtkaGgdgyXGIR7jrVyDgEnEuslN/hJ0cuU8TqhiXrzHMw7bJwIGhWw==}
engines: {node: 12.* || 14.* || >= 16}
@@ -2106,21 +2106,8 @@ packages:
'@embroider/webpack':
optional: true
- '@embroider/util@1.13.2':
- resolution: {integrity: sha512-6/0sK4dtFK7Ld+t5Ovn9EilBVySoysMRdDAf/jGteOO7jdLKNgHnONg0w1T7ZZaMFUQfwJdRrk3u0dM+Idhiew==}
- engines: {node: 12.* || 14.* || >= 16}
- peerDependencies:
- '@glint/environment-ember-loose': ^1.0.0
- '@glint/template': ^1.0.0
- ember-source: '*'
- peerDependenciesMeta:
- '@glint/environment-ember-loose':
- optional: true
- '@glint/template':
- optional: true
-
- '@embroider/util@1.13.3':
- resolution: {integrity: sha512-fb9S137zZqSI1IeWpGKVJ+WZHsRiIrD9D2A4aVwVH0dZeBKDg6lMaMN2MiXJ/ldUAG3DUFxnClnpiG5m2g3JFA==}
+ '@embroider/util@1.13.4':
+ resolution: {integrity: sha512-TqA0SNQarSJUdYGv+39MBCHkiuxhr2u0iKJP/JnDmQkCiVhvuFWy3P3n5sI26fVrVwG3DJLfxE2XVnB37udFOA==}
engines: {node: 12.* || 14.* || >= 16}
peerDependencies:
'@glint/environment-ember-loose': ^1.0.0
@@ -2462,18 +2449,25 @@ packages:
'@handlebars/parser@2.0.0':
resolution: {integrity: sha512-EP9uEDZv/L5Qh9IWuMUGJRfwhXJ4h1dqKTT4/3+tY0eu7sPis7xh23j61SYUnNF4vqCQvvUXpDo9Bh/+q1zASA==}
- '@hashicorp/design-system-components@4.20.2':
- resolution: {integrity: sha512-0FDaDlvaQQVVXoSoWsExmW1TUgmuJNoCz11JuwaOwin59Vl4ttVLsNvY8DviGJlh6VhV1yYlGJa7X2xhQG+ESQ==}
+ '@hashicorp/design-system-components@4.24.0':
+ resolution: {integrity: sha512-gvy8/kEt0i27l8sV9UXfmwVo8ao99X6+x6oV3jF4SeK10/5oWjdEhv35IyDcArLyEVio5pTw+r0CHjuomCyhaA==}
engines: {node: '>= 18'}
peerDependencies:
+ '@ember/string': ^3.1.1 || ^4.0.0
ember-engines: '>= 0.11.0'
+ ember-intl: ^7.3.0
peerDependenciesMeta:
ember-engines:
optional: true
+ ember-intl:
+ optional: true
'@hashicorp/design-system-tokens@2.3.0':
resolution: {integrity: sha512-T2XhcgUeiGkNqvPu73yittDghEccUpIZc7Fh/g4PG7KEvJwbXItFWTRWoHSGR8T6r6LpOP5E6CC4hSVwGRugRg==}
+ '@hashicorp/design-system-tokens@3.0.0':
+ resolution: {integrity: sha512-ODOtuwDvOdAHQNnhVgqi43enCVWjXTnweYrxHDi3QkVUY1ysLkGrfAySnr4hVwd3UHmzT5HkQQbjN5zgr8l94w==}
+
'@hashicorp/ember-asciinema-player@https://codeload.github.com/hashicorp/ember-asciinema-player/tar.gz/e047a096039cff70234c232efe75dcad74c6358a':
resolution: {tarball: https://codeload.github.com/hashicorp/ember-asciinema-player/tar.gz/e047a096039cff70234c232efe75dcad74c6358a}
version: 0.0.0
@@ -2484,8 +2478,8 @@ packages:
'@hashicorp/flight-icons@3.10.0':
resolution: {integrity: sha512-wtufYZ5Ntihmy+vbR0dM+Q7X56xPX/dtpcfVs4nCRgvYZZic5ayqE8tefs2FGtxauH6zuzzVk48s5S6psv9g+g==}
- '@hashicorp/flight-icons@3.11.1':
- resolution: {integrity: sha512-FQOHB2qCzHoG3dm6zidS39D4U0ida/7Sge5EG+KqcebH5jsbJQiMyB/qMc3YQBo5vGBe8XUa+rVW8v4JNpzk1Q==}
+ '@hashicorp/flight-icons@3.13.0':
+ resolution: {integrity: sha512-eUPjix112KlkS+LVzqHsvxxAaPNpjG1PmQYzTtUdmLNJkG1PsN1Mcaz3gU7HGNpUSVvOqlNbyCSxgeITBJ+7cw==}
'@humanwhocodes/config-array@0.13.0':
resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==}
@@ -2667,6 +2661,9 @@ packages:
'@nullvoxpopuli/ember-composable-helpers@5.2.10':
resolution: {integrity: sha512-60lzfYZ3Gn5f5NX7KKh01vOUeMYCgNFMxVwRBtxcyotCmBob3eoW6JL1hO3zStv79k/n/vijdr1o/G1N3+l8Fg==}
+ '@nullvoxpopuli/ember-composable-helpers@5.3.0':
+ resolution: {integrity: sha512-pjuYVAxJJETaFFmDME9sPH++kSNcTJjxHqHUSJOwoYvxSRBHIysJbCFD/CHQjJtbI5D4pVouYU80ugmyGrZoFA==}
+
'@nuxt/opencollective@0.4.1':
resolution: {integrity: sha512-GXD3wy50qYbxCJ652bDrDzgMr3NFEkIS374+IgFQKkCvk9yiYcLvX2XDYr7UyQxf4wK0e+yqDYRubZ0DtOxnmQ==}
engines: {node: ^14.18.0 || >=16.10.0, npm: '>=5.10.0'}
@@ -10575,12 +10572,6 @@ snapshots:
- '@babel/core'
- supports-color
- '@ember/string@3.1.1':
- dependencies:
- ember-cli-babel: 7.26.11
- transitivePeerDependencies:
- - supports-color
-
'@ember/string@4.0.1': {}
'@ember/test-helpers@5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2)':
@@ -10622,11 +10613,11 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@embroider/macros@1.16.13(@glint/template@1.5.2)':
+ '@embroider/macros@1.18.0(@glint/template@1.5.2)':
dependencies:
- '@embroider/shared-internals': 2.9.0
+ '@embroider/shared-internals': 3.0.0
assert-never: 1.4.0
- babel-import-util: 2.1.1
+ babel-import-util: 3.0.1
ember-cli-babel: 7.26.11
find-up: 5.0.0
lodash: 4.17.21
@@ -10637,9 +10628,9 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@embroider/macros@1.18.0(@glint/template@1.5.2)':
+ '@embroider/macros@1.18.2(@glint/template@1.5.2)':
dependencies:
- '@embroider/shared-internals': 3.0.0
+ '@embroider/shared-internals': 3.0.1
assert-never: 1.4.0
babel-import-util: 3.0.1
ember-cli-babel: 7.26.11
@@ -10687,25 +10678,32 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@embroider/test-setup@4.0.0':
+ '@embroider/shared-internals@3.0.1':
dependencies:
+ babel-import-util: 3.0.1
+ debug: 4.4.1
+ ember-rfc176-data: 0.3.18
+ fs-extra: 9.1.0
+ is-subdir: 1.2.0
+ js-string-escape: 1.0.1
lodash: 4.17.21
- resolve: 1.22.10
-
- '@embroider/util@1.13.2(@glint/template@1.5.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8))':
- dependencies:
- '@embroider/macros': 1.18.0(@glint/template@1.5.2)
- broccoli-funnel: 3.0.8
- ember-cli-babel: 7.26.11
- ember-source: 5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8)
- optionalDependencies:
- '@glint/template': 1.5.2
+ minimatch: 3.1.2
+ pkg-entry-points: 1.1.1
+ resolve-package-path: 4.0.3
+ resolve.exports: 2.0.3
+ semver: 7.7.2
+ typescript-memoize: 1.1.1
transitivePeerDependencies:
- supports-color
- '@embroider/util@1.13.3(@glint/template@1.5.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8))':
+ '@embroider/test-setup@4.0.0':
+ dependencies:
+ lodash: 4.17.21
+ resolve: 1.22.10
+
+ '@embroider/util@1.13.4(@glint/template@1.5.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8))':
dependencies:
- '@embroider/macros': 1.16.13(@glint/template@1.5.2)
+ '@embroider/macros': 1.18.2(@glint/template@1.5.2)
broccoli-funnel: 3.0.8
ember-cli-babel: 7.26.11
ember-source: 5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8)
@@ -11094,7 +11092,7 @@ snapshots:
'@handlebars/parser@2.0.0': {}
- '@hashicorp/design-system-components@4.20.2(@babel/core@7.27.1)(@ember/test-helpers@5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2))(@glimmer/component@2.0.0)(@glint/template@1.5.2)(ember-basic-dropdown@8.6.1(@babel/core@7.27.1)(@ember/string@3.1.1)(@ember/test-helpers@5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2))(@glimmer/component@2.0.0)(@glint/template@1.5.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8)))(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8))':
+ '@hashicorp/design-system-components@4.24.0(@babel/core@7.27.1)(@ember/string@4.0.1)(@ember/test-helpers@5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2))(@glimmer/component@2.0.0)(@glint/template@1.5.2)(ember-basic-dropdown@8.6.1(@babel/core@7.27.1)(@ember/string@4.0.1)(@ember/test-helpers@5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2))(@glimmer/component@2.0.0)(@glint/template@1.5.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8)))(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8))':
dependencies:
'@codemirror/commands': 6.8.1
'@codemirror/lang-go': 6.0.1
@@ -11109,16 +11107,16 @@ snapshots:
'@codemirror/state': 6.5.2
'@codemirror/view': 6.36.8
'@ember/render-modifiers': 2.1.0(@babel/core@7.27.1)(@glint/template@1.5.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8))
- '@ember/string': 3.1.1
+ '@ember/string': 4.0.1
'@ember/test-waiters': 3.1.0
'@embroider/addon-shim': 1.10.0
- '@embroider/macros': 1.18.0(@glint/template@1.5.2)
- '@embroider/util': 1.13.2(@glint/template@1.5.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8))
+ '@embroider/macros': 1.18.2(@glint/template@1.5.2)
+ '@embroider/util': 1.13.4(@glint/template@1.5.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8))
'@floating-ui/dom': 1.7.0
- '@hashicorp/design-system-tokens': 2.3.0
- '@hashicorp/flight-icons': 3.11.1
+ '@hashicorp/design-system-tokens': 3.0.0
+ '@hashicorp/flight-icons': 3.13.0
'@lezer/highlight': 1.2.1
- '@nullvoxpopuli/ember-composable-helpers': 5.2.10(@babel/core@7.27.1)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8))
+ '@nullvoxpopuli/ember-composable-helpers': 5.3.0(@babel/core@7.27.1)
clipboard-polyfill: 4.1.1
codemirror-lang-hcl: 0.0.0-beta.2
decorator-transforms: 2.3.0(@babel/core@7.27.1)
@@ -11129,15 +11127,16 @@ snapshots:
ember-focus-trap: 1.1.1(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8))
ember-get-config: 2.1.1(@glint/template@1.5.2)
ember-modifier: 4.2.2(@babel/core@7.27.1)
- ember-power-select: 8.7.1(@babel/core@7.27.1)(@ember/test-helpers@5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2))(@glimmer/component@2.0.0)(@glint/template@1.5.2)(ember-basic-dropdown@8.6.1(@babel/core@7.27.1)(@ember/string@3.1.1)(@ember/test-helpers@5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2))(@glimmer/component@2.0.0)(@glint/template@1.5.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8)))(ember-concurrency@4.0.4(@babel/core@7.27.1)(@glint/template@1.5.2))(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8))
+ ember-power-select: 8.7.1(@babel/core@7.27.1)(@ember/test-helpers@5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2))(@glimmer/component@2.0.0)(@glint/template@1.5.2)(ember-basic-dropdown@8.6.1(@babel/core@7.27.1)(@ember/string@4.0.1)(@ember/test-helpers@5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2))(@glimmer/component@2.0.0)(@glint/template@1.5.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8)))(ember-concurrency@4.0.4(@babel/core@7.27.1)(@glint/template@1.5.2))(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8))
ember-stargate: 0.6.0(@babel/core@7.27.1)(@glint/template@1.5.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8))
- ember-style-modifier: 4.4.0(@babel/core@7.27.1)(@ember/string@3.1.1)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8))
+ ember-style-modifier: 4.4.0(@babel/core@7.27.1)(@ember/string@4.0.1)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8))
ember-truth-helpers: 4.0.3(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8))
luxon: 3.6.1
prismjs: 1.30.0
sass: 1.88.0
tabbable: 6.2.0
tippy.js: 6.3.7
+ tracked-built-ins: 4.0.0(@babel/core@7.27.1)
transitivePeerDependencies:
- '@babel/core'
- '@ember/test-helpers'
@@ -11150,6 +11149,8 @@ snapshots:
'@hashicorp/design-system-tokens@2.3.0': {}
+ '@hashicorp/design-system-tokens@3.0.0': {}
+
'@hashicorp/ember-asciinema-player@https://codeload.github.com/hashicorp/ember-asciinema-player/tar.gz/e047a096039cff70234c232efe75dcad74c6358a(@glint/template@1.5.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8))(webpack@5.99.8)':
dependencies:
asciinema-player: 3.4.0
@@ -11164,7 +11165,7 @@ snapshots:
'@hashicorp/flight-icons@3.10.0': {}
- '@hashicorp/flight-icons@3.11.1': {}
+ '@hashicorp/flight-icons@3.13.0': {}
'@humanwhocodes/config-array@0.13.0':
dependencies:
@@ -11371,6 +11372,14 @@ snapshots:
- ember-source
- supports-color
+ '@nullvoxpopuli/ember-composable-helpers@5.3.0(@babel/core@7.27.1)':
+ dependencies:
+ '@embroider/addon-shim': 1.10.0
+ decorator-transforms: 2.3.0(@babel/core@7.27.1)
+ transitivePeerDependencies:
+ - '@babel/core'
+ - supports-color
+
'@nuxt/opencollective@0.4.1':
dependencies:
consola: 3.4.2
@@ -13676,18 +13685,18 @@ snapshots:
- supports-color
- webpack
- ember-basic-dropdown@8.6.1(@babel/core@7.27.1)(@ember/string@3.1.1)(@ember/test-helpers@5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2))(@glimmer/component@2.0.0)(@glint/template@1.5.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8)):
+ ember-basic-dropdown@8.6.1(@babel/core@7.27.1)(@ember/string@4.0.1)(@ember/test-helpers@5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2))(@glimmer/component@2.0.0)(@glint/template@1.5.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8)):
dependencies:
'@ember/test-helpers': 5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2)
'@embroider/addon-shim': 1.10.0
- '@embroider/macros': 1.18.0(@glint/template@1.5.2)
- '@embroider/util': 1.13.3(@glint/template@1.5.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8))
+ '@embroider/macros': 1.18.2(@glint/template@1.5.2)
+ '@embroider/util': 1.13.4(@glint/template@1.5.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8))
'@glimmer/component': 2.0.0
decorator-transforms: 2.3.0(@babel/core@7.27.1)
ember-element-helper: 0.8.8
ember-lifeline: 7.0.0(@ember/test-helpers@5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2))
ember-modifier: 4.2.2(@babel/core@7.27.1)
- ember-style-modifier: 4.4.0(@babel/core@7.27.1)(@ember/string@3.1.1)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8))
+ ember-style-modifier: 4.4.0(@babel/core@7.27.1)(@ember/string@4.0.1)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8))
ember-truth-helpers: 4.0.3(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8))
transitivePeerDependencies:
- '@babel/core'
@@ -14585,15 +14594,15 @@ snapshots:
transitivePeerDependencies:
- supports-color
- ember-power-select@8.7.1(@babel/core@7.27.1)(@ember/test-helpers@5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2))(@glimmer/component@2.0.0)(@glint/template@1.5.2)(ember-basic-dropdown@8.6.1(@babel/core@7.27.1)(@ember/string@3.1.1)(@ember/test-helpers@5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2))(@glimmer/component@2.0.0)(@glint/template@1.5.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8)))(ember-concurrency@4.0.4(@babel/core@7.27.1)(@glint/template@1.5.2))(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8)):
+ ember-power-select@8.7.1(@babel/core@7.27.1)(@ember/test-helpers@5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2))(@glimmer/component@2.0.0)(@glint/template@1.5.2)(ember-basic-dropdown@8.6.1(@babel/core@7.27.1)(@ember/string@4.0.1)(@ember/test-helpers@5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2))(@glimmer/component@2.0.0)(@glint/template@1.5.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8)))(ember-concurrency@4.0.4(@babel/core@7.27.1)(@glint/template@1.5.2))(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8)):
dependencies:
'@ember/test-helpers': 5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2)
'@embroider/addon-shim': 1.10.0
- '@embroider/util': 1.13.2(@glint/template@1.5.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8))
+ '@embroider/util': 1.13.4(@glint/template@1.5.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8))
'@glimmer/component': 2.0.0
decorator-transforms: 2.3.0(@babel/core@7.27.1)
ember-assign-helper: 0.5.1
- ember-basic-dropdown: 8.6.1(@babel/core@7.27.1)(@ember/string@3.1.1)(@ember/test-helpers@5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2))(@glimmer/component@2.0.0)(@glint/template@1.5.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8))
+ ember-basic-dropdown: 8.6.1(@babel/core@7.27.1)(@ember/string@4.0.1)(@ember/test-helpers@5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2))(@glimmer/component@2.0.0)(@glint/template@1.5.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8))
ember-concurrency: 4.0.4(@babel/core@7.27.1)(@glint/template@1.5.2)
ember-lifeline: 7.0.0(@ember/test-helpers@5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2))
ember-modifier: 4.2.2(@babel/core@7.27.1)
@@ -14650,7 +14659,7 @@ snapshots:
ember-resources@7.0.4(@glimmer/component@2.0.0)(@glint/template@1.5.2):
dependencies:
'@embroider/addon-shim': 1.10.0
- '@embroider/macros': 1.18.0(@glint/template@1.5.2)
+ '@embroider/macros': 1.18.2(@glint/template@1.5.2)
'@glint/template': 1.5.2
optionalDependencies:
'@glimmer/component': 2.0.0
@@ -14804,9 +14813,9 @@ snapshots:
- ember-source
- supports-color
- ember-style-modifier@4.4.0(@babel/core@7.27.1)(@ember/string@3.1.1)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8)):
+ ember-style-modifier@4.4.0(@babel/core@7.27.1)(@ember/string@4.0.1)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8)):
dependencies:
- '@ember/string': 3.1.1
+ '@ember/string': 4.0.1
'@embroider/addon-shim': 1.10.0
csstype: 3.1.3
decorator-transforms: 2.3.0(@babel/core@7.27.1)
diff --git a/ui/admin/app/components/form/auth-method/oidc/index.hbs b/ui/admin/app/components/form/auth-method/oidc/index.hbs
index 7f61daa811..9641b88ae2 100644
--- a/ui/admin/app/components/form/auth-method/oidc/index.hbs
+++ b/ui/admin/app/components/form/auth-method/oidc/index.hbs
@@ -247,45 +247,21 @@
{{! Account Claim Maps }}
-
- <:fieldset as |F|>
-
- {{t 'form.account_claim_maps.label'}}
-
-
- {{t 'form.account_claim_maps.help'}}
-
-
- {{#if @model.errors.account_claim_maps}}
-
- {{#each @model.errors.account_claim_maps as |error|}}
- {{error.message}}
- {{/each}}
-
- {{/if}}
-
- <:field as |F|>
-
- <:key as |K|>
-
-
- <:value as |V|>
-
-
-
-
-
+ @isOptional={{true}}
+ @keyFieldType='text'
+ @valueFieldType='select'
+ @valueFieldOptions={{this.toClaims}}
+ @keyLabel={{t 'form.from_claim.label'}}
+ @valueLabel={{t 'form.to_claim.label'}}
+ />
{{! Certificates }}
-
- <:fieldset as |F|>
-
- {{t 'resources.credential-library.form.critical_options.label'}}
-
-
- {{t 'resources.credential-library.form.critical_options.help'}}
-
-
- {{#if @model.errors.critical_options}}
-
- {{#each @model.errors.critical_options as |error|}}
- {{error.message}}
- {{/each}}
-
- {{/if}}
-
-
- <:field as |F|>
-
- <:key as |K|>
-
-
- <:value as |K|>
-
-
-
-
-
-
+ @isOptional={{true}}
+ @keyFieldType='text'
+ @valueFieldType='text'
+ />
+ <:header as |H|>
+ {{#if @legend}}
+ {{@legend}}
+ {{/if}}
+ {{#if @helperText}}
+ {{@helperText}}
+ {{/if}}
+
+ <:row as |R|>
+
+ {{or @keyLabel (t 'form.key.label')}}
+ {{#if (eq @keyFieldType 'select')}}
+
+
+ {{#each @keyFieldOptions as |selectOption|}}
+
+ {{/each}}
+
+ {{else if (eq @keyFieldType 'textarea')}}
+
+ {{else}}
+ {{! Default to text input }}
+
+ {{/if}}
+
+
+ {{#unless this.hideValueField}}
+
+ {{or @valueLabel (t 'form.value.label')}}
+ {{#if (eq @valueFieldType 'select')}}
+
+
+ {{#each @valueFieldOptions as |selectOption|}}
+
+ {{/each}}
+
+ {{else if (eq @valueFieldType 'textarea')}}
+
+ {{else}}
+ {{! Default to text input }}
+
+ {{/if}}
+
+ {{/unless}}
+
+
+
+ <:footer as |F|>
+
+ {{#if @errors}}
+
+ {{#each @errors as |error|}}
+ {{error.message}}
+ {{/each}}
+
+ {{/if}}
+
+
+
\ No newline at end of file
diff --git a/ui/admin/app/components/form/field/key-value/index.js b/ui/admin/app/components/form/field/key-value/index.js
new file mode 100644
index 0000000000..975be028c4
--- /dev/null
+++ b/ui/admin/app/components/form/field/key-value/index.js
@@ -0,0 +1,95 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { action } from '@ember/object';
+
+export default class FormFieldKeyValueComponent extends Component {
+ // =properties
+
+ @tracked data = this.args.data?.length
+ ? [...this.args.data]
+ : [this.createEmptyRow()];
+
+ /**
+ * Determine if the value field should be shown
+ * Defaults to false (show the value field by default)
+ * @type {boolean}
+ */
+ get hideValueField() {
+ return this.args.hideValueField ?? false;
+ }
+
+ createEmptyRow() {
+ const row = { key: '' };
+ if (!this.hideValueField) {
+ row.value = '';
+ }
+ return row;
+ }
+
+ // =actions
+
+ /**
+ * Updates the key value for a specific row and triggers change notification
+ * @param {Object} rowData
+ * @param {Event} event
+ */
+ @action
+ updateKey(rowData, { target: { value } }) {
+ this.updateRowProperty(rowData, 'key', value);
+ }
+
+ /**
+ * Updates the value field for a specific row and triggers change notification
+ * @param {Object} rowData
+ * @param {Event} event
+ */
+ @action
+ updateValue(rowData, { target: { value } }) {
+ this.updateRowProperty(rowData, 'value', value);
+ }
+
+ @action
+ addNewRow() {
+ this.data = [...this.data, this.createEmptyRow()];
+ this.notifyChange();
+ }
+
+ @action
+ removeRow(rowData) {
+ this.data = this.data.filter((item) => item !== rowData);
+
+ // Ensure at least one row exists
+ if (this.data.length === 0) {
+ this.data = [this.createEmptyRow()];
+ }
+ this.notifyChange();
+ }
+
+ /**
+ * Helper method to update a property on a row
+ * @param {Object} rowData - The row object to update
+ * @param {string} property - The property name to update
+ * @param {string} value - The new value
+ */
+ updateRowProperty(rowData, property, value) {
+ rowData[property] = value;
+ this.data = [...this.data];
+ this.notifyChange();
+ }
+
+ /**
+ * Notifies parent component of data changes with filtered results
+ */
+ notifyChange() {
+ if (this.args.onChange) {
+ // Filter out entries with empty/whitespace keys
+ const filteredData = this.data.filter((item) => item.key?.trim());
+ this.args.onChange(filteredData);
+ }
+ }
+}
diff --git a/ui/admin/tests/acceptance/auth-methods/create-test.js b/ui/admin/tests/acceptance/auth-methods/create-test.js
index 9eae7412b4..85918ce3ed 100644
--- a/ui/admin/tests/acceptance/auth-methods/create-test.js
+++ b/ui/admin/tests/acceptance/auth-methods/create-test.js
@@ -129,11 +129,11 @@ module('Acceptance | auth-methods | create', function (hooks) {
selectors.FIELD_ACCOUNT_CLAIM_MAPS_FROM_CLAIM,
selectors.FIELD_ACCOUNT_CLAIM_MAPS_FROM_CLAIM_VALUE,
);
+
await select(
selectors.FIELD_ACCOUNT_CLAIM_MAPS_TO_CLAIM,
selectors.FIELD_ACCOUNT_CLAIM_MAPS_TO_CLAIM_VALUE,
);
- await click(selectors.FIELD_ACCOUNT_CLAIM_MAPS_ADD_BTN);
await fillIn(selectors.FIELD_IDP_CERTS, selectors.FIELD_IDP_CERTS_VALUE);
await click(selectors.FIELD_IDP_CERTS_ADD_BTN);
diff --git a/ui/admin/tests/acceptance/auth-methods/selectors.js b/ui/admin/tests/acceptance/auth-methods/selectors.js
index 362ca5a403..c25146b1bb 100644
--- a/ui/admin/tests/acceptance/auth-methods/selectors.js
+++ b/ui/admin/tests/acceptance/auth-methods/selectors.js
@@ -26,15 +26,12 @@ export const FIELD_CLAIMS_SCOPES_ADD_BTN = '[name=claims_scopes] button';
export const FIELD_CLAIMS_SCOPES_DELETE_BTN =
'[name=claims_scopes] tbody td:last-child button[aria-label=Remove]';
export const FIELD_ACCOUNT_CLAIM_MAPS_FROM_CLAIM =
- '[name=account_claim_maps] tbody td:nth-of-type(1) input';
+ '[name=account_claim_maps][data-test-key-input]';
export const FIELD_ACCOUNT_CLAIM_MAPS_FROM_CLAIM_VALUE = 'from_claim';
export const FIELD_ACCOUNT_CLAIM_MAPS_TO_CLAIM =
- '[name=account_claim_maps] tbody td:nth-of-type(2) select';
+ '[name=account_claim_maps][data-test-value-input]';
export const FIELD_ACCOUNT_CLAIM_MAPS_TO_CLAIM_VALUE = 'email';
-export const FIELD_ACCOUNT_CLAIM_MAPS_ADD_BTN =
- '[name=account_claim_maps] button';
-export const FIELD_ACCOUNT_CLAIM_MAPS_DELETE_BTN =
- '[name=account_claim_maps] tbody td:last-child button[aria-label=Remove]';
+export const FIELD_ACCOUNT_CLAIM_MAPS_DELETE_BTN = '[data-test-delete-button]';
export const FIELD_IDP_CERTS = '[name=idp_ca_certs] textarea';
export const FIELD_IDP_CERTS_VALUE = 'IDP certificates';
export const FIELD_IDP_CERTS_ADD_BTN = '[name=idp_ca_certs] button';
diff --git a/ui/admin/tests/acceptance/auth-methods/update-test.js b/ui/admin/tests/acceptance/auth-methods/update-test.js
index be390fc043..e1b3faef1c 100644
--- a/ui/admin/tests/acceptance/auth-methods/update-test.js
+++ b/ui/admin/tests/acceptance/auth-methods/update-test.js
@@ -187,8 +187,6 @@ module('Acceptance | auth-methods | update', function (hooks) {
selectors.FIELD_ACCOUNT_CLAIM_MAPS_TO_CLAIM_VALUE,
);
- await click(selectors.FIELD_ACCOUNT_CLAIM_MAPS_ADD_BTN);
-
// Remove all certificates
const certificatesList = findAll(selectors.FIELD_IDP_CERTS_DELETE_BTN);
diff --git a/ui/admin/tests/acceptance/credential-library/create-test.js b/ui/admin/tests/acceptance/credential-library/create-test.js
index 3851eec3fb..9f56f70198 100644
--- a/ui/admin/tests/acceptance/credential-library/create-test.js
+++ b/ui/admin/tests/acceptance/credential-library/create-test.js
@@ -231,7 +231,6 @@ module('Acceptance | credential-libraries | create', function (hooks) {
await fillIn(selectors.FIELD_KEY_ID, selectors.FIELD_KEY_ID_VALUE);
await fillIn(selectors.FIELD_CRIT_OPTS_KEY, 'co_key');
await fillIn(selectors.FIELD_CRIT_OPTS_VALUE, 'co_value');
- await click(selectors.FIELD_CRIT_OPTS_BTN);
await fillIn(selectors.FIELD_EXT_KEY, 'ext_key');
await fillIn(selectors.FIELD_EXT_VALUE, 'ext_value');
await click(selectors.FIELD_EXT_BTN);
diff --git a/ui/admin/tests/acceptance/credential-library/selectors.js b/ui/admin/tests/acceptance/credential-library/selectors.js
index d66b0891d3..a76403b6d0 100644
--- a/ui/admin/tests/acceptance/credential-library/selectors.js
+++ b/ui/admin/tests/acceptance/credential-library/selectors.js
@@ -41,10 +41,9 @@ export const TYPE_VAULT_LDAP = '[value="vault-ldap"]';
export const TYPE_VAULT_GENERIC = '[value="vault-generic"]';
export const FIELD_CRIT_OPTS_KEY =
- '[name="critical_options"] tbody td:nth-of-type(1) input';
+ '[name="critical_options"][data-test-key-input]';
export const FIELD_CRIT_OPTS_VALUE =
- '[name="critical_options"] tbody td:nth-of-type(2) input';
-export const FIELD_CRIT_OPTS_BTN = '[name="critical_options"] button';
+ '[name="critical_options"][data-test-value-input]';
export const FIELD_EXT_KEY =
'[name="extensions"] tbody td:nth-of-type(1) input';
diff --git a/ui/admin/tests/acceptance/credential-library/update-test.js b/ui/admin/tests/acceptance/credential-library/update-test.js
index 58bf2d9ddc..4bf65d2e1f 100644
--- a/ui/admin/tests/acceptance/credential-library/update-test.js
+++ b/ui/admin/tests/acceptance/credential-library/update-test.js
@@ -402,10 +402,10 @@ module('Acceptance | credential-libraries | update', function (hooks) {
await fillIn(selectors.FIELD_KEY_ID, selectors.FIELD_KEY_ID_VALUE);
await fillIn(selectors.FIELD_CRIT_OPTS_KEY, 'co_key');
await fillIn(selectors.FIELD_CRIT_OPTS_VALUE, 'co_value');
- await click(selectors.FIELD_CRIT_OPTS_BTN);
await fillIn(selectors.FIELD_EXT_KEY, 'ext_key');
await fillIn(selectors.FIELD_EXT_VALUE, 'ext_value');
await click(selectors.FIELD_EXT_BTN);
+
await click(commonSelectors.SAVE_BTN);
const credentialLibrary = this.server.schema.credentialLibraries.findBy({
diff --git a/ui/admin/tests/integration/components/form/field/key-value-test.js b/ui/admin/tests/integration/components/form/field/key-value-test.js
new file mode 100644
index 0000000000..8f3452997c
--- /dev/null
+++ b/ui/admin/tests/integration/components/form/field/key-value-test.js
@@ -0,0 +1,386 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render, click, fillIn, findAll } from '@ember/test-helpers';
+import { hbs } from 'ember-cli-htmlbars';
+import { setupIntl } from 'ember-intl/test-support';
+
+module('Integration | Component | form/field/key-value', function (hooks) {
+ setupRenderingTest(hooks);
+ setupIntl(hooks, 'en-us');
+
+ // Helper functions
+ const getKeyInputs = () => findAll('[data-test-key-input]');
+ const getValueInputs = () => findAll('[data-test-value-input]');
+ const getDeleteButtons = () => findAll('[data-test-delete-button]');
+
+ test('it renders with default empty row', async function (assert) {
+ await render(hbs``);
+
+ assert.dom('[data-test-key-input]').exists();
+ assert.dom('[data-test-value-input]').exists();
+ assert.dom('[data-test-add-button]').exists();
+ assert.dom('[data-test-delete-button]').exists();
+ });
+
+ test('it renders with provided data', async function (assert) {
+ this.set('data', [
+ { key: 'env', value: 'production' },
+ { key: 'region', value: 'us-east-1' },
+ ]);
+
+ await render(hbs``);
+
+ const keyInputs = getKeyInputs();
+ const valueInputs = getValueInputs();
+
+ assert.strictEqual(keyInputs.length, 2);
+ assert.strictEqual(valueInputs.length, 2);
+ assert.dom(keyInputs[0]).hasValue('env');
+ assert.dom(keyInputs[1]).hasValue('region');
+ assert.dom(valueInputs[0]).hasValue('production');
+ assert.dom(valueInputs[1]).hasValue('us-east-1');
+ });
+
+ test('it renders all provided arguments correctly', async function (assert) {
+ await render(hbs`
+
+ `);
+
+ // Check name attribute
+ assert.dom('[data-test-key-input]').hasAttribute('name', 'test-name');
+ assert.dom('[data-test-value-input]').hasAttribute('name', 'test-name');
+
+ // Check disabled state
+ assert.dom('[data-test-key-input]').hasAttribute('disabled');
+ assert.dom('[data-test-value-input]').hasAttribute('disabled');
+
+ // Check that labels are rendered (they should be present in the DOM text)
+ assert.dom(this.element).includesText('Custom Key Label');
+ assert.dom(this.element).includesText('Custom Value Label');
+
+ // Check legend and helper text using the correct selectors
+ assert.dom('[data-test-legend]').includesText('Test Legend');
+ assert.dom('[data-test-helper-text]').hasText('Test helper text');
+ });
+
+ test('it adds new rows', async function (assert) {
+ let updatedData = null;
+ this.set('onChange', (data) => {
+ updatedData = data;
+ });
+
+ await render(hbs`
+
+ `);
+
+ assert.strictEqual(getKeyInputs().length, 1);
+
+ await click('[data-test-add-button]');
+
+ assert.strictEqual(getKeyInputs().length, 2);
+ assert.strictEqual(getValueInputs().length, 2);
+ // Empty keys are filtered out when notifying parent
+ assert.strictEqual(updatedData.length, 0);
+ assert.deepEqual(updatedData, []);
+
+ await fillIn(getKeyInputs()[1], 'test-key');
+
+ assert.strictEqual(updatedData.length, 1);
+ assert.deepEqual(updatedData[0], { key: 'test-key', value: '' });
+ });
+
+ test('it removes rows', async function (assert) {
+ let updatedData = null;
+ this.set('data', [
+ { key: 'key1', value: 'value1' },
+ { key: 'key2', value: 'value2' },
+ ]);
+ this.set('onChange', (data) => {
+ updatedData = data;
+ });
+
+ await render(hbs`
+
+ `);
+
+ assert.strictEqual(getKeyInputs().length, 2);
+
+ await click(getDeleteButtons()[0]);
+
+ assert.strictEqual(getKeyInputs().length, 1);
+ assert.strictEqual(updatedData.length, 1);
+ assert.dom('[data-test-key-input]').hasValue('key2');
+ assert.dom('[data-test-value-input]').hasValue('value2');
+ });
+
+ test('it removes one row and replaces with new data', async function (assert) {
+ let updatedData = null;
+ this.set('data', [{ key: 'key1', value: 'value1' }]);
+ this.set('onChange', (data) => {
+ updatedData = data;
+ });
+
+ await render(hbs`
+
+ `);
+
+ assert.strictEqual(getKeyInputs().length, 1);
+
+ // Remove row (component ensures at least one empty row remains)
+ await click(getDeleteButtons()[0]);
+
+ // Fill in the new row
+ await fillIn('[data-test-key-input]', 'newKey');
+ await fillIn('[data-test-value-input]', 'newValue');
+
+ assert.strictEqual(getKeyInputs().length, 1);
+ assert.strictEqual(updatedData.length, 1);
+ assert.deepEqual(updatedData[0], { key: 'newKey', value: 'newValue' });
+ });
+
+ // Field type tests
+ test('it handles text input field types (default)', async function (assert) {
+ await render(hbs``);
+
+ assert.dom('[data-test-key-input]').hasTagName('input');
+ assert.dom('[data-test-key-input]').hasAttribute('type', 'text');
+ assert.dom('[data-test-key-input]').hasAttribute('name', 'test-form');
+ assert.dom('[data-test-value-input]').hasTagName('input');
+ assert.dom('[data-test-value-input]').hasAttribute('type', 'text');
+ assert.dom('[data-test-value-input]').hasAttribute('name', 'test-form');
+ });
+
+ test('it handles select field types', async function (assert) {
+ this.set('selectOptions', ['subject', 'email', 'name']);
+
+ await render(hbs`
+
+ `);
+
+ assert.dom('[data-test-key-input]').hasTagName('input');
+ assert.dom('[data-test-value-input]').hasTagName('select');
+
+ const options = findAll('[data-test-value-input] option');
+ assert.strictEqual(options.length, 4);
+ assert.dom(options[0]).hasText('Choose an option');
+ assert.dom(options[1]).hasText('subject');
+ assert.dom(options[2]).hasText('email');
+ assert.dom(options[3]).hasText('name');
+ });
+
+ test('it handles textarea field types', async function (assert) {
+ await render(hbs`
+
+ `);
+
+ assert.dom('[data-test-key-input]').hasTagName('textarea');
+ assert.dom('[data-test-key-input]').hasAttribute('name', 'test-form');
+ assert.dom('[data-test-value-input]').hasTagName('textarea');
+ assert.dom('[data-test-value-input]').hasAttribute('name', 'test-form');
+ });
+
+ test('it handles mixed field types', async function (assert) {
+ this.set('keyOptions', ['key1', 'key2']);
+
+ await render(hbs`
+
+ `);
+
+ assert.dom('[data-test-key-input]').hasTagName('select');
+ assert.dom('[data-test-key-input]').hasAttribute('name', 'test-form');
+ assert.dom('[data-test-value-input]').hasTagName('textarea');
+ assert.dom('[data-test-value-input]').hasAttribute('name', 'test-form');
+ });
+
+ // Hide value field tests
+ test('it hides value field when `hideValueField` is true', async function (assert) {
+ await render(hbs``);
+
+ assert.dom('[data-test-key-input]').exists();
+ assert.dom('[data-test-value-input]').doesNotExist();
+ assert.dom('[data-test-add-button]').exists();
+ assert.dom('[data-test-delete-button]').exists();
+ });
+
+ test('it shows value field when `hideValueField` is false', async function (assert) {
+ await render(hbs``);
+
+ assert.dom('[data-test-key-input]').exists();
+ assert.dom('[data-test-value-input]').exists();
+ });
+
+ test('it shows value field by default when `hideValueField` is not provided', async function (assert) {
+ await render(hbs``);
+
+ assert.dom('[data-test-key-input]').exists();
+ assert.dom('[data-test-value-input]').exists();
+ });
+
+ // Error handling tests
+ test('it displays error messages in footer', async function (assert) {
+ this.set('errors', [
+ { message: 'Key is required' },
+ { message: 'Value must be unique' },
+ ]);
+
+ await render(hbs`
+
+ `);
+
+ assert.dom('[data-test-error-message]').exists({ count: 2 });
+ assert.dom('[data-test-error-message]').hasText('Key is required');
+ assert
+ .dom('[data-test-error-message]:nth-child(2)')
+ .hasText('Value must be unique');
+ });
+
+ test('it does not show error messages when no errors are provided', async function (assert) {
+ await render(hbs``);
+
+ assert.dom('[data-test-error-message]').doesNotExist();
+ });
+
+ // Update tests
+ test('it updates textarea key values correctly', async function (assert) {
+ let updatedData = null;
+ this.set('onChange', (data) => {
+ updatedData = data;
+ });
+
+ await render(hbs`
+
+ `);
+
+ await fillIn('[data-test-key-input]', 'textarea');
+
+ assert.strictEqual(updatedData.length, 1);
+ assert.strictEqual(updatedData[0].key, 'textarea');
+ });
+
+ test('it updates textarea value fields correctly', async function (assert) {
+ let updatedData = null;
+ this.set('onChange', (data) => {
+ updatedData = data;
+ });
+
+ await render(hbs`
+
+ `);
+
+ await fillIn('[data-test-key-input]', 'test-key');
+ await fillIn('[data-test-value-input]', 'test-value');
+
+ assert.strictEqual(updatedData.length, 1);
+ assert.strictEqual(updatedData[0].key, 'test-key');
+ assert.strictEqual(updatedData[0].value, 'test-value');
+ });
+
+ test('it updates select field values', async function (assert) {
+ let updatedData = null;
+ this.set('onChange', (data) => {
+ updatedData = data;
+ });
+ this.set('selectOptions', ['subject', 'email', 'name']);
+
+ await render(hbs`
+
+ `);
+
+ await fillIn('[data-test-value-input]', 'email');
+
+ // Empty key is filtered out, so no data is sent to parent
+ assert.strictEqual(updatedData.length, 0);
+
+ await fillIn('[data-test-key-input]', 'claim');
+ await fillIn('[data-test-value-input]', 'email');
+
+ assert.strictEqual(updatedData.length, 1);
+ assert.strictEqual(updatedData[0].key, 'claim');
+ assert.strictEqual(updatedData[0].value, 'email');
+ });
+
+ test('it updates key values', async function (assert) {
+ let updatedData = null;
+ this.set('onChange', (data) => {
+ updatedData = data;
+ });
+
+ await render(hbs``);
+ await fillIn('[data-test-key-input]', 'test-key');
+
+ assert.strictEqual(updatedData[0].key, 'test-key');
+ assert.strictEqual(updatedData[0].value, '');
+ });
+
+ test('it filters out rows with empty keys when updating values', async function (assert) {
+ let updatedData = null;
+ this.set('onChange', (data) => {
+ updatedData = data;
+ });
+
+ await render(hbs``);
+ await fillIn('[data-test-value-input]', 'test-value');
+
+ // Empty key is filtered out, so no data is sent to parent
+ assert.strictEqual(updatedData.length, 0);
+ });
+
+ test('it updates value fields when key is present', async function (assert) {
+ let updatedData = null;
+ this.set('onChange', (data) => {
+ updatedData = data;
+ });
+
+ await render(hbs``);
+ await fillIn('[data-test-key-input]', 'test-key');
+ await fillIn('[data-test-value-input]', 'test-value');
+
+ assert.strictEqual(updatedData.length, 1);
+ assert.strictEqual(updatedData[0].key, 'test-key');
+ assert.strictEqual(updatedData[0].value, 'test-value');
+ });
+});