Compare commits

...
Author SHA1 Message Date
Darin Kelkhoff 299ba66496 feat(formAdjusters): Add ability to disable a form in a form adjuster.
e.g., based on values in a record, make it non-editable.

a future enhancement (which doesn't replace this, but just makes it even better) adds similar functionality on the view screen, to disable even getting to the edit screen.
2025-12-11 12:17:57 -06:00
3 changed files with 167 additions and 3 deletions

No files matched your search

@@ -40,6 +40,8 @@ public class FormAdjusterOutput
private Set<String> fieldsToClear = null;
private Map<String, QFieldSection> updatedSectionMetaData = null;
private Boolean isFormDisabled = false;
private String formDisabledMessage = null;
/*******************************************************************************
@@ -202,4 +204,81 @@ public class FormAdjusterOutput
return (this);
}
/*******************************************************************************
* Getter for isFormDisabled
* @see #withIsFormDisabled(Boolean)
*******************************************************************************/
public Boolean getIsFormDisabled()
{
return (this.isFormDisabled);
}
/*******************************************************************************
* Setter for isFormDisabled
* @see #withIsFormDisabled(Boolean)
*******************************************************************************/
public void setIsFormDisabled(Boolean isFormDisabled)
{
this.isFormDisabled = isFormDisabled;
}
/*******************************************************************************
* Fluent setter for isFormDisabled
*
* @param isFormDisabled
* boolean, defaults to false. If set to true, then the frontend should disable
* the form, fully blocking edits.
* @return this
*******************************************************************************/
public FormAdjusterOutput withIsFormDisabled(Boolean isFormDisabled)
{
this.isFormDisabled = isFormDisabled;
return (this);
}
/*******************************************************************************
* Getter for formDisabledMessage
* @see #withFormDisabledMessage(String)
*******************************************************************************/
public String getFormDisabledMessage()
{
return (this.formDisabledMessage);
}
/*******************************************************************************
* Setter for formDisabledMessage
* @see #withFormDisabledMessage(String)
*******************************************************************************/
public void setFormDisabledMessage(String formDisabledMessage)
{
this.formDisabledMessage = formDisabledMessage;
}
/*******************************************************************************
* Fluent setter for formDisabledMessage
*
* @param formDisabledMessage
* Message the frontend should display to the user when the form is disabled
* (see withIsFormDisabled()). If this message isn't set, then the frontend
* will display a generic message.
* @return this
*******************************************************************************/
public FormAdjusterOutput withFormDisabledMessage(String formDisabledMessage)
{
this.formDisabledMessage = formDisabledMessage;
return (this);
}
}
+37 -3
View File
@@ -26,6 +26,7 @@ import Card from "@mui/material/Card";
import Grid from "@mui/material/Grid";
import Icon from "@mui/material/Icon";
import Modal from "@mui/material/Modal";
import Tooltip from "@mui/material/Tooltip/Tooltip";
import {Capability} from "@qrunio/qqq-frontend-core/lib/model/metaData/Capability";
import {QFieldMetaData} from "@qrunio/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
import {QFieldType} from "@qrunio/qqq-frontend-core/lib/model/metaData/QFieldType";
@@ -117,6 +118,10 @@ function EntityForm(props: Props): JSX.Element
const [alertContent, setAlertContent] = useState("");
const [warningContent, setWarningContent] = useState("");
const [isSaveDisabled, setIsSaveDisabled] = useState(false);
const [saveDisabledTooltip, setSaveDisabledTooltip] = useState(null as string);
const [mayCloseAlert, setMayCloseAlert] = useState(true);
const [mayCloseWarning, setMayCloseWarning] = useState(true);
const [asyncLoadInited, setAsyncLoadInited] = useState(false);
const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData);
@@ -772,6 +777,31 @@ function EntityForm(props: Props): JSX.Element
{
updateSections(table, response.updatedSectionMetaData, true);
}
///////////////////////////////////////////
// disable all the things if so directed //
///////////////////////////////////////////
if (response?.isFormDisabled)
{
let message = response.formDisabledMessage;
if(!message)
{
if((props.id && props.isCopy) || !props.id)
{
message = "You are not allowed to create a new record.";
}
else
{
message = "You are not allowed to edit this record.";
}
}
setAlertContent(message);
setSaveDisabledTooltip(message);
table.fields.forEach((field) => field.isEditable = false);
setIsSaveDisabled(true);
setMayCloseAlert(false);
}
};
@@ -1621,12 +1651,12 @@ function EntityForm(props: Props): JSX.Element
<Grid item xs={12}>
{alertContent ? (
<Box mb={3}>
<Alert severity="error" onClose={() => setAlertContent(null)}>{alertContent}</Alert>
<Alert severity="error" onClose={mayCloseAlert ? () => setAlertContent(null) : undefined}>{alertContent}</Alert>
</Box>
) : ("")}
{warningContent ? (
<Box mb={3}>
<Alert severity="warning" onClose={() => setWarningContent(null)}>{warningContent}</Alert>
<Alert severity="warning" onClose={mayCloseWarning ? () => setWarningContent(null) : undefined}>{warningContent}</Alert>
</Box>
) : ("")}
</Grid>
@@ -1753,7 +1783,11 @@ function EntityForm(props: Props): JSX.Element
<Box component="div" p={3} className={props.isModal ? "modalBottomButtonBar" : "stickyBottomButtonBar"}>
<Grid container justifyContent="flex-end" spacing={3}>
<QCancelButton onClickHandler={props.isModal ? props.closeModalHandler : handleCancelClicked} disabled={isSubmitting} />
<QSaveButton disabled={isSubmitting} label={props.saveButtonLabel} iconName={props.saveButtonIcon} />
<Tooltip title={saveDisabledTooltip}>
<Box>
<QSaveButton disabled={isSubmitting || isSaveDisabled} label={props.saveButtonLabel} iconName={props.saveButtonIcon} />
</Box>
</Tooltip>
</Grid>
</Box>
}
@@ -30,12 +30,15 @@ import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.AbstractWidgetRenderer;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.FilterAndColumnsSetupData;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.WidgetType;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
@@ -59,6 +62,7 @@ import org.junit.jupiter.api.Test;
import org.openqa.selenium.Keys;
import org.openqa.selenium.WebElement;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -70,6 +74,10 @@ public class FormAdjusterIT extends QBaseSeleniumWithQApplicationTest
{
private static final String TABLE_NAME = "tableWithFormAdjuster";
public static final String MAY_NOT_EDIT_WITH_CUSTOM_MESSAGE_NAME = "may-not-edit-custom";
public static final String MAY_NOT_EDIT_WITH_DEFAULT_MESSAGE_NAME = "may-not-edit-generic";
public static final Integer MAY_NOT_EDIT_WITH_CUSTOM_MESSAGE_ID = 1;
public static final Integer MAY_NOT_EDIT_WITH_DEFAULT_MESSAGE_ID = 2;
/***************************************************************************
@@ -114,6 +122,20 @@ public class FormAdjusterIT extends QBaseSeleniumWithQApplicationTest
/***************************************************************************
*
***************************************************************************/
@Override
protected void setupTestData() throws QException
{
new InsertAction().execute(new InsertInput(TABLE_NAME).withRecords(List.of(
new QRecord().withValue("id", MAY_NOT_EDIT_WITH_CUSTOM_MESSAGE_ID).withValue("name", MAY_NOT_EDIT_WITH_CUSTOM_MESSAGE_NAME),
new QRecord().withValue("id", MAY_NOT_EDIT_WITH_DEFAULT_MESSAGE_ID).withValue("name", MAY_NOT_EDIT_WITH_DEFAULT_MESSAGE_NAME)
)));
}
/***************************************************************************
*
***************************************************************************/
@@ -125,6 +147,15 @@ public class FormAdjusterIT extends QBaseSeleniumWithQApplicationTest
@Override
public FormAdjusterOutput execute(FormAdjusterInput input) throws QException
{
if(MAY_NOT_EDIT_WITH_CUSTOM_MESSAGE_NAME.equals(input.getFieldValue("name")))
{
return (new FormAdjusterOutput().withIsFormDisabled(true).withFormDisabledMessage("No soup for you."));
}
else if(MAY_NOT_EDIT_WITH_DEFAULT_MESSAGE_NAME.equals(input.getFieldValue("name")))
{
return (new FormAdjusterOutput().withIsFormDisabled(true));
}
Set<String> names = Collections.emptySet();
if("name".equals(input.getFieldName()))
{
@@ -217,6 +248,26 @@ public class FormAdjusterIT extends QBaseSeleniumWithQApplicationTest
/*******************************************************************************
**
*******************************************************************************/
@Test
void testDisablingForm()
{
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/peopleApp/greetingsApp/" + TABLE_NAME + "/" + MAY_NOT_EDIT_WITH_CUSTOM_MESSAGE_ID + "/edit",
"Edit Table With Form Adjuster: " + MAY_NOT_EDIT_WITH_CUSTOM_MESSAGE_ID);
qfmdSeleniumLib.waitForAlert("No soup for you.");
assertThatThrownBy(() -> qfmdSeleniumLib.inputTextField("size", "XL"));
qSeleniumLib.waitForSelectorContaining("button[disabled]", "save");
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/peopleApp/greetingsApp/" + TABLE_NAME + "/" + MAY_NOT_EDIT_WITH_DEFAULT_MESSAGE_ID + "/edit",
"Edit Table With Form Adjuster: " + MAY_NOT_EDIT_WITH_DEFAULT_MESSAGE_ID);
qfmdSeleniumLib.waitForAlert("You are not allowed to edit this record");
qSeleniumLib.waitForSelectorContaining("button[disabled]", "save");
}
/***************************************************************************
*
***************************************************************************/