| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276 |
- import { View, Pressable } from 'app/design/view'
- import { appSetting, menuItemsByName } from 'app/lib/util';
- import { components } from 'app/components/registry';
- import { useCurrentUser } from 'app/context/user'
- import { useMemo, memo, useContext } from "react";
- import { NeoButton, toNeoStyle, toControlSize } from 'app/design/controls';
- import DynamicMenu from 'app/components/nav/menu-dynamic';
- import { DropdownMenuOpenContext } from 'app/ui/atoms/dropdown-menu';
- import { Platform } from 'react-native'
- import { useOpenModalByUrl } from 'app/context/jotai/modal';
- import { useTranslation } from 'react-i18next';
- // DropdownMenu trigger host classes by the inner NeoButton's controlSize (static literals for Tailwind/Uniwind).
- const TRIGGER_HOST_CLASS = { mini: 'u-neo-btn-link hit-area-8', small: 'u-neo-btn-link hit-area-4' };
- const ButtonEx = memo(({ visibleItemsCount, params }) => {
- const isOpen = useContext(DropdownMenuOpenContext) ?? false;
- return (
- <View key="btn" className="">
- <NeoButton
- image="Ellipsis"
- style={params.button_style || toNeoStyle(params.button_variant || 'default').style}
- controlSize={toControlSize(params.button_size)}
- borderShape={params.button_border_shape ?? (params.button_rounded === true || params.button_rounded === 'true' ? 'circle' : undefined)}
- // Parent DropdownPopup Pressable owns onPress; interactive
- // enables hover/focus visuals on the NeoButton surface.
- interactive
- selected={isOpen}
- />
- </View>
- );
- });
- const MenuItemEx = memo(({ object,item, index, sDisplayType, params }) => {
- const ItemType = components['menu-item'][String(item.display_type ? item.display_type : sDisplayType)];
- const newButtonVariant = params?.button_variant === 'none' ? '' : params?.button_variant;
- return (
- <ItemType object={object} mode="dropdown-menu" key={item.id ? item.id : item.name} {...item} params={{ ...params, button_variant: newButtonVariant, button_size: 'sm' }} />
- )
- });
- const MenuItem = memo(({ object, item, itemRefs, index, visibleItemsCount, params, bShowVertical, sAlignItems, isUseStaticWidth, isWeb, sDisplayType }) => {
- const ItemType = components['menu-item'][String(item.display_type ? item.display_type : sDisplayType)];
- const isLastVisible = typeof visibleItemsCount === 'number' ? index >= (visibleItemsCount - 1) : false;
- let spacingClass = '';
- if (bShowVertical) {
- spacingClass = 'w-full ';
- } else if (params?.button_full_width === true) {
- spacingClass = ' flex-1 ';
- } else {
- spacingClass = isLastVisible ? ' ' : 'mt-0';
- }
- return (
- <View className={` ${spacingClass} ${sAlignItems == 'stretch' ? 'flex-auto' : ''} `}>
- <ItemType object={object} key={item.id ? item.id : item.name} {...item} params={params} />
- </View>
- )
- });
- export default function ElementMenu(oProps) {
- const isWeb = Platform.OS == 'web'
- const { currentUser } = useCurrentUser();
- const openModalByUrl = useOpenModalByUrl();
- const { t } = useTranslation();
- /*
- * Display type specified in menu can be overwritten with display type specified in item.
- * default display types: mixed, link, button, element, etc.
- */
- const sDisplayType = oProps.displayType || 'secondary';
- //--- auto-filter items using app settings.
- const bAutoFilter = oProps?.autoFilter !== 'false';
- //--- show only items which match with menu's display_type
- const bShowMatched = oProps?.showMatched === true;
- const bAutoSize = oProps?.autoSize || false;
- //--- show only items with selected display_type and doesn't take in account the menu's display_type
- const sShowSelected = oProps?.showSelected || false;
- //--- except the following items from output
- const aExcept = oProps?.except || [''];
- const aExceptTitle = oProps?.except_title || ['BxTemplView', 'BxTemplFavorite', 'BxTemplFeature', 'BxTemplReport', 'BxTimelineModule'];
- let sClassName = oProps?.params?.className || 'bx-menu ';
- //--- show vertical
- const bShowVertical = oProps?.params?.showVertical === true;
- const oParams = oProps?.params || {};
- // Horizontal gap keeps neighbouring buttons' 44px hit areas apart (mini needs 8px, small 4px).
- // `menu_gap` is a class literal from settings; items with their own `menu_item_spacing` get none.
- const sRowGap = oParams.menu_gap ?? (oParams.menu_item_spacing != null ? '' : 'gap-x-2');
- sClassName += bShowVertical ? ' flex-col items-center gap-y-2 w-full ' : ` flex-row ${sRowGap} `;
- //--- horizontal menu items alignment
- const sAlignItems = oProps.alignItems || oParams.justify_items || 'between';
- sClassName += `justify-${sAlignItems}`;
- if (isWeb) {
- sClassName += ' ';
- }
- //--- show menu's content only
- const bShowContent = oParams.showContent === 'true';
- //--- use iconset if available
- let iconset = { ...appSetting('menu_items', 'iconset'), ...appSetting('menu_items', oProps.object, 'iconset') };
- if (iconset) oParams.iconset = iconset;
- const sortedItems = [...(oProps.items || [])].sort((a, b) => {
-
- const pa = (a.primary === true || a.primary === 1) ? 1 : 0;
- const pb = (b.primary === true || b.primary === 1) ? 1 : 0;
- return pb - pa;
- });
- const filteredItems = useMemo(() => {
- return (bAutoFilter ? menuItemsByName(oProps.object, sortedItems, currentUser) : sortedItems).filter((aItem) => {
- // Check if item should be shown based on `bShowMatched` and `sDisplayType`
- if (bShowMatched && aItem.display_type !== sDisplayType) {
- return false;
- }
- // Check if item should be shown based on `sShowSelected`
- if (
- sShowSelected !== false &&
- ((aItem.display_type === undefined && sShowSelected !== 'undefined') ||
- (aItem.display_type !== undefined && aItem.display_type !== sShowSelected))
- ) {
- return false;
- }
- // Ensure the item has an id or name, and is not in the except lists
- if (!(aItem.id || aItem.name) || aExcept.includes(aItem.name) || aExceptTitle.includes(aItem.title)) {
- return false;
- }
- // Check if the `display_type` of the item is supported by `componentsMap`
- const sDisplayTypeItem = aItem.display_type ? aItem.display_type : (aItem.link ? 'button' : sDisplayType);
- if (sDisplayTypeItem == 'button') {
- aItem.display_type='button';
- }
- if (!components['menu-item'][sDisplayTypeItem]) {
- return false;
- }
- // menu-item/element returns nothing without a resolvable molecule —
- // skip so wrappers don't leave an empty flex slot (cover actions gap).
- if (sDisplayTypeItem === 'element') {
- const moleculeType = aItem?.data?.type;
- if (!moleculeType || !components['molecule'][String(moleculeType)]) {
- return false;
- }
- }
- // If all checks pass, the item should be included in the filtered list
- return true;
- });
- }, [bAutoFilter, oProps.object, sortedItems, currentUser, bShowMatched, sDisplayType, sShowSelected, aExcept, aExceptTitle]);
- if (!oProps.items){
- return;
- }
- if (!oProps?.items?.length)
- return [];
- let isUseStaticWidth = bShowContent || !bAutoSize || !isWeb;
- if (oProps.persistent > 0 && !bShowVertical) {
- // isUseStaticWidth = false;
- }
- if (!isWeb) {
- isUseStaticWidth = true;
- }
- if (oProps?.params?.menu_width) {
- sClassName += ` ${oProps.params.menu_width}`;
- }
- if (isUseStaticWidth) {
- // Prepare only actually rendered items to correctly identify the last visible one
- const preparedItems = filteredItems
- .map((item, originalIndex) => {
- const ItemType = components['menu-item'][item.display_type || sDisplayType];
- const element = <ItemType object={oProps.object} key={item.id ? item.id : item.name} {...item} params={oProps.params} />
- return { item, element, originalIndex };
- })
- .filter(({ element }) => element != null);
- const sItems = preparedItems.map(({ item, element, originalIndex }, i) => {
- const Wrapper = item.noAction ? Pressable : View;
- const cntProps = {};
- if (item.noAction) {
- cntProps.onPress = () => openModalByUrl(item.link);
- }
- const isLast = i === preparedItems.length - 1;
- let spacingClass = '';
- if (bShowVertical) {
- spacingClass = 'w-full ';
- } else if (oProps?.params?.button_full_width === true) {
- spacingClass = ' flex-1 ';
- } else {
- spacingClass = oProps?.params?.menu_item_spacing != null ? ` ${oProps.params.menu_item_spacing} ` : '';
- }
- return (
- <View
- key={`menu${originalIndex}`}
- collapsable={false}
- className={` ${spacingClass} ${sAlignItems === 'stretch' ? 'flex-auto' : ''} `}
- style={!isWeb && sAlignItems !== 'stretch' ? { flexGrow: 0, flexShrink: 0 } : undefined}
- >
- <Wrapper {...cntProps}>{element}</Wrapper>
- </View>
- );
- });
- if (bShowContent)
- return sItems;
- if (!bAutoSize) {
- // Native Expo UI Host needs a non-collapsible, content-sized row.
- // A stretched/collapsed wrapper (default Yoga in CoverMorph overlay)
- // reports 0x0 and the buttons never paint — web CSS does not.
- return (
- <View
- collapsable={false}
- className={sClassName}
- style={!isWeb ? { flexGrow: 0, flexShrink: 0, alignSelf: 'flex-start' } : undefined}
- >
- {sItems}
- </View>
- )
- }
- }
- return <DynamicMenu
- name="menu"
- object={oProps.object}
- ButtonEx={({ visibleItemsCount }) => <ButtonEx visibleItemsCount={visibleItemsCount} params={oProps.params} />}
- MenuItemEx={({ item, index }) => <MenuItemEx object={oProps.object} item={item} index={index} sDisplayType={sDisplayType} params={oProps.params} />}
- MenuItem={({ item, itemRefs, visibleItemsCount, index }) => {
- return <MenuItem object={oProps.object} item={item} index={index} visibleItemsCount={visibleItemsCount}
- bShowVertical={bShowVertical}
- isWeb={isWeb}
- itemRefs={itemRefs}
- sAlignItems={sAlignItems}
- isUseStaticWidth={isUseStaticWidth}
- sDisplayType={sDisplayType} params={oProps.params} />
- }}
- isFixedCount={oProps?.params?.isFixedCount}
- persistent={oProps.persistent}
- allowZeroPersistant={oProps.allowZeroPersistant}
- containerClasses={oProps.containerClasses || "w-full "}
- items={filteredItems}
- menuClasses={sClassName}
- isButtonOutside={false}
- menuExClasses="mr-auto ml-3 sm:ml-4 items-end gap-y-2"
- triggerAccessibilityLabel={t('More options')}
- triggerClassName={TRIGGER_HOST_CLASS[toControlSize(oProps?.params?.button_size)] ?? 'u-neo-btn-link'}
- />
- }
|