diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py index ce77996d..d19472cc 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py @@ -785,7 +785,6 @@ def process_instance_list( report_id: Optional[int] = None, ) -> flask.wrappers.Response: """Process_instance_list.""" - process_instance_report = ProcessInstanceReportService.report_with_identifier( g.user, report_id, report_identifier ) @@ -942,12 +941,18 @@ def process_instance_list( if column["accessor"] in stock_columns: continue instance_metadata_alias = aliased(ProcessInstanceMetadataModel) - process_instance_query = process_instance_query.outerjoin( + + filter_for_column = next((f for f in process_instance_report.report_metadata['filter_by'] if f['field_name'] == column['accessor']), None) + isouter = True + conditions = [ProcessInstanceModel.id == instance_metadata_alias.process_instance_id, + instance_metadata_alias.key == column["accessor"]] + if filter_for_column: + isouter = False + conditions.append(instance_metadata_alias.value == filter_for_column["field_value"]) + process_instance_query = process_instance_query.join( instance_metadata_alias, - and_( - ProcessInstanceModel.id == instance_metadata_alias.process_instance_id, - instance_metadata_alias.key == column["accessor"], - ), + and_(*conditions), + isouter=isouter ).add_columns(func.max(instance_metadata_alias.value).label(column["accessor"])) process_instances = ( diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py index 3486316f..d9096d63 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py @@ -29,8 +29,6 @@ class ProcessInstanceReportFilter: """To_dict.""" d = {} - print(f"dir(self): {dir(self)}") - if self.process_model_identifier is not None: d["process_model_identifier"] = self.process_model_identifier if self.start_from is not None: @@ -86,7 +84,7 @@ class ProcessInstanceReportService: # TODO replace with system reports that are loaded on launch (or similar) temp_system_metadata_map = { - "default": {"columns": cls.builtin_column_options()}, + "default": {"columns": cls.builtin_column_options(), "filter_by": [], 'order_by': ['-start_in_seconds', '-id']}, "system_report_instances_initiated_by_me": { "columns": [ {"Header": "id", "accessor": "id"}, @@ -98,13 +96,13 @@ class ProcessInstanceReportService: {"Header": "end_in_seconds", "accessor": "end_in_seconds"}, {"Header": "status", "accessor": "status"}, ], - "filter_by": [{"field_name": "initiated_by_me", "field_value": True}], + "filter_by": [{"field_name": "initiated_by_me", "field_value": True}],'order_by': ['-start_in_seconds', '-id'] }, "system_report_instances_with_tasks_completed_by_me": { "columns": cls.builtin_column_options(), "filter_by": [ {"field_name": "with_tasks_completed_by_me", "field_value": True} - ], + ],'order_by': ['-start_in_seconds', '-id'] }, "system_report_instances_with_tasks_completed_by_my_groups": { "columns": cls.builtin_column_options(), @@ -113,16 +111,15 @@ class ProcessInstanceReportService: "field_name": "with_tasks_completed_by_my_group", "field_value": True, } - ], + ],'order_by': ['-start_in_seconds', '-id'] }, } process_instance_report = ProcessInstanceReportModel( identifier=report_identifier, created_by_id=user.id, - report_metadata=temp_system_metadata_map[report_identifier], # type: ignore + report_metadata=temp_system_metadata_map[report_identifier], ) - # db.session.add(pro return process_instance_report # type: ignore diff --git a/spiffworkflow-frontend/src/components/ProcessInstanceListSaveAsReport.tsx b/spiffworkflow-frontend/src/components/ProcessInstanceListSaveAsReport.tsx index 2942ba4d..de5ea22c 100644 --- a/spiffworkflow-frontend/src/components/ProcessInstanceListSaveAsReport.tsx +++ b/spiffworkflow-frontend/src/components/ProcessInstanceListSaveAsReport.tsx @@ -6,12 +6,18 @@ import { Stack, // @ts-ignore } from '@carbon/react'; -import { ProcessInstanceReport, ProcessModel } from '../interfaces'; +import { + ReportFilter, + ProcessInstanceReport, + ProcessModel, + ReportColumn, + ReportMetadata, +} from '../interfaces'; import HttpService from '../services/HttpService'; type OwnProps = { onSuccess: (..._args: any[]) => any; - columnArray: { Header: string; accessor: string }; + columnArray: ReportColumn[]; orderBy: string; processModelSelection: ProcessModel | null; processStatusSelection: string[]; @@ -22,6 +28,7 @@ type OwnProps = { buttonText?: string; buttonClassName?: string; processInstanceReportSelection?: ProcessInstanceReport | null; + reportMetadata: ReportMetadata; }; export default function ProcessInstanceListSaveAsReport({ @@ -37,6 +44,7 @@ export default function ProcessInstanceListSaveAsReport({ endToSeconds, buttonText = 'Save as Perspective', buttonClassName, + reportMetadata, }: OwnProps) { const [identifier, setIdentifier] = useState( processInstanceReportSelection?.identifier || '' @@ -59,7 +67,11 @@ export default function ProcessInstanceListSaveAsReport({ const addProcessInstanceReport = (event: any) => { event.preventDefault(); - const orderByArray = orderBy.split(',').filter((n) => n); + // TODO: make a field to set this + let orderByArray = ['-start_in_seconds', '-id']; + if (orderBy) { + orderByArray = orderBy.split(',').filter((n) => n); + } const filterByArray: any = []; if (processModelSelection) { @@ -72,7 +84,8 @@ export default function ProcessInstanceListSaveAsReport({ if (processStatusSelection.length > 0) { filterByArray.push({ field_name: 'process_status', - field_value: processStatusSelection[0], // TODO: support more than one status + field_value: processStatusSelection.join(','), + operator: 'in', }); } @@ -104,6 +117,17 @@ export default function ProcessInstanceListSaveAsReport({ }); } + reportMetadata.filter_by.forEach((reportFilter: ReportFilter) => { + columnArray.forEach((reportColumn: ReportColumn) => { + if ( + reportColumn.accessor === reportFilter.field_name && + reportColumn.filterable + ) { + filterByArray.push(reportFilter); + } + }); + }); + let path = `/process-instances/reports`; let httpMethod = 'POST'; if (isEditMode() && processInstanceReportSelection) { diff --git a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx index cb782c03..92355fe9 100644 --- a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx +++ b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx @@ -57,6 +57,9 @@ import { ProcessInstanceReport, ProcessInstance, ReportColumn, + ReportColumnForEditing, + ReportMetadata, + ReportFilter, } from '../interfaces'; import ProcessModelSearch from './ProcessModelSearch'; import ProcessInstanceReportSearch from './ProcessInstanceReportSearch'; @@ -98,7 +101,7 @@ export default function ProcessInstanceListTable({ const navigate = useNavigate(); const [processInstances, setProcessInstances] = useState([]); - const [reportMetadata, setReportMetadata] = useState({}); + const [reportMetadata, setReportMetadata] = useState(); const [pagination, setPagination] = useState(null); const [processInstanceFilters, setProcessInstanceFilters] = useState({}); @@ -143,7 +146,7 @@ export default function ProcessInstanceListTable({ const [showReportColumnForm, setShowReportColumnForm] = useState(false); const [reportColumnToOperateOn, setReportColumnToOperateOn] = - useState(null); + useState(null); const [reportColumnFormMode, setReportColumnFormMode] = useState(''); const dateParametersToAlwaysFilterBy: dateParameters = useMemo(() => { @@ -630,17 +633,19 @@ export default function ProcessInstanceListTable({ endToSeconds, } = calculateStartAndEndSeconds(); - if (!valid) { + if (!valid || !reportMetadata) { return null; } return ( { - const reportMetadataCopy = { ...reportMetadata }; - const newColumns = reportColumns().filter( - (rc: ReportColumn) => rc.accessor !== reportColumn.accessor - ); - Object.assign(reportMetadataCopy, { columns: newColumns }); - setReportMetadata(reportMetadataCopy); + if (reportMetadata) { + const reportMetadataCopy = { ...reportMetadata }; + const newColumns = reportColumns().filter( + (rc: ReportColumn) => rc.accessor !== reportColumn.accessor + ); + Object.assign(reportMetadataCopy, { columns: newColumns }); + setReportMetadata(reportMetadataCopy); + } }; const handleColumnFormClose = () => { @@ -664,8 +671,49 @@ export default function ProcessInstanceListTable({ setReportColumnToOperateOn(null); }; - const handleUpdateColumn = () => { - if (reportColumnToOperateOn) { + const getFilterByFromReportMetadata = (reportColumnAccessor: string) => { + if (reportMetadata) { + return reportMetadata.filter_by.find((reportFilter: ReportFilter) => { + return reportColumnAccessor === reportFilter.field_name; + }); + } + return null; + }; + + const getNewFiltersFromReportForEditing = ( + reportColumnForEditing: ReportColumnForEditing + ) => { + if (!reportMetadata) { + return null; + } + const reportMetadataCopy = { ...reportMetadata }; + let newReportFilters = reportMetadataCopy.filter_by; + if (reportColumnForEditing.filterable) { + const newReportFilter: ReportFilter = { + field_name: reportColumnForEditing.accessor, + field_value: reportColumnForEditing.filter_field_value, + operator: reportColumnForEditing.filter_operator || 'equals', + }; + const existingReportFilter = getFilterByFromReportMetadata( + reportColumnForEditing.accessor + ); + if (existingReportFilter) { + const existingReportFilterIndex = + reportMetadataCopy.filter_by.indexOf(existingReportFilter); + if (reportColumnForEditing.filter_field_value) { + newReportFilters[existingReportFilterIndex] = newReportFilter; + } else { + newReportFilters.splice(existingReportFilterIndex, 1); + } + } else { + newReportFilters = newReportFilters.concat([newReportFilter]); + } + } + return newReportFilters; + }; + + const handleUpdateReportColumn = () => { + if (reportColumnToOperateOn && reportMetadata) { const reportMetadataCopy = { ...reportMetadata }; let newReportColumns = null; if (reportColumnFormMode === 'new') { @@ -680,6 +728,7 @@ export default function ProcessInstanceListTable({ } Object.assign(reportMetadataCopy, { columns: newReportColumns, + filter_by: getNewFiltersFromReportForEditing(reportColumnToOperateOn), }); setReportMetadata(reportMetadataCopy); setReportColumnToOperateOn(null); @@ -688,8 +737,27 @@ export default function ProcessInstanceListTable({ } }; + const reportColumnToReportColumnForEditing = (reportColumn: ReportColumn) => { + const reportColumnForEditing: ReportColumnForEditing = Object.assign( + reportColumn, + { filter_field_value: '', filter_operator: '' } + ); + const reportFilter = getFilterByFromReportMetadata( + reportColumnForEditing.accessor + ); + if (reportFilter) { + reportColumnForEditing.filter_field_value = reportFilter.field_value; + reportColumnForEditing.filter_operator = + reportFilter.operator || 'equals'; + } + return reportColumnForEditing; + }; + const updateReportColumn = (event: any) => { - setReportColumnToOperateOn(event.selectedItem); + const reportColumnForEditing = reportColumnToReportColumnForEditing( + event.selectedItem + ); + setReportColumnToOperateOn(reportColumnForEditing); }; // options includes item and inputValue @@ -709,7 +777,7 @@ export default function ProcessInstanceListTable({ const reportColumnToOperateOnCopy = { ...reportColumnToOperateOn, }; - reportColumnToOperateOnCopy.condition_value = event.target.value; + reportColumnToOperateOnCopy.filter_field_value = event.target.value; setReportColumnToOperateOn(reportColumnToOperateOnCopy); } }; @@ -737,7 +805,6 @@ export default function ProcessInstanceListTable({ />, ]; if (reportColumnToOperateOn && reportColumnToOperateOn.filterable) { - console.log('reportColumnToOperateOn', reportColumnToOperateOn); formElements.push( @@ -799,13 +866,16 @@ export default function ProcessInstanceListTable({ const tags: any = []; (reportColumns() as any).forEach((reportColumn: ReportColumn) => { + const reportColumnForEditing = + reportColumnToReportColumnForEditing(reportColumn); + let tagType = 'cool-gray'; - if (reportColumn.filterable) { + if (reportColumnForEditing.filterable) { tagType = 'green'; } - let reportColumnLabel = reportColumn.Header; - if (reportColumn.condition_value) { - reportColumnLabel = `${reportColumnLabel}=${reportColumn.condition_value}`; + let reportColumnLabel = reportColumnForEditing.Header; + if (reportColumnForEditing.filter_field_value) { + reportColumnLabel = `${reportColumnLabel}=${reportColumnForEditing.filter_field_value}`; } tags.push( @@ -813,9 +883,9 @@ export default function ProcessInstanceListTable({ kind="ghost" size="sm" className="button-tag-icon" - title={`Edit ${reportColumn.accessor}`} + title={`Edit ${reportColumnForEditing.accessor}`} onClick={() => { - setReportColumnToOperateOn(reportColumn); + setReportColumnToOperateOn(reportColumnForEditing); setShowReportColumnForm(true); setReportColumnFormMode('edit'); }} @@ -830,7 +900,7 @@ export default function ProcessInstanceListTable({ hasIconOnly size="sm" kind="ghost" - onClick={() => removeColumn(reportColumn)} + onClick={() => removeColumn(reportColumnForEditing)} /> ); @@ -933,11 +1003,14 @@ export default function ProcessInstanceListTable({ - + + {saveAsReportComponent()} + + - - - {saveAsReportComponent()} - - ); }; @@ -1079,7 +1148,11 @@ export default function ProcessInstanceListTable({ /> , ]; - if (processInstanceReportSelection && showFilterOptions) { + if ( + processInstanceReportSelection && + showFilterOptions && + reportMetadata + ) { columns.push( { - return `${truncateString(processInstanceReport.identifier, 20)} (${ + return `${truncateString(processInstanceReport.identifier, 20)} (Id: ${ processInstanceReport.id })`; }; diff --git a/spiffworkflow-frontend/src/config.tsx b/spiffworkflow-frontend/src/config.tsx index 5e7e96fe..b0816a39 100644 --- a/spiffworkflow-frontend/src/config.tsx +++ b/spiffworkflow-frontend/src/config.tsx @@ -14,6 +14,7 @@ export const PROCESS_STATUSES = [ 'complete', 'error', 'suspended', + 'terminated', ]; // with time: yyyy-MM-dd HH:mm:ss diff --git a/spiffworkflow-frontend/src/index.css b/spiffworkflow-frontend/src/index.css index 1c708fe1..6b02ea35 100644 --- a/spiffworkflow-frontend/src/index.css +++ b/spiffworkflow-frontend/src/index.css @@ -346,3 +346,10 @@ td.actions-cell { .combo-box-in-modal { height: 300px; } + +.cds--btn.narrow-button { + max-width: 10rem; + min-width: 5rem; + word-break: normal; + +} diff --git a/spiffworkflow-frontend/src/interfaces.ts b/spiffworkflow-frontend/src/interfaces.ts index 8d7abc45..a75b9a82 100644 --- a/spiffworkflow-frontend/src/interfaces.ts +++ b/spiffworkflow-frontend/src/interfaces.ts @@ -63,15 +63,27 @@ export interface MessageInstance { message_correlations?: MessageCorrelations; } +export interface ReportFilter { + field_name: string; + field_value: string; + operator?: string; +} + export interface ReportColumn { Header: string; accessor: string; filterable: boolean; - condition_value?: string; +} + +export interface ReportColumnForEditing extends ReportColumn { + filter_field_value: string; + filter_operator: string; } export interface ReportMetadata { columns: ReportColumn[]; + filter_by: ReportFilter[]; + order_by: string[]; } export interface ProcessInstanceReport {