Compare commits

...

13 Commits

Author SHA1 Message Date
27dbc72db4 CE-1727 - Add standardColor and isAlert 2024-09-20 19:39:16 -05:00
983a93d38c CE-1727 - Remove un-intentional commit of bulk-load-mapping wip changes 2024-09-20 10:04:38 -05:00
28ad0661d1 CE-1727 - Mark as serializable 2024-09-20 10:00:11 -05:00
a8e235c155 CE-1727 - Update JSON serialization at the end of doProcessInitOrStep to specify to include null and empty values 2024-09-20 09:59:59 -05:00
c96bb9dda8 CE-1727 - Add support for QCodeReferenceLambda 2024-09-20 09:57:30 -05:00
e4bef88406 CE-1727 - Introduce concept of stepFlow to processes - LINEAR (the previous), and STATE_MACHINE (designed to be more flexible) 2024-09-20 09:57:02 -05:00
c18aa44010 CE-1727 - Initial checkin 2024-09-20 09:53:41 -05:00
47e95d74e3 CE-1727 - Add 'conditional' attribute 2024-09-20 09:47:35 -05:00
cf4c6d2144 CE-1727 - migrating from updatedFrontendStepList to processMetaDataAdjustment - so one can update fields in a process (original intent for inline PVS's) 2024-09-20 09:44:37 -05:00
780341b5cc CE-1727 - Initial checkin 2024-09-20 09:19:29 -05:00
20d4a9ffeb CE-1727 - new Layout (FLEX_ROW_CENTER) 2024-09-20 09:16:30 -05:00
4f1310ded9 CE-1727 - Initial checkin of new blocks 2024-09-20 09:15:59 -05:00
fb16a041fb CE-1727 - Add inlinePossibleValueSources option to fields 2024-09-09 11:53:01 -05:00
34 changed files with 2452 additions and 228 deletions

View File

@ -40,6 +40,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReferenceLambda;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMetaData;
@ -257,11 +258,20 @@ public class RunBackendStepAction
{ {
runBackendStepOutput.seedFromRequest(runBackendStepInput); runBackendStepOutput.seedFromRequest(runBackendStepInput);
Class<?> codeClass = Class.forName(code.getName()); Object codeObject;
Object codeObject = codeClass.getConstructor().newInstance(); if(code instanceof QCodeReferenceLambda<?> qCodeReferenceLambda)
{
codeObject = qCodeReferenceLambda.getLambda();
}
else
{
Class<?> codeClass = Class.forName(code.getName());
codeObject = codeClass.getConstructor().newInstance();
}
if(!(codeObject instanceof BackendStep backendStepCodeObject)) if(!(codeObject instanceof BackendStep backendStepCodeObject))
{ {
throw (new QException("The supplied code [" + codeClass.getName() + "] is not an instance of BackendStep")); throw (new QException("The supplied codeReference [" + code + "] is not a reference to a BackendStep"));
} }
backendStepCodeObject.run(runBackendStepInput, runBackendStepOutput); backendStepCodeObject.run(runBackendStepInput, runBackendStepOutput);

View File

@ -28,6 +28,7 @@ import java.time.temporal.ChronoUnit;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import com.kingsrook.qqq.backend.core.actions.ActionHelper; import com.kingsrook.qqq.backend.core.actions.ActionHelper;
@ -58,6 +59,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaD
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponentMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponentMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QStateMachineStep;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.processes.implementations.basepull.BasepullConfiguration; import com.kingsrook.qqq.backend.core.processes.implementations.basepull.BasepullConfiguration;
@ -133,90 +135,11 @@ public class RunProcessAction
try try
{ {
String lastStepName = runProcessInput.getStartAfterStep(); switch(Objects.requireNonNull(process.getStepFlow(), "Process [" + process.getName() + "] has a null stepFlow."))
STEP_LOOP:
while(true)
{ {
/////////////////////////////////////////////////////////////////////////////////////////////////////// case LINEAR -> runLinearStepLoop(process, processState, stateKey, runProcessInput, runProcessOutput);
// always refresh the step list - as any step that runs can modify it (in the process state). // case STATE_MACHINE -> runStateMachineStep(runProcessInput.getStartAfterStep(), process, processState, stateKey, runProcessInput, runProcessOutput, 0);
// this is why we don't do a loop over the step list - as we'd get ConcurrentModificationExceptions. // default -> throw (new QException("Unhandled process step flow: " + process.getStepFlow()));
///////////////////////////////////////////////////////////////////////////////////////////////////////
List<QStepMetaData> stepList = getAvailableStepList(processState, process, lastStepName);
if(stepList.isEmpty())
{
break;
}
QStepMetaData step = stepList.get(0);
lastStepName = step.getName();
if(step instanceof QFrontendStepMetaData frontendStep)
{
////////////////////////////////////////////////////////////////
// Handle what to do with frontend steps, per request setting //
////////////////////////////////////////////////////////////////
switch(runProcessInput.getFrontendStepBehavior())
{
case BREAK ->
{
LOG.trace("Breaking process [" + process.getName() + "] at frontend step (as requested by caller): " + step.getName());
processFrontendStepFieldDefaultValues(processState, frontendStep);
processFrontendComponents(processState, frontendStep);
processState.setNextStepName(step.getName());
break STEP_LOOP;
}
case SKIP ->
{
LOG.trace("Skipping frontend step [" + step.getName() + "] in process [" + process.getName() + "] (as requested by caller)");
//////////////////////////////////////////////////////////////////////
// much less error prone in case this code changes in the future... //
//////////////////////////////////////////////////////////////////////
// noinspection UnnecessaryContinue
continue;
}
case FAIL ->
{
LOG.trace("Throwing error for frontend step [" + step.getName() + "] in process [" + process.getName() + "] (as requested by caller)");
throw (new QException("Failing process at step " + step.getName() + " (as requested, to fail on frontend steps)"));
}
default -> throw new IllegalStateException("Unexpected value: " + runProcessInput.getFrontendStepBehavior());
}
}
else if(step instanceof QBackendStepMetaData backendStepMetaData)
{
///////////////////////
// Run backend steps //
///////////////////////
LOG.debug("Running backend step [" + step.getName() + "] in process [" + process.getName() + "]");
RunBackendStepOutput runBackendStepOutput = runBackendStep(runProcessInput, process, runProcessOutput, stateKey, backendStepMetaData, process, processState);
/////////////////////////////////////////////////////////////////////////////////////////
// if the step returned an override lastStepName, use that to determine how we proceed //
/////////////////////////////////////////////////////////////////////////////////////////
if(runBackendStepOutput.getOverrideLastStepName() != null)
{
LOG.debug("Process step [" + lastStepName + "] returned an overrideLastStepName [" + runBackendStepOutput.getOverrideLastStepName() + "]!");
lastStepName = runBackendStepOutput.getOverrideLastStepName();
}
/////////////////////////////////////////////////////////////////////////////////////////////
// similarly, if the step produced an updatedFrontendStepList, propagate that data outward //
/////////////////////////////////////////////////////////////////////////////////////////////
if(runBackendStepOutput.getUpdatedFrontendStepList() != null)
{
LOG.debug("Process step [" + lastStepName + "] generated an updatedFrontendStepList [" + runBackendStepOutput.getUpdatedFrontendStepList().stream().map(s -> s.getName()).toList() + "]!");
runProcessOutput.setUpdatedFrontendStepList(runBackendStepOutput.getUpdatedFrontendStepList());
}
}
else
{
//////////////////////////////////////////////////
// in case we have a different step type, throw //
//////////////////////////////////////////////////
throw (new QException("Unsure how to run a step of type: " + step.getClass().getName()));
}
} }
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
@ -258,6 +181,270 @@ public class RunProcessAction
/***************************************************************************
**
***************************************************************************/
private void runLinearStepLoop(QProcessMetaData process, ProcessState processState, UUIDAndTypeStateKey stateKey, RunProcessInput runProcessInput, RunProcessOutput runProcessOutput) throws Exception
{
String lastStepName = runProcessInput.getStartAfterStep();
while(true)
{
///////////////////////////////////////////////////////////////////////////////////////////////////////
// always refresh the step list - as any step that runs can modify it (in the process state). //
// this is why we don't do a loop over the step list - as we'd get ConcurrentModificationExceptions. //
///////////////////////////////////////////////////////////////////////////////////////////////////////
List<QStepMetaData> stepList = getAvailableStepList(processState, process, lastStepName);
if(stepList.isEmpty())
{
break;
}
QStepMetaData step = stepList.get(0);
lastStepName = step.getName();
if(step instanceof QFrontendStepMetaData frontendStep)
{
LoopTodo loopTodo = prepareForFrontendStep(runProcessInput, process, frontendStep, processState);
if(loopTodo == LoopTodo.BREAK)
{
break;
}
}
else if(step instanceof QBackendStepMetaData backendStepMetaData)
{
RunBackendStepOutput runBackendStepOutput = runBackendStep(process, processState, stateKey, runProcessInput, runProcessOutput, backendStepMetaData, step);
/////////////////////////////////////////////////////////////////////////////////////////
// if the step returned an override lastStepName, use that to determine how we proceed //
/////////////////////////////////////////////////////////////////////////////////////////
if(runBackendStepOutput.getOverrideLastStepName() != null)
{
LOG.debug("Process step [" + lastStepName + "] returned an overrideLastStepName [" + runBackendStepOutput.getOverrideLastStepName() + "]!");
lastStepName = runBackendStepOutput.getOverrideLastStepName();
}
}
else
{
//////////////////////////////////////////////////
// in case we have a different step type, throw //
//////////////////////////////////////////////////
throw (new QException("Unsure how to run a step of type: " + step.getClass().getName()));
}
}
}
/***************************************************************************
**
***************************************************************************/
private enum LoopTodo
{
BREAK,
CONTINUE
}
/***************************************************************************
**
***************************************************************************/
private LoopTodo prepareForFrontendStep(RunProcessInput runProcessInput, QProcessMetaData process, QFrontendStepMetaData step, ProcessState processState) throws QException
{
////////////////////////////////////////////////////////////////
// Handle what to do with frontend steps, per request setting //
////////////////////////////////////////////////////////////////
switch(runProcessInput.getFrontendStepBehavior())
{
case BREAK ->
{
LOG.trace("Breaking process [" + process.getName() + "] at frontend step (as requested by caller): " + step.getName());
processFrontendStepFieldDefaultValues(processState, step);
processFrontendComponents(processState, step);
processState.setNextStepName(step.getName());
return LoopTodo.BREAK;
}
case SKIP ->
{
LOG.trace("Skipping frontend step [" + step.getName() + "] in process [" + process.getName() + "] (as requested by caller)");
return LoopTodo.CONTINUE;
}
case FAIL ->
{
LOG.trace("Throwing error for frontend step [" + step.getName() + "] in process [" + process.getName() + "] (as requested by caller)");
throw (new QException("Failing process at step " + step.getName() + " (as requested, to fail on frontend steps)"));
}
default -> throw new IllegalStateException("Unexpected value: " + runProcessInput.getFrontendStepBehavior());
}
}
/***************************************************************************
**
***************************************************************************/
private void runStateMachineStep(String lastStepName, QProcessMetaData process, ProcessState processState, UUIDAndTypeStateKey stateKey, RunProcessInput runProcessInput, RunProcessOutput runProcessOutput, int stackDepth) throws Exception
{
//////////////////////////////
// check for stack-overflow //
//////////////////////////////
Integer maxStateMachineProcessStepFlowStackDepth = Objects.requireNonNullElse(runProcessInput.getValueInteger("maxStateMachineProcessStepFlowStackDepth"), 20);
if(stackDepth > maxStateMachineProcessStepFlowStackDepth)
{
throw (new QException("StateMachine process recurred too many times (exceeded maxStateMachineProcessStepFlowStackDepth of " + maxStateMachineProcessStepFlowStackDepth + ")"));
}
//////////////////////////////////
// figure out what step to run: //
//////////////////////////////////
QStepMetaData step = null;
if(!StringUtils.hasContent(lastStepName))
{
////////////////////////////////////////////////////////////////////
// if no lastStepName is given, start at the process's first step //
////////////////////////////////////////////////////////////////////
if(CollectionUtils.nullSafeIsEmpty(process.getStepList()))
{
throw (new QException("Process [" + process.getName() + "] does not have a step list defined."));
}
step = process.getStepList().get(0);
}
else
{
/////////////////////////////////////
// else run the given lastStepName //
/////////////////////////////////////
processState.clearNextStepName();
step = process.getStep(lastStepName);
if(step == null)
{
throw (new QException("Could not find step by name [" + lastStepName + "]"));
}
}
/////////////////////////////////////////////////////////////////////////
// for the flow of: //
// we were on a frontend step (as a sub-step of a state machine step), //
// and now we're here to run that state-step's backend step - //
// find the state-machine step containing this frontend step. //
/////////////////////////////////////////////////////////////////////////
String skipSubStepsUntil = null;
if(step instanceof QFrontendStepMetaData frontendStepMetaData)
{
QStateMachineStep stateMachineStep = getStateMachineStepContainingSubStep(process, frontendStepMetaData.getName());
if(stateMachineStep == null)
{
throw (new QException("Could not find stateMachineStep that contains last-frontend step: " + frontendStepMetaData.getName()));
}
step = stateMachineStep;
//////////////////////////////////////////////////////////////////////////////////
// set this flag, to know to skip this frontend step in the sub-step loop below //
//////////////////////////////////////////////////////////////////////////////////
skipSubStepsUntil = frontendStepMetaData.getName();
}
if(!(step instanceof QStateMachineStep stateMachineStep))
{
throw (new QException("Have a non-stateMachineStep in a process using stateMachine flow... " + step.getClass().getName()));
}
///////////////////////
// run the sub-steps //
///////////////////////
boolean ranAnySubSteps = false;
for(QStepMetaData subStep : stateMachineStep.getSubSteps())
{
///////////////////////////////////////////////////////////////////////////////////////////////
// ok, well, skip them if this flag is set (and clear the flag once we've hit this sub-step) //
///////////////////////////////////////////////////////////////////////////////////////////////
if(skipSubStepsUntil != null)
{
if(skipSubStepsUntil.equals(subStep.getName()))
{
skipSubStepsUntil = null;
}
continue;
}
ranAnySubSteps = true;
if(subStep instanceof QFrontendStepMetaData frontendStep)
{
LoopTodo loopTodo = prepareForFrontendStep(runProcessInput, process, frontendStep, processState);
if(loopTodo == LoopTodo.BREAK)
{
return;
}
}
else if(subStep instanceof QBackendStepMetaData backendStepMetaData)
{
RunBackendStepOutput runBackendStepOutput = runBackendStep(process, processState, stateKey, runProcessInput, runProcessOutput, backendStepMetaData, step);
Optional<String> nextStepName = runBackendStepOutput.getProcessState().getNextStepName();
if(nextStepName.isEmpty() && StringUtils.hasContent(stateMachineStep.getDefaultNextStepName()))
{
nextStepName = Optional.of(stateMachineStep.getDefaultNextStepName());
}
if(nextStepName.isPresent())
{
//////////////////////////////////////////////////////////////////////////////////////////////////////
// if we've been given a next-step-name, go to that step now. //
// it might be a backend-only stateMachineStep, in which case, we should run that backend step now. //
// or it might be a frontend-then-backend step, in which case, we want to go to that frontend step. //
// if we weren't given a next-step-name, then we should stay in the same state - either to finish //
// its sub-steps, or, to fall out of the loop and end the process. //
//////////////////////////////////////////////////////////////////////////////////////////////////////
processState.clearNextStepName();
runStateMachineStep(nextStepName.get(), process, processState, stateKey, runProcessInput, runProcessOutput, stackDepth + 1);
return;
}
}
else
{
//////////////////////////////////////////////////
// in case we have a different step type, throw //
//////////////////////////////////////////////////
throw (new QException("Unsure how to run a step of type: " + step.getClass().getName()));
}
}
if(!ranAnySubSteps)
{
if(StringUtils.hasContent(stateMachineStep.getDefaultNextStepName()))
{
runStateMachineStep(stateMachineStep.getDefaultNextStepName(), process, processState, stateKey, runProcessInput, runProcessOutput, stackDepth + 1);
}
}
}
/*******************************************************************************
**
*******************************************************************************/
public QStateMachineStep getStateMachineStepContainingSubStep(QProcessMetaData process, String stepName)
{
for(QStepMetaData step : process.getAllSteps().values())
{
if(step instanceof QStateMachineStep stateMachineStep)
{
for(QStepMetaData subStep : stateMachineStep.getSubSteps())
{
if(subStep.getName().equals(stepName))
{
return (stateMachineStep);
}
}
}
}
return (null);
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@ -335,12 +522,12 @@ public class RunProcessAction
/////////////////////////////////////////////////// ///////////////////////////////////////////////////
runProcessInput.seedFromProcessState(optionalProcessState.get()); runProcessInput.seedFromProcessState(optionalProcessState.get());
/////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////
// if we're restoring an old state, we can discard a previously stored updatedFrontendStepList - // // if we're restoring an old state, we can discard a previously stored processMetaDataAdjustment - //
// it is only needed on the transitional edge from a backend-step to a frontend step, but not // // it is only needed on the transitional edge from a backend-step to a frontend step, but not //
// in the other directly // // in the other directly //
/////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////
optionalProcessState.get().setUpdatedFrontendStepList(null); optionalProcessState.get().setProcessMetaDataAdjustment(null);
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
// if there were values from the caller, put those (back) in the request // // if there were values from the caller, put those (back) in the request //
@ -355,16 +542,40 @@ public class RunProcessAction
} }
ProcessState processState = optionalProcessState.get(); ProcessState processState = optionalProcessState.get();
processState.clearNextStepName();
return processState; return processState;
} }
/***************************************************************************
**
***************************************************************************/
private RunBackendStepOutput runBackendStep(QProcessMetaData process, ProcessState processState, UUIDAndTypeStateKey stateKey, RunProcessInput runProcessInput, RunProcessOutput runProcessOutput, QBackendStepMetaData backendStepMetaData, QStepMetaData step) throws Exception
{
///////////////////////
// Run backend steps //
///////////////////////
LOG.debug("Running backend step [" + step.getName() + "] in process [" + process.getName() + "]");
RunBackendStepOutput runBackendStepOutput = runBackendStep(runProcessInput, process, runProcessOutput, stateKey, backendStepMetaData, process, processState);
//////////////////////////////////////////////////////////////////////////////////////////////
// similarly, if the step produced a processMetaDataAdjustment, propagate that data outward //
//////////////////////////////////////////////////////////////////////////////////////////////
if(runBackendStepOutput.getProcessMetaDataAdjustment() != null)
{
LOG.debug("Process step [" + step.getName() + "] generated a ProcessMetaDataAdjustment [" + runBackendStepOutput.getProcessMetaDataAdjustment() + "]!");
runProcessOutput.setProcessMetaDataAdjustment(runBackendStepOutput.getProcessMetaDataAdjustment());
}
return runBackendStepOutput;
}
/******************************************************************************* /*******************************************************************************
** Run a single backend step. ** Run a single backend step.
*******************************************************************************/ *******************************************************************************/
protected RunBackendStepOutput runBackendStep(RunProcessInput runProcessInput, QProcessMetaData process, RunProcessOutput runProcessOutput, UUIDAndTypeStateKey stateKey, QBackendStepMetaData backendStep, QProcessMetaData qProcessMetaData, ProcessState processState) throws Exception RunBackendStepOutput runBackendStep(RunProcessInput runProcessInput, QProcessMetaData process, RunProcessOutput runProcessOutput, UUIDAndTypeStateKey stateKey, QBackendStepMetaData backendStep, QProcessMetaData qProcessMetaData, ProcessState processState) throws Exception
{ {
RunBackendStepInput runBackendStepInput = new RunBackendStepInput(processState); RunBackendStepInput runBackendStepInput = new RunBackendStepInput(processState);
runBackendStepInput.setProcessName(process.getName()); runBackendStepInput.setProcessName(process.getName());

View File

@ -58,6 +58,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QComponentType;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponentMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponentMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QStateMachineStep;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QSupplementalProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QSupplementalProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportDataSource; import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportDataSource;
@ -410,10 +411,27 @@ public class QInstanceEnricher
** **
*******************************************************************************/ *******************************************************************************/
private void enrichStep(QStepMetaData step) private void enrichStep(QStepMetaData step)
{
enrichStep(step, false);
}
/***************************************************************************
**
***************************************************************************/
private void enrichStep(QStepMetaData step, boolean isSubStep)
{ {
if(!StringUtils.hasContent(step.getLabel())) if(!StringUtils.hasContent(step.getLabel()))
{ {
step.setLabel(nameToLabel(step.getName())); if(isSubStep && (step.getName().endsWith(".backend") || step.getName().endsWith(".frontend")))
{
step.setLabel(nameToLabel(step.getName().replaceFirst("\\.(backend|frontend)", "")));
}
else
{
step.setLabel(nameToLabel(step.getName()));
}
} }
step.getInputFields().forEach(this::enrichField); step.getInputFields().forEach(this::enrichField);
@ -434,6 +452,13 @@ public class QInstanceEnricher
frontendStepMetaData.getRecordListFields().forEach(this::enrichField); frontendStepMetaData.getRecordListFields().forEach(this::enrichField);
} }
} }
else if(step instanceof QStateMachineStep stateMachineStep)
{
for(QStepMetaData subStep : CollectionUtils.nonNullList(stateMachineStep.getSubSteps()))
{
enrichStep(subStep, true);
}
}
} }

