From 3dc92aec884a46232b0953aa8fe38a2d8efaed5b Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 9 Sep 2024 16:01:30 -0500 Subject: [PATCH 01/18] CE-1727 - Add inlinePossibleValueSources option to fields; pass more data into DynamicSelect in a named object, so it's a little less loosely defined. --- src/qqq/components/forms/DynamicForm.tsx | 6 +- src/qqq/components/forms/DynamicFormUtils.ts | 53 ++++++------ src/qqq/components/forms/DynamicSelect.tsx | 84 +++++++++++++++---- .../query/FilterCriteriaRowValues.tsx | 7 +- src/qqq/components/scripts/ScriptEditor.tsx | 4 +- src/qqq/components/sharing/ShareModal.tsx | 3 +- .../models/fields/FieldPossibleValueProps.ts | 38 +++++++++ .../records/query/GridFilterOperators.tsx | 8 +- 8 files changed, 142 insertions(+), 61 deletions(-) create mode 100644 src/qqq/models/fields/FieldPossibleValueProps.ts diff --git a/src/qqq/components/forms/DynamicForm.tsx b/src/qqq/components/forms/DynamicForm.tsx index 0823648..8c59440 100644 --- a/src/qqq/components/forms/DynamicForm.tsx +++ b/src/qqq/components/forms/DynamicForm.tsx @@ -172,14 +172,10 @@ function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHa {labelElement} const qController = Client.getInstance(); -function DynamicSelect({tableName, processName, fieldName, possibleValueSourceName, overrideId, fieldLabel, inForm, initialValue, initialDisplayValue, initialValues, onChange, isEditable, isMultiple, bulkEditMode, bulkEditSwitchChangeHandler, otherValues, variant, initiallyOpen, useCase}: Props) +function DynamicSelect({fieldPossibleValueProps, overrideId, fieldLabel, inForm, initialValue, initialValues, onChange, isEditable, isMultiple, bulkEditMode, bulkEditSwitchChangeHandler, otherValues, variant, initiallyOpen, useCase}: Props) { + const {fieldName, initialDisplayValue, possibleValueSourceName, possibleValues, processName, tableName} = fieldPossibleValueProps; + const [open, setOpen] = useState(initiallyOpen); const [options, setOptions] = useState([]); const [searchTerm, setSearchTerm] = useState(null); @@ -172,6 +166,35 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa setFieldValueRef = setFieldValue; } + + /******************************************************************************* + ** + *******************************************************************************/ + const filterInlinePossibleValues = (searchTerm: string, possibleValues: QPossibleValue[]): QPossibleValue[] => + { + return possibleValues.filter(pv => pv.label?.toLowerCase().startsWith(searchTerm)); + } + + + /*************************************************************************** + ** + ***************************************************************************/ + const loadResults = async (): Promise => + { + if(possibleValues) + { + return filterInlinePossibleValues(searchTerm, possibleValues) + } + else + { + return await qController.possibleValues(tableName, processName, possibleValueSourceName ?? fieldName, searchTerm ?? "", null, otherValues, useCase); + } + } + + + /*************************************************************************** + ** + ***************************************************************************/ useEffect(() => { if (firstRender) @@ -195,7 +218,7 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa (async () => { // console.log(`doing a search with ${searchTerm}`); - const results: QPossibleValue[] = await qController.possibleValues(tableName, processName, possibleValueSourceName ?? fieldName, searchTerm ?? "", null, otherValues, useCase); + const results: QPossibleValue[] = await loadResults(); if (tableMetaData == null && tableName) { @@ -218,7 +241,10 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa }; }, [searchTerm]); - // todo - finish... call it in onOpen? + + /*************************************************************************** + ** todo - finish... call it in onOpen? + ***************************************************************************/ const reloadIfOtherValuesAreChanged = () => { if (JSON.stringify(Object.fromEntries(otherValues)) != otherValuesWhenResultsWereLoaded) @@ -227,8 +253,10 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa { setLoading(true); setOptions([]); + console.log("Refreshing possible values..."); - const results: QPossibleValue[] = await qController.possibleValues(tableName, processName, possibleValueSourceName ?? fieldName, searchTerm ?? "", null, otherValues, useCase); + const results: QPossibleValue[] = await loadResults(); + setLoading(false); setOptions([...results]); setOtherValuesWhenResultsWereLoaded(JSON.stringify(Object.fromEntries(otherValues))); @@ -236,6 +264,10 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa } }; + + /*************************************************************************** + ** + ***************************************************************************/ const inputChanged = (event: React.SyntheticEvent, value: string, reason: string) => { // console.log(`input changed. Reason: ${reason}, setting search term to ${value}`); @@ -246,11 +278,19 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa } }; + + /*************************************************************************** + ** + ***************************************************************************/ const handleBlur = (x: any) => { setSearchTerm(null); }; + + /*************************************************************************** + ** + ***************************************************************************/ const handleChanged = (event: React.SyntheticEvent, value: any | any[], reason: string, details?: string) => { // console.log("handleChanged. value is:"); @@ -274,6 +314,10 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa } }; + + /*************************************************************************** + ** + ***************************************************************************/ const filterOptions = (options: { id: any; label: string; }[], state: FilterOptionsState<{ id: any; label: string; }>): { id: any; label: string; }[] => { ///////////////////////////////////////////////////////////////////////////////// @@ -283,6 +327,10 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa return (options); }; + + /*************************************************************************** + ** + ***************************************************************************/ // @ts-ignore const renderOption = (props: Object, option: any, {selected}) => { @@ -331,6 +379,10 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa ); }; + + /*************************************************************************** + ** + ***************************************************************************/ const bulkEditSwitchChanged = () => { const newSwitchValue = !switchChecked; @@ -351,7 +403,7 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa const autocomplete = ( - {!isDisabled &&
{msg}} />
} + {!isDisabled &&
{msg}} />
}
} diff --git a/src/qqq/components/query/FilterCriteriaRowValues.tsx b/src/qqq/components/query/FilterCriteriaRowValues.tsx index 793497a..0e4fa18 100644 --- a/src/qqq/components/query/FilterCriteriaRowValues.tsx +++ b/src/qqq/components/query/FilterCriteriaRowValues.tsx @@ -367,13 +367,11 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC ) : ( valueChangeHandler(null, 0, value)} variant="standard" @@ -402,8 +400,7 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC } return - + - + diff --git a/src/qqq/components/sharing/ShareModal.tsx b/src/qqq/components/sharing/ShareModal.tsx index 1a83082..2c7e73f 100644 --- a/src/qqq/components/sharing/ShareModal.tsx +++ b/src/qqq/components/sharing/ShareModal.tsx @@ -391,10 +391,9 @@ export default function ShareModal({open, onClose, tableMetaData, record}: Share . + */ + +import {QPossibleValue} from "@kingsrook/qqq-frontend-core/lib/model/QPossibleValue"; + +/******************************************************************************* + ** Properties attached to a (formik?) form field, to denote how it behaves as + ** as related to a possible value source. + *******************************************************************************/ +export interface FieldPossibleValueProps +{ + isPossibleValue?: boolean; + possibleValues?: QPossibleValue[]; + initialDisplayValue: string | null; + fieldName?: string; + tableName?: string; + processName?: string; + possibleValueSourceName?: string; +} + diff --git a/src/qqq/pages/records/query/GridFilterOperators.tsx b/src/qqq/pages/records/query/GridFilterOperators.tsx index c9b9517..fbf7a57 100644 --- a/src/qqq/pages/records/query/GridFilterOperators.tsx +++ b/src/qqq/pages/records/query/GridFilterOperators.tsx @@ -779,11 +779,9 @@ function InputPossibleValueSourceSingle(tableName: string, field: QFieldMetaData }} > Date: Mon, 9 Sep 2024 16:04:16 -0500 Subject: [PATCH 02/18] CE-1727 - Switch from use of updatedFrontendStepList to processMetaDataAdjustment, which can include updatedFields (specifically adding to change a dynamic possible value source, but, seems more flexible than just that) --- src/qqq/pages/processes/ProcessRun.tsx | 56 ++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/src/qqq/pages/processes/ProcessRun.tsx b/src/qqq/pages/processes/ProcessRun.tsx index bea6f0b..5a409c8 100644 --- a/src/qqq/pages/processes/ProcessRun.tsx +++ b/src/qqq/pages/processes/ProcessRun.tsx @@ -120,6 +120,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is const [activeStepIndex, setActiveStepIndex] = useState(0); const [activeStep, setActiveStep] = useState(null as QFrontendStepMetaData); const [newStep, setNewStep] = useState(null); + const [stepInstanceCounter, setStepInstanceCounter] = useState(0); const [steps, setSteps] = useState([] as QFrontendStepMetaData[]); const [needInitialLoad, setNeedInitialLoad] = useState(true); const [lastForcedReInit, setLastForcedReInit] = useState(null as number); @@ -136,6 +137,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is ); const [showErrorDetail, setShowErrorDetail] = useState(false); const [showFullHelpText, setShowFullHelpText] = useState(false); + const [previouslySeenUpdatedFieldMetaDataMap, setPreviouslySeenUpdatedFieldMetaDataMap] = useState(new Map); const [renderedWidgets, setRenderedWidgets] = useState({} as { [step: string]: { [widgetName: string]: any } }); @@ -994,7 +996,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is setValidationFunction(() => true); } } - }, [newStep]); + }, [newStep, stepInstanceCounter]); // maybe we could just use stepInstanceCounter... ///////////////////////////////////////////////////////////////////////////////////////////// // if there are records to load: build a record config, and set the needRecords state flag // @@ -1088,6 +1090,47 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is } }, [needRecords]); + + /*************************************************************************** + ** + ***************************************************************************/ + function updateFieldsInProcess(steps: QFrontendStepMetaData[], updatedFields: Map) + { + if (updatedFields) + { + updatedFields.forEach((field) => previouslySeenUpdatedFieldMetaDataMap.set(field.name, field)); + setPreviouslySeenUpdatedFieldMetaDataMap(previouslySeenUpdatedFieldMetaDataMap) + } + + for (let step of steps) + { + if (step && step.formFields) + { + for (let i = 0; i < step.formFields.length; i++) + { + let field = step.formFields[i]; + if (previouslySeenUpdatedFieldMetaDataMap.has(field.name)) + { + step.formFields[i] = previouslySeenUpdatedFieldMetaDataMap.get(field.name); + } + } + } + } + + if (processValues.inputFieldList) + { + for (let i = 0; i < processValues.inputFieldList.length; i++) + { + let field = new QFieldMetaData(processValues.inputFieldList[i]); + if (previouslySeenUpdatedFieldMetaDataMap.has(field.name)) + { + processValues.inputFieldList[i] = previouslySeenUpdatedFieldMetaDataMap.get(field.name); // todo - uh, not an object? + } + } + } + } + + ////////////////////////////////////////////////////////////////////////////////////////////////////////// // handle a response from the server - e.g., after starting a backend job, or getting its status/result // ////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -1112,13 +1155,19 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is // if the process step sent a new frontend-step-list, then refresh what we have in state (constructing new full model objects) // ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// let frontendSteps = steps; - const updatedFrontendStepList = qJobComplete.updatedFrontendStepList; + const updatedFrontendStepList = qJobComplete.processMetaDataAdjustment?.updatedFrontendStepList; if (updatedFrontendStepList) { - setSteps(updatedFrontendStepList); frontendSteps = updatedFrontendStepList; + setSteps(frontendSteps); } + //////////////////////////////////////////////////////////////////////////////////////////////////////// + // always merge the new updatedFields map (if there is one) with existing updates and existing fields // + //////////////////////////////////////////////////////////////////////////////////////////////////////// + updateFieldsInProcess(frontendSteps, qJobComplete.processMetaDataAdjustment?.updatedFields); + setSteps(frontendSteps); + /////////////////////////////////////////////////////////////////////////////////// // if the next screen has any PVS fields - look up their labels (display values) // /////////////////////////////////////////////////////////////////////////////////// @@ -1159,6 +1208,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is setJobUUID(null); setNewStep(nextStepName); + setStepInstanceCounter(1 + stepInstanceCounter); setProcessValues(newValues); setQJobRunning(null); From dc8fdb33dc5797e9bd5923ed28133aae51e23bca Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Fri, 20 Sep 2024 10:42:22 -0500 Subject: [PATCH 03/18] CE-1727 - New blocks and styles for (mobile-style) widgets in processes, plus callbacks and contextValues --- src/qqq/components/widgets/WidgetBlock.tsx | 17 ++- .../widgets/blocks/ActionButtonBlock.tsx | 60 ++++++++ .../components/widgets/blocks/AudioBlock.tsx | 40 ++++++ .../components/widgets/blocks/ImageBlock.tsx | 59 ++++++++ .../widgets/blocks/InputFieldBlock.tsx | 128 ++++++++++++++++++ .../components/widgets/blocks/TextBlock.tsx | 49 ++++++- 6 files changed, 350 insertions(+), 3 deletions(-) create mode 100644 src/qqq/components/widgets/blocks/ActionButtonBlock.tsx create mode 100644 src/qqq/components/widgets/blocks/AudioBlock.tsx create mode 100644 src/qqq/components/widgets/blocks/ImageBlock.tsx create mode 100644 src/qqq/components/widgets/blocks/InputFieldBlock.tsx diff --git a/src/qqq/components/widgets/WidgetBlock.tsx b/src/qqq/components/widgets/WidgetBlock.tsx index fab8f6d..1115741 100644 --- a/src/qqq/components/widgets/WidgetBlock.tsx +++ b/src/qqq/components/widgets/WidgetBlock.tsx @@ -22,6 +22,9 @@ import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData"; import {Alert, Skeleton} from "@mui/material"; +import ActionButtonBlock from "qqq/components/widgets/blocks/ActionButtonBlock"; +import AudioBlock from "qqq/components/widgets/blocks/AudioBlock"; +import InputFieldBlock from "qqq/components/widgets/blocks/InputFieldBlock"; import React from "react"; import BigNumberBlock from "qqq/components/widgets/blocks/BigNumberBlock"; import {BlockData} from "qqq/components/widgets/blocks/BlockModels"; @@ -32,19 +35,21 @@ import TableSubRowDetailRowBlock from "qqq/components/widgets/blocks/TableSubRow import TextBlock from "qqq/components/widgets/blocks/TextBlock"; import UpOrDownNumberBlock from "qqq/components/widgets/blocks/UpOrDownNumberBlock"; import CompositeWidget from "qqq/components/widgets/CompositeWidget"; +import ImageBlock from "./blocks/ImageBlock"; interface WidgetBlockProps { widgetMetaData: QWidgetMetaData; block: BlockData; + actionCallback?: (blockData: BlockData) => boolean; } /******************************************************************************* ** Component to render a single Block in the widget framework! *******************************************************************************/ -export default function WidgetBlock({widgetMetaData, block}: WidgetBlockProps): JSX.Element +export default function WidgetBlock({widgetMetaData, block, actionCallback}: WidgetBlockProps): JSX.Element { if(!block) { @@ -64,7 +69,7 @@ export default function WidgetBlock({widgetMetaData, block}: WidgetBlockProps): if(block.blockTypeName == "COMPOSITE") { // @ts-ignore - special case for composite type block... - return (); + return (); } switch(block.blockTypeName) @@ -83,6 +88,14 @@ export default function WidgetBlock({widgetMetaData, block}: WidgetBlockProps): return (); case "BIG_NUMBER": return (); + case "INPUT_FIELD": + return (); + case "ACTION_BUTTON": + return (); + case "AUDIO": + return (); + case "IMAGE": + return (); default: return (Unsupported block type: {block.blockTypeName}) } diff --git a/src/qqq/components/widgets/blocks/ActionButtonBlock.tsx b/src/qqq/components/widgets/blocks/ActionButtonBlock.tsx new file mode 100644 index 0000000..8b0e2ec --- /dev/null +++ b/src/qqq/components/widgets/blocks/ActionButtonBlock.tsx @@ -0,0 +1,60 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import Box from "@mui/material/Box"; +import Icon from "@mui/material/Icon"; +import {standardWidth} from "qqq/components/buttons/DefaultButtons"; +import MDButton from "qqq/components/legacy/MDButton"; +import BlockElementWrapper from "qqq/components/widgets/blocks/BlockElementWrapper"; +import {StandardBlockComponentProps} from "qqq/components/widgets/blocks/BlockModels"; +import React from "react"; + + +/******************************************************************************* + ** Block that renders ... an action button... + ** + *******************************************************************************/ +export default function ActionButtonBlock({widgetMetaData, data, actionCallback}: StandardBlockComponentProps): JSX.Element +{ + const icon = data.values.iconName ? {data.values.iconName} : null; + + function onClick() + { + if(actionCallback) + { + actionCallback(data, {actionCode: data.values?.actionCode}) + } + else + { + console.log("ActionButtonBlock onClick with no actionCallback present, so, noop"); + } + } + + return ( + + + + {data.values.label ?? "Action"} + + + + ); +} diff --git a/src/qqq/components/widgets/blocks/AudioBlock.tsx b/src/qqq/components/widgets/blocks/AudioBlock.tsx new file mode 100644 index 0000000..210eaaf --- /dev/null +++ b/src/qqq/components/widgets/blocks/AudioBlock.tsx @@ -0,0 +1,40 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import Box from "@mui/material/Box"; +import BlockElementWrapper from "qqq/components/widgets/blocks/BlockElementWrapper"; +import {StandardBlockComponentProps} from "qqq/components/widgets/blocks/BlockModels"; +import DumpJsonBox from "qqq/utils/DumpJsonBox"; +import React from "react"; + +/******************************************************************************* + ** Block that renders ... an audio tag + ** + **