Skip to content
6 changes: 5 additions & 1 deletion shesha-reactjs/src/components/dataList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -602,7 +602,7 @@ export const DataList: FC<Partial<IDataListProps>> = ({
return (
<div key={`row-${index}`}>
<ConditionalWrap
condition={selectionMode !== 'none'}
condition={selectionMode === 'multiple'}
wrap={(children) => (
<Checkbox
className={classNames(styles.shaDatalistComponentItemCheckbox, { selected })}
Expand All @@ -621,6 +621,10 @@ export const DataList: FC<Partial<IDataListProps>> = ({
{ selected },
)}
onClick={() => {
// For single and multiple selection modes, trigger selection when clicking on row
if (selectionMode === 'single' || selectionMode === 'multiple') {
onSelectRowLocal(index, item);
}
// Trigger onListItemClick event
if (onListItemClick) {
onListItemClick(index, item);
Expand Down
2 changes: 1 addition & 1 deletion shesha-reactjs/src/components/dataTable/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -878,7 +878,7 @@ export const DataTable: FC<Partial<IIndexTableProps>> = ({
data: tableData,
// Disable sorting if we're in create mode so that the new row is always the first
defaultSorting: defaultSorting,
useMultiSelect: selectionMode === 'multiple' || selectionMode === 'single' || (selectionMode === undefined && useMultiSelect),
useMultiSelect: selectionMode === 'multiple' || (selectionMode === undefined && useMultiSelect),
selectionMode,
freezeHeaders,
onSelectRow: selectionMode === 'none' ? undefined : onSelectRowLocal,
Expand Down
16 changes: 14 additions & 2 deletions shesha-reactjs/src/components/reactTable/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ export const ReactTable: FC<IReactTableProps> = ({
rows,
columns: tableColumns,
toggleAllRowsSelected,
toggleRowSelected,
} = useTable(
{
columns: preparedColumns,
Expand Down Expand Up @@ -422,8 +423,19 @@ export const ReactTable: FC<IReactTableProps> = ({
const onResizeClick = (event: React.MouseEvent<HTMLDivElement, MouseEvent>): void => event?.stopPropagation();

const handleSelectRow = (row: Row<object>): void => {
if (!omitClick && !(canEditInline || canDeleteInline) && onSelectRow) {
onSelectRow(row?.index, row?.original);
if (!omitClick && !(canEditInline || canDeleteInline)) {
// For both single and multiple selection modes, update the row selection state
if (selectionMode === 'single' || selectionMode === 'multiple') {
if (selectionMode === 'single') {
// For single selection, first clear all selections then select this row
toggleAllRowsSelected(false);
}
toggleRowSelected(row.id);
}
// Call the onSelectRow callback
if (onSelectRow) {
onSelectRow(row?.index, row?.original);
}
}
};

Expand Down
2 changes: 1 addition & 1 deletion shesha-reactjs/src/components/reactTable/tableRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ export const TableRow: FC<ISortableRowProps> = (props) => {
styles.tr,
styles.trBody,
{ [styles.trOdd]: striped && index % 2 === 0 },
{ [styles.trSelected]: selectedRowIndex === row?.index },
{ [styles.trSelected]: selectedRowIndex === row?.index || row?.isSelected },
)}
key={rowId}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const ListComponent: IToolboxComponent<IListComponentProps> = {
submitHttpVerb: 'POST',
labelCol: prev['labelCol'] ?? 8,
wrapperCol: prev['wrapperCol'] ?? 16,
selectionMode: prev['selectionMode'] ?? 'none',
selectionMode: prev['selectionMode'] ?? 'single',
deleteConfirmMessage: prev['deleteConfirmMessage'] ?? `return '';`,
totalRecords: 100,
buttons: prev['buttons'] ?? [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import { useConfigurableActionDispatcher } from '@/providers/configurableActions
import { useAvailableConstantsData } from '@/providers/form/utils';
import { DataContextTopLevels, isNavigationActionConfiguration, useShaRouting, useTheme } from '@/index';
import { useAsyncMemo } from '@/hooks/useAsyncMemo';
import { IFullAuditedEntity } from '@/publicJsApis/entities';
import { useStyles } from './style';

export interface IConfigurableButtonProps extends Omit<IButtonItem, 'style' | 'itemSubType'> {
style?: CSSProperties;
form: FormInstance<any>;
dynamicItem?: any;
dynamicItem?: IFullAuditedEntity;
}

export const ConfigurableButton: FC<IConfigurableButtonProps> = (props) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const DataListComponent: IToolboxComponent<IDataListComponentProps> = {
.add<IDataListComponentProps>(0, (prev) => ({
...prev,
formSelectionMode: 'name',
selectionMode: 'none',
selectionMode: 'single',
items: [],
// Set default form to the starter template
// formId: { name: 'data-list-dummy-default', module: 'Shesha' }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const migrateV12toV13 = (props: ITableComponentProps, _context: SettingsM
if (typeof useMultiselect === 'boolean') {
// If useMultiselect was true, set selectionMode to 'multiple'
// If useMultiselect was false, set selectionMode to 'none' (no checkboxes)
const newSelectionMode = useMultiselect ? 'multiple' : 'none';
const newSelectionMode = useMultiselect ? 'multiple' : 'single';
Comment on lines 12 to +15
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Update the comment to reflect the new mapping.

The comment on line 14 states "set selectionMode to 'none'" but the code now maps false to 'single'. The comment should be updated to match the implementation.

Apply this diff:

     // If useMultiselect was explicitly set
     if (typeof useMultiselect === 'boolean') {
       // If useMultiselect was true, set selectionMode to 'multiple'
-      // If useMultiselect was false, set selectionMode to 'none' (no checkboxes)
+      // If useMultiselect was false, set selectionMode to 'single'
       const newSelectionMode = useMultiselect ? 'multiple' : 'single';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (typeof useMultiselect === 'boolean') {
// If useMultiselect was true, set selectionMode to 'multiple'
// If useMultiselect was false, set selectionMode to 'none' (no checkboxes)
const newSelectionMode = useMultiselect ? 'multiple' : 'none';
const newSelectionMode = useMultiselect ? 'multiple' : 'single';
if (typeof useMultiselect === 'boolean') {
// If useMultiselect was true, set selectionMode to 'multiple'
// If useMultiselect was false, set selectionMode to 'single'
const newSelectionMode = useMultiselect ? 'multiple' : 'single';
🤖 Prompt for AI Agents
In
shesha-reactjs/src/designer-components/dataTable/table/migrations/migrate-v13.ts
around lines 12 to 15, the inline comment incorrectly says "set selectionMode to
'none'" when the code maps useMultiselect === false to 'single'; update the
comment to reflect the actual mapping (i.e., when useMultiselect is true set
selectionMode to 'multiple', when false set selectionMode to 'single') so the
comment matches the implementation.


// Remove the old useMultiselect property and set the new selectionMode
const { useMultiselect: removed, ...propsWithoutUseMultiselect } = props;
Expand All @@ -28,6 +28,6 @@ export const migrateV12toV13 = (props: ITableComponentProps, _context: SettingsM
// ensure selectionMode has a default value
return {
...props,
selectionMode: props.selectionMode || 'none',
selectionMode: props.selectionMode || 'single',
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,10 @@ const TableComponent: TableComponentDefinition = {
return <TableComponentFactory model={model} />;
},
initModel: (model: ITableComponentProps) => {
const defaults = defaultStyles();
return {
...model,
...defaults,
items: [],
striped: true,
};
Expand All @@ -60,7 +62,7 @@ const TableComponent: TableComponentDefinition = {
...prev,
items: items,
useMultiselect: prev['useMultiselect'] ?? false,
selectionMode: prev['selectionMode'] ?? 'none',
selectionMode: prev['selectionMode'] ?? 'single',
crud: prev['crud'] ?? false,
flexibleHeight: prev['flexibleHeight'] ?? false,
striped: prev['striped'] ?? true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,7 @@ export const TableWrapper: FC<ITableComponentProps> = (props) => {
rowHoverBackgroundColor={props.rowHoverBackgroundColor}
rowSelectedBackgroundColor={props.rowSelectedBackgroundColor}
border={props.border}
striped={props.striped}
hoverHighlight={props.hoverHighlight}
backgroundColor={props.background?.color}
headerFontSize={props.headerFontSize}
Expand Down
20 changes: 17 additions & 3 deletions shesha-reactjs/src/designer-components/profileDropdown/index.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import {
IConfigurableActionConfiguration,
ConfigurableForm,
FormIdentifier,
IConfigurableFormComponent,
IToolboxComponent,
useAuth,
useForm,
useFormExpression,
useGlobalState,
useSidebarMenu,
useSheshaApplication,
} from '@/index';
import { useConfigurableActionDispatcher } from '@/providers/configurableActionsDispatcher';
import { useAvailableConstantsData } from '@/providers/form/utils';
import { IFullAuditedEntity } from '@/publicJsApis/entities';
import {
ButtonGroupItemProps,
IButtonGroup,
Expand Down Expand Up @@ -76,8 +79,9 @@ const ProfileDropdown: IToolboxComponent<IProfileDropdown> = {
const { loginInfo, logoutUser } = useAuth();
const { formData } = useForm();
const { globalState } = useGlobalState();
const { executeActionViaConfiguration } = useFormExpression();
const { executeAction } = useConfigurableActionDispatcher();
const { anyOfPermissionsGranted } = useSheshaApplication();
const allData = useAvailableConstantsData();

const sidebar = useSidebarMenu(false);
const { accountDropdownListItems } = sidebar || {};
Expand Down Expand Up @@ -138,7 +142,17 @@ const ProfileDropdown: IToolboxComponent<IProfileDropdown> = {
return (isItem(item) && isVisibleBase(item)) || (isGroup(item) && isGroupVisible(item, getIsVisible));
};

const menuItems = getMenuItem(finalItems, executeActionViaConfiguration, getIsVisible);
// Custom execute function that includes dynamicItem in the context
const executeActionWithDynamicContext = (actionConfiguration: IConfigurableActionConfiguration, dynamicItem?: IFullAuditedEntity): void => {
if (actionConfiguration) {
executeAction({
actionConfiguration,
argumentsEvaluationContext: { ...allData, dynamicItem },
});
}
};
Comment on lines +145 to +153
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Dynamic context wrapper is correct; consider stabilizing with useCallback

The executeActionWithDynamicContext helper correctly:

  • Guards against missing actionConfiguration.
  • Injects both allData and an optional dynamicItem into argumentsEvaluationContext, with dynamicItem cleanly layered on top.

To avoid unnecessary re-renders in consumers that might depend on function identity, you could optionally wrap this helper in useCallback:

-    const executeActionWithDynamicContext = (actionConfiguration: IConfigurableActionConfiguration, dynamicItem?: IFullAuditedEntity): void => {
-      if (actionConfiguration) {
-        executeAction({
-          actionConfiguration,
-          argumentsEvaluationContext: { ...allData, dynamicItem },
-        });
-      }
-    };
+    const executeActionWithDynamicContext = React.useCallback(
+      (actionConfiguration: IConfigurableActionConfiguration, dynamicItem?: IFullAuditedEntity): void => {
+        if (!actionConfiguration) return;
+        executeAction({
+          actionConfiguration,
+          argumentsEvaluationContext: { ...allData, dynamicItem },
+        });
+      },
+      [executeAction, allData],
+    );

Not mandatory unless downstream memoization depends on a stable callback.

🤖 Prompt for AI Agents
In shesha-reactjs/src/designer-components/profileDropdown/index.tsx around lines
145 to 153, the helper executeActionWithDynamicContext should be stabilized with
useCallback to avoid changing function identity on each render; wrap the
function in React.useCallback, include executeAction and allData (and any other
values referenced) in the dependency array, and keep the same parameter
signature and guard logic so behavior is unchanged.


const menuItems = getMenuItem(finalItems, executeActionWithDynamicContext, getIsVisible);

const accountMenuItems = getAccountMenuItems(accountDropdownListItems, logoutUser);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {
IButtonItem,
IConfigurableActionConfiguration,
IconType,
IHeaderAction,
Expand All @@ -10,7 +9,9 @@ import {
ButtonGroupItemProps,
IButtonGroup,
isGroup,
isButtonItem,
} from '@/providers/buttonGroupConfigurator/models';
import { IFullAuditedEntity } from '@/publicJsApis/entities';
import { IAuthenticator } from '@/providers/auth';
import { LoginOutlined } from '@ant-design/icons';
import { MenuProps } from 'antd';
Expand Down Expand Up @@ -46,7 +47,7 @@ const filterVisibleItems = (

export const getMenuItem = (
items: ButtonGroupItemProps[] = [],
execute: (payload: IConfigurableActionConfiguration) => void,
execute: (payload: IConfigurableActionConfiguration, dynamicItem?: IFullAuditedEntity) => void,
visibilityChecker?: ItemVisibilityFunc,
): ItemType[] => {
// Filter items based on visibility if checker is provided
Expand All @@ -55,6 +56,7 @@ export const getMenuItem = (
return visibleItems.map((item) => {
const { id, icon, label } = item;
const childItems = isGroup(item) ? (item as IButtonGroup).childItems : undefined;
const dynamicItem = isButtonItem(item) ? item.dynamicItem : undefined;

return {
key: id,
Expand All @@ -64,7 +66,7 @@ export const getMenuItem = (
</Fragment>
),
children: childItems ? getMenuItem(childItems, execute, visibilityChecker) : undefined,
onClick: () => execute((item as IButtonItem)?.actionConfiguration),
onClick: () => isButtonItem(item) ? execute(item.actionConfiguration, dynamicItem) : undefined,
};
});
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import { IConfigurableActionConfiguration } from '@/interfaces/configurableActio
import { IDynamicActionsConfiguration } from '@/designer-components/dynamicActionsConfigurator/models';
import { EditMode, IStyleType } from '@/index';
import React from 'react';
import { IFullAuditedEntity } from '@/publicJsApis/entities';
import { ListItemWithId } from '@/components/listEditor/models';


type ButtonGroupItemType = 'item' | 'group';

export type ButtonGroupItemProps = IButtonGroupItem | IButtonGroup;
Expand Down Expand Up @@ -66,6 +68,7 @@ export interface IButtonGroupItem extends IButtonGroupItemBase, ListItemWithId {

export interface IButtonItem extends Omit<IButtonGroupItem, 'type'> {
actionConfiguration?: IConfigurableActionConfiguration;
dynamicItem?: IFullAuditedEntity;
}

export const isItem = (item: IButtonGroupItemBase): item is IButtonGroupItem => {
Expand Down