View File

@ -74,6 +74,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppSection; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppSection;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
@ -928,13 +929,8 @@ public class QInstanceValidator
assertCondition(Objects.equals(fieldName, field.getName()), assertCondition(Objects.equals(fieldName, field.getName()),
"Inconsistent naming in table " + tableName + " for field " + fieldName + "/" + field.getName() + "."); "Inconsistent naming in table " + tableName + " for field " + fieldName + "/" + field.getName() + ".");
if(field.getPossibleValueSourceName() != null)
{
assertCondition(qInstance.getPossibleValueSource(field.getPossibleValueSourceName()) != null,
"Unrecognized possibleValueSourceName " + field.getPossibleValueSourceName() + " in table " + tableName + " for field " + fieldName + ".");
}
String prefix = "Field " + fieldName + " in table " + tableName + " "; String prefix = "Field " + fieldName + " in table " + tableName + " ";
validateFieldPossibleValueSourceAttributes(qInstance, field, prefix);
/////////////////////////////////////////////////// ///////////////////////////////////////////////////
// validate things we know about field behaviors // // validate things we know about field behaviors //
@ -1039,6 +1035,31 @@ public class QInstanceValidator
/***************************************************************************
**
***************************************************************************/
private void validateFieldPossibleValueSourceAttributes(QInstance qInstance, QFieldMetaData field, String prefix)
{
if(field.getPossibleValueSourceName() != null)
{
assertCondition(qInstance.getPossibleValueSource(field.getPossibleValueSourceName()) != null,
prefix + "has an unrecognized possibleValueSourceName " + field.getPossibleValueSourceName());
assertCondition(field.getInlinePossibleValueSource() == null, prefix.trim() + " has both a possibleValueSourceName and an inlinePossibleValueSource, which is not allowed.");
}
if(field.getInlinePossibleValueSource() != null)
{
String name = "inlinePossibleValueSource for " + prefix.trim();
if(assertCondition(QPossibleValueSourceType.ENUM.equals(field.getInlinePossibleValueSource().getType()), name + " must have a type of ENUM."))
{
validatePossibleValueSource(qInstance, name, field.getInlinePossibleValueSource());
}
}
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@ -1546,6 +1567,16 @@ public class QInstanceValidator
} }
} }
for(QFieldMetaData field : process.getInputFields())
{
validateFieldPossibleValueSourceAttributes(qInstance, field, "Process " + processName + ", input field " + field.getName());
}
for(QFieldMetaData field : process.getOutputFields())
{
validateFieldPossibleValueSourceAttributes(qInstance, field, "Process " + processName + ", output field " + field.getName());
}
if(process.getCancelStep() != null) if(process.getCancelStep() != null)
{ {
if(assertCondition(process.getCancelStep().getCode() != null, "Cancel step is missing a code reference, in process " + processName)) if(assertCondition(process.getCancelStep().getCode() != null, "Cancel step is missing a code reference, in process " + processName))
@ -1948,78 +1979,88 @@ public class QInstanceValidator
qInstance.getPossibleValueSources().forEach((pvsName, possibleValueSource) -> qInstance.getPossibleValueSources().forEach((pvsName, possibleValueSource) ->
{ {
assertCondition(Objects.equals(pvsName, possibleValueSource.getName()), "Inconsistent naming for possibleValueSource: " + pvsName + "/" + possibleValueSource.getName() + "."); assertCondition(Objects.equals(pvsName, possibleValueSource.getName()), "Inconsistent naming for possibleValueSource: " + pvsName + "/" + possibleValueSource.getName() + ".");
if(assertCondition(possibleValueSource.getType() != null, "Missing type for possibleValueSource: " + pvsName)) validatePossibleValueSource(qInstance, pvsName, possibleValueSource);
});
}
}
/***************************************************************************
**
***************************************************************************/
private void validatePossibleValueSource(QInstance qInstance, String name, QPossibleValueSource possibleValueSource)
{
if(assertCondition(possibleValueSource.getType() != null, "Missing type for possibleValueSource: " + name))
{
////////////////////////////////////////////////////////////////////////////////////////////////
// assert about fields that should and should not be set, based on possible value source type //
// do additional type-specific validations as well //
////////////////////////////////////////////////////////////////////////////////////////////////
switch(possibleValueSource.getType())
{
case ENUM ->
{ {
//////////////////////////////////////////////////////////////////////////////////////////////// assertCondition(!StringUtils.hasContent(possibleValueSource.getTableName()), "enum-type possibleValueSource " + name + " should not have a tableName.");
// assert about fields that should and should not be set, based on possible value source type // assertCondition(!CollectionUtils.nullSafeHasContents(possibleValueSource.getSearchFields()), "enum-type possibleValueSource " + name + " should not have searchFields.");
// do additional type-specific validations as well // assertCondition(!CollectionUtils.nullSafeHasContents(possibleValueSource.getOrderByFields()), "enum-type possibleValueSource " + name + " should not have orderByFields.");
//////////////////////////////////////////////////////////////////////////////////////////////// assertCondition(possibleValueSource.getCustomCodeReference() == null, "enum-type possibleValueSource " + name + " should not have a customCodeReference.");
switch(possibleValueSource.getType())
assertCondition(CollectionUtils.nullSafeHasContents(possibleValueSource.getEnumValues()), "enum-type possibleValueSource " + name + " is missing enum values");
}
case TABLE ->
{
assertCondition(CollectionUtils.nullSafeIsEmpty(possibleValueSource.getEnumValues()), "table-type possibleValueSource " + name + " should not have enum values.");
assertCondition(possibleValueSource.getCustomCodeReference() == null, "table-type possibleValueSource " + name + " should not have a customCodeReference.");
QTableMetaData tableMetaData = null;
if(assertCondition(StringUtils.hasContent(possibleValueSource.getTableName()), "table-type possibleValueSource " + name + " is missing a tableName."))
{ {
case ENUM -> tableMetaData = qInstance.getTable(possibleValueSource.getTableName());
{ assertCondition(tableMetaData != null, "Unrecognized table " + possibleValueSource.getTableName() + " for possibleValueSource " + name + ".");
assertCondition(!StringUtils.hasContent(possibleValueSource.getTableName()), "enum-type possibleValueSource " + pvsName + " should not have a tableName.");
assertCondition(!CollectionUtils.nullSafeHasContents(possibleValueSource.getSearchFields()), "enum-type possibleValueSource " + pvsName + " should not have searchFields.");
assertCondition(!CollectionUtils.nullSafeHasContents(possibleValueSource.getOrderByFields()), "enum-type possibleValueSource " + pvsName + " should not have orderByFields.");
assertCondition(possibleValueSource.getCustomCodeReference() == null, "enum-type possibleValueSource " + pvsName + " should not have a customCodeReference.");
assertCondition(CollectionUtils.nullSafeHasContents(possibleValueSource.getEnumValues()), "enum-type possibleValueSource " + pvsName + " is missing enum values");
}
case TABLE ->
{
assertCondition(CollectionUtils.nullSafeIsEmpty(possibleValueSource.getEnumValues()), "table-type possibleValueSource " + pvsName + " should not have enum values.");
assertCondition(possibleValueSource.getCustomCodeReference() == null, "table-type possibleValueSource " + pvsName + " should not have a customCodeReference.");
QTableMetaData tableMetaData = null;
if(assertCondition(StringUtils.hasContent(possibleValueSource.getTableName()), "table-type possibleValueSource " + pvsName + " is missing a tableName."))
{
tableMetaData = qInstance.getTable(possibleValueSource.getTableName());
assertCondition(tableMetaData != null, "Unrecognized table " + possibleValueSource.getTableName() + " for possibleValueSource " + pvsName + ".");
}
if(assertCondition(CollectionUtils.nullSafeHasContents(possibleValueSource.getSearchFields()), "table-type possibleValueSource " + pvsName + " is missing searchFields."))
{
if(tableMetaData != null)
{
QTableMetaData finalTableMetaData = tableMetaData;
for(String searchField : possibleValueSource.getSearchFields())
{
assertNoException(() -> finalTableMetaData.getField(searchField), "possibleValueSource " + pvsName + " has an unrecognized searchField: " + searchField);
}
}
}
if(assertCondition(CollectionUtils.nullSafeHasContents(possibleValueSource.getOrderByFields()), "table-type possibleValueSource " + pvsName + " is missing orderByFields."))
{
if(tableMetaData != null)
{
QTableMetaData finalTableMetaData = tableMetaData;
for(QFilterOrderBy orderByField : possibleValueSource.getOrderByFields())
{
assertNoException(() -> finalTableMetaData.getField(orderByField.getFieldName()), "possibleValueSource " + pvsName + " has an unrecognized orderByField: " + orderByField.getFieldName());
}
}
}
}
case CUSTOM ->
{
assertCondition(CollectionUtils.nullSafeIsEmpty(possibleValueSource.getEnumValues()), "custom-type possibleValueSource " + pvsName + " should not have enum values.");
assertCondition(!StringUtils.hasContent(possibleValueSource.getTableName()), "custom-type possibleValueSource " + pvsName + " should not have a tableName.");
assertCondition(!CollectionUtils.nullSafeHasContents(possibleValueSource.getSearchFields()), "custom-type possibleValueSource " + pvsName + " should not have searchFields.");
assertCondition(!CollectionUtils.nullSafeHasContents(possibleValueSource.getOrderByFields()), "custom-type possibleValueSource " + pvsName + " should not have orderByFields.");
if(assertCondition(possibleValueSource.getCustomCodeReference() != null, "custom-type possibleValueSource " + pvsName + " is missing a customCodeReference."))
{
validateSimpleCodeReference("PossibleValueSource " + pvsName + " custom code reference: ", possibleValueSource.getCustomCodeReference(), QCustomPossibleValueProvider.class);
}
}
default -> errors.add("Unexpected possibleValueSource type: " + possibleValueSource.getType());
} }
runPlugins(QPossibleValueSource.class, possibleValueSource, qInstance); if(assertCondition(CollectionUtils.nullSafeHasContents(possibleValueSource.getSearchFields()), "table-type possibleValueSource " + name + " is missing searchFields."))
{
if(tableMetaData != null)
{
QTableMetaData finalTableMetaData = tableMetaData;
for(String searchField : possibleValueSource.getSearchFields())
{
assertNoException(() -> finalTableMetaData.getField(searchField), "possibleValueSource " + name + " has an unrecognized searchField: " + searchField);
}
}
}
if(assertCondition(CollectionUtils.nullSafeHasContents(possibleValueSource.getOrderByFields()), "table-type possibleValueSource " + name + " is missing orderByFields."))
{
if(tableMetaData != null)
{
QTableMetaData finalTableMetaData = tableMetaData;
for(QFilterOrderBy orderByField : possibleValueSource.getOrderByFields())
{
assertNoException(() -> finalTableMetaData.getField(orderByField.getFieldName()), "possibleValueSource " + name + " has an unrecognized orderByField: " + orderByField.getFieldName());
}
}
}
} }
}); case CUSTOM ->
{
assertCondition(CollectionUtils.nullSafeIsEmpty(possibleValueSource.getEnumValues()), "custom-type possibleValueSource " + name + " should not have enum values.");
assertCondition(!StringUtils.hasContent(possibleValueSource.getTableName()), "custom-type possibleValueSource " + name + " should not have a tableName.");
assertCondition(!CollectionUtils.nullSafeHasContents(possibleValueSource.getSearchFields()), "custom-type possibleValueSource " + name + " should not have searchFields.");
assertCondition(!CollectionUtils.nullSafeHasContents(possibleValueSource.getOrderByFields()), "custom-type possibleValueSource " + name + " should not have orderByFields.");
if(assertCondition(possibleValueSource.getCustomCodeReference() != null, "custom-type possibleValueSource " + name + " is missing a customCodeReference."))
{
validateSimpleCodeReference("PossibleValueSource " + name + " custom code reference: ", possibleValueSource.getCustomCodeReference(), QCustomPossibleValueProvider.class);
}
}
default -> errors.add("Unexpected possibleValueSource type: " + possibleValueSource.getType());
}
runPlugins(QPossibleValueSource.class, possibleValueSource, qInstance);
} }
} }

