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'); + }); +});