diff --git a/src/card/settings/CardSettingsFooter.tsx b/src/card/settings/CardSettingsFooter.tsx index e70da3d2f..8f16c2688 100644 --- a/src/card/settings/CardSettingsFooter.tsx +++ b/src/card/settings/CardSettingsFooter.tsx @@ -148,6 +148,7 @@ const NeoCardSettingsFooter = ({ settingValue={reportSettings[actionsToCustomize]} type={type} fields={fields} + preConditionsSetting={reportSettings?.preConditions} customReportActionsModalOpen={customReportActionsModalOpen} setCustomReportActionsModalOpen={setCustomReportActionsModalOpen} onReportSettingUpdate={onReportSettingUpdate} diff --git a/src/chart/table/TableActionsHelper.ts b/src/chart/table/TableActionsHelper.ts index 7b946df2e..2033ab940 100644 --- a/src/chart/table/TableActionsHelper.ts +++ b/src/chart/table/TableActionsHelper.ts @@ -3,6 +3,55 @@ export const hasCheckboxes = (actionsRules) => { return rules.length > 0; }; +export const hasPreCondition = (preConditions) => { + return preConditions.length > 0; +}; + +const groupConditionsByField = (conditions) => { + return conditions.reduce((acc, condition) => { + if (!acc[condition.field]) { + acc[condition.field] = []; + } + acc[condition.field].push(condition); + return acc; + }, {}); +}; + +const evaluateGroupedConditions = (groupedConditions, row) => { + return Object.keys(groupedConditions).every((field) => { + // Logical OR between conditions for the same field + return groupedConditions[field].some((condition) => evaluateCondition(condition, row)); + }); +}; + +export const convertConditionsToExpression = (conditions, row) => { + const groupedConditions = groupConditionsByField(conditions); + return !evaluateGroupedConditions(groupedConditions, row); +}; + +const evaluateCondition = (condition, row) => { + let fieldValue = row[condition.field]; + + // Handle Neo4j integer format + if (fieldValue && typeof fieldValue === 'object' && 'low' in fieldValue && 'high' in fieldValue) { + // Assuming we only care about the 'low' value for comparisons + fieldValue = String(fieldValue.low); + } + + switch (condition.condition) { + case '=': + return fieldValue === condition.value; + case '!=': + return fieldValue !== condition.value; + case 'contains': + return typeof fieldValue === 'string' && fieldValue.includes(condition.value); + case 'not_contains': + return typeof fieldValue === 'string' && !fieldValue.includes(condition.value); + default: + return false; + } +}; + export const getCheckboxes = (actionsRules, rows, getGlobalParameter) => { let rules = actionsRules.filter((rule) => rule.condition && rule.condition == 'rowCheck'); const params = rules.map((rule) => `neodash_${rule.customizationValue}`); diff --git a/src/chart/table/TableChart.tsx b/src/chart/table/TableChart.tsx index 28ae25f45..0b6b14f6a 100644 --- a/src/chart/table/TableChart.tsx +++ b/src/chart/table/TableChart.tsx @@ -1,5 +1,5 @@ import React, { useEffect } from 'react'; -import { DataGrid, GridColumnVisibilityModel } from '@mui/x-data-grid'; +import { DataGrid, GridColumnVisibilityModel, GridRowId } from '@mui/x-data-grid'; import { ChartProps } from '../Chart'; import { evaluateRulesOnDict, @@ -17,14 +17,21 @@ import { performActionOnElement, } from '../../extensions/advancedcharts/Utils'; import { IconButton } from '@neo4j-ndl/react'; -import { CloudArrowDownIconOutline, XMarkIconOutline } from '@neo4j-ndl/react/icons'; +import { CloudArrowDownIconOutline, ArrowPathIconOutline, XMarkIconOutline } from '@neo4j-ndl/react/icons'; import { ThemeProvider, createTheme } from '@mui/material/styles'; import Button from '@mui/material/Button'; import { extensionEnabled } from '../../utils/ReportUtils'; import { renderCellExpand } from '../../component/misc/DataGridExpandRenderer'; -import { getCheckboxes, hasCheckboxes, updateCheckBoxes } from './TableActionsHelper'; +import { + convertConditionsToExpression, + getCheckboxes, + hasCheckboxes, + hasPreCondition, + updateCheckBoxes, +} from './TableActionsHelper'; import ApiService from '../../utils/apiService'; import { AxiosResponse } from 'axios'; +import Notification from '../../component/custom/Notification'; const TABLE_HEADER_HEIGHT = 32; const TABLE_FOOTER_HEIGHT = 62; @@ -96,6 +103,10 @@ export const NeoTableChart = (props: ChartProps) => { extensionEnabled(props.extensions, 'actions') && props.settings && props.settings.actionsRules ? props.settings.actionsRules : []; + const preConditions = + extensionEnabled(props.extensions, 'actions') && props.settings && props.settings.preConditions + ? props.settings.preConditions + : []; const compact = props.settings && props.settings.compact !== undefined ? props.settings.compact : false; const styleRules = useStyleRules( extensionEnabled(props.extensions, 'styling'), @@ -129,6 +140,7 @@ export const NeoTableChart = (props: ChartProps) => { } const { records } = props; + const isApiSpecEnabled = props.settings?.apiSpec && props.settings?.apiSpec.apiEnabled; const generateSafeColumnKey = (key) => { return key != 'id' ? key : `${key} `; @@ -142,6 +154,13 @@ export const NeoTableChart = (props: ChartProps) => { setAnchorEl(null); }; + const [alertOpen, setAlertOpen] = React.useState(false); + const [notificationMessage, setNotificationMessage] = React.useState(''); + const [notificationSeverity, setNotificationSeverity] = React.useState<'success' | 'warning' | 'error'>('success'); + + const handleNotificationClose = () => { + setAlertOpen(false); + }; const lineBreakColumns: string[] = props.settings?.lineBreaksAfterListEntry; const actionableFields = actionsRules.filter((r) => r.condition !== 'rowCheck').map((r) => r.field); @@ -333,60 +352,92 @@ export const NeoTableChart = (props: ChartProps) => { throw new Error(`Unsupported method: ${method}`); } - props.updateReportSetting('apiSpec', { ...props.settings.apiSpec, response }); + props.updateReportSetting('apiSpec', { ...props.settings?.apiSpec, response }); + setNotificationMessage('RUPS package created. Please find the link above'); + setNotificationSeverity('success'); + setAlertOpen(true); } catch (error) { // Handle errors here console.error('API call error:', error); + setNotificationMessage('RUPS package creation is currently not working. Please try again later.'); + setNotificationSeverity('error'); + setAlertOpen(true); } finally { setApiLoading(false); } }; + const handleResetApiResponse = () => { + props.updateReportSetting('apiSpec', { ...props.settings?.apiSpec, response: null }); + }; + + useEffect(() => { + if (isApiSpecEnabled) { + handleResetApiResponse(); + } + }, [records]); + const apiCallButton = () => ( - - - + {props.settings?.apiSpec.response && ( + - {props.settings.apiSpec.response ? ( - - - - {props.settings?.apiSpec.response.data} - - - - ) : ( - <> - )} - + )} + {props.settings?.apiSpec.response ? ( + + + + {props.settings?.apiSpec.response.data} + + + + ) : ( + <> + )} ); - const isApiSpecEnabled = props.settings?.apiSpec && props.settings?.apiSpec.apiEnabled; - const tableStyle: any = isApiSpecEnabled ? { marginTop: 10, height: '90%', width: '100%', position: 'relative' } : { height: '100%', width: '100%', position: 'relative' }; + const isRowSelectable = (params: { id: GridRowId; row: any }) => { + if (hasPreCondition(preConditions)) { + return convertConditionsToExpression(preConditions, params.row); + } + return true; + }; + return ( + {isApiSpecEnabled ? apiCallButton() : <>}
{ onSelectionModelChange={(selection) => updateCheckBoxes(actionsRules, rows, selection, props.setGlobalParameter) } + isRowSelectable={isRowSelectable} autoPageSize pagination disableSelectionOnClick diff --git a/src/component/custom/Notification.tsx b/src/component/custom/Notification.tsx new file mode 100644 index 000000000..4ac11a668 --- /dev/null +++ b/src/component/custom/Notification.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import Snackbar from '@mui/material/Snackbar'; +import MuiAlert, { AlertProps } from '@mui/material/Alert'; + +interface NotificationProps { + open: boolean; + message: string; + severity: 'success' | 'warning' | 'error'; + onClose: () => void; +} + +const Alert = React.forwardRef((props, ref) => { + return ; +}); + +const Notification: React.FC = ({ open, message, severity, onClose }) => { + return ( + + + {message} + + + ); +}; + +export default Notification; diff --git a/src/extensions/actions/ActionsRuleCreationModal.tsx b/src/extensions/actions/ActionsRuleCreationModal.tsx index 48d6486ad..52c93b176 100644 --- a/src/extensions/actions/ActionsRuleCreationModal.tsx +++ b/src/extensions/actions/ActionsRuleCreationModal.tsx @@ -8,7 +8,34 @@ import { } from '@neo4j-ndl/react/icons'; import { getPageNumbersAndNamesList } from '../advancedcharts/Utils'; import { IconButton, Button, Dialog, Dropdown, TextInput } from '@neo4j-ndl/react'; -import { Autocomplete, TextField } from '@mui/material'; +import { Autocomplete, TextField, Typography } from '@mui/material'; + +// Pre conditions + +const PRE_CONDITIONS_RULES = [ + { + value: '=', + label: '=', + }, + { + value: '!=', + label: '!=', + }, + { + value: 'contains', + label: 'contains', + }, + { + value: 'not_contains', + label: 'not_contains', + }, +]; + +const defaultPreCondition = { + condition: '=', + field: '', + value: '', +}; // The set of conditional checks that are included in the rule specification. const RULE_CONDITIONS = { @@ -166,12 +193,17 @@ export const NeoCustomReportActionsModal = ({ fields, setCustomReportActionsModalOpen, onReportSettingUpdate, + preConditionsSetting, }) => { // The rule set defined in this modal is updated whenever the setting value is externally changed. const [rules, setRules] = React.useState([]); + const [preConditions, setPreConditions] = React.useState([defaultPreCondition]); useEffect(() => { if (settingValue) { setRules(settingValue); + if (preConditionsSetting) { + setPreConditions(preConditionsSetting); + } } }, [settingValue]); @@ -183,6 +215,12 @@ export const NeoCustomReportActionsModal = ({ } else { onReportSettingUpdate(settingName, rules); } + + if (preConditions.length === 0) { + onReportSettingUpdate('preConditions', undefined); + } else { + onReportSettingUpdate('preConditions', preConditions); + } setCustomReportActionsModalOpen(false); }; @@ -193,6 +231,10 @@ export const NeoCustomReportActionsModal = ({ setRules(newRules); }; + const updatePreConditionFieldById = (j, field, value) => { + setPreConditions((prevItems) => prevItems.map((item, i) => (i === j ? { ...item, [field]: value } : item))); + }; + /** * Create the list of suggestions used in the autocomplete box of the rule specification window. * This will be dynamic based on the type of report we are customizing. @@ -535,6 +577,115 @@ export const NeoCustomReportActionsModal = ({ + {rules.some((rule) => rule?.condition === 'rowCheck') && ( + + + + + + + + {preConditions.map((con, i) => { + return ( + + + + + + + ); + })} + + + + +
+
+ Report Pre Conditions +
+ {i + 1}. + IF + +
+ + e.toLowerCase().includes(con.field.toLowerCase()) + )} + value={con.field ? con.field : ''} + inputValue={con.field ? con.field : ''} + popupIcon={<>} + style={{ minWidth: 125 }} + onInputChange={(event, value) => { + updatePreConditionFieldById(i, 'field', value); + }} + onChange={(event, newValue) => { + updatePreConditionFieldById(i, 'field', newValue); + }} + renderInput={(params) => ( + + )} + /> + updatePreConditionFieldById(i, 'condition', newValue?.value), + options: PRE_CONDITIONS_RULES.map((option) => ({ + label: option.label, + value: option.value, + })), + value: { label: con.condition, value: con.condition }, + }} + style={{ minWidth: 70, display: 'inline-block' }} + fluid + /> + updatePreConditionFieldById(i, 'value', e.target.value)} + fluid + > +
+
+ { + setPreConditions((prevItems) => prevItems.filter((_, j) => j !== i)); + }} + > + + +
+
+ { + setPreConditions([...preConditions, defaultPreCondition]); + }} + > + + +
+
+ )}