View File

@ -0,0 +1,139 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.actions.processes;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
** Object that stores adjustments that a process wants to make, at run-time,
** to its meta-data.
**
** e.g., changing the steps; updating fields (e.g., changing an inline PVS,
** or an isRequired attribute)
*******************************************************************************/
public class ProcessMetaDataAdjustment
{
private static final QLogger LOG = QLogger.getLogger(ProcessMetaDataAdjustment.class);
private List<QFrontendStepMetaData> updatedFrontendStepList = null;
private Map<String, QFieldMetaData> updatedFields = null;
/*******************************************************************************
**
*******************************************************************************/
public ProcessMetaDataAdjustment withUpdatedField(QFieldMetaData field)
{
if(updatedFields == null)
{
updatedFields = new LinkedHashMap<>();
}
if(!StringUtils.hasContent(field.getName()))
{
LOG.warn("Missing name on field in withUpdatedField - no update will happen.");
}
else
{
if(updatedFields.containsKey(field.getName()))
{
LOG.info("UpdatedFields map already contained a field with this name - overwriting it.", logPair("fieldName", field.getName()));
}
updatedFields.put(field.getName(), field);
}
return (this);
}
/*******************************************************************************
** Getter for updatedFrontendStepList
*******************************************************************************/
public List<QFrontendStepMetaData> getUpdatedFrontendStepList()
{
return (this.updatedFrontendStepList);
}
/*******************************************************************************
** Setter for updatedFrontendStepList
*******************************************************************************/
public void setUpdatedFrontendStepList(List<QFrontendStepMetaData> updatedFrontendStepList)
{
this.updatedFrontendStepList = updatedFrontendStepList;
}
/*******************************************************************************
** Fluent setter for updatedFrontendStepList
*******************************************************************************/
public ProcessMetaDataAdjustment withUpdatedFrontendStepList(List<QFrontendStepMetaData> updatedFrontendStepList)
{
this.updatedFrontendStepList = updatedFrontendStepList;
return (this);
}
/*******************************************************************************
** Getter for updatedFields
*******************************************************************************/
public Map<String, QFieldMetaData> getUpdatedFields()
{
return (this.updatedFields);
}
/*******************************************************************************
** Setter for updatedFields
*******************************************************************************/
public void setUpdatedFields(Map<String, QFieldMetaData> updatedFields)
{
this.updatedFields = updatedFields;
}
/*******************************************************************************
** Fluent setter for updatedFields
*******************************************************************************/
public ProcessMetaDataAdjustment withUpdatedFields(Map<String, QFieldMetaData> updatedFields)
{
this.updatedFields = updatedFields;
return (this);
}
}

