diff --git a/changelog/31326.txt b/changelog/31326.txt new file mode 100644 index 0000000000..59720005a0 --- /dev/null +++ b/changelog/31326.txt @@ -0,0 +1,3 @@ +```release-note:bug +ui: Fix selecting multiple namespaces in the namespace picker when the path contains matching nodes +``` diff --git a/ui/app/components/namespace-picker.hbs b/ui/app/components/namespace-picker.hbs index 60a153c2c8..c24ba0dea0 100644 --- a/ui/app/components/namespace-picker.hbs +++ b/ui/app/components/namespace-picker.hbs @@ -8,7 +8,9 @@ {{#each this.visibleNamespaceOptions as |option|}} diff --git a/ui/app/components/namespace-picker.ts b/ui/app/components/namespace-picker.ts index 80eeae2986..53effbb4ca 100644 --- a/ui/app/components/namespace-picker.ts +++ b/ui/app/components/namespace-picker.ts @@ -17,7 +17,6 @@ import type Store from '@ember-data/store'; import errorMessage from 'vault/utils/error-message'; interface NamespaceOption { - id: string; path: string; label: string; } @@ -56,11 +55,7 @@ export default class NamespacePicker extends Component { } get allNamespaces(): NamespaceOption[] { - return this.getOptions( - this.namespace?.accessibleNamespaces, - this.namespace?.currentNamespace, - this.namespace?.path - ); + return this.getOptions(this.namespace?.accessibleNamespaces); } get selectedNamespace(): NamespaceOption | null { @@ -75,45 +70,33 @@ export default class NamespacePicker extends Component { return options.find((option) => this.matchesPath(option, currentPath)); } - private getOptions( - accessibleNamespaces: string[], - currentNamespace: string, - path: string - ): NamespaceOption[] { - /* Each namespace option has 3 properties: { id, path, and label } - * - id: node / namespace name (displayed when the namespace picker is closed) + private getOptions(accessibleNamespaces: string[]): NamespaceOption[] { + /* Each namespace option has 2 properties: { path and label } * - path: full namespace path (used to navigate to the namespace) - * - label: text displayed inside the namespace picker dropdown (if root, then label = id, else label = path) + * - label: text displayed inside the namespace picker dropdown (if root, then path is "", else label = path) * * Example: - * | id | path | label | - * | --- | ---- | ----- | - * | 'root' | '' | 'root' | - * | 'parent' | 'parent' | 'parent' | - * | 'child' | 'parent/child' | 'parent/child' | + * | path | label | + * | ---- | ----- | + * | '' | 'root' | + * | 'parent' | 'parent' | + * | 'parent/child' | 'parent/child' | */ - const options = [ - ...(accessibleNamespaces || []).map((ns: string) => { - const parts = ns.split('/'); - return { id: parts[parts.length - 1] || '', path: ns, label: ns }; - }), - ]; + const options = (accessibleNamespaces || []).map((ns: string) => ({ path: ns, label: ns })); // Add the user's root namespace because `sys/internal/ui/namespaces` does not include it. const userRootNamespace = this.auth.authData?.userRootNamespace; if (!options?.find((o) => o.path === userRootNamespace)) { - const ns = userRootNamespace === '' ? 'root' : userRootNamespace; - options.unshift({ id: ns, path: userRootNamespace, label: ns }); + // the 'root' namespace is technically an empty string so we manually add the 'root' label. + const label = userRootNamespace === '' ? 'root' : userRootNamespace; + options.unshift({ path: userRootNamespace, label }); } // If there are no namespaces returned by the internal endpoint, add the current namespace // to the list of options. This is a fallback for when the user has access to a single namespace. if (options.length === 0) { - options.push({ - id: currentNamespace, - path: path, - label: path, - }); + // 'path' defined in the namespace service is the full namespace path + options.push({ path: this.namespace.path, label: this.namespace.path }); } return options; diff --git a/ui/tests/integration/components/namespace-picker-test.js b/ui/tests/integration/components/namespace-picker-test.js index 5e6f44a2bf..431816230c 100644 --- a/ui/tests/integration/components/namespace-picker-test.js +++ b/ui/tests/integration/components/namespace-picker-test.js @@ -48,6 +48,16 @@ module('Integration | Component | namespace-picker', function (hooks) { ); }); + test('it selects the current namespace', async function (assert) { + await render(hbs``); + assert.dom(GENERAL.button('namespace-picker')).hasText('child1', 'it just displays the namespace node'); + await click(GENERAL.button('namespace-picker')); + assert + .dom(GENERAL.button(this.nsService.path)) + .hasAttribute('aria-selected', 'true', 'the current namespace path is selected'); + assert.dom(`${GENERAL.button(this.nsService.path)} ${GENERAL.icon('check')}`).exists(); + }); + test('it filters namespace options based on search input', async function (assert) { await render(hbs``); await click(GENERAL.button('namespace-picker')); @@ -160,4 +170,19 @@ module('Integration | Component | namespace-picker', function (hooks) { assert.dom(GENERAL.button('admin')).exists(); assert.dom(GENERAL.button('admin/child1')).exists(); }); + + test('it selects the correct namespace when matching nodes exist', async function (assert) { + // stub response so that two namespaces have matching node names 'child1' + this.server.get('/sys/internal/ui/namespaces', () => { + return { data: { keys: ['parent1/', 'parent1/child1', 'anotherParent/', 'anotherParent/child1'] } }; + }); + await render(hbs``); + assert.dom(GENERAL.button('namespace-picker')).hasText('child1', 'it displays the namespace node'); + await click(GENERAL.button('namespace-picker')); + assert + .dom(GENERAL.button(this.nsService.path)) + .hasAttribute('aria-selected', 'true', 'the current namespace path is selected'); + assert.dom('[aria-selected="true"]').exists({ count: 1 }, 'only one option is selected'); + assert.dom(GENERAL.icon('check')).exists({ count: 1 }, 'only one check mark renders'); + }); });