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 46067031..25dc7eea 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py @@ -816,7 +816,6 @@ def process_instance_list( ) ) - # process_model_identifier = un_modify_modified_process_model_id(modified_process_model_identifier) process_instance_query = ProcessInstanceModel.query # Always join that hot user table for good performance at serialization time. process_instance_query = process_instance_query.options( @@ -983,9 +982,8 @@ def process_instance_report_column_list() -> flask.wrappers.Response: table_columns = ProcessInstanceReportService.builtin_column_options() columns_for_metadata = db.session.query(ProcessInstanceMetadataModel.key).distinct().all() # type: ignore columns_for_metadata_strings = [ - {"Header": i[0], "accessor": i[0]} for i in columns_for_metadata + {"Header": i[0], "accessor": i[0], "filterable": True} for i in columns_for_metadata ] - # columns = sorted(table_columns + columns_for_metadata_strings) return make_response(jsonify(table_columns + columns_for_metadata_strings), 200) @@ -1139,7 +1137,6 @@ def process_instance_report_show( page: int = 1, per_page: int = 100, ) -> flask.wrappers.Response: - """Process_instance_list.""" process_instances = ProcessInstanceModel.query.order_by( # .filter_by(process_model_identifier=process_model.id) ProcessInstanceModel.start_in_seconds.desc(), ProcessInstanceModel.id.desc() # type: ignore ).paginate( 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 6397cc20..3c579a24 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 @@ -243,13 +243,13 @@ class ProcessInstanceReportService: def builtin_column_options(cls) -> list[dict]: """Builtin_column_options.""" return [ - {"Header": "Id", "accessor": "id"}, + {"Header": "Id", "accessor": "id", "filterable": False}, { "Header": "Process", - "accessor": "process_model_display_name", + "accessor": "process_model_display_name", "filterable": False, }, - {"Header": "Start", "accessor": "start_in_seconds"}, - {"Header": "End", "accessor": "end_in_seconds"}, - {"Header": "Username", "accessor": "username"}, - {"Header": "Status", "accessor": "status"}, + {"Header": "Start", "accessor": "start_in_seconds", "filterable": False}, + {"Header": "End", "accessor": "end_in_seconds", "filterable": False}, + {"Header": "Username", "accessor": "username", "filterable": False}, + {"Header": "Status", "accessor": "status", "filterable": False}, ] diff --git a/spiffworkflow-frontend/src/components/ProcessInstanceListSaveAsReport.tsx b/spiffworkflow-frontend/src/components/ProcessInstanceListSaveAsReport.tsx index d70aab3e..5d93b66e 100644 --- a/spiffworkflow-frontend/src/components/ProcessInstanceListSaveAsReport.tsx +++ b/spiffworkflow-frontend/src/components/ProcessInstanceListSaveAsReport.tsx @@ -50,7 +50,6 @@ export default function ProcessInstanceListSaveAsReport({ event.preventDefault(); const orderByArray = orderBy.split(',').filter((n) => n); - const filterByArray: any = []; if (processModelSelection) { @@ -122,7 +121,7 @@ export default function ProcessInstanceListSaveAsReport({ value={identifier} onChange={(e: any) => setIdentifier(e.target.value)} /> - diff --git a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx index ebf6a446..548418b1 100644 --- a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx +++ b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx @@ -7,7 +7,7 @@ import { } from 'react-router-dom'; // @ts-ignore -import { Filter, Close } from '@carbon/icons-react'; +import { Filter, Close, AddAlt, AddFilled } from '@carbon/icons-react'; import { Button, ButtonSet, @@ -22,6 +22,11 @@ import { TableRow, TimePicker, Tag, + InlineNotification, + Stack, + Modal, + ComboBox, + TextInput, // @ts-ignore } from '@carbon/react'; import { PROCESS_STATUSES, DATE_FORMAT, DATE_FORMAT_CARBON } from '../config'; @@ -88,7 +93,7 @@ export default function ProcessInstanceListTable({ autoReload = false, }: OwnProps) { const params = useParams(); - const [searchParams] = useSearchParams(); + const [searchParams, setSearchParams] = useSearchParams(); const navigate = useNavigate(); const [processInstances, setProcessInstances] = useState([]); @@ -132,6 +137,12 @@ export default function ProcessInstanceListTable({ const [availableReportColumns, setAvailableReportColumns] = useState< ReportColumn[] >([]); + const [processInstanceReportJustSaved, setProcessInstanceReportJustSaved] = + useState(false); + const [showColumnForm, setShowColumnForm] = useState(false); + const [reportColumnToOperateOn, setReportColumnToOperateOn] = + useState(null); + const [columnFormMode, setColumnFormMode] = useState(''); const dateParametersToAlwaysFilterBy: dateParameters = useMemo(() => { return { @@ -357,6 +368,23 @@ export default function ProcessInstanceListTable({ processModelAvailableItems, ]); + const processInstanceReportSaveTag = () => { + if (processInstanceReportJustSaved) { + return ( + + ); + } + return null; + }; + // does the comparison, but also returns false if either argument // is not truthy and therefore not comparable. const isTrueComparison = (param1: any, operation: any, param2: any) => { @@ -473,6 +501,7 @@ export default function ProcessInstanceListTable({ } setErrorMessage(null); + setProcessInstanceReportJustSaved(false); navigate(`/admin/process-instances?${queryParamString}`); }; @@ -560,17 +589,22 @@ export default function ProcessInstanceListTable({ setEndToTime(''); }; - const processInstanceReportDidChange = (selection: any) => { + const processInstanceReportDidChange = ( + selection: any, + savedReport: boolean = false + ) => { clearFilters(); const selectedReport = selection.selectedItem; setProcessInstanceReportSelection(selectedReport); - const queryParamString = selectedReport - ? `&report_identifier=${selectedReport.id}` - : ''; + let queryParamString = ''; + if (selectedReport) { + queryParamString = `&report_identifier=${selectedReport.id}`; + } setErrorMessage(null); + setProcessInstanceReportJustSaved(savedReport); navigate(`/admin/process-instances?${queryParamString}`); }; @@ -578,12 +612,21 @@ export default function ProcessInstanceListTable({ return (reportMetadata as any).columns; }; + const reportColumnAccessors = () => { + return reportColumns().map((reportColumn: ReportColumn) => { + return reportColumn.accessor; + }); + }; + const saveAsReportComponent = () => { // TODO onSuccess reload/select the new report in the report search const callback = (identifier: string) => { - processInstanceReportDidChange({ - selectedItem: { id: identifier, display_name: identifier }, - }); + processInstanceReportDidChange( + { + selectedItem: { id: identifier, display_name: identifier }, + }, + true + ); }; const { valid, @@ -611,18 +654,146 @@ export default function ProcessInstanceListTable({ ); }; + const removeColumn = (reportColumn: ReportColumn) => { + const reportMetadataCopy = { ...reportMetadata }; + const newColumns = reportColumns().filter( + (rc: ReportColumn) => rc.accessor !== reportColumn.accessor + ); + Object.assign(reportMetadataCopy, { columns: newColumns }); + setReportMetadata(reportMetadataCopy); + }; + + const handleColumnFormClose = () => { + setShowColumnForm(false); + setColumnFormMode(''); + setReportColumnToOperateOn(null); + }; + + const handleUpdateColumn = () => { + if (reportColumnToOperateOn) { + const reportMetadataCopy = { ...reportMetadata }; + let newReportColumns = null; + if (columnFormMode === 'new') { + newReportColumns = reportColumns().concat([reportColumnToOperateOn]); + } else { + newReportColumns = reportColumns().map((rc: ReportColumn) => { + if (rc.accessor === reportColumnToOperateOn.accessor) { + return reportColumnToOperateOn; + } + return rc; + }); + } + Object.assign(reportMetadataCopy, { + columns: newReportColumns, + }); + setReportMetadata(reportMetadataCopy); + setReportColumnToOperateOn(null); + setShowColumnForm(false); + setShowColumnForm(false); + } + }; + + const updateReportColumn = (event: any) => { + setReportColumnToOperateOn(event.selectedItem); + }; + + // options includes item and inputValue + const shouldFilterReportColumn = (options: any) => { + const reportColumn: ReportColumn = options.item; + const { inputValue } = options; + return ( + !reportColumnAccessors().includes(reportColumn.accessor) && + (reportColumn.accessor || '') + .toLowerCase() + .includes((inputValue || '').toLowerCase()) + ); + }; + + const columnForm = () => { + if (columnFormMode === '') { + return null; + } + const formElements = [ + { + if (reportColumnToOperateOn) { + const reportColumnToOperateOnCopy = { + ...reportColumnToOperateOn, + }; + reportColumnToOperateOnCopy.Header = event.target.value; + setReportColumnToOperateOn(reportColumnToOperateOnCopy); + } + }} + />, + ]; + if (columnFormMode === 'new') { + formElements.push( + { + if (reportColumn) { + return reportColumn.accessor; + } + return null; + }} + shouldFilterItem={shouldFilterReportColumn} + placeholder="Choose a report column" + titleText="Report Column" + /> + ); + } + const modalHeading = + columnFormMode === 'new' + ? 'Add Column' + : `Edit ${ + reportColumnToOperateOn ? reportColumnToOperateOn.accessor : '' + } column`; + return ( + + {formElements} + + ); + }; + const columnSelections = () => { if (reportColumns()) { const tags: any = []; (reportColumns() as any).forEach((reportColumn: ReportColumn) => { + let tagType = 'cool-gray'; + if (reportColumn.filterable) { + tagType = 'green'; + } tags.push( - + @@ -634,12 +805,29 @@ export default function ProcessInstanceListTable({ hasIconOnly size="sm" kind="ghost" - onClick={toggleShowFilterOptions} + onClick={() => removeColumn(reportColumn)} /> ); }); - return tags; + return ( + + {tags} +