View File

@ -29,7 +29,6 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
/******************************************************************************* /*******************************************************************************
@ -42,10 +41,7 @@ public class ProcessState implements Serializable
private List<String> stepList = new ArrayList<>(); private List<String> stepList = new ArrayList<>();
private Optional<String> nextStepName = Optional.empty(); private Optional<String> nextStepName = Optional.empty();
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// private ProcessMetaDataAdjustment processMetaDataAdjustment = null;
// maybe, remove this altogether - just let the frontend compute & send if needed... but how does it know last version...? //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
private List<QFrontendStepMetaData> updatedFrontendStepList = null;
@ -148,33 +144,36 @@ public class ProcessState implements Serializable
/******************************************************************************* /*******************************************************************************
** Getter for updatedFrontendStepList ** Getter for processMetaDataAdjustment
*******************************************************************************/ *******************************************************************************/
public List<QFrontendStepMetaData> getUpdatedFrontendStepList() public ProcessMetaDataAdjustment getProcessMetaDataAdjustment()
{ {
return (this.updatedFrontendStepList); return (this.processMetaDataAdjustment);
} }
/******************************************************************************* /*******************************************************************************
** Setter for updatedFrontendStepList ** Setter for processMetaDataAdjustment
*******************************************************************************/ *******************************************************************************/
public void setUpdatedFrontendStepList(List<QFrontendStepMetaData> updatedFrontendStepList) public void setProcessMetaDataAdjustment(ProcessMetaDataAdjustment processMetaDataAdjustment)
{ {
this.updatedFrontendStepList = updatedFrontendStepList; this.processMetaDataAdjustment = processMetaDataAdjustment;
} }
/******************************************************************************* /*******************************************************************************
** Fluent setter for updatedFrontendStepList ** Fluent setter for processMetaDataAdjustment
*******************************************************************************/ *******************************************************************************/
public ProcessState withUpdatedFrontendStepList(List<QFrontendStepMetaData> updatedFrontendStepList) public ProcessState withProcessMetaDataAdjustment(ProcessMetaDataAdjustment processMetaDataAdjustment)
{ {
this.updatedFrontendStepList = updatedFrontendStepList; this.processMetaDataAdjustment = processMetaDataAdjustment;
return (this); return (this);
} }
} }

View File

@ -374,7 +374,13 @@ public class RunBackendStepOutput extends AbstractActionOutput implements Serial
.map(step -> (QFrontendStepMetaData) step) .map(step -> (QFrontendStepMetaData) step)
.toList()); .toList());
setUpdatedFrontendStepList(updatedFrontendStepList); ProcessMetaDataAdjustment processMetaDataAdjustment = getProcessMetaDataAdjustment();
if(processMetaDataAdjustment == null)
{
processMetaDataAdjustment = new ProcessMetaDataAdjustment();
}
processMetaDataAdjustment.setUpdatedFrontendStepList(updatedFrontendStepList);
setProcessMetaDataAdjustment(processMetaDataAdjustment);
} }
@ -411,21 +417,21 @@ public class RunBackendStepOutput extends AbstractActionOutput implements Serial
/******************************************************************************* /*******************************************************************************
** Getter for updatedFrontendStepList ** Getter for ProcessMetaDataAdjustment (pass-through to processState)
*******************************************************************************/ *******************************************************************************/
public List<QFrontendStepMetaData> getUpdatedFrontendStepList() public ProcessMetaDataAdjustment getProcessMetaDataAdjustment()
{ {
return (this.processState.getUpdatedFrontendStepList()); return (this.processState.getProcessMetaDataAdjustment());
} }
/******************************************************************************* /*******************************************************************************
** Setter for updatedFrontendStepList ** Setter for updatedFrontendStepList (pass-through to processState)
*******************************************************************************/ *******************************************************************************/
public void setUpdatedFrontendStepList(List<QFrontendStepMetaData> updatedFrontendStepList) public void setProcessMetaDataAdjustment(ProcessMetaDataAdjustment processMetaDataAdjustment)
{ {
this.processState.setUpdatedFrontendStepList(updatedFrontendStepList); this.processState.setProcessMetaDataAdjustment(processMetaDataAdjustment);
} }
} }

View File

@ -33,6 +33,7 @@ import java.util.Optional;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput; import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
import com.kingsrook.qqq.backend.core.utils.ObjectUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils; import com.kingsrook.qqq.backend.core.utils.ValueUtils;
@ -336,7 +337,12 @@ public class RunProcessOutput extends AbstractActionOutput implements Serializab
*******************************************************************************/ *******************************************************************************/
public void setUpdatedFrontendStepList(List<QFrontendStepMetaData> updatedFrontendStepList) public void setUpdatedFrontendStepList(List<QFrontendStepMetaData> updatedFrontendStepList)
{ {
this.processState.setUpdatedFrontendStepList(updatedFrontendStepList); if(this.processState.getProcessMetaDataAdjustment() == null)
{
this.processState.setProcessMetaDataAdjustment(new ProcessMetaDataAdjustment());
}
this.processState.getProcessMetaDataAdjustment().setUpdatedFrontendStepList(updatedFrontendStepList);
} }
@ -346,7 +352,27 @@ public class RunProcessOutput extends AbstractActionOutput implements Serializab
*******************************************************************************/ *******************************************************************************/
public List<QFrontendStepMetaData> getUpdatedFrontendStepList() public List<QFrontendStepMetaData> getUpdatedFrontendStepList()
{ {
return this.processState.getUpdatedFrontendStepList(); return ObjectUtils.tryElse(() -> this.processState.getProcessMetaDataAdjustment().getUpdatedFrontendStepList(), null);
}
/*******************************************************************************
** Getter for processMetaDataAdjustment
*******************************************************************************/
public ProcessMetaDataAdjustment getProcessMetaDataAdjustment()
{
return (this.processState.getProcessMetaDataAdjustment());
}
/*******************************************************************************
** Setter for processMetaDataAdjustment
*******************************************************************************/
public void setProcessMetaDataAdjustment(ProcessMetaDataAdjustment processMetaDataAdjustment)
{
this.processState.setProcessMetaDataAdjustment(processMetaDataAdjustment);
} }
} }

View File

@ -52,12 +52,13 @@ public class CompositeWidgetData extends AbstractBlockWidgetData<CompositeWidget
*******************************************************************************/ *******************************************************************************/
public enum Layout public enum Layout
{ {
///////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////
// note, these are used in QQQ FMD CompositeWidgetData.tsx // // note, these are used in QQQ FMD CompositeWidget.tsx //
///////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////
FLEX_COLUMN, FLEX_COLUMN,
FLEX_ROW_WRAPPED, FLEX_ROW_WRAPPED,
FLEX_ROW_SPACE_BETWEEN, FLEX_ROW_SPACE_BETWEEN,
FLEX_ROW_CENTER,
TABLE_SUB_ROW_DETAILS, TABLE_SUB_ROW_DETAILS,
BADGES_WRAPPER BADGES_WRAPPER
} }

View File

@ -31,7 +31,7 @@ import java.util.Map;
** Base class for the data returned by rendering a Widget. ** Base class for the data returned by rendering a Widget.
** **
*******************************************************************************/ *******************************************************************************/
public abstract class QWidgetData public abstract class QWidgetData implements Serializable
{ {
private String label; private String label;
private String sublabel; private String sublabel;

View File

@ -52,6 +52,11 @@ public abstract class AbstractBlockWidgetData<
private V values; private V values;
private SX styles; private SX styles;
///////////////////////////////////////////////////////////////////////////////////
// optional field name to act as a 'guard' for the block - e.g., only include it //
// if the value for this field is true //
///////////////////////////////////////////////////////////////////////////////////
private String conditional;
/******************************************************************************* /*******************************************************************************
@ -443,4 +448,35 @@ public abstract class AbstractBlockWidgetData<
return (T) this; return (T) this;
} }
/*******************************************************************************
** Getter for conditional
*******************************************************************************/
public String getConditional()
{
return (this.conditional);
}
/*******************************************************************************
** Setter for conditional
*******************************************************************************/
public void setConditional(String conditional)
{
this.conditional = conditional;
}
/*******************************************************************************
** Fluent setter for conditional
*******************************************************************************/
public AbstractBlockWidgetData withConditional(String conditional)
{
this.conditional = conditional;
return (this);
}
} }

View File

@ -0,0 +1,47 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.actionbutton;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.AbstractBlockWidgetData;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.base.BaseSlots;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.base.BaseStyles;
/*******************************************************************************
** a button (for a process - not sure yet what this could do in a standalone
** widget?) to submit the process screen to run a specific action (e.g., not just
** 'next')
*******************************************************************************/
public class ActionButtonBlockData extends AbstractBlockWidgetData<ActionButtonBlockData, ActionButtonValues, BaseSlots, BaseStyles>
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public String getBlockTypeName()
{
return "ACTION_BUTTON";
}
}

