mirror of https://github.com/rancher/dashboard.git
Merge pull request #13357 from rak-phillip/task/12776-table-actions
Replace ActionMenu usage for Table actions and Global Settings
This commit is contained in:
commit
07289b9f81
|
|
@ -0,0 +1,15 @@
|
||||||
|
import ComponentPo from '@/cypress/e2e/po/components/component.po';
|
||||||
|
|
||||||
|
export default class ActionMenuPo extends ComponentPo {
|
||||||
|
constructor(arg:any) {
|
||||||
|
super(arg || cy.get('[dropdown-menu-collection]'));
|
||||||
|
}
|
||||||
|
|
||||||
|
clickMenuItem(index: number) {
|
||||||
|
return this.self().find('[dropdown-menu-item]').eq(index).click();
|
||||||
|
}
|
||||||
|
|
||||||
|
getMenuItem(name: string) {
|
||||||
|
return this.self().get('[dropdown-menu-item]').contains(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,7 +11,7 @@ export default class ListRowPo extends ComponentPo {
|
||||||
* the action button could be in a different column
|
* the action button could be in a different column
|
||||||
*/
|
*/
|
||||||
actionBtn() {
|
actionBtn() {
|
||||||
return this.self().find('.btn.actions');
|
return this.self().find('[data-testid*="action-button"]');
|
||||||
}
|
}
|
||||||
|
|
||||||
get(selector: string, options?: any) {
|
get(selector: string, options?: any) {
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,9 @@ export default class Shell extends ComponentPo {
|
||||||
|
|
||||||
openTerminal() {
|
openTerminal() {
|
||||||
// get and click on the first row's action menu button
|
// get and click on the first row's action menu button
|
||||||
cy.get(`button[data-testid="sortable-table-0-action-button"]`).first().click();
|
cy.get(`[data-testid="sortable-table-0-action-button"`).first().click();
|
||||||
// get and click on the action menu's first option (execute shell)
|
// get and click on the action menu's first option (execute shell)
|
||||||
cy.get(`li[data-testid="action-menu-0-item"]`).click();
|
cy.get(`[dropdown-menu-item]`).contains('Execute Shell').click();
|
||||||
this.self().get('.window.show-grid .text-success').should('contain', 'Connected');
|
this.self().get('.window.show-grid .text-success').should('contain', 'Connected');
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import ComponentPo, { GetOptions } from '@/cypress/e2e/po/components/component.po';
|
import ComponentPo, { GetOptions } from '@/cypress/e2e/po/components/component.po';
|
||||||
import ActionMenuPo from '@/cypress/e2e/po/components/action-menu.po';
|
import ActionMenuPo from '@/cypress/e2e/po/components/action-menu-shell.po';
|
||||||
import CheckboxInputPo from '@/cypress/e2e/po/components/checkbox-input.po';
|
import CheckboxInputPo from '@/cypress/e2e/po/components/checkbox-input.po';
|
||||||
import ListRowPo from '@/cypress/e2e/po/components/list-row.po';
|
import ListRowPo from '@/cypress/e2e/po/components/list-row.po';
|
||||||
import PromptRemove from '@/cypress/e2e/po/prompts/promptRemove.po';
|
import PromptRemove from '@/cypress/e2e/po/prompts/promptRemove.po';
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ export default class ChartRepositoriesListPo extends BaseResourceList {
|
||||||
}
|
}
|
||||||
|
|
||||||
closeActionMenu() {
|
closeActionMenu() {
|
||||||
cy.get('body').click(0, 0); // Click outside of the action menu
|
cy.get('body').type('{esc}');
|
||||||
}
|
}
|
||||||
|
|
||||||
openBulkActionDropdown() {
|
openBulkActionDropdown() {
|
||||||
|
|
@ -25,8 +25,11 @@ export default class ChartRepositoriesListPo extends BaseResourceList {
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshRepo(repoName: string) {
|
refreshRepo(repoName: string) {
|
||||||
return this.resourceTable().sortableTable().rowActionMenuOpen(repoName).getMenuItem('Refresh')
|
return this.resourceTable()
|
||||||
.click();
|
.sortableTable()
|
||||||
|
.rowActionMenuOpen(repoName)
|
||||||
|
.getMenuItem('Refresh')
|
||||||
|
.click({ force: true }); // We shouldn't require force, but other methods don't work
|
||||||
}
|
}
|
||||||
|
|
||||||
state(repoName: string) {
|
state(repoName: string) {
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ export class SettingsPagePo extends RootClusterPage {
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
actionButtonByLabel(label: string) {
|
actionButtonByLabel(label: string) {
|
||||||
return this.advancedSettingRow(label).find('.action > button');
|
return this.advancedSettingRow(label).find('[data-testid*="action-button"]');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ export default class PageActionsPo extends ComponentPo {
|
||||||
* @returns {Cypress.Chainable}
|
* @returns {Cypress.Chainable}
|
||||||
*/
|
*/
|
||||||
static open(): Cypress.Chainable {
|
static open(): Cypress.Chainable {
|
||||||
return cy.getId('page-actions-menu').should('be.visible').click();
|
return cy.getId('page-actions-menu-action-button').should('be.visible').click();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@ describe('Cluster Management Helm Repositories', { testIsolation: 'off', tags: [
|
||||||
ChartRepositoriesPagePo.navTo();
|
ChartRepositoriesPagePo.navTo();
|
||||||
repositoriesPage.waitForPage();
|
repositoriesPage.waitForPage();
|
||||||
cy.intercept('PUT', `/v1/catalog.cattle.io.clusterrepos/${ this.repoName }`).as('refreshRepo');
|
cy.intercept('PUT', `/v1/catalog.cattle.io.clusterrepos/${ this.repoName }`).as('refreshRepo');
|
||||||
repositoriesPage.list().actionMenu(this.repoName).getMenuItem('Refresh').click();
|
repositoriesPage.list().actionMenu(this.repoName).getMenuItem('Refresh').click({ force: true });
|
||||||
cy.wait('@refreshRepo').its('response.statusCode').should('eq', 200);
|
cy.wait('@refreshRepo').its('response.statusCode').should('eq', 200);
|
||||||
|
|
||||||
// check list details
|
// check list details
|
||||||
|
|
@ -278,7 +278,10 @@ describe('Cluster Management Helm Repositories', { testIsolation: 'off', tags: [
|
||||||
repositoriesPage.list().actionMenu(this.repoName).getMenuItem('Refresh').should('be.visible');
|
repositoriesPage.list().actionMenu(this.repoName).getMenuItem('Refresh').should('be.visible');
|
||||||
// close action menu
|
// close action menu
|
||||||
repositoriesPage.list().closeActionMenu();
|
repositoriesPage.list().closeActionMenu();
|
||||||
|
|
||||||
// disable repo
|
// disable repo
|
||||||
|
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||||
|
cy.wait(1500);
|
||||||
repositoriesPage.list().actionMenu(this.repoName).getMenuItem('Disable').click();
|
repositoriesPage.list().actionMenu(this.repoName).getMenuItem('Disable').click();
|
||||||
repositoriesPage.list().details(this.repoName, 1).contains('Disabled', { timeout: 10000 }).scrollIntoView()
|
repositoriesPage.list().details(this.repoName, 1).contains('Disabled', { timeout: 10000 }).scrollIntoView()
|
||||||
.should('be.visible');
|
.should('be.visible');
|
||||||
|
|
@ -287,7 +290,10 @@ describe('Cluster Management Helm Repositories', { testIsolation: 'off', tags: [
|
||||||
repositoriesPage.list().actionMenu(this.repoName).getMenuItem('Refresh').should('not.exist');
|
repositoriesPage.list().actionMenu(this.repoName).getMenuItem('Refresh').should('not.exist');
|
||||||
// close action menu
|
// close action menu
|
||||||
repositoriesPage.list().closeActionMenu();
|
repositoriesPage.list().closeActionMenu();
|
||||||
|
|
||||||
// enable repo
|
// enable repo
|
||||||
|
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||||
|
cy.wait(1500);
|
||||||
repositoriesPage.list().actionMenu(this.repoName).getMenuItem('Enable').click();
|
repositoriesPage.list().actionMenu(this.repoName).getMenuItem('Enable').click();
|
||||||
repositoriesPage.list().details(this.repoName, 1).contains('Active', LONG_TIMEOUT_OPT).scrollIntoView()
|
repositoriesPage.list().details(this.repoName, 1).contains('Active', LONG_TIMEOUT_OPT).scrollIntoView()
|
||||||
.should('be.visible');
|
.should('be.visible');
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ module.exports = {
|
||||||
transform: {
|
transform: {
|
||||||
'^.+\\.js$': '<rootDir>/node_modules/babel-jest', // process js with `babel-jest`
|
'^.+\\.js$': '<rootDir>/node_modules/babel-jest', // process js with `babel-jest`
|
||||||
'.*\\.(vue)$': '<rootDir>/node_modules/@vue/vue3-jest', // process `*.vue` files with `vue-jest`
|
'.*\\.(vue)$': '<rootDir>/node_modules/@vue/vue3-jest', // process `*.vue` files with `vue-jest`
|
||||||
|
'^.+\\.vue$': './vue3JestRegisterTs.js', // point to a different transformer than vue-jest and call registerTs before exporting vue-jest
|
||||||
'^.+\\.tsx?$': 'ts-jest', // process `*.ts` files with `ts-jest`
|
'^.+\\.tsx?$': 'ts-jest', // process `*.ts` files with `ts-jest`
|
||||||
'^.+\\.svg$': '<rootDir>/svgTransform.js' // to mock `*.svg` files
|
'^.+\\.svg$': '<rootDir>/svgTransform.js' // to mock `*.svg` files
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -65,25 +65,30 @@ defineExpose({ focus });
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.role-link {
|
|
||||||
&:focus, &.focused {
|
|
||||||
outline: var(--outline-width) solid var(--border);
|
|
||||||
box-shadow: 0 0 0 var(--outline-width) var(--outline);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
button {
|
||||||
|
&.role-link {
|
||||||
|
&:focus, &.focused {
|
||||||
|
@include focus-outline;
|
||||||
|
outline-offset: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--accent-btn);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.role-ghost {
|
&.role-ghost {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
|
||||||
&:focus, &.focused {
|
&:focus, &.focused {
|
||||||
outline: 2px solid var(--primary-keyboard-focus);
|
@include focus-outline;
|
||||||
outline-offset: 0;
|
outline-offset: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
outline: 2px solid var(--primary-keyboard-focus);
|
@include focus-outline;
|
||||||
outline-offset: 0;
|
outline-offset: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@ defineProps<{
|
||||||
ariaLabel?: string
|
ariaLabel?: string
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:open']);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isMenuOpen,
|
isMenuOpen,
|
||||||
showMenu,
|
showMenu,
|
||||||
|
|
@ -35,8 +37,7 @@ const {
|
||||||
setFocus,
|
setFocus,
|
||||||
provideDropdownContext,
|
provideDropdownContext,
|
||||||
registerDropdownCollection,
|
registerDropdownCollection,
|
||||||
handleKeydown,
|
} = useDropdownContext(emit);
|
||||||
} = useDropdownContext();
|
|
||||||
|
|
||||||
provideDropdownContext();
|
provideDropdownContext();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,12 +53,12 @@ const findNewIndex = (shouldAdvance: boolean, activeIndex: number, itemsArr: Ele
|
||||||
return newIndex;
|
return newIndex;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = (e: MouseEvent) => {
|
||||||
if (props.disabled) {
|
if (props.disabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
emits('click');
|
emits('click', e);
|
||||||
close();
|
close();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -85,6 +85,9 @@ const handleActivate = (e: KeyboardEvent) => {
|
||||||
@keydown.enter.space="handleActivate"
|
@keydown.enter.space="handleActivate"
|
||||||
@keydown.up.down.stop="handleKeydown"
|
@keydown.up.down.stop="handleKeydown"
|
||||||
>
|
>
|
||||||
|
<slot name="before">
|
||||||
|
<!--Empty slot content-->
|
||||||
|
</slot>
|
||||||
<slot name="default">
|
<slot name="default">
|
||||||
<!--Empty slot content-->
|
<!--Empty slot content-->
|
||||||
</slot>
|
</slot>
|
||||||
|
|
@ -93,6 +96,9 @@ const handleActivate = (e: KeyboardEvent) => {
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
[dropdown-menu-item] {
|
[dropdown-menu-item] {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
padding: 9px 8px;
|
padding: 9px 8px;
|
||||||
margin: 0 9px;
|
margin: 0 9px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
RcDropdown,
|
||||||
|
RcDropdownItem,
|
||||||
|
RcDropdownSeparator,
|
||||||
|
RcDropdownTrigger
|
||||||
|
} from '@components/RcDropdown';
|
||||||
|
import { RcDropdownMenuComponentProps, DropdownOption } from './types';
|
||||||
|
import IconOrSvg from '@shell/components/IconOrSvg';
|
||||||
|
|
||||||
|
// eslint-disable-next-line vue/no-setup-props-destructure
|
||||||
|
const { buttonRole = 'primary', buttonSize = '' } = defineProps<RcDropdownMenuComponentProps>();
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:open', 'select']);
|
||||||
|
|
||||||
|
const hasOptions = (options: DropdownOption[]) => {
|
||||||
|
return options.length !== undefined ? options.length : Object.keys(options).length > 0;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<rc-dropdown
|
||||||
|
:aria-label="dropdownAriaLabel"
|
||||||
|
@update:open="(e: boolean) => emit('update:open', e)"
|
||||||
|
>
|
||||||
|
<rc-dropdown-trigger
|
||||||
|
:[buttonRole]="true"
|
||||||
|
:[buttonSize]="true"
|
||||||
|
:data-testid="dataTestid"
|
||||||
|
:aria-label="buttonAriaLabel"
|
||||||
|
>
|
||||||
|
<i class="icon icon-actions" />
|
||||||
|
</rc-dropdown-trigger>
|
||||||
|
<template #dropdownCollection>
|
||||||
|
<template
|
||||||
|
v-for="(a) in options"
|
||||||
|
:key="a.label"
|
||||||
|
>
|
||||||
|
<rc-dropdown-item
|
||||||
|
v-if="!a.divider"
|
||||||
|
@click="(e: MouseEvent) => emit('select', e, a)"
|
||||||
|
>
|
||||||
|
<template #before>
|
||||||
|
<IconOrSvg
|
||||||
|
v-if="a.icon || a.svg"
|
||||||
|
:icon="a.icon"
|
||||||
|
:src="a.svg"
|
||||||
|
class="icon"
|
||||||
|
color="header"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
{{ a.label }}
|
||||||
|
</rc-dropdown-item>
|
||||||
|
<rc-dropdown-separator
|
||||||
|
v-else
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<rc-dropdown-item
|
||||||
|
v-if="!hasOptions(options)"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
No actions available
|
||||||
|
</rc-dropdown-item>
|
||||||
|
</template>
|
||||||
|
</rc-dropdown>
|
||||||
|
</template>
|
||||||
|
|
@ -2,3 +2,4 @@ export { default as RcDropdown } from './RcDropdown.vue';
|
||||||
export { default as RcDropdownItem } from './RcDropdownItem.vue';
|
export { default as RcDropdownItem } from './RcDropdownItem.vue';
|
||||||
export { default as RcDropdownSeparator } from './RcDropdownSeparator.vue';
|
export { default as RcDropdownSeparator } from './RcDropdownSeparator.vue';
|
||||||
export { default as RcDropdownTrigger } from './RcDropdownTrigger.vue';
|
export { default as RcDropdownTrigger } from './RcDropdownTrigger.vue';
|
||||||
|
export { default as RcDropdownMenu } from './RcDropdownMenu.vue';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { Ref, ref } from 'vue';
|
import { Ref, ref } from 'vue';
|
||||||
import type { RcButtonType } from '@components/RcButton';
|
import type { RcButtonType } from '@components/RcButton';
|
||||||
|
import { ButtonRoleProps, ButtonSizeProps } from '@components/RcButton/types';
|
||||||
|
|
||||||
export type DropdownContext = {
|
export type DropdownContext = {
|
||||||
handleKeydown: () => void;
|
handleKeydown: () => void;
|
||||||
|
|
@ -20,3 +21,29 @@ export const defaultContext: DropdownContext = {
|
||||||
isMenuOpen: ref(false),
|
isMenuOpen: ref(false),
|
||||||
close: () => null,
|
close: () => null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DropdownOption = {
|
||||||
|
action?: string;
|
||||||
|
divider?: boolean;
|
||||||
|
enabled: boolean;
|
||||||
|
icon?: string;
|
||||||
|
svg?: string;
|
||||||
|
label?: string;
|
||||||
|
total: number;
|
||||||
|
allEnabled: boolean;
|
||||||
|
anyEnabled: boolean;
|
||||||
|
available: number;
|
||||||
|
bulkable?: boolean;
|
||||||
|
bulkAction?: string;
|
||||||
|
altAction?: string;
|
||||||
|
weight?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RcDropdownMenuComponentProps = {
|
||||||
|
options: DropdownOption[];
|
||||||
|
buttonRole?: keyof ButtonRoleProps;
|
||||||
|
buttonSize?: keyof ButtonSizeProps;
|
||||||
|
buttonAriaLabel?: string;
|
||||||
|
dropdownAriaLabel?: string;
|
||||||
|
dataTestid?: string;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
import { ref, provide, nextTick } from 'vue';
|
import { ref, provide, nextTick, defineEmits } from 'vue';
|
||||||
import { useDropdownCollection } from './useDropdownCollection';
|
import { useDropdownCollection } from './useDropdownCollection';
|
||||||
import { RcButtonType } from '@components/RcButton';
|
import { RcButtonType } from '@components/RcButton';
|
||||||
|
|
||||||
|
const rcDropdownEmits = defineEmits(['update:open']);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Composable that provides the context for a dropdown menu. Includes methods
|
* Composable that provides the context for a dropdown menu. Includes methods
|
||||||
* and state for managing the dropdown's visibility, focus, and keyboard
|
* and state for managing the dropdown's visibility, focus, and keyboard
|
||||||
|
|
@ -11,7 +13,7 @@ import { RcButtonType } from '@components/RcButton';
|
||||||
* @returns Dropdown context methods and state. Used for programmatic
|
* @returns Dropdown context methods and state. Used for programmatic
|
||||||
* interactions and setting focus.
|
* interactions and setting focus.
|
||||||
*/
|
*/
|
||||||
export const useDropdownContext = () => {
|
export const useDropdownContext = (emit: typeof rcDropdownEmits) => {
|
||||||
const {
|
const {
|
||||||
dropdownItems,
|
dropdownItems,
|
||||||
firstDropdownItem,
|
firstDropdownItem,
|
||||||
|
|
@ -30,6 +32,7 @@ export const useDropdownContext = () => {
|
||||||
didKeydown.value = false;
|
didKeydown.value = false;
|
||||||
}
|
}
|
||||||
isMenuOpen.value = show;
|
isMenuOpen.value = show;
|
||||||
|
emit('update:open', show);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ export {
|
||||||
RcDropdown,
|
RcDropdown,
|
||||||
RcDropdownItem,
|
RcDropdownItem,
|
||||||
RcDropdownSeparator,
|
RcDropdownSeparator,
|
||||||
RcDropdownTrigger
|
RcDropdownTrigger,
|
||||||
|
RcDropdownMenu
|
||||||
} from './components/RcDropdown';
|
} from './components/RcDropdown';
|
||||||
export * from './components/Form';
|
export * from './components/Form';
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useStore } from 'vuex';
|
||||||
|
|
||||||
|
import { isAlternate } from '@shell/utils/platform';
|
||||||
|
import { RcDropdownMenu } from '@components/RcDropdown';
|
||||||
|
import { ButtonRoleProps, ButtonSizeProps } from '@components/RcButton/types';
|
||||||
|
|
||||||
|
const store = useStore();
|
||||||
|
|
||||||
|
const options = computed(() => store.getters['action-menu/optionsArray']);
|
||||||
|
|
||||||
|
type RcDropdownMenuComponentProps = {
|
||||||
|
buttonRole?: keyof ButtonRoleProps;
|
||||||
|
buttonSize?: keyof ButtonSizeProps;
|
||||||
|
buttonAriaLabel?: string;
|
||||||
|
dropdownAriaLabel?: string;
|
||||||
|
dataTestid?: string;
|
||||||
|
resource: Object;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps <RcDropdownMenuComponentProps>();
|
||||||
|
|
||||||
|
const openChanged = (event: boolean) => {
|
||||||
|
if (event) {
|
||||||
|
store.dispatch('action-menu/setResource', props.resource);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const execute = (action: any, event: MouseEvent, args?: any) => {
|
||||||
|
if (action.disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// this will come from extensions...
|
||||||
|
if (action.invoke) {
|
||||||
|
const fn = action.invoke;
|
||||||
|
|
||||||
|
if (fn && action.enabled) {
|
||||||
|
const resources = store.getters['action-menu/resources'];
|
||||||
|
const opts = {
|
||||||
|
event,
|
||||||
|
action,
|
||||||
|
isAlt: isAlternate(event)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (resources.length === 1) {
|
||||||
|
fn.apply(this, [opts, resources]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If the state of this component is controlled
|
||||||
|
// by Vuex, mutate the store when an action is clicked.
|
||||||
|
const opts = { alt: isAlternate(event) };
|
||||||
|
|
||||||
|
store.dispatch('action-menu/execute', {
|
||||||
|
action, args, opts
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<rc-dropdown-menu
|
||||||
|
:button-role="buttonRole || 'link'"
|
||||||
|
:button-size="buttonSize || 'small'"
|
||||||
|
:button-aria-label="buttonAriaLabel"
|
||||||
|
:dropdown-aria-label="dropdownAriaLabel"
|
||||||
|
:options="options"
|
||||||
|
:data-testid="dataTestid"
|
||||||
|
@update:open="openChanged"
|
||||||
|
@select="(e: MouseEvent, option: object) => execute(option, e)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
@ -23,6 +23,7 @@ import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
import { getParent } from '@shell/utils/dom';
|
import { getParent } from '@shell/utils/dom';
|
||||||
import { FORMATTERS } from '@shell/components/SortableTable/sortable-config';
|
import { FORMATTERS } from '@shell/components/SortableTable/sortable-config';
|
||||||
import ButtonMultiAction from '@shell/components/ButtonMultiAction.vue';
|
import ButtonMultiAction from '@shell/components/ButtonMultiAction.vue';
|
||||||
|
import ActionMenu from '@shell/components/ActionMenuShell.vue';
|
||||||
|
|
||||||
// Uncomment for table performance debugging
|
// Uncomment for table performance debugging
|
||||||
// import tableDebug from './debug';
|
// import tableDebug from './debug';
|
||||||
|
|
@ -58,6 +59,7 @@ export default {
|
||||||
ActionDropdown,
|
ActionDropdown,
|
||||||
LabeledSelect,
|
LabeledSelect,
|
||||||
ButtonMultiAction,
|
ButtonMultiAction,
|
||||||
|
ActionMenu,
|
||||||
},
|
},
|
||||||
mixins: [
|
mixins: [
|
||||||
filtering,
|
filtering,
|
||||||
|
|
@ -1468,23 +1470,16 @@ export default {
|
||||||
</template>
|
</template>
|
||||||
<td
|
<td
|
||||||
v-if="rowActions"
|
v-if="rowActions"
|
||||||
align="middle"
|
|
||||||
>
|
>
|
||||||
<slot
|
<slot
|
||||||
name="row-actions"
|
name="row-actions"
|
||||||
:row="row.row"
|
:row="row.row"
|
||||||
|
:index="i"
|
||||||
>
|
>
|
||||||
<ButtonMultiAction
|
<ActionMenu
|
||||||
:id="`actionButton+${i}+${(row.row && row.row.name) ? row.row.name : ''}`"
|
:resource="row.row"
|
||||||
:ref="`actionButton${i}`"
|
|
||||||
aria-haspopup="true"
|
|
||||||
aria-expanded="false"
|
|
||||||
:aria-label="t('sortableTable.tableActionsLabel', { resource: row?.row?.id || '' })"
|
|
||||||
:data-testid="componentTestid + '-' + i + '-action-button'"
|
:data-testid="componentTestid + '-' + i + '-action-button'"
|
||||||
:borderless="true"
|
:button-aria-label="t('sortableTable.tableActionsLabel', { resource: row?.row?.id || '' })"
|
||||||
@click="handleActionButtonClick(i, $event)"
|
|
||||||
@keyup.enter="handleActionButtonClick(i, $event)"
|
|
||||||
@keyup.space="handleActionButtonClick(i, $event)"
|
|
||||||
/>
|
/>
|
||||||
</slot>
|
</slot>
|
||||||
</td>
|
</td>
|
||||||
|
|
@ -1782,7 +1777,6 @@ export default {
|
||||||
min-width: 400px;
|
min-width: 400px;
|
||||||
border-radius: 5px 5px 0 0;
|
border-radius: 5px 5px 0 0;
|
||||||
outline: 1px solid var(--border);
|
outline: 1px solid var(--border);
|
||||||
overflow: hidden;
|
|
||||||
background: var(--sortable-table-bg);
|
background: var(--sortable-table-bg);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,51 +1,22 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { useStore } from 'vuex';
|
import { useStore } from 'vuex';
|
||||||
import {
|
import { RcDropdownMenu } from '@components/RcDropdown';
|
||||||
RcDropdown,
|
|
||||||
RcDropdownItem,
|
|
||||||
RcDropdownSeparator,
|
|
||||||
RcDropdownTrigger
|
|
||||||
} from '@components/RcDropdown';
|
|
||||||
|
|
||||||
const isPageActionMenuOpen = ref(false);
|
|
||||||
|
|
||||||
const showPageActionsMenu = (show: boolean) => {
|
|
||||||
isPageActionMenuOpen.value = show;
|
|
||||||
};
|
|
||||||
|
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
const pageActions = computed(() => store.getters.pageActions);
|
const pageActions = computed(() => store.getters.pageActions);
|
||||||
const pageAction = (action: string) => {
|
const pageAction = (_event: Event, action: string) => {
|
||||||
store.dispatch('handlePageAction', action);
|
store.dispatch('handlePageAction', action);
|
||||||
showPageActionsMenu(false);
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<rc-dropdown :aria-label="t('nav.actionMenu.label')">
|
<rc-dropdown-menu
|
||||||
<rc-dropdown-trigger
|
:options="pageActions"
|
||||||
tertiary
|
:button-aria-label="t('nav.actionMenu.label')"
|
||||||
data-testid="page-actions-menu"
|
:dropdown-aria-label="t('nav.actionMenu.button.label')"
|
||||||
:aria-label="t('nav.actionMenu.button.label')"
|
data-testid="page-actions-menu-action-button"
|
||||||
>
|
button-role="tertiary"
|
||||||
<i class="icon icon-actions" />
|
@select="pageAction"
|
||||||
</rc-dropdown-trigger>
|
/>
|
||||||
<template #dropdownCollection>
|
|
||||||
<template
|
|
||||||
v-for="(a) in pageActions"
|
|
||||||
:key="a.label"
|
|
||||||
>
|
|
||||||
<rc-dropdown-item
|
|
||||||
v-if="!a.separator"
|
|
||||||
@click="pageAction(a)"
|
|
||||||
>
|
|
||||||
{{ a.labelKey ? t(a.labelKey) : a.label }}
|
|
||||||
</rc-dropdown-item>
|
|
||||||
<rc-dropdown-separator
|
|
||||||
v-else
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
</rc-dropdown>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -85,8 +85,8 @@ export default {
|
||||||
|
|
||||||
if (canSetAsHome) {
|
if (canSetAsHome) {
|
||||||
pageActions.push({
|
pageActions.push({
|
||||||
labelKey: 'nav.header.setLoginPage',
|
label: this.t('nav.header.setLoginPage'),
|
||||||
action: SET_LOGIN_ACTION
|
action: SET_LOGIN_ACTION
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,12 @@ import { ALLOWED_SETTINGS } from '@shell/config/settings';
|
||||||
import { Banner } from '@components/Banner';
|
import { Banner } from '@components/Banner';
|
||||||
import Loading from '@shell/components/Loading';
|
import Loading from '@shell/components/Loading';
|
||||||
import { VIEW_IN_API } from '@shell/store/prefs';
|
import { VIEW_IN_API } from '@shell/store/prefs';
|
||||||
|
import ActionMenu from '@shell/components/ActionMenuShell.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { Banner, Loading },
|
components: {
|
||||||
|
Banner, Loading, ActionMenu
|
||||||
|
},
|
||||||
|
|
||||||
async fetch() {
|
async fetch() {
|
||||||
const viewInApi = this.$store.getters['prefs/get'](VIEW_IN_API);
|
const viewInApi = this.$store.getters['prefs/get'](VIEW_IN_API);
|
||||||
|
|
@ -64,31 +67,8 @@ export default {
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({ t: 'i18n/t' }),
|
...mapGetters({ t: 'i18n/t' }),
|
||||||
...mapGetters({
|
...mapGetters({ options: 'action-menu/optionsArray' }),
|
||||||
// Use either these Vuex getters
|
|
||||||
// OR the props to set the action menu state,
|
|
||||||
// but don't use both.
|
|
||||||
targetElem: 'action-menu/elem',
|
|
||||||
shouldShow: 'action-menu/showing',
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
|
||||||
toggleActionMenu(e, setting) {
|
|
||||||
const actionElement = e.srcElement;
|
|
||||||
|
|
||||||
if (!this.targetElem && !this.shouldShow) {
|
|
||||||
this.$store.commit(`action-menu/show`, {
|
|
||||||
resources: setting.data,
|
|
||||||
elem: actionElement
|
|
||||||
});
|
|
||||||
} else if (this.targetElem === actionElement && this.shouldShow) {
|
|
||||||
// this condition is needed so that we can "toggle" the action menu with
|
|
||||||
// the keyboard for accessibility (row action menu)
|
|
||||||
this.$store.commit('action-menu/hide');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -104,8 +84,8 @@ export default {
|
||||||
</div>
|
</div>
|
||||||
</Banner>
|
</Banner>
|
||||||
<div
|
<div
|
||||||
v-for="(setting, i) in settings"
|
v-for="(setting) in settings"
|
||||||
:key="i"
|
:key="setting.id"
|
||||||
class="advanced-setting mb-20"
|
class="advanced-setting mb-20"
|
||||||
:data-testid="`advanced-setting__option-${setting.id}`"
|
:data-testid="`advanced-setting__option-${setting.id}`"
|
||||||
>
|
>
|
||||||
|
|
@ -128,20 +108,12 @@ export default {
|
||||||
v-if="setting.hasActions"
|
v-if="setting.hasActions"
|
||||||
class="action"
|
class="action"
|
||||||
>
|
>
|
||||||
<button
|
<action-menu
|
||||||
aria-haspopup="true"
|
:resource="setting.data"
|
||||||
aria-expanded="false"
|
:button-aria-label="t('advancedSettings.edit.label')"
|
||||||
type="button"
|
data-testid="action-button"
|
||||||
class="btn btn-sm role-multi-action actions"
|
button-role="tertiary"
|
||||||
role="button"
|
/>
|
||||||
:aria-label="t('advancedSettings.edit.moreActions', { setting: setting.id })"
|
|
||||||
@click="toggleActionMenu($event, setting)"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
class="icon icon-actions"
|
|
||||||
:alt="t('advancedSettings.edit.moreActions', { setting: setting.id })"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div value>
|
<div value>
|
||||||
|
|
|
||||||
|
|
@ -53,17 +53,17 @@ export default defineComponent({
|
||||||
// Page actions don't change on the Home Page
|
// Page actions don't change on the Home Page
|
||||||
pageActions: [
|
pageActions: [
|
||||||
{
|
{
|
||||||
labelKey: 'nav.header.setLoginPage',
|
label: this.t('nav.header.setLoginPage'),
|
||||||
action: SET_LOGIN_ACTION
|
action: SET_LOGIN_ACTION
|
||||||
},
|
},
|
||||||
{ separator: true },
|
{ divider: true },
|
||||||
{
|
{
|
||||||
labelKey: 'nav.header.showHideBanner',
|
label: this.t('nav.header.showHideBanner'),
|
||||||
action: SHOW_HIDE_BANNER_ACTION
|
action: SHOW_HIDE_BANNER_ACTION
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
labelKey: 'nav.header.restoreCards',
|
label: this.t('nav.header.restoreCards'),
|
||||||
action: RESET_CARDS_ACTION
|
action: RESET_CARDS_ACTION
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
vendor: getVendor(),
|
vendor: getVendor(),
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ export const getters = {
|
||||||
event: (state) => state.event,
|
event: (state) => state.event,
|
||||||
resources: (state) => state.resources,
|
resources: (state) => state.resources,
|
||||||
|
|
||||||
options(state) {
|
optionsArray(state) {
|
||||||
let selected = state.resources;
|
let selected = state.resources;
|
||||||
|
|
||||||
if ( !selected ) {
|
if ( !selected ) {
|
||||||
|
|
@ -50,7 +50,10 @@ export const getters = {
|
||||||
|
|
||||||
const out = _filter(map);
|
const out = _filter(map);
|
||||||
|
|
||||||
return { ...out };
|
return [...out];
|
||||||
|
},
|
||||||
|
options(_state, getters) {
|
||||||
|
return { ...getters.optionsArray };
|
||||||
},
|
},
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
@ -144,12 +147,19 @@ export const mutations = {
|
||||||
|
|
||||||
state.modalData = data;
|
state.modalData = data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
SET_RESOURCE(state, resources) {
|
||||||
|
state.resources = !isArray(resources) ? [resources] : resources;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
execute({ state }, { action, args, opts }) {
|
execute({ state }, { action, args, opts }) {
|
||||||
return _execute(state.resources, action, args, opts);
|
return _execute(state.resources, action, args, opts);
|
||||||
},
|
},
|
||||||
|
setResource({ commit }, resource) {
|
||||||
|
commit('SET_RESOURCE', resource);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// -----------------------------
|
// -----------------------------
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
/**
|
||||||
|
* Unit tests are failing with the following error:
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* [@vue/compiler-sfc] No fs option provided to `compileScript` in non-Node environment. File system access is required for resolving imported types.
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* It seems TypeScript does not populate ts.sys when loaded in Jest. In order to
|
||||||
|
* resolve this issue, we can use the hack below to point to a different
|
||||||
|
* transformer than vue-jest and call registerTs before exporting vue-jest.
|
||||||
|
*
|
||||||
|
* SEE: https://github.com/vuejs/core/issues/8301
|
||||||
|
*/
|
||||||
|
require('@vue/compiler-sfc').registerTS(() => require('typescript'));
|
||||||
|
module.exports = require('@vue/vue3-jest');
|
||||||
Loading…
Reference in New Issue