diff --git a/CHANGELOG.md b/CHANGELOG.md index dc85e34..d5040bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,8 @@ All notable changes are documented here, following [Keep a Changelog](https://ke ### Fixed +- Bounded STRING custom fields carry QQQ's native `ValueTooLongBehavior.ERROR`, so a write longer than `maxLength` is rejected with a field error and the stored value is preserved, instead of passing through unchecked (the QQQ default `PASS_THROUGH`). +- Persisted help content is reloaded after each committed runtime metadata refresh, so sections and fields created by publishing a custom-field definition receive their stored `helpContent` rows without a restart. - QBit metadata reports the Maven artifact version from a filtered resource, including release versions. - `presence.changed` events and presence snapshots always carry `viewers`, sending `[]` once the last viewer leaves, as the schema requires (#52). diff --git a/src/main/java/com/kingsrook/qbits/collaboration/FeatureSettingsCustomizer.java b/src/main/java/com/kingsrook/qbits/collaboration/FeatureSettingsCustomizer.java index 8f58f7d..f87cbe6 100644 --- a/src/main/java/com/kingsrook/qbits/collaboration/FeatureSettingsCustomizer.java +++ b/src/main/java/com/kingsrook/qbits/collaboration/FeatureSettingsCustomizer.java @@ -29,10 +29,12 @@ import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizerInterfa import com.kingsrook.qqq.backend.core.context.CapturedContext; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.instances.QInstanceHelpContentManager; import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput; import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; /******************************************************************************* ** Publish committed policy changes through the core overlay. @@ -77,7 +79,7 @@ public class FeatureSettingsCustomizer implements TableCustomizerInterface var instance = QContext.getQInstance(); if(transaction == null) { - RuntimeMetaDataOverlay.refresh(instance); + refreshMetadata(instance); } else { @@ -86,7 +88,7 @@ public class FeatureSettingsCustomizer implements TableCustomizerInterface { try { - QContext.withTemporaryContext(context, () -> RuntimeMetaDataOverlay.refresh(instance)); + QContext.withTemporaryContext(context, () -> refreshMetadata(instance)); } catch(QException e) { @@ -95,4 +97,13 @@ public class FeatureSettingsCustomizer implements TableCustomizerInterface }); } } + + /******************************************************************************* + ** Bind persisted help after the overlay creates its dynamic fields and sections. + *******************************************************************************/ + private static void refreshMetadata(QInstance instance) throws QException + { + RuntimeMetaDataOverlay.refresh(instance); + QInstanceHelpContentManager.reloadHelpContent(instance); + } } diff --git a/src/main/java/com/kingsrook/qbits/collaboration/customfields/CustomFieldsProvider.java b/src/main/java/com/kingsrook/qbits/collaboration/customfields/CustomFieldsProvider.java index 4b36687..dedd52b 100644 --- a/src/main/java/com/kingsrook/qbits/collaboration/customfields/CustomFieldsProvider.java +++ b/src/main/java/com/kingsrook/qbits/collaboration/customfields/CustomFieldsProvider.java @@ -43,6 +43,7 @@ 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.QVirtualFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueRangeBehavior; +import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior; 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.possiblevalues.QPossibleValueSourceType; @@ -123,6 +124,10 @@ public class CustomFieldsProvider implements RuntimeMetaDataProviderInterface .withCriteriaExpander(new QCodeReference(FieldCriteriaExpander.class)); field.withLabel(definition.getValueString("label")).withIsRequired(json && Boolean.TRUE.equals(definition.getValueBoolean("required"))) .withMaxLength(definition.getValueInteger("maxLength")); + if(type == QFieldType.STRING && definition.getValueInteger("maxLength") != null) + { + field.withBehavior(ValueTooLongBehavior.ERROR); + } if(definition.getValue("defaultValue") != null && json) { field.withDefaultValue(com.kingsrook.qqq.backend.core.utils.ValueUtils.getValueAsFieldType(type, definition.getValue("defaultValue"))); diff --git a/src/test/java/com/kingsrook/qbits/collaboration/CustomFieldsTest.java b/src/test/java/com/kingsrook/qbits/collaboration/CustomFieldsTest.java index 2fde99c..939af4c 100644 --- a/src/test/java/com/kingsrook/qbits/collaboration/CustomFieldsTest.java +++ b/src/test/java/com/kingsrook/qbits/collaboration/CustomFieldsTest.java @@ -37,6 +37,7 @@ import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction; import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction; import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher; import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput; @@ -44,9 +45,12 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.helpcontent.HelpContentMetaDataProvider; 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.JsonPathStorage; +import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel; +import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules; import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData; import com.kingsrook.qbits.collaboration.app.CollaborationHome; import com.kingsrook.qqq.backend.core.model.dashboard.widgets.TableData; @@ -59,6 +63,48 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; *******************************************************************************/ public class CustomFieldsTest extends PersonalServiceFixture { + /******************************************************************************* + ** Publication binds stored guidance to sections that did not exist at startup. + *******************************************************************************/ + @Test + void publishedSectionReceivesStoredHelp() throws Exception + { + new HelpContentMetaDataProvider().defineAll(instance, "memory", table -> + table.withPermissionRules(new QPermissionRules() + .withLevel(PermissionLevel.NOT_PROTECTED))); + new QInstanceEnricher(instance).enrich(); + admin(); + var help = new InsertAction().execute(new InsertInput("helpContent").withRecords(List.of(new QRecord() + .withValue("key", "table:document;section:customFields").withValue("role", "ALL_SCREENS") + .withValue("format", "MARKDOWN").withValue("content", "Synthetic dynamic section guidance.")))).getRecords().getFirst(); + assertThat(help.getErrors()).isNullOrEmpty(); + QRecord definition = draft("guided", "STRING"); + FieldDefinitions.apply(definition.getValue("id"), "Publish"); + assertThat(instance.getTable("document").getSection("customFields").getHelpContents()) + .anySatisfy(content -> assertThat(content.getContent()).contains("Synthetic dynamic section guidance.")); + } + + /******************************************************************************* + ** A configured string bound rejects writes without replacing persisted values. + *******************************************************************************/ + @Test + void publishedStringRejectsValuesBeyondItsMaximumLength() throws Exception + { + admin(); + QRecord definition = draft("note", "STRING"); + new UpdateAction().execute(new UpdateInput(CollaborationTables.FIELD_DEFINITION).withRecords(List.of( + new QRecord().withValue("id", definition.getValue("id")).withValue("maxLength", 5)))); + FieldDefinitions.apply(definition.getValue("id"), "Publish"); + login("alice"); + var valid = new UpdateAction().execute(new UpdateInput("document").withRecords(List.of( + new QRecord().withValue("id", 1).withValue("cf_note", "valid")))).getRecords().getFirst(); + assertThat(valid.getErrors()).isNullOrEmpty(); + var invalid = new UpdateAction().execute(new UpdateInput("document").withRecords(List.of( + new QRecord().withValue("id", 1).withValue("cf_note", "too long")))).getRecords().getFirst(); + assertThat(invalid.getErrors()).isNotEmpty(); + assertThat(query("cf_note", QCriteriaOperator.EQUALS, "valid")).hasSize(1); + } + /******************************************************************************* ** Refreshing a SELECT must replace its generated vocabulary and allow later fields. *******************************************************************************/