View File

@ -0,0 +1,120 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.actionbutton;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockValuesInterface;
/*******************************************************************************
**
*******************************************************************************/
public class ActionButtonValues implements BlockValuesInterface
{
private String label;
private String actionCode;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public ActionButtonValues()
{
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public ActionButtonValues(String label, String actionCode)
{
setLabel(label);
setActionCode(actionCode);
}
/*******************************************************************************
** Getter for label
*******************************************************************************/
public String getLabel()
{
return (this.label);
}
/*******************************************************************************
** Setter for label
*******************************************************************************/
public void setLabel(String label)
{
this.label = label;
}
/*******************************************************************************
** Fluent setter for label
*******************************************************************************/
public ActionButtonValues withLabel(String label)
{
this.label = label;
return (this);
}
/*******************************************************************************
** Getter for actionCode
*******************************************************************************/
public String getActionCode()
{
return (this.actionCode);
}
/*******************************************************************************
** Setter for actionCode
*******************************************************************************/
public void setActionCode(String actionCode)
{
this.actionCode = actionCode;
}
/*******************************************************************************
** Fluent setter for actionCode
*******************************************************************************/
public ActionButtonValues withActionCode(String actionCode)
{
this.actionCode = actionCode;
return (this);
}
}

View File

@ -0,0 +1,45 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.audio;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.AbstractBlockWidgetData;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.base.BaseSlots;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.base.BaseStyles;
/*******************************************************************************
** block that plays an audio file
*******************************************************************************/
public class AudioBlockData extends AbstractBlockWidgetData<AudioBlockData, AudioValues, BaseSlots, BaseStyles>
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public String getBlockTypeName()
{
return "AUDIO";
}
}

View File

@ -0,0 +1,130 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.audio;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockValuesInterface;
/*******************************************************************************
**
*******************************************************************************/
public class AudioValues implements BlockValuesInterface
{
private String path;
private boolean showControls = false;
private boolean autoPlay = true;
/*******************************************************************************
** Getter for path
*******************************************************************************/
public String getPath()
{
return (this.path);
}
/*******************************************************************************
** Setter for path
*******************************************************************************/
public void setPath(String path)
{
this.path = path;
}
/*******************************************************************************
** Fluent setter for path
*******************************************************************************/
public AudioValues withPath(String path)
{
this.path = path;
return (this);
}
/*******************************************************************************
** Getter for showControls
*******************************************************************************/
public boolean getShowControls()
{
return (this.showControls);
}
/*******************************************************************************
** Setter for showControls
*******************************************************************************/
public void setShowControls(boolean showControls)
{
this.showControls = showControls;
}
/*******************************************************************************
** Fluent setter for showControls
*******************************************************************************/
public AudioValues withShowControls(boolean showControls)
{
this.showControls = showControls;
return (this);
}
/*******************************************************************************
** Getter for autoPlay
*******************************************************************************/
public boolean getAutoPlay()
{
return (this.autoPlay);
}
/*******************************************************************************
** Setter for autoPlay
*******************************************************************************/
public void setAutoPlay(boolean autoPlay)
{
this.autoPlay = autoPlay;
}
/*******************************************************************************
** Fluent setter for autoPlay
*******************************************************************************/
public AudioValues withAutoPlay(boolean autoPlay)
{
this.autoPlay = autoPlay;
return (this);
}
}

View File

@ -0,0 +1,44 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.image;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.AbstractBlockWidgetData;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.base.BaseSlots;
/*******************************************************************************
** block to display an image
*******************************************************************************/
public class ImageBlockData extends AbstractBlockWidgetData<ImageBlockData, ImageValues, BaseSlots, ImageStyles>
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public String getBlockTypeName()
{
return "IMAGE";
}
}

View File

@ -0,0 +1,130 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.image;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockStylesInterface;
/*******************************************************************************
**
*******************************************************************************/
public class ImageStyles implements BlockStylesInterface
{
private String width;
private String height;
private boolean bordered = false;
/*******************************************************************************
** Getter for bordered
*******************************************************************************/
public boolean getBordered()
{
return (this.bordered);
}
/*******************************************************************************
** Setter for bordered
*******************************************************************************/
public void setBordered(boolean bordered)
{
this.bordered = bordered;
}
/*******************************************************************************
** Fluent setter for bordered
*******************************************************************************/
public ImageStyles withBordered(boolean bordered)
{
this.bordered = bordered;
return (this);
}
/*******************************************************************************
** Getter for width
*******************************************************************************/
public String getWidth()
{
return (this.width);
}
/*******************************************************************************
** Setter for width
*******************************************************************************/
public void setWidth(String width)
{
this.width = width;
}
/*******************************************************************************
** Fluent setter for width
*******************************************************************************/
public ImageStyles withWidth(String width)
{
this.width = width;
return (this);
}
/*******************************************************************************
** Getter for height
*******************************************************************************/
public String getHeight()
{
return (this.height);
}
/*******************************************************************************
** Setter for height
*******************************************************************************/
public void setHeight(String height)
{
this.height = height;
}
/*******************************************************************************
** Fluent setter for height
*******************************************************************************/
public ImageStyles withHeight(String height)
{
this.height = height;
return (this);
}
}

View File

@ -0,0 +1,98 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.image;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockValuesInterface;
/*******************************************************************************
**
*******************************************************************************/
public class ImageValues implements BlockValuesInterface
{
private String path;
private String alt;
/*******************************************************************************
** Getter for path
*******************************************************************************/
public String getPath()
{
return (this.path);
}
/*******************************************************************************
** Setter for path
*******************************************************************************/
public void setPath(String path)
{
this.path = path;
}
/*******************************************************************************
** Fluent setter for path
*******************************************************************************/
public ImageValues withPath(String path)
{
this.path = path;
return (this);
}
/*******************************************************************************
** Getter for alt
*******************************************************************************/
public String getAlt()
{
return (this.alt);
}
/*******************************************************************************
** Setter for alt
*******************************************************************************/
public void setAlt(String alt)
{
this.alt = alt;
}
/*******************************************************************************
** Fluent setter for alt
*******************************************************************************/
public ImageValues withAlt(String alt)
{
this.alt = alt;
return (this);
}
}

View File

@ -0,0 +1,45 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.inputfield;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.AbstractBlockWidgetData;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.base.BaseSlots;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.base.BaseStyles;
/*******************************************************************************
** block to display an input field - initially targeted at widgets-in-processes
*******************************************************************************/
public class InputFieldBlockData extends AbstractBlockWidgetData<InputFieldBlockData, InputFieldValues, BaseSlots, BaseStyles>
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public String getBlockTypeName()
{
return "INPUT_FIELD";
}
}

View File

@ -0,0 +1,152 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.inputfield;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockValuesInterface;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
/*******************************************************************************
**
*******************************************************************************/
public class InputFieldValues implements BlockValuesInterface
{
private QFieldMetaData fieldMetaData;
private Boolean autoFocus;
private Boolean submitOnEnter;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public InputFieldValues()
{
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public InputFieldValues(QFieldMetaData fieldMetaData)
{
setFieldMetaData(fieldMetaData);
}
/*******************************************************************************
** Getter for fieldMetaData
*******************************************************************************/
public QFieldMetaData getFieldMetaData()
{
return (this.fieldMetaData);
}
/*******************************************************************************
** Setter for fieldMetaData
*******************************************************************************/
public void setFieldMetaData(QFieldMetaData fieldMetaData)
{
this.fieldMetaData = fieldMetaData;
}
/*******************************************************************************
** Fluent setter for fieldMetaData
*******************************************************************************/
public InputFieldValues withFieldMetaData(QFieldMetaData fieldMetaData)
{
this.fieldMetaData = fieldMetaData;
return (this);
}
/*******************************************************************************
** Getter for autoFocus
*******************************************************************************/
public Boolean getAutoFocus()
{
return (this.autoFocus);
}
/*******************************************************************************
** Setter for autoFocus
*******************************************************************************/
public void setAutoFocus(Boolean autoFocus)
{
this.autoFocus = autoFocus;
}
/*******************************************************************************
** Fluent setter for autoFocus
*******************************************************************************/
public InputFieldValues withAutoFocus(Boolean autoFocus)
{
this.autoFocus = autoFocus;
return (this);
}
/*******************************************************************************
** Getter for submitOnEnter
*******************************************************************************/
public Boolean getSubmitOnEnter()
{
return (this.submitOnEnter);
}
/*******************************************************************************
** Setter for submitOnEnter
*******************************************************************************/
public void setSubmitOnEnter(Boolean submitOnEnter)
{
this.submitOnEnter = submitOnEnter;
}
/*******************************************************************************
** Fluent setter for submitOnEnter
*******************************************************************************/
public InputFieldValues withSubmitOnEnter(Boolean submitOnEnter)
{
this.submitOnEnter = submitOnEnter;
return (this);
}
}

View File

@ -30,4 +30,104 @@ import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockStyles
*******************************************************************************/ *******************************************************************************/
public class TextStyles implements BlockStylesInterface public class TextStyles implements BlockStylesInterface
{ {
private StandardColor standardColor;
private boolean isAlert;
/***************************************************************************
**
***************************************************************************/
public enum StandardColor
{
SUCCESS,
WARNING,
ERROR,
INFO,
MUTED
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public TextStyles()
{
}
/***************************************************************************
**
***************************************************************************/
public TextStyles(StandardColor standardColor)
{
setStandardColor(standardColor);
}
/*******************************************************************************
** Getter for standardColor
*******************************************************************************/
public StandardColor getStandardColor()
{
return (this.standardColor);
}
/*******************************************************************************
** Setter for standardColor
*******************************************************************************/
public void setStandardColor(StandardColor standardColor)
{
this.standardColor = standardColor;
}
/*******************************************************************************
** Fluent setter for standardColor
*******************************************************************************/
public TextStyles withStandardColor(StandardColor standardColor)
{
this.standardColor = standardColor;
return (this);
}
/*******************************************************************************
** Getter for isAlert
*******************************************************************************/
public boolean getIsAlert()
{
return (this.isAlert);
}
/*******************************************************************************
** Setter for isAlert
*******************************************************************************/
public void setIsAlert(boolean isAlert)
{
this.isAlert = isAlert;
}
/*******************************************************************************
** Fluent setter for isAlert
*******************************************************************************/
public TextStyles withIsAlert(boolean isAlert)
{
this.isAlert = isAlert;
return (this);
}
} }

View File

@ -0,0 +1,58 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.metadata.code;
/*******************************************************************************
** Specialized type of QCodeReference that takes a lambda function object.
**
** Originally intended for more concise setup of backend steps in tests - but,
** may be generally useful.
*******************************************************************************/
public class QCodeReferenceLambda<T> extends QCodeReference
{
private final T lambda;
/***************************************************************************
**
***************************************************************************/
public QCodeReferenceLambda(T lambda)
{
this.lambda = lambda;
this.setCodeType(QCodeType.JAVA);
this.setName("[Lambda:" + lambda.toString() + "]");
}
/*******************************************************************************
** Getter for lambda
**
*******************************************************************************/
public T getLambda()
{
return lambda;
}
}

View File

@ -42,6 +42,7 @@ import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.help.HelpRole; import com.kingsrook.qqq.backend.core.model.metadata.help.HelpRole;
import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpContent; import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpContent;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.security.FieldSecurityLock; import com.kingsrook.qqq.backend.core.model.metadata.security.FieldSecurityLock;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils;
@ -73,10 +74,12 @@ public class QFieldMetaData implements Cloneable
// propose doing that in a secondary field, e.g., "onlyEditableOn=insert|update" // // propose doing that in a secondary field, e.g., "onlyEditableOn=insert|update" //
/////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////
private String displayFormat = "%s"; private String displayFormat = "%s";
private Serializable defaultValue; private Serializable defaultValue;
private String possibleValueSourceName;
private QQueryFilter possibleValueSourceFilter; private String possibleValueSourceName;
private QQueryFilter possibleValueSourceFilter;
private QPossibleValueSource inlinePossibleValueSource;
private Integer maxLength; private Integer maxLength;
private Set<FieldBehavior<?>> behaviors; private Set<FieldBehavior<?>> behaviors;
@ -1058,4 +1061,35 @@ public class QFieldMetaData implements Cloneable
QInstanceHelpContentManager.removeHelpContentByRoleSetFromList(roles, this.helpContents); QInstanceHelpContentManager.removeHelpContentByRoleSetFromList(roles, this.helpContents);
} }
/*******************************************************************************
** Getter for inlinePossibleValueSource
*******************************************************************************/
public QPossibleValueSource getInlinePossibleValueSource()
{
return (this.inlinePossibleValueSource);
}
/*******************************************************************************
** Setter for inlinePossibleValueSource
*******************************************************************************/
public void setInlinePossibleValueSource(QPossibleValueSource inlinePossibleValueSource)
{
this.inlinePossibleValueSource = inlinePossibleValueSource;
}
/*******************************************************************************
** Fluent setter for inlinePossibleValueSource
*******************************************************************************/
public QFieldMetaData withInlinePossibleValueSource(QPossibleValueSource inlinePossibleValueSource)
{
this.inlinePossibleValueSource = inlinePossibleValueSource;
return (this);
}
} }

