Create a SortableTable ActionMenu component

Signed-off-by: Phillip Rak <rak.phillip@gmail.com>
This commit is contained in:
Phillip Rak 2025-02-06 16:40:40 -07:00
parent df18acc984
commit 828ec858a2
6 changed files with 123 additions and 19 deletions

View File

@ -28,6 +28,8 @@ defineProps<{
ariaLabel?: string
}>();
const emit = defineEmits(['update:open']);
const {
isMenuOpen,
showMenu,
@ -35,8 +37,7 @@ const {
setFocus,
provideDropdownContext,
registerDropdownCollection,
handleKeydown,
} = useDropdownContext();
} = useDropdownContext(emit);
provideDropdownContext();

View File

@ -53,12 +53,12 @@ const findNewIndex = (shouldAdvance: boolean, activeIndex: number, itemsArr: Ele
return newIndex;
};
const handleClick = () => {
const handleClick = (e: MouseEvent) => {
if (props.disabled) {
return;
}
emits('click');
emits('click', e);
close();
};
@ -97,6 +97,9 @@ const handleMouseEnter = (e: MouseEvent) => {
@keydown.up.down.stop="handleKeydown"
@mouseenter="handleMouseEnter"
>
<slot name="before">
<!--Empty slot content-->
</slot>
<slot name="default">
<!--Empty slot content-->
</slot>
@ -105,6 +108,9 @@ const handleMouseEnter = (e: MouseEvent) => {
<style lang="scss" scoped>
[dropdown-menu-item] {
display: flex;
gap: 8px;
align-items: center;
padding: 9px 8px;
margin: 0 9px;
border-radius: 4px;

View File

@ -1,7 +1,9 @@
import { ref, provide, nextTick } from 'vue';
import { ref, provide, nextTick, defineEmits } from 'vue';
import { useDropdownCollection } from './useDropdownCollection';
import { RcButtonType } from '@components/RcButton';
const rcDropdownEmits = defineEmits(['update:open']);
/**
* Composable that provides the context for a dropdown menu. Includes methods
* 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
* interactions and setting focus.
*/
export const useDropdownContext = () => {
export const useDropdownContext = (emit: typeof rcDropdownEmits) => {
const {
dropdownItems,
firstDropdownItem,
@ -30,6 +32,7 @@ export const useDropdownContext = () => {
didKeydown.value = false;
}
isMenuOpen.value = show;
emit('update:open', show);
};
/**

View File

@ -0,0 +1,96 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useStore } from 'vuex';
import IconOrSvg from '@shell/components/IconOrSvg';
import { isAlternate } from '@shell/utils/platform';
import {
RcDropdown,
RcDropdownItem,
RcDropdownSeparator,
RcDropdownTrigger
} from '@components/RcDropdown';
const store = useStore();
const options = computed(() => store.getters['action-menu/options']);
const props = defineProps < { resource: Object }>();
const openChanged = () => {
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
:aria-label="t('nav.actionMenu.label')"
@update:open="openChanged"
>
<rc-dropdown-trigger
link
small
data-testid="page-actions-menu"
:aria-label="t('nav.actionMenu.button.label')"
>
<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) => execute(a, e)"
>
<template #before>
<IconOrSvg
v-if="a.icon || a.svg"
:icon="a.icon"
:src="a.svg"
class="icon"
color="header"
/>
</template>
{{ a.labelKey ? t(a.labelKey) : a.label }}
</rc-dropdown-item>
<rc-dropdown-separator
v-else
/>
</template>
</template>
</rc-dropdown>
</template>

View File

@ -23,6 +23,7 @@ import LabeledSelect from '@shell/components/form/LabeledSelect';
import { getParent } from '@shell/utils/dom';
import { FORMATTERS } from '@shell/components/SortableTable/sortable-config';
import ButtonMultiAction from '@shell/components/ButtonMultiAction.vue';
import ActionMenu from '@shell/components/SortableTable/ActionMenu.vue';
// Uncomment for table performance debugging
// import tableDebug from './debug';
@ -58,6 +59,7 @@ export default {
ActionDropdown,
LabeledSelect,
ButtonMultiAction,
ActionMenu,
},
mixins: [
filtering,
@ -1468,24 +1470,13 @@ export default {
</template>
<td
v-if="rowActions"
align="middle"
>
<slot
name="row-actions"
:row="row.row"
:index="i"
>
<ButtonMultiAction
:id="`actionButton+${i}+${(row.row && row.row.name) ? row.row.name : ''}`"
:ref="`actionButton${i}`"
aria-haspopup="true"
aria-expanded="false"
:aria-label="t('sortableTable.tableActionsLabel', { resource: row?.row?.id || '' })"
:data-testid="componentTestid + '-' + i + '-action-button'"
:borderless="true"
@click="handleActionButtonClick(i, $event)"
@keyup.enter="handleActionButtonClick(i, $event)"
@keyup.space="handleActionButtonClick(i, $event)"
/>
<ActionMenu :resource="row.row" />
</slot>
</td>
</tr>

View File

@ -144,12 +144,19 @@ export const mutations = {
state.modalData = data;
},
SET_RESOURCE(state, resources) {
state.resources = !isArray(resources) ? [resources] : resources;
}
};
export const actions = {
execute({ state }, { action, args, opts }) {
return _execute(state.resources, action, args, opts);
},
setResource({ commit }, resource) {
commit('SET_RESOURCE', resource);
}
};
// -----------------------------