View File

@ -33,6 +33,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldBehaviorForFron
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpContent; import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpContent;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
@ -56,6 +57,7 @@ public class QFrontendFieldMetaData
private List<FieldAdornment> adornments; private List<FieldAdornment> adornments;
private List<QHelpContent> helpContents; private List<QHelpContent> helpContents;
private QPossibleValueSource inlinePossibleValueSource;
private List<FieldBehaviorForFrontend> behaviors; private List<FieldBehaviorForFrontend> behaviors;
@ -81,6 +83,7 @@ public class QFrontendFieldMetaData
this.adornments = fieldMetaData.getAdornments(); this.adornments = fieldMetaData.getAdornments();
this.defaultValue = fieldMetaData.getDefaultValue(); this.defaultValue = fieldMetaData.getDefaultValue();
this.helpContents = fieldMetaData.getHelpContents(); this.helpContents = fieldMetaData.getHelpContents();
this.inlinePossibleValueSource = fieldMetaData.getInlinePossibleValueSource();
for(FieldBehavior<?> behavior : CollectionUtils.nonNullCollection(fieldMetaData.getBehaviors())) for(FieldBehavior<?> behavior : CollectionUtils.nonNullCollection(fieldMetaData.getBehaviors()))
{ {
@ -218,6 +221,17 @@ public class QFrontendFieldMetaData
/*******************************************************************************
** Getter for inlinePossibleValueSource
**
*******************************************************************************/
public QPossibleValueSource getInlinePossibleValueSource()
{
return inlinePossibleValueSource;
}
/******************************************************************************* /*******************************************************************************
** Getter for fieldBehaviors ** Getter for fieldBehaviors
** **

View File

@ -31,6 +31,7 @@ import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput; import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QStateMachineStep;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
@ -50,6 +51,7 @@ public class QFrontendProcessMetaData
private String iconName; private String iconName;
private List<QFrontendStepMetaData> frontendSteps; private List<QFrontendStepMetaData> frontendSteps;
private String stepFlow;
private boolean hasPermission; private boolean hasPermission;
@ -68,15 +70,27 @@ public class QFrontendProcessMetaData
this.label = processMetaData.getLabel(); this.label = processMetaData.getLabel();
this.tableName = processMetaData.getTableName(); this.tableName = processMetaData.getTableName();
this.isHidden = processMetaData.getIsHidden(); this.isHidden = processMetaData.getIsHidden();
this.stepFlow = processMetaData.getStepFlow().toString();
if(includeSteps) if(includeSteps)
{ {
if(CollectionUtils.nullSafeHasContents(processMetaData.getStepList())) if(CollectionUtils.nullSafeHasContents(processMetaData.getStepList()))
{ {
this.frontendSteps = processMetaData.getStepList().stream() this.frontendSteps = switch(processMetaData.getStepFlow())
.filter(QFrontendStepMetaData.class::isInstance) {
.map(QFrontendStepMetaData.class::cast) case LINEAR -> processMetaData.getStepList().stream()
.collect(Collectors.toList()); .filter(QFrontendStepMetaData.class::isInstance)
.map(QFrontendStepMetaData.class::cast)
.collect(Collectors.toList());
case STATE_MACHINE -> processMetaData.getAllSteps().values().stream()
.filter(QStateMachineStep.class::isInstance)
.map(QStateMachineStep.class::cast)
.flatMap(step -> step.getSubSteps().stream())
.filter(QFrontendStepMetaData.class::isInstance)
.map(QFrontendStepMetaData.class::cast)
.collect(Collectors.toList());
};
} }
else else
{ {
@ -180,4 +194,14 @@ public class QFrontendProcessMetaData
return hasPermission; return hasPermission;
} }
/*******************************************************************************
** Getter for stepFlow
**
*******************************************************************************/
public String getStepFlow()
{
return stepFlow;
}
} }

View File

@ -0,0 +1,38 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.metadata.processes;
/*******************************************************************************
** Possible ways the steps of a process can flow.
**
** LINEAR - (the default) - the list of steps in the process are executed in-order
**
** STATE_MACHINE - concept of "states", each which has a backend & frontend step;
** a backend step can (must?) set the field "stepState" (or "nextStepName") to
** say what the next (frontend) step is.
*******************************************************************************/
public enum ProcessStepFlow
{
LINEAR,
STATE_MACHINE
}

View File

@ -57,6 +57,7 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi
private Integer minInputRecords = null; private Integer minInputRecords = null;
private Integer maxInputRecords = null; private Integer maxInputRecords = null;
private ProcessStepFlow stepFlow = ProcessStepFlow.LINEAR;
private List<QStepMetaData> stepList; // these are the steps that are ran, by-default, in the order they are ran in private List<QStepMetaData> stepList; // these are the steps that are ran, by-default, in the order they are ran in
private Map<String, QStepMetaData> steps; // this is the full map of possible steps private Map<String, QStepMetaData> steps; // this is the full map of possible steps
@ -213,11 +214,10 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi
/******************************************************************************* /***************************************************************************
** add a step to the stepList and map
** **
*******************************************************************************/ ***************************************************************************/
public QProcessMetaData addStep(QStepMetaData step) public QProcessMetaData withStep(QStepMetaData step)
{ {
int index = 0; int index = 0;
if(this.stepList != null) if(this.stepList != null)
@ -231,11 +231,23 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi
/*******************************************************************************
** add a step to the stepList and map
**
*******************************************************************************/
@Deprecated(since = "withStep was added")
public QProcessMetaData addStep(QStepMetaData step)
{
return (withStep(step));
}
/******************************************************************************* /*******************************************************************************
** add a step to the stepList (at the specified index) and the step map ** add a step to the stepList (at the specified index) and the step map
** **
*******************************************************************************/ *******************************************************************************/
public QProcessMetaData addStep(int index, QStepMetaData step) public QProcessMetaData withStep(int index, QStepMetaData step)
{ {
if(this.stepList == null) if(this.stepList == null)
{ {
@ -260,11 +272,23 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi
/*******************************************************************************
** add a step to the stepList (at the specified index) and the step map
**
*******************************************************************************/
@Deprecated(since = "withStep was added")
public QProcessMetaData addStep(int index, QStepMetaData step)
{
return (withStep(index, step));
}
/******************************************************************************* /*******************************************************************************
** add a step ONLY to the step map - NOT the list w/ default execution order. ** add a step ONLY to the step map - NOT the list w/ default execution order.
** **
*******************************************************************************/ *******************************************************************************/
public QProcessMetaData addOptionalStep(QStepMetaData step) public QProcessMetaData withOptionalStep(QStepMetaData step)
{ {
if(this.steps == null) if(this.steps == null)
{ {
@ -283,6 +307,18 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi
/*******************************************************************************
** add a step ONLY to the step map - NOT the list w/ default execution order.
**
*******************************************************************************/
@Deprecated(since = "withOptionalStep was added")
public QProcessMetaData addOptionalStep(QStepMetaData step)
{
return (withOptionalStep(step));
}
/******************************************************************************* /*******************************************************************************
** Setter for stepList ** Setter for stepList
** **
@ -299,7 +335,26 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi
*******************************************************************************/ *******************************************************************************/
public QStepMetaData getStep(String stepName) public QStepMetaData getStep(String stepName)
{ {
return (steps.get(stepName)); if(steps.containsKey(stepName))
{
return steps.get(stepName);
}
for(QStepMetaData step : steps.values())
{
if(step instanceof QStateMachineStep stateMachineStep)
{
for(QStepMetaData subStep : stateMachineStep.getSubSteps())
{
if(subStep.getName().equals(stepName))
{
return (subStep);
}
}
}
}
return (null);
} }
@ -780,4 +835,35 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi
return (this); return (this);
} }
/*******************************************************************************
** Getter for stepFlow
*******************************************************************************/
public ProcessStepFlow getStepFlow()
{
return (this.stepFlow);
}
/*******************************************************************************
** Setter for stepFlow
*******************************************************************************/
public void setStepFlow(ProcessStepFlow stepFlow)
{
this.stepFlow = stepFlow;
}
/*******************************************************************************
** Fluent setter for stepFlow
*******************************************************************************/
public QProcessMetaData withStepFlow(ProcessStepFlow stepFlow)
{
this.stepFlow = stepFlow;
return (this);
}
} }

View File

@ -0,0 +1,188 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.metadata.processes;
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
/*******************************************************************************
** A step for a state-machine flow based Process.
**
** Consists of 1 or 2 sub-steps, which are frontend and/or backend.
*******************************************************************************/
public class QStateMachineStep extends QStepMetaData
{
private List<QStepMetaData> subSteps = new ArrayList<>();
private String defaultNextStepName;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
private QStateMachineStep(List<QStepMetaData> subSteps)
{
setStepType("stateMachine");
this.subSteps.addAll(subSteps);
}
/***************************************************************************
**
***************************************************************************/
public static QStateMachineStep frontendOnly(String name, QFrontendStepMetaData frontendStepMetaData)
{
if(!StringUtils.hasContent(frontendStepMetaData.getName()))
{
frontendStepMetaData.setName(name + ".frontend");
}
return (new QStateMachineStep(List.of(frontendStepMetaData)).withName(name));
}
/***************************************************************************
**
***************************************************************************/
public static QStateMachineStep backendOnly(String name, QBackendStepMetaData backendStepMetaData)
{
if(!StringUtils.hasContent(backendStepMetaData.getName()))
{
backendStepMetaData.setName(name + ".backend");
}
return (new QStateMachineStep(List.of(backendStepMetaData)).withName(name));
}
/***************************************************************************
**
***************************************************************************/
public static QStateMachineStep frontendThenBackend(String name, QFrontendStepMetaData frontendStepMetaData, QBackendStepMetaData backendStepMetaData)
{
if(!StringUtils.hasContent(frontendStepMetaData.getName()))
{
frontendStepMetaData.setName(name + ".frontend");
}
if(!StringUtils.hasContent(backendStepMetaData.getName()))
{
backendStepMetaData.setName(name + ".backend");
}
return (new QStateMachineStep(List.of(frontendStepMetaData, backendStepMetaData)).withName(name));
}
/***************************************************************************
**
***************************************************************************/
@Override
public QStateMachineStep withName(String name)
{
super.withName(name);
return (this);
}
/***************************************************************************
**
***************************************************************************/
@Override
public QStateMachineStep withLabel(String label)
{
super.withLabel(label);
return (this);
}
/*******************************************************************************
** Getter for subSteps
**
*******************************************************************************/
public List<QStepMetaData> getSubSteps()
{
return subSteps;
}
/*******************************************************************************
** Getter for defaultNextStepName
*******************************************************************************/
public String getDefaultNextStepName()
{
return (this.defaultNextStepName);
}
/*******************************************************************************
** Setter for defaultNextStepName
*******************************************************************************/
public void setDefaultNextStepName(String defaultNextStepName)
{
this.defaultNextStepName = defaultNextStepName;
}
/*******************************************************************************
** Fluent setter for defaultNextStepName
*******************************************************************************/
public QStateMachineStep withDefaultNextStepName(String defaultNextStepName)
{
this.defaultNextStepName = defaultNextStepName;
return (this);
}
/*******************************************************************************
** Get a list of all of the input fields used by this step (all of its sub-steps)
*******************************************************************************/
@JsonIgnore
@Override
public List<QFieldMetaData> getInputFields()
{
List<QFieldMetaData> rs = new ArrayList<>();
for(QStepMetaData subStep : subSteps)
{
rs.addAll(subStep.getInputFields());
}
return (rs);
}
}

View File

@ -176,9 +176,9 @@ public class StreamedETLExecuteStep extends BaseStreamedETLStep implements Backe
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
// propagate data from inner-step state to process-level step state // // propagate data from inner-step state to process-level step state //
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
if(postRunOutput.getUpdatedFrontendStepList() != null) if(postRunOutput.getProcessMetaDataAdjustment() != null)
{ {
runBackendStepOutput.setUpdatedFrontendStepList(postRunOutput.getUpdatedFrontendStepList()); runBackendStepOutput.setProcessMetaDataAdjustment(postRunOutput.getProcessMetaDataAdjustment());
} }
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@ -281,10 +281,10 @@ public class StreamedETLExecuteStep extends BaseStreamedETLStep implements Backe
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
// propagate data from inner-step state to process-level step state // // propagate data from inner-step state to process-level step state //
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
if(streamedBackendStepOutput.getUpdatedFrontendStepList() != null) if(streamedBackendStepOutput.getProcessMetaDataAdjustment() != null)
{ {
runBackendStepOutput.getProcessState().setStepList(streamedBackendStepOutput.getProcessState().getStepList()); runBackendStepOutput.getProcessState().setStepList(streamedBackendStepOutput.getProcessState().getStepList());
runBackendStepOutput.setUpdatedFrontendStepList(streamedBackendStepOutput.getUpdatedFrontendStepList()); runBackendStepOutput.getProcessState().setProcessMetaDataAdjustment(streamedBackendStepOutput.getProcessMetaDataAdjustment());
} }
//////////////////////////////////////////////// ////////////////////////////////////////////////
@ -299,10 +299,10 @@ public class StreamedETLExecuteStep extends BaseStreamedETLStep implements Backe
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
// propagate data from inner-step state to process-level step state // // propagate data from inner-step state to process-level step state //
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
if(streamedBackendStepOutput.getUpdatedFrontendStepList() != null) if(streamedBackendStepOutput.getProcessMetaDataAdjustment() != null)
{ {
runBackendStepOutput.getProcessState().setStepList(streamedBackendStepOutput.getProcessState().getStepList()); runBackendStepOutput.getProcessState().setStepList(streamedBackendStepOutput.getProcessState().getStepList());
runBackendStepOutput.setUpdatedFrontendStepList(streamedBackendStepOutput.getUpdatedFrontendStepList()); runBackendStepOutput.getProcessState().setProcessMetaDataAdjustment(streamedBackendStepOutput.getProcessMetaDataAdjustment());
} }
/////////////////////////////////////////////////////// ///////////////////////////////////////////////////////

View File

@ -148,9 +148,9 @@ public class StreamedETLPreviewStep extends BaseStreamedETLStep implements Backe
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
// propagate data from inner-step state to process-level step state // // propagate data from inner-step state to process-level step state //
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
if(postRunOutput.getUpdatedFrontendStepList() != null) if(postRunOutput.getProcessMetaDataAdjustment() != null)
{ {
runBackendStepOutput.setUpdatedFrontendStepList(postRunOutput.getUpdatedFrontendStepList()); runBackendStepOutput.setProcessMetaDataAdjustment(postRunOutput.getProcessMetaDataAdjustment());
} }
} }
@ -219,10 +219,9 @@ public class StreamedETLPreviewStep extends BaseStreamedETLStep implements Backe
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
// propagate data from inner-step state to process-level step state // // propagate data from inner-step state to process-level step state //
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
if(streamedBackendStepOutput.getUpdatedFrontendStepList() != null) if(streamedBackendStepOutput.getProcessMetaDataAdjustment() != null)
{ {
runBackendStepOutput.getProcessState().setStepList(streamedBackendStepOutput.getProcessState().getStepList()); runBackendStepOutput.getProcessState().setProcessMetaDataAdjustment(streamedBackendStepOutput.getProcessMetaDataAdjustment());
runBackendStepOutput.setUpdatedFrontendStepList(streamedBackendStepOutput.getUpdatedFrontendStepList());
} }
//////////////////////////////////////////////////// ////////////////////////////////////////////////////

View File

@ -145,9 +145,9 @@ public class StreamedETLValidateStep extends BaseStreamedETLStep implements Back
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
// propagate data from inner-step state to process-level step state // // propagate data from inner-step state to process-level step state //
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
if(postRunOutput.getUpdatedFrontendStepList() != null) if(postRunOutput.getProcessMetaDataAdjustment() != null)
{ {
runBackendStepOutput.setUpdatedFrontendStepList(postRunOutput.getUpdatedFrontendStepList()); runBackendStepOutput.setProcessMetaDataAdjustment(postRunOutput.getProcessMetaDataAdjustment());
} }
} }
@ -183,10 +183,9 @@ public class StreamedETLValidateStep extends BaseStreamedETLStep implements Back
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
// propagate data from inner-step state to process-level step state // // propagate data from inner-step state to process-level step state //
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
if(streamedBackendStepOutput.getUpdatedFrontendStepList() != null) if(streamedBackendStepOutput.getProcessMetaDataAdjustment() != null)
{ {
runBackendStepOutput.getProcessState().setStepList(streamedBackendStepOutput.getProcessState().getStepList()); runBackendStepOutput.setProcessMetaDataAdjustment(streamedBackendStepOutput.getProcessMetaDataAdjustment());
runBackendStepOutput.setUpdatedFrontendStepList(streamedBackendStepOutput.getUpdatedFrontendStepList());
} }
/////////////////////////////////////////////////////// ///////////////////////////////////////////////////////

View File

@ -0,0 +1,327 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.processes;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReferenceLambda;
import com.kingsrook.qqq.backend.core.model.metadata.processes.ProcessStepFlow;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QStateMachineStep;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertEquals;
/*******************************************************************************
** Unit test for RunProcessAction
*******************************************************************************/
class RunProcessActionTest extends BaseTest
{
private static List<String> log = new ArrayList<>();
/*******************************************************************************
**
*******************************************************************************/
@BeforeEach
void beforeEach()
{
log.clear();
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testStateMachineTwoBackendSteps() throws QException
{
QProcessMetaData process = new QProcessMetaData().withName("test")
/////////////////////////////////////////////////////////////////
// two-steps - a, points at b; b has no next-step, so it exits //
/////////////////////////////////////////////////////////////////
.addStep(QStateMachineStep.backendOnly("a", new QBackendStepMetaData().withName("aBackend")
.withCode(new QCodeReferenceLambda<BackendStep>((runBackendStepInput, runBackendStepOutput) ->
{
log.add("in StepA");
runBackendStepOutput.getProcessState().setNextStepName("b");
}))))
.addStep(QStateMachineStep.backendOnly("b", new QBackendStepMetaData().withName("bBackend")
.withCode(new QCodeReferenceLambda<BackendStep>((runBackendStepInput, runBackendStepOutput) ->
{
log.add("in StepB");
}))))
.withStepFlow(ProcessStepFlow.STATE_MACHINE);
QContext.getQInstance().addProcess(process);
RunProcessInput input = new RunProcessInput();
input.setProcessName("test");
input.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.BREAK);
RunProcessOutput runProcessOutput = new RunProcessAction().execute(input);
assertEquals(List.of("in StepA", "in StepB"), log);
assertThat(runProcessOutput.getProcessState().getNextStepName()).isEmpty();
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testStateMachineTwoFrontendOnlySteps() throws QException
{
QProcessMetaData process = new QProcessMetaData().withName("test")
.addStep(QStateMachineStep.frontendOnly("a", new QFrontendStepMetaData().withName("aFrontend")).withDefaultNextStepName("b"))
.addStep(QStateMachineStep.frontendOnly("b", new QFrontendStepMetaData().withName("bFrontend")))
.withStepFlow(ProcessStepFlow.STATE_MACHINE);
QContext.getQInstance().addProcess(process);
RunProcessInput input = new RunProcessInput();
input.setProcessName("test");
input.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.BREAK);
RunProcessOutput runProcessOutput = new RunProcessAction().execute(input);
assertThat(runProcessOutput.getProcessState().getNextStepName())
.isPresent().get()
.isEqualTo("aFrontend");
/////////////////////////////
// resume after a, go to b //
/////////////////////////////
input.setStartAfterStep("aFrontend");
runProcessOutput = new RunProcessAction().execute(input);
assertThat(runProcessOutput.getProcessState().getNextStepName())
.isPresent().get()
.isEqualTo("bFrontend");
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testStateMachineOneBackendStepReferencingItselfDoesNotInfiniteLoop() throws QException
{
QProcessMetaData process = new QProcessMetaData().withName("test")
///////////////////////////////////////////////////////////////
// set up step that always points back at itself. //
// since it never goes to the frontend, it'll stack overflow //
// (though we'll catch it ourselves before JVM does) //
///////////////////////////////////////////////////////////////
.addStep(QStateMachineStep.backendOnly("a", new QBackendStepMetaData().withName("aBackend")
.withCode(new QCodeReferenceLambda<BackendStep>((runBackendStepInput, runBackendStepOutput) ->
{
log.add("in StepA");
runBackendStepOutput.getProcessState().setNextStepName("a");
}))))
.withStepFlow(ProcessStepFlow.STATE_MACHINE);
QContext.getQInstance().addProcess(process);
RunProcessInput input = new RunProcessInput();
input.setProcessName("test");
input.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.BREAK);
assertThatThrownBy(() -> new RunProcessAction().execute(input))
.isInstanceOf(QException.class)
.hasMessageContaining("maxStateMachineProcessStepFlowStackDepth of 20");
///////////////////////////////////////////////////
// make sure we can set a custom max-stack-depth //
///////////////////////////////////////////////////
input.addValue("maxStateMachineProcessStepFlowStackDepth", 5);
assertThatThrownBy(() -> new RunProcessAction().execute(input))
.isInstanceOf(QException.class)
.hasMessageContaining("maxStateMachineProcessStepFlowStackDepth of 5");
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testStateMachineTwoBackendStepsReferencingEachOtherDoesNotInfiniteLoop() throws QException
{
QProcessMetaData process = new QProcessMetaData().withName("test")
///////////////////////////////////////////////////////////////
// set up two steps that always points back at each other. //
// since it never goes to the frontend, it'll stack overflow //
// (though we'll catch it ourselves before JVM does) //
///////////////////////////////////////////////////////////////
.addStep(QStateMachineStep.backendOnly("a", new QBackendStepMetaData().withName("aBackend")
.withCode(new QCodeReferenceLambda<BackendStep>((runBackendStepInput, runBackendStepOutput) ->
{
log.add("in StepA");
runBackendStepOutput.getProcessState().setNextStepName("b");
}))))
.addStep(QStateMachineStep.backendOnly("b", new QBackendStepMetaData().withName("bBackend")
.withCode(new QCodeReferenceLambda<BackendStep>((runBackendStepInput, runBackendStepOutput) ->
{
log.add("in StepB");
runBackendStepOutput.getProcessState().setNextStepName("a");
}))))
.withStepFlow(ProcessStepFlow.STATE_MACHINE);
QContext.getQInstance().addProcess(process);
RunProcessInput input = new RunProcessInput();
input.setProcessName("test");
input.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.BREAK);
assertThatThrownBy(() -> new RunProcessAction().execute(input))
.isInstanceOf(QException.class)
.hasMessageContaining("maxStateMachineProcessStepFlowStackDepth of 20");
///////////////////////////////////////////////////
// make sure we can set a custom max-stack-depth //
///////////////////////////////////////////////////
input.addValue("maxStateMachineProcessStepFlowStackDepth", 5);
assertThatThrownBy(() -> new RunProcessAction().execute(input))
.isInstanceOf(QException.class)
.hasMessageContaining("maxStateMachineProcessStepFlowStackDepth of 5");
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testStateSequenceOfFrontendAndBackendSteps() throws QException
{
QProcessMetaData process = new QProcessMetaData().withName("test")
.addStep(QStateMachineStep.frontendThenBackend("a",
new QFrontendStepMetaData().withName("aFrontend"),
new QBackendStepMetaData().withName("aBackend")
.withCode(new QCodeReferenceLambda<BackendStep>((runBackendStepInput, runBackendStepOutput) ->
{
log.add("in StepA");
runBackendStepOutput.getProcessState().setNextStepName("b");
}))))
.addStep(QStateMachineStep.frontendThenBackend("b",
new QFrontendStepMetaData().withName("bFrontend"),
new QBackendStepMetaData().withName("bBackend")
.withCode(new QCodeReferenceLambda<BackendStep>((runBackendStepInput, runBackendStepOutput) ->
{
log.add("in StepB");
runBackendStepOutput.getProcessState().setNextStepName("c");
}))))
.addStep(QStateMachineStep.frontendThenBackend("c",
new QFrontendStepMetaData().withName("cFrontend"),
new QBackendStepMetaData().withName("cBackend")
.withCode(new QCodeReferenceLambda<BackendStep>((runBackendStepInput, runBackendStepOutput) ->
{
log.add("in StepC");
runBackendStepOutput.getProcessState().setNextStepName("d");
}))))
.addStep(QStateMachineStep.frontendOnly("d",
new QFrontendStepMetaData().withName("dFrontend")))
.withStepFlow(ProcessStepFlow.STATE_MACHINE);
QContext.getQInstance().addProcess(process);
RunProcessInput input = new RunProcessInput();
input.setProcessName("test");
input.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.BREAK);
////////////////////////////////////////////////////////
// start the process - we should be sent to aFrontend //
////////////////////////////////////////////////////////
RunProcessOutput runProcessOutput = new RunProcessAction().execute(input);
assertThat(runProcessOutput.getProcessState().getNextStepName())
.isPresent().get()
.isEqualTo("aFrontend");
///////////////////////////////////////////////////////////////////////////////////////////
// resume after aFrontend - we should run StepA (backend), and then be sent to bFrontend //
///////////////////////////////////////////////////////////////////////////////////////////
input.setStartAfterStep("aFrontend");
runProcessOutput = new RunProcessAction().execute(input);
assertEquals(List.of("in StepA"), log);
assertThat(runProcessOutput.getProcessState().getNextStepName())
.isPresent().get()
.isEqualTo("bFrontend");
///////////////////////////////////////////////////////////////////////////////////////////
// resume after bFrontend - we should run StepB (backend), and then be sent to cFrontend //
///////////////////////////////////////////////////////////////////////////////////////////
input.setStartAfterStep("bFrontend");
runProcessOutput = new RunProcessAction().execute(input);
assertEquals(List.of("in StepA", "in StepB"), log);
assertThat(runProcessOutput.getProcessState().getNextStepName())
.isPresent().get()
.isEqualTo("cFrontend");
///////////////////////////////////////////////////////////////////////////////////////////
// resume after cFrontend - we should run StepC (backend), and then be sent to dFrontend //
///////////////////////////////////////////////////////////////////////////////////////////
input.setStartAfterStep("cFrontend");
runProcessOutput = new RunProcessAction().execute(input);
assertEquals(List.of("in StepA", "in StepB", "in StepC"), log);
assertThat(runProcessOutput.getProcessState().getNextStepName())
.isPresent().get()
.isEqualTo("dFrontend");
////////////////////////////////////////////////////////////////////////////////////
// if we resume again here, we'll be past the end of the process, so no next-step //
////////////////////////////////////////////////////////////////////////////////////
input.setStartAfterStep("dFrontend");
runProcessOutput = new RunProcessAction().execute(input);
assertEquals(List.of("in StepA", "in StepB", "in StepC"), log);
assertThat(runProcessOutput.getProcessState().getNextStepName()).isEmpty();
}
}

View File

@ -265,6 +265,38 @@ public class QInstanceValidatorTest extends BaseTest
/*******************************************************************************
**
*******************************************************************************/
@Test
void testTableFieldInlinePossibleValueSource()
{
////////////////////////////////////////////////////
// make sure can't have both named and inline PVS //
////////////////////////////////////////////////////
assertValidationFailureReasonsAllowingExtraReasons((qInstance) -> qInstance.getTable("person").getField("homeStateId")
.withInlinePossibleValueSource(new QPossibleValueSource().withType(QPossibleValueSourceType.TABLE).withTableName("person")),
"both a possibleValueSourceName and an inlinePossibleValueSource");
/////////////////////////////////////////////
// make require inline PVS to be enum type //
/////////////////////////////////////////////
assertValidationFailureReasonsAllowingExtraReasons((qInstance) -> qInstance.getTable("person").getField("homeStateId")
.withPossibleValueSourceName(null)
.withInlinePossibleValueSource(new QPossibleValueSource().withType(QPossibleValueSourceType.TABLE)),
"must have a type of ENUM");
////////////////////////////////////////////////////
// make sure validation on the inline PVS happens //
////////////////////////////////////////////////////
assertValidationFailureReasonsAllowingExtraReasons((qInstance) -> qInstance.getTable("person").getField("homeStateId")
.withPossibleValueSourceName(null)
.withInlinePossibleValueSource(new QPossibleValueSource().withType(QPossibleValueSourceType.ENUM)),
"missing enum values");
}
/******************************************************************************* /*******************************************************************************
** Test that if a process specifies a table that doesn't exist, that it fails. ** Test that if a process specifies a table that doesn't exist, that it fails.
** **
@ -717,8 +749,8 @@ public class QInstanceValidatorTest extends BaseTest
@Test @Test
public void test_validateFieldWithMissingPossibleValueSource() public void test_validateFieldWithMissingPossibleValueSource()
{ {
assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").getField("homeStateId").setPossibleValueSourceName("not a real possible value source"), assertValidationFailureReasonsAllowingExtraReasons((qInstance) -> qInstance.getTable("person").getField("homeStateId").setPossibleValueSourceName("not a real possible value source"),
"Unrecognized possibleValueSourceName"); "unrecognized possibleValueSourceName");
} }

View File

@ -40,6 +40,7 @@ import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobManager; import com.kingsrook.qqq.backend.core.actions.async.AsyncJobManager;
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobState; import com.kingsrook.qqq.backend.core.actions.async.AsyncJobState;
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobStatus; import com.kingsrook.qqq.backend.core.actions.async.AsyncJobStatus;
@ -432,7 +433,18 @@ public class QJavalinProcessHandler
QJavalinAccessLogger.logEndSuccess(); QJavalinAccessLogger.logEndSuccess();
} }
context.result(JsonUtils.toJson(resultForCaller)); ///////////////////////////////////////////////////////////////////////////////////
// Note: originally we did not have this serializationInclusion:ALWAYS here - //
// which meant that null and empty values from backend would not go to frontend, //
// which made things like clearing out a value in a field not happen. //
// So, this is added to get that beneficial effect. Unclear if there are any //
// negative side-effects - but be aware. //
// One could imagine that we'd need this to be configurable in the future? //
///////////////////////////////////////////////////////////////////////////////////
context.result(JsonUtils.toJson(resultForCaller, mapper ->
{
mapper.setSerializationInclusion(JsonInclude.Include.ALWAYS);
}));
} }
@ -453,11 +465,19 @@ public class QJavalinProcessHandler
resultForCaller.put("values", runProcessOutput.getValues()); resultForCaller.put("values", runProcessOutput.getValues());
runProcessOutput.getProcessState().getNextStepName().ifPresent(nextStep -> resultForCaller.put("nextStep", nextStep)); runProcessOutput.getProcessState().getNextStepName().ifPresent(nextStep -> resultForCaller.put("nextStep", nextStep));
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// todo - delete after all frontends look for processMetaDataAdjustment instead of updatedFrontendStepList //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
List<QFrontendStepMetaData> updatedFrontendStepList = runProcessOutput.getUpdatedFrontendStepList(); List<QFrontendStepMetaData> updatedFrontendStepList = runProcessOutput.getUpdatedFrontendStepList();
if(updatedFrontendStepList != null) if(updatedFrontendStepList != null)
{ {
resultForCaller.put("updatedFrontendStepList", updatedFrontendStepList); resultForCaller.put("updatedFrontendStepList", updatedFrontendStepList);
} }
if(runProcessOutput.getProcessMetaDataAdjustment() != null)
{
resultForCaller.put("processMetaDataAdjustment", runProcessOutput.getProcessMetaDataAdjustment());
}
} }