diff --git a/.circleci/config.yml b/.circleci/config.yml
index 609a8dc..f632b2c 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -13,7 +13,7 @@ workflows:
branches:
ignore: /(develop|main|release/.*|hotfix/.*|integration.*)/
tags:
- only: []
+ only: []
publish_snapshot:
jobs:
diff --git a/README.md b/README.md
index d6e3032..cd96b0c 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,18 @@
-# qbit-customizable-table-views
+# QBit: Customizable Table Views
+[](https://circleci.com/gh/Kingsrook/qbit-customizable-table-views)
+[](https://central.sonatype.com/namespace/com.kingsrook.qbits)
+[](https://adoptium.net/)
+[](LICENSE.txt)
+
+**QBit Customizable Table Views** provides table personalization capabilities for QQQ applications, allowing users to customize which fields and widgets they see based on their roles and permissions.
+
+## 🚀 What Makes This QBit Different?
+
+**Role-Based Table Customization** - This QBit enables fine-grained control over table presentation without modifying application code. Users see different fields and widgets based on their assigned roles, providing a personalized experience while maintaining security and data integrity.
+
+## 🔓 Open Source & Full Control
+
+**This QBit is 100% open source** - you have complete ownership and control:
User-defined saved views for QQQ dashboard tables.
diff --git a/pom.xml b/pom.xml
index 475f5ce..18d62e9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -20,8 +20,7 @@
~ along with this program. If not, see .
-->
-
- 4.0.0
+
com.kingsrook
@@ -31,13 +30,15 @@
com.kingsrook.qbits
qbit-customizable-table-views
- ${revision}
- jar
+ 0.4.0-SNAPSHOT
+ jar
QBit Customizable Table Views
Customizable table views module for the QBit framework, providing table personalization and field/widget access control
https://github.com/Kingsrook/qbit-customizable-table-views
+ 4.0.0
+
GNU Affero General Public License v3.0
@@ -64,23 +65,15 @@
- 0.3.0
+ 0.4.0-RC.1
-
com.kingsrook.qqq
qqq-bom-pom
- 0.27.9
- pom
- import
-
-
- org.junit
- junit-bom
- 5.10.0
+ 0.40.0-SNAPSHOT
pom
import
@@ -95,6 +88,13 @@
com.kingsrook.qqq
qqq-backend-module-rdbms
+ test
+
+
+ com.h2database
+ h2
+ 2.2.220
+ test
@@ -110,4 +110,11 @@
+
+
+ sonatype-snapshots
+ https://central.sonatype.com/repository/maven-snapshots/
+
+
+
diff --git a/src/main/java/com/kingsrook/qbits/customizabletableviews/logic/CustomizableTableViewsTablePersonalizer.java b/src/main/java/com/kingsrook/qbits/customizabletableviews/logic/CustomizableTableViewsTablePersonalizer.java
index 4b55671..4a20f38 100644
--- a/src/main/java/com/kingsrook/qbits/customizabletableviews/logic/CustomizableTableViewsTablePersonalizer.java
+++ b/src/main/java/com/kingsrook/qbits/customizabletableviews/logic/CustomizableTableViewsTablePersonalizer.java
@@ -25,8 +25,10 @@ package com.kingsrook.qbits.customizabletableviews.logic;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -35,6 +37,7 @@ import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import com.kingsrook.qbits.customizabletableviews.model.CustomizableTable;
+import com.kingsrook.qbits.customizabletableviews.model.CustomizableTableViewsFieldMetaData;
import com.kingsrook.qbits.customizabletableviews.model.FieldAccessLevel;
import com.kingsrook.qbits.customizabletableviews.model.TableView;
import com.kingsrook.qbits.customizabletableviews.model.TableViewField;
@@ -56,14 +59,22 @@ import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInpu
import com.kingsrook.qqq.backend.core.model.actions.tables.QInputSource;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
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.QueryJoin;
+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.code.QCodeReference;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.DynamicDefaultValueBehavior;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAndJoinTable;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QSupplementalFieldMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QVirtualFieldMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.security.MultiRecordSecurityLock;
+import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.session.QUser;
@@ -103,7 +114,6 @@ public class CustomizableTableViewsTablePersonalizer implements TableMetaDataPer
-
/***************************************************************************
*
***************************************************************************/
@@ -153,7 +163,7 @@ public class CustomizableTableViewsTablePersonalizer implements TableMetaDataPer
if(tableView != null)
{
- return applyViewToTable(tableView, tableActionInput.getTable().clone());
+ return applyViewToTable(tableView, tableActionInput.getTable().clone(), tableActionInput);
}
return tableActionInput.getTable();
@@ -164,69 +174,60 @@ public class CustomizableTableViewsTablePersonalizer implements TableMetaDataPer
/***************************************************************************
*
***************************************************************************/
- QTableMetaData applyViewToTable(TableView tableView, QTableMetaData cloneTable)
+ QTableMetaData applyViewToTable(TableView tableView, QTableMetaData cloneTable, AbstractTableActionInput tableActionInput)
{
- Map cloneFields = cloneTable.getFields();
- if(cloneFields == null)
- {
- cloneFields = new LinkedHashMap<>();
- cloneTable.setFields(cloneFields);
- }
-
- Map fieldsToKeep = new LinkedHashMap<>();
-
- ///////////////////////////////////////////
- // figure out which fields the user gets //
- ///////////////////////////////////////////
- for(TableViewField tableViewField : CollectionUtils.nonNullList(tableView.getFields()))
- {
- try
- {
- String fieldName = tableViewField.getFieldName().split("\\.")[1];
- QFieldMetaData fieldMetaData = cloneFields.get(fieldName);
- if(fieldMetaData != null)
- {
- FieldAccessLevel fieldAccessLevel = FieldAccessLevel.getById(tableViewField.getAccessLevel());
- if(fieldAccessLevel != null)
- {
- fieldAccessLevel.apply(fieldMetaData);
- }
-
- fieldsToKeep.put(fieldName, fieldMetaData);
- }
- }
- catch(Exception e)
- {
- LOG.warn("Error processing tableViewField", e, logPair("fieldName", tableViewField.getFieldName()));
- }
- }
-
- ///////////////////////////////////////////////////////////
- // always keep the primary key field and required fields //
- ///////////////////////////////////////////////////////////
- Set requiredAndPrimaryKeyFields = new HashSet<>();
- for(QFieldMetaData field : cloneTable.getFields().values())
- {
- String fieldName = field.getName();
- if(field.getIsRequired() || Objects.equals(fieldName, cloneTable.getPrimaryKeyField()))
- {
- if(!fieldsToKeep.containsKey(fieldName))
- {
- fieldsToKeep.put(fieldName, cloneFields.get(fieldName));
- }
- }
- }
-
+ Map fieldsToKeep = getFieldsToKeepForTable(tableView, cloneTable, tableActionInput);
cloneTable.setFields(fieldsToKeep);
+ Map virtualFieldsToKeep = getVirtualFieldsToKeepForTable(tableView, cloneTable, tableActionInput);
+ cloneTable.setVirtualFields(virtualFieldsToKeep);
+
///////////////////////////////////////////////////////////
// remove field names which aren't present from sections //
///////////////////////////////////////////////////////////
for(QFieldSection section : CollectionUtils.nonNullList(cloneTable.getSections()))
{
- if(section.getFieldNames() != null)
+ Map> getFieldsToKeepFromJoinTableCache = null;
+
+ Iterator fieldListIterator = CollectionUtils.nonNullList(section.getFieldNames()).iterator();
+ while(fieldListIterator.hasNext())
{
- section.getFieldNames().removeIf(next -> !fieldsToKeep.containsKey(next));
+ try
+ {
+ String fieldName = fieldListIterator.next();
+ FieldAndJoinTable fieldAndJoinTable = FieldAndJoinTable.get(cloneTable, fieldName);
+
+ if(fieldAndJoinTable.joinTable().getName().equals(cloneTable.getName()))
+ {
+ ////////////////////////////////////////////////////////////////////////////////////////////
+ // if the field is from this table, then remove it unless it's in the set of ones to keep //
+ ////////////////////////////////////////////////////////////////////////////////////////////
+ if(!fieldsToKeep.containsKey(fieldName))
+ {
+ fieldListIterator.remove();
+ }
+ }
+ else
+ {
+ getFieldsToKeepFromJoinTableCache = Objects.requireNonNullElseGet(getFieldsToKeepFromJoinTableCache, HashMap::new);
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // else the field is from a join table - get that join table's allowed fields for the user, and keep or remove based on that //
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ Set fieldsToKeepFromJoinTable = getFieldsToKeepFromJoinTable(fieldAndJoinTable.joinTable().getName(), tableActionInput, getFieldsToKeepFromJoinTableCache);
+ if(!fieldsToKeepFromJoinTable.contains(fieldAndJoinTable.field().getName()))
+ {
+ fieldListIterator.remove();
+ }
+ }
+ }
+ catch(Exception e)
+ {
+ /////////////////////////////////////////////////////////
+ // an unknown field, let's assume it should be removed //
+ /////////////////////////////////////////////////////////
+ fieldListIterator.remove();
+ }
}
}
@@ -266,6 +267,258 @@ public class CustomizableTableViewsTablePersonalizer implements TableMetaDataPer
+ /***************************************************************************
+ *
+ ***************************************************************************/
+ protected Set getFieldsToKeepFromJoinTable(String joinTableName, AbstractTableActionInput tableActionInput, Map> getFieldsToKeepFromJoinTableCache)
+ {
+ return (getFieldsToKeepFromJoinTableCache.computeIfAbsent(joinTableName, k ->
+ {
+ try
+ {
+ if(isTableCustomizable(joinTableName))
+ {
+ ////////////////////////////////////////////////////////////////
+ // if the table is customizable, return set of fields to keep //
+ ////////////////////////////////////////////////////////////////
+ QTableMetaData joinTableClone = QContext.getQInstance().getTable(joinTableName).clone();
+ TableView joinTableView = getEffectiveTableViewForCurrentSession(joinTableName);
+ Map joinTableFieldsToKeep = getFieldsToKeepForTable(joinTableView, joinTableClone, tableActionInput);
+ return joinTableFieldsToKeep.keySet();
+ }
+ else
+ {
+ ///////////////////////////////////////////////
+ // else, not customizable, return all fields //
+ ///////////////////////////////////////////////
+ return (QContext.getQInstance().getTable(joinTableName).getFields().keySet());
+ }
+ }
+ catch(QException e)
+ {
+ return Collections.emptySet();
+ }
+ }));
+ }
+
+
+
+ /***************************************************************************
+ *
+ ***************************************************************************/
+ protected Map getFieldsToKeepForTable(TableView tableView, QTableMetaData cloneTable, AbstractTableActionInput tableActionInput)
+ {
+ Map cloneFields = cloneTable.getFields();
+ if(cloneFields == null)
+ {
+ cloneFields = new LinkedHashMap<>();
+ cloneTable.setFields(cloneFields);
+ }
+
+ Map fieldsToKeep = new LinkedHashMap<>();
+
+ /////////////////////////////////////////////
+ // figure out which fields the user gets //
+ // start with ones in the user's TableView //
+ /////////////////////////////////////////////
+ for(TableViewField tableViewField : CollectionUtils.nonNullList(tableView.getFields()))
+ {
+ try
+ {
+ String fieldName = tableViewField.getFieldName().split("\\.")[1];
+ QFieldMetaData fieldMetaData = cloneFields.get(fieldName);
+ if(fieldMetaData != null)
+ {
+ FieldAccessLevel fieldAccessLevel = FieldAccessLevel.getById(tableViewField.getAccessLevel());
+ if(fieldAccessLevel != null)
+ {
+ fieldAccessLevel.apply(fieldMetaData);
+ }
+
+ fieldsToKeep.put(fieldName, fieldMetaData);
+ }
+ }
+ catch(Exception e)
+ {
+ LOG.warn("Error processing tableViewField", e, logPair("fieldName", tableViewField.getFieldName()));
+ }
+ }
+
+ /////////////////////////////////////////////
+ // next look for ones that always get kept //
+ /////////////////////////////////////////////
+ for(QFieldMetaData field : cloneTable.getFields().values())
+ {
+ String fieldName = field.getName();
+ if(!fieldsToKeep.containsKey(fieldName))
+ {
+ if(shouldKeepFieldDueToRules(cloneTable, field, tableActionInput))
+ {
+ fieldsToKeep.put(fieldName, cloneFields.get(fieldName));
+ }
+ }
+ }
+
+ return fieldsToKeep;
+ }
+
+
+
+ /***************************************************************************
+ *
+ ***************************************************************************/
+ protected Map getVirtualFieldsToKeepForTable(TableView tableView, QTableMetaData cloneTable, AbstractTableActionInput tableActionInput)
+ {
+ Map cloneFields = cloneTable.getVirtualFields();
+ if(cloneFields == null)
+ {
+ return Collections.emptyMap();
+ }
+
+ Map fieldsToKeep = new LinkedHashMap<>();
+
+ /////////////////////////////////////////////
+ // figure out which fields the user gets //
+ // start with ones in the user's TableView //
+ /////////////////////////////////////////////
+ for(TableViewField tableViewField : CollectionUtils.nonNullList(tableView.getFields()))
+ {
+ try
+ {
+ String fieldName = tableViewField.getFieldName().split("\\.")[1];
+ QVirtualFieldMetaData fieldMetaData = cloneFields.get(fieldName);
+ if(fieldMetaData != null)
+ {
+ FieldAccessLevel fieldAccessLevel = FieldAccessLevel.getById(tableViewField.getAccessLevel());
+ if(fieldAccessLevel != null)
+ {
+ fieldAccessLevel.apply(fieldMetaData);
+ }
+
+ fieldsToKeep.put(fieldName, fieldMetaData);
+ }
+ }
+ catch(Exception e)
+ {
+ LOG.warn("Error processing tableViewField", e, logPair("fieldName", tableViewField.getFieldName()));
+ }
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////
+ // unlike normal fields, there are no 'always keep' rules for virtual fields //
+ ///////////////////////////////////////////////////////////////////////////////
+
+ return fieldsToKeep;
+ }
+
+
+
+ /***************************************************************************
+ * check if any system-defined rules make it so that a field should always
+ * be kept.
+ *
+ * built-in rules are:
+ *
+ * - field is required
+ * - field is primary key
+ * - field is part of table's security locks
+ * - there's supplemental metadata from this qbit that says to keep the field
+ * - this is an insert or update, and there's a {@link DynamicDefaultValueBehavior}
+ *
+ *
+ * also - an application could override this method in a subclass to add
+ * additional rules.
+ ***************************************************************************/
+ protected boolean shouldKeepFieldDueToRules(QTableMetaData cloneTable, QFieldMetaData field, AbstractTableActionInput tableActionInput)
+ {
+ if(field.getIsRequired() || Objects.equals(field.getName(), cloneTable.getPrimaryKeyField()))
+ {
+ ///////////////////////////////////////////////////////////
+ // always keep the primary key field and required fields //
+ ///////////////////////////////////////////////////////////
+ return (true);
+ }
+
+ if(doRecordSecurityLocksContainField(cloneTable.getRecordSecurityLocks(), field))
+ {
+ /////////////////////////////////////////////////////////////
+ // keep fields that are part of the table's security locks //
+ /////////////////////////////////////////////////////////////
+ return (true);
+ }
+
+ if(doesSupplementalMetaDataSayToKeepField(field))
+ {
+ //////////////////////////////////////////////////////////
+ // keep a field if it's marked as being always-included //
+ //////////////////////////////////////////////////////////
+ return (true);
+ }
+
+ boolean isInsertOrUpdate = (tableActionInput instanceof InsertInput) || (tableActionInput instanceof UpdateInput);
+ if(isInsertOrUpdate && field.getBehaviorOnlyIfSet(DynamicDefaultValueBehavior.class) != null)
+ {
+ //////////////////////////////////////////////////////////////////////////////////////////////////////
+ // if this is for an insert or update, then keep any fields that have a DynamicDefaultValueBehavior //
+ // e.g., createDates, modifyDates, things that capture userId //
+ //////////////////////////////////////////////////////////////////////////////////////////////////////
+ return (true);
+ }
+
+ ////////////////////////////////////////////
+ // by default, no rule matched, so say no //
+ ////////////////////////////////////////////
+ return (false);
+ }
+
+
+
+ /***************************************************************************
+ * Check if the supplemental meta data from this qbit for a field says
+ * that the field should always be kept.
+ ***************************************************************************/
+ protected boolean doesSupplementalMetaDataSayToKeepField(QFieldMetaData field)
+ {
+ QSupplementalFieldMetaData supplementalMetaData = field.getSupplementalMetaData(CustomizableTableViewsFieldMetaData.TYPE);
+ if(supplementalMetaData instanceof CustomizableTableViewsFieldMetaData customizableTableViewsFieldMetaData)
+ {
+ if(CustomizableTableViewsFieldMetaData.Rule.ALWAYS_KEEP_FIELD.equals(customizableTableViewsFieldMetaData.getRule()))
+ {
+ return (true);
+ }
+ }
+
+ return (false);
+ }
+
+
+
+ /***************************************************************************
+ * Check if a list of record security locks include a field, recursively
+ * processing {@link MultiRecordSecurityLock}s
+ ***************************************************************************/
+ protected boolean doRecordSecurityLocksContainField(List locks, QFieldMetaData field)
+ {
+ for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(locks))
+ {
+ if(recordSecurityLock instanceof MultiRecordSecurityLock multiRecordSecurityLock)
+ {
+ if(doRecordSecurityLocksContainField(multiRecordSecurityLock.getLocks(), field))
+ {
+ return (true);
+ }
+ }
+ else if(field.getName().equals(recordSecurityLock.getFieldName()))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+
+
/***************************************************************************
* the assumption is, that this method is called AFTER we've identified that
* a table is customizable (and customizaztion is active) - so - if we fail
@@ -294,7 +547,9 @@ public class CustomizableTableViewsTablePersonalizer implements TableMetaDataPer
{
Set roleIds = Arrays.stream(roleIdsString.split(",")).map(Integer::parseInt).collect(Collectors.toSet());
tableViews = new QueryAction().execute(new QueryInput(TableView.TABLE_NAME)
- .withFilter(new QQueryFilter().withCriteria(new QFilterCriteria(TableViewRoleInt.TABLE_NAME + ".roleId", QCriteriaOperator.IN, roleIds)))
+ .withFilter(new QQueryFilter()
+ .withCriteria(new QFilterCriteria(TableViewRoleInt.TABLE_NAME + ".roleId", QCriteriaOperator.IN, roleIds))
+ .withCriteria(new QFilterCriteria(CustomizableTable.TABLE_NAME + ".tableName", QCriteriaOperator.EQUALS, tableName)))
.withQueryJoin(new QueryJoin(TableViewRoleInt.TABLE_NAME))
.withIncludeAssociations(true))
.getRecords();
diff --git a/src/main/java/com/kingsrook/qbits/customizabletableviews/model/CustomizableTableFieldPVS.java b/src/main/java/com/kingsrook/qbits/customizabletableviews/model/CustomizableTableFieldPVS.java
index 4b1f502..53ec048 100644
--- a/src/main/java/com/kingsrook/qbits/customizabletableviews/model/CustomizableTableFieldPVS.java
+++ b/src/main/java/com/kingsrook/qbits/customizabletableviews/model/CustomizableTableFieldPVS.java
@@ -96,6 +96,12 @@ public class CustomizableTableFieldPVS implements QCustomPossibleValueProvider(idString, fieldMetaData.getLabel()));
}
+
+ fieldMetaData = table.getVirtualFields() == null ? null : table.getVirtualFields().get(fieldName);
+ if(fieldMetaData != null)
+ {
+ return (new QPossibleValue<>(idString, fieldMetaData.getLabel()));
+ }
}
}
@@ -128,7 +134,13 @@ public class CustomizableTableFieldPVS implements QCustomPossibleValueProvider fields = new ArrayList<>(table.getFields().values());
+ if(table.getVirtualFields() != null)
+ {
+ fields.addAll(table.getVirtualFields().values());
+ }
+
+ for(QFieldMetaData field : fields)
{
if(field.getName().equals(table.getPrimaryKeyField()))
{
diff --git a/src/main/java/com/kingsrook/qbits/customizabletableviews/model/CustomizableTableViewsFieldMetaData.java b/src/main/java/com/kingsrook/qbits/customizabletableviews/model/CustomizableTableViewsFieldMetaData.java
new file mode 100644
index 0000000..a3163ed
--- /dev/null
+++ b/src/main/java/com/kingsrook/qbits/customizabletableviews/model/CustomizableTableViewsFieldMetaData.java
@@ -0,0 +1,89 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2026. 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 .
+ */
+
+package com.kingsrook.qbits.customizabletableviews.model;
+
+
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QSupplementalFieldMetaData;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class CustomizableTableViewsFieldMetaData extends QSupplementalFieldMetaData
+{
+ public static final String TYPE = CustomizableTableViewsFieldMetaData.class.getName();
+
+ private Rule rule;
+
+ public enum Rule
+ {
+ ALWAYS_KEEP_FIELD
+ }
+
+ /***************************************************************************
+ *
+ ***************************************************************************/
+ @Override
+ public String getType()
+ {
+ return TYPE;
+ }
+
+
+ /*******************************************************************************
+ * Getter for rule
+ * @see #withRule(Rule)
+ *******************************************************************************/
+ public Rule getRule()
+ {
+ return (this.rule);
+ }
+
+
+
+ /*******************************************************************************
+ * Setter for rule
+ * @see #withRule(Rule)
+ *******************************************************************************/
+ public void setRule(Rule rule)
+ {
+ this.rule = rule;
+ }
+
+
+
+ /*******************************************************************************
+ * Fluent setter for rule
+ *
+ * @param rule
+ * TODO document this property
+ *
+ * @return this
+ *******************************************************************************/
+ public CustomizableTableViewsFieldMetaData withRule(Rule rule)
+ {
+ this.rule = rule;
+ return (this);
+ }
+
+
+}
diff --git a/src/main/java/com/kingsrook/qbits/customizabletableviews/model/FieldAccessLevel.java b/src/main/java/com/kingsrook/qbits/customizabletableviews/model/FieldAccessLevel.java
index 038e2b5..35808ea 100644
--- a/src/main/java/com/kingsrook/qbits/customizabletableviews/model/FieldAccessLevel.java
+++ b/src/main/java/com/kingsrook/qbits/customizabletableviews/model/FieldAccessLevel.java
@@ -24,6 +24,7 @@ package com.kingsrook.qbits.customizabletableviews.model;
import java.util.Objects;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QVirtualFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PossibleValueEnum;
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.QMetaDataProducingPossibleValueEnum;
@@ -204,7 +205,7 @@ public enum FieldAccessLevel implements PossibleValueEnum
return ("This field is defined as hidden by the system - you may not make it " + getLabel());
}
- if(!fieldMetaData.getIsEditable())
+ if(!fieldMetaData.getIsEditable() || fieldMetaData instanceof QVirtualFieldMetaData)
{
if(this == READ_ONLY)
{
diff --git a/src/main/java/com/kingsrook/qbits/customizabletableviews/tables/customizers/TableViewFieldCustomizer.java b/src/main/java/com/kingsrook/qbits/customizabletableviews/tables/customizers/TableViewFieldCustomizer.java
index 089f559..5e21127 100644
--- a/src/main/java/com/kingsrook/qbits/customizabletableviews/tables/customizers/TableViewFieldCustomizer.java
+++ b/src/main/java/com/kingsrook/qbits/customizabletableviews/tables/customizers/TableViewFieldCustomizer.java
@@ -64,7 +64,7 @@ public class TableViewFieldCustomizer implements TableCustomizerInterface
String tableName = parts[0];
fieldName = parts[1];
- QFieldMetaData field = QContext.getQInstance().getTable(tableName).getField(fieldName);
+ QFieldMetaData field = QContext.getQInstance().getTable(tableName).getFieldOrVirtualField(fieldName);
FieldAccessLevel fieldAccessLevel = FieldAccessLevel.getById(accessLevelId);
if(fieldAccessLevel != null)
diff --git a/src/test/java/com/kingsrook/qbits/customizabletableviews/logic/CustomizableTableViewsTablePersonalizerTest.java b/src/test/java/com/kingsrook/qbits/customizabletableviews/logic/CustomizableTableViewsTablePersonalizerTest.java
index 913591f..7b47a5d 100644
--- a/src/test/java/com/kingsrook/qbits/customizabletableviews/logic/CustomizableTableViewsTablePersonalizerTest.java
+++ b/src/test/java/com/kingsrook/qbits/customizabletableviews/logic/CustomizableTableViewsTablePersonalizerTest.java
@@ -34,6 +34,7 @@ import java.util.stream.Collectors;
import com.kingsrook.qbits.customizabletableviews.BaseTest;
import com.kingsrook.qbits.customizabletableviews.QFieldMetaDataAssert;
import com.kingsrook.qbits.customizabletableviews.model.CustomizableTable;
+import com.kingsrook.qbits.customizabletableviews.model.CustomizableTableViewsFieldMetaData;
import com.kingsrook.qbits.customizabletableviews.model.FieldAccessLevel;
import com.kingsrook.qbits.customizabletableviews.model.TableView;
import com.kingsrook.qbits.customizabletableviews.model.TableViewField;
@@ -44,6 +45,8 @@ import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction;
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.AbstractTableActionInput;
+import com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataInput;
import com.kingsrook.qqq.backend.core.model.actions.metadata.personalization.TableMetaDataPersonalizerInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.QInputSource;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
@@ -51,8 +54,17 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
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.update.UpdateInput;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.DynamicDefaultValueBehavior;
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.joins.JoinOn;
+import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType;
+import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.security.MultiRecordSecurityLock;
+import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.SectionFactory;
import com.kingsrook.qqq.backend.core.model.session.QUser;
@@ -262,8 +274,10 @@ class CustomizableTableViewsTablePersonalizerTest extends BaseTest
{
CustomizableTableViewsTablePersonalizer personalizer = new CustomizableTableViewsTablePersonalizer();
- String tableWithoutDefaultView = "tableWithoutDefaultView";
- String tableWithDefaultView = "tableWithDefaultView";
+ String tableWithoutDefaultView = "tableWithoutDefaultView";
+ String tableWithDefaultView = "tableWithDefaultView";
+ Integer tableWithoutDefaultViewId = 1;
+ Integer tableWithDefaultViewId = 2;
QContext.getQInstance().addTable(new QTableMetaData()
.withName(tableWithDefaultView)
@@ -272,8 +286,8 @@ class CustomizableTableViewsTablePersonalizerTest extends BaseTest
.withField(new QFieldMetaData("c", QFieldType.STRING)));
new InsertAction().execute(new InsertInput(CustomizableTable.TABLE_NAME).withRecordEntities(List.of(
- new CustomizableTable().withId(1).withTableName(tableWithoutDefaultView).withDefaultTableViewId(null),
- new CustomizableTable().withId(2).withTableName(tableWithDefaultView).withDefaultTableViewId(1)
+ new CustomizableTable().withId(tableWithoutDefaultViewId).withTableName(tableWithoutDefaultView).withDefaultTableViewId(null),
+ new CustomizableTable().withId(tableWithDefaultViewId).withTableName(tableWithDefaultView).withDefaultTableViewId(1)
)));
//////////////////////
@@ -293,6 +307,13 @@ class CustomizableTableViewsTablePersonalizerTest extends BaseTest
QContext.getQSession().setValue("roleIds", "");
assertEmptyView(personalizer.getEffectiveTableViewForCurrentSession(tableWithoutDefaultView));
+ /////////////////////////////////////////////////////////////////////////////////////////////////
+ // put roles in session - there still aren't any view-role-int's for this table, so empty view //
+ /////////////////////////////////////////////////////////////////////////////////////////////////
+ QContext.getQSession().setUser(new QUser().withIdReference(UUID.randomUUID().toString()));
+ QContext.getQSession().setValue("roleIds", "1,2");
+ assertEmptyView(personalizer.getEffectiveTableViewForCurrentSession(tableWithoutDefaultView));
+
//////////////////////////////////////////////////////////////////////////////////////
// this table has a defaultViewId, but that view doesn't exist, so get null for now //
//////////////////////////////////////////////////////////////////////////////////////
@@ -300,31 +321,24 @@ class CustomizableTableViewsTablePersonalizerTest extends BaseTest
QContext.getQSession().setValue("roleIds", null);
assertEmptyView(personalizer.getEffectiveTableViewForCurrentSession(tableWithDefaultView));
- /////////////////////
- // insert the view //
- /////////////////////
+ ////////////////////////////////////////////////
+ // insert that default view, and a few others //
+ ////////////////////////////////////////////////
new InsertAction().execute(new InsertInput(TableView.TABLE_NAME).withRecordEntities(List.of(
- new TableView().withId(1).withCustomizableTableId(2).withName("a").withFields(List.of(new TableViewField().withFieldName(tableWithDefaultView + ".a").withAccessLevel(EDITABLE_OPTIONAL))),
- new TableView().withId(2).withCustomizableTableId(2).withName("b").withFields(List.of(new TableViewField().withFieldName(tableWithDefaultView + ".b").withAccessLevel(EDITABLE_OPTIONAL))),
- new TableView().withId(3).withCustomizableTableId(2).withName("c").withFields(List.of(new TableViewField().withFieldName(tableWithDefaultView + ".c").withAccessLevel(EDITABLE_OPTIONAL)))
+ new TableView().withId(1).withCustomizableTableId(tableWithDefaultViewId).withName("a").withFields(List.of(new TableViewField().withFieldName(tableWithDefaultView + ".a").withAccessLevel(EDITABLE_OPTIONAL))),
+ new TableView().withId(2).withCustomizableTableId(tableWithDefaultViewId).withName("b").withFields(List.of(new TableViewField().withFieldName(tableWithDefaultView + ".b").withAccessLevel(EDITABLE_OPTIONAL))),
+ new TableView().withId(3).withCustomizableTableId(tableWithDefaultViewId).withName("c").withFields(List.of(new TableViewField().withFieldName(tableWithDefaultView + ".c").withAccessLevel(EDITABLE_OPTIONAL)))
)));
- ///////////////////////
- // now we can get it //
- ///////////////////////
+ ////////////////////////////////////////////////////
+ // now we can get the default view for this table //
+ ////////////////////////////////////////////////////
QContext.getQSession().setUser(new QUser().withIdReference(UUID.randomUUID().toString()));
QContext.getQSession().setValue("roleIds", null);
assertThat(personalizer.getEffectiveTableViewForCurrentSession(tableWithDefaultView))
.isNotNull()
.extracting("name").isEqualTo("a");
- /////////////////////////////////////////////////////////////
- // put roles in session - but fail to find a view-role-int //
- /////////////////////////////////////////////////////////////
- QContext.getQSession().setUser(new QUser().withIdReference(UUID.randomUUID().toString()));
- QContext.getQSession().setValue("roleIds", "1,2");
- assertEmptyView(personalizer.getEffectiveTableViewForCurrentSession(tableWithoutDefaultView));
-
///////////////////////////////
// build some view-role ints //
///////////////////////////////
@@ -339,7 +353,7 @@ class CustomizableTableViewsTablePersonalizerTest extends BaseTest
//////////////////////////////////////////
QContext.getQSession().setUser(new QUser().withIdReference(UUID.randomUUID().toString()));
QContext.getQSession().setValue("roleIds", "1");
- assertThat(personalizer.getEffectiveTableViewForCurrentSession(tableWithoutDefaultView))
+ assertThat(personalizer.getEffectiveTableViewForCurrentSession(tableWithDefaultView))
.isNotNull()
.extracting("name").isEqualTo("a");
@@ -348,23 +362,20 @@ class CustomizableTableViewsTablePersonalizerTest extends BaseTest
////////////////////////////////////////////////////
QContext.getQSession().setUser(new QUser().withIdReference(UUID.randomUUID().toString()));
QContext.getQSession().setValue("roleIds", "2");
- assertThat(personalizer.getEffectiveTableViewForCurrentSession(tableWithoutDefaultView))
+ assertThat(personalizer.getEffectiveTableViewForCurrentSession(tableWithDefaultView))
.isNotNull()
.extracting("name").isEqualTo("b");
/////////////////////////////////////////////////////////////////////////////
// find 2 views (and when they merge, their name is lost) through role int //
+ // do this last lookup twice, clearing memory record store //
+ // in between to demonstrate memoization being used. //
/////////////////////////////////////////////////////////////////////////////
QContext.getQSession().setUser(new QUser().withIdReference(UUID.randomUUID().toString()));
QContext.getQSession().setValue("roleIds", "2,3");
-
- /////////////////////////////////////////////////////////////
- // do this last lookup twice, clearing memory record store //
- // in between to demonstrate memoization being used. //
- /////////////////////////////////////////////////////////////
for(int i = 0; i < 2; i++)
{
- TableView effectiveTableViewForCurrentSession = personalizer.getEffectiveTableViewForCurrentSession(tableWithoutDefaultView);
+ TableView effectiveTableViewForCurrentSession = personalizer.getEffectiveTableViewForCurrentSession(tableWithDefaultView);
assertThat(effectiveTableViewForCurrentSession)
.isNotNull()
.extracting("name").isNull();
@@ -400,17 +411,20 @@ class CustomizableTableViewsTablePersonalizerTest extends BaseTest
CustomizableTableViewsTablePersonalizer personalizer = new CustomizableTableViewsTablePersonalizer();
QTableMetaData tableMetaData;
+ TableMetaDataInput tableActionInput = new TableMetaDataInput();
+
///////////////////////////
// empty with empty case //
///////////////////////////
tableMetaData = personalizer.applyViewToTable(
new TableView(),
- new QTableMetaData()
- );
+ new QTableMetaData(),
+ tableActionInput);
assertThat(tableMetaData.getFields()).isNullOrEmpty();
assertThat(tableMetaData.getSections()).isNullOrEmpty();
QTableMetaData baseTable = new QTableMetaData()
+ .withName("baseTable")
.withPrimaryKeyField("id")
.withField(new QFieldMetaData("id", QFieldType.STRING).withIsEditable(false))
.withField(new QFieldMetaData("secret", QFieldType.STRING).withIsHidden(true))
@@ -426,7 +440,7 @@ class CustomizableTableViewsTablePersonalizerTest extends BaseTest
////////////////////////////////////////////////////////////////////////////////
tableMetaData = personalizer.applyViewToTable(
new TableView().withFields(List.of()),
- baseTable.clone());
+ baseTable.clone(), tableActionInput);
assertEquals(2, tableMetaData.getFields().size());
QFieldMetaDataAssert.assertThat(tableMetaData.getField("id")).isNotHidden().isNotEditable().isNotRequired();
@@ -444,12 +458,12 @@ class CustomizableTableViewsTablePersonalizerTest extends BaseTest
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
tableMetaData = personalizer.applyViewToTable(
new TableView().withFields(List.of(
- new TableViewField().withFieldName("x.id").withAccessLevel(EDITABLE_OPTIONAL),
- new TableViewField().withFieldName("x.secret").withAccessLevel(EDITABLE_OPTIONAL),
- new TableViewField().withFieldName("x.mandatory").withAccessLevel(EDITABLE_OPTIONAL),
- new TableViewField().withFieldName("x.optional").withAccessLevel(EDITABLE_OPTIONAL)
+ new TableViewField().withFieldName("baseTable.id").withAccessLevel(EDITABLE_OPTIONAL),
+ new TableViewField().withFieldName("baseTable.secret").withAccessLevel(EDITABLE_OPTIONAL),
+ new TableViewField().withFieldName("baseTable.mandatory").withAccessLevel(EDITABLE_OPTIONAL),
+ new TableViewField().withFieldName("baseTable.optional").withAccessLevel(EDITABLE_OPTIONAL)
)),
- baseTable.clone());
+ baseTable.clone(), tableActionInput);
assertEquals(4, tableMetaData.getFields().size());
QFieldMetaDataAssert.assertThat(tableMetaData.getField("id")).isNotHidden().isNotEditable().isNotRequired();
@@ -478,7 +492,7 @@ class CustomizableTableViewsTablePersonalizerTest extends BaseTest
new TableView().withWidgets(List.of(
new TableViewWidget().withWidgetName("lilWidgy").withAccessLevel(WidgetAccessLevel.HAS_ACCESS)
)),
- baseTable.clone());
+ baseTable.clone(), tableActionInput);
assertEquals(2, tableMetaData.getFields().size());
assertEquals(3, tableMetaData.getSections().size()); // one for id, one for mandatory, and 1 for our widget
assertEquals(List.of("s0", "s1", "w0"), tableMetaData.getSections().stream().map(s -> s.getName()).toList());
@@ -486,6 +500,288 @@ class CustomizableTableViewsTablePersonalizerTest extends BaseTest
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testApplyViewToTableWithVirtualFields()
+ {
+ CustomizableTableViewsTablePersonalizer personalizer = new CustomizableTableViewsTablePersonalizer();
+ TableMetaDataInput tableActionInput = new TableMetaDataInput();
+
+ QTableMetaData baseTable = new QTableMetaData()
+ .withName("vfTable")
+ .withPrimaryKeyField("id")
+ .withField(new QFieldMetaData("id", QFieldType.STRING).withIsEditable(false))
+ .withField(new QFieldMetaData("optional", QFieldType.STRING))
+ .withVirtualField(new QVirtualFieldMetaData("computed", QFieldType.STRING).withLabel("Computed"))
+ .withVirtualField(new QVirtualFieldMetaData("derived", QFieldType.STRING).withLabel("Derived"))
+ .withSection(SectionFactory.defaultT1("id").withName("s0"))
+ .withSection(SectionFactory.defaultT2("optional").withName("s1"));
+
+ /////////////////////////////////////////////////////////////////////////////////
+ // empty view - no virtual fields kept (no "always keep" rules for virtuals) //
+ /////////////////////////////////////////////////////////////////////////////////
+ QTableMetaData tableMetaData = personalizer.applyViewToTable(
+ new TableView().withFields(List.of()),
+ baseTable.clone(), tableActionInput);
+
+ assertEquals(1, tableMetaData.getFields().size());
+ assertNotNull(tableMetaData.getField("id"));
+ assertTrue(tableMetaData.getVirtualFields().isEmpty());
+
+ ///////////////////////////////////////////////////////////////
+ // view references one virtual field - only that one is kept //
+ ///////////////////////////////////////////////////////////////
+ tableMetaData = personalizer.applyViewToTable(
+ new TableView().withFields(List.of(
+ new TableViewField().withFieldName("vfTable.computed").withAccessLevel(READ_ONLY)
+ )),
+ baseTable.clone(), tableActionInput);
+
+ assertEquals(1, tableMetaData.getFields().size());
+ assertEquals(1, tableMetaData.getVirtualFields().size());
+ assertNotNull(tableMetaData.getVirtualFields().get("computed"));
+ assertNull(tableMetaData.getVirtualFields().get("derived"));
+
+ /////////////////////////////////////////////////////////////////////////////
+ // view references both virtual fields and a regular field - all are kept //
+ /////////////////////////////////////////////////////////////////////////////
+ tableMetaData = personalizer.applyViewToTable(
+ new TableView().withFields(List.of(
+ new TableViewField().withFieldName("vfTable.optional").withAccessLevel(EDITABLE_OPTIONAL),
+ new TableViewField().withFieldName("vfTable.computed").withAccessLevel(READ_ONLY),
+ new TableViewField().withFieldName("vfTable.derived").withAccessLevel(READ_ONLY)
+ )),
+ baseTable.clone(), tableActionInput);
+
+ assertEquals(2, tableMetaData.getFields().size()); // id + optional
+ assertEquals(2, tableMetaData.getVirtualFields().size()); // computed + derived
+
+ assertEquals(List.of("optional"), tableMetaData.getSection("s1").getFieldNames());
+
+ //////////////////////////////////////////////////////////////////
+ // table with no virtual fields at all - should not break //
+ //////////////////////////////////////////////////////////////////
+ QTableMetaData noVfTable = new QTableMetaData()
+ .withName("noVfTable")
+ .withPrimaryKeyField("id")
+ .withField(new QFieldMetaData("id", QFieldType.STRING).withIsEditable(false))
+ .withSection(SectionFactory.defaultT1("id").withName("s0"));
+
+ tableMetaData = personalizer.applyViewToTable(
+ new TableView().withFields(List.of()),
+ noVfTable.clone(), tableActionInput);
+
+ assertEquals(1, tableMetaData.getFields().size());
+ assertTrue(tableMetaData.getVirtualFields() == null || tableMetaData.getVirtualFields().isEmpty());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testDynamicDefaultValueFields()
+ {
+ ///////////////////////////
+ // empty with empty case //
+ ///////////////////////////
+ QTableMetaData baseTable = new QTableMetaData()
+ .withPrimaryKeyField("id")
+ .withField(new QFieldMetaData("id", QFieldType.STRING).withIsEditable(false))
+ .withField(new QFieldMetaData("secret", QFieldType.STRING).withIsHidden(true))
+ .withField(new QFieldMetaData("mandatory", QFieldType.STRING).withIsRequired(true))
+ .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withBehavior(DynamicDefaultValueBehavior.CREATE_DATE))
+ .withField(new QFieldMetaData("optional", QFieldType.STRING))
+ .withSection(SectionFactory.defaultT1("id").withName("s0"))
+ .withSection(SectionFactory.defaultT2("secret", "mandatory").withName("s1"))
+ .withSection(SectionFactory.defaultT2("optional", "createDate").withName("s2"));
+
+ ////////////////////////////////////////////////////////////////////////
+ // for actions that aren't insert or update, just get pkey & required //
+ ////////////////////////////////////////////////////////////////////////
+ for(AbstractTableActionInput actionInput : List.of(new QueryInput(), new DeleteInput(), new TableMetaDataInput()))
+ {
+ QTableMetaData tableMetaData = new CustomizableTableViewsTablePersonalizer().applyViewToTable(
+ new TableView().withFields(List.of()),
+ baseTable.clone(), actionInput);
+
+ assertEquals(2, tableMetaData.getFields().size());
+ QFieldMetaDataAssert.assertThat(tableMetaData.getField("id")).isNotNull();
+ QFieldMetaDataAssert.assertThat(tableMetaData.getField("mandatory")).isNotNull();
+ }
+
+ /////////////////////////////////////////////////////
+ // for insert or update action, get createDate too //
+ /////////////////////////////////////////////////////
+ for(AbstractTableActionInput actionInput : List.of(new InsertInput(), new UpdateInput()))
+ {
+ QTableMetaData tableMetaData = new CustomizableTableViewsTablePersonalizer().applyViewToTable(
+ new TableView().withFields(List.of()),
+ baseTable.clone(), actionInput);
+
+ assertEquals(3, tableMetaData.getFields().size());
+ QFieldMetaDataAssert.assertThat(tableMetaData.getField("id")).isNotNull();
+ QFieldMetaDataAssert.assertThat(tableMetaData.getField("mandatory")).isNotNull();
+ QFieldMetaDataAssert.assertThat(tableMetaData.getField("createDate")).isNotNull();
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testFieldsInSecurityLocks()
+ {
+ ///////////////////////////
+ // empty with empty case //
+ ///////////////////////////
+ QTableMetaData baseTable = new QTableMetaData()
+ .withPrimaryKeyField("id")
+ .withField(new QFieldMetaData("id", QFieldType.STRING).withIsEditable(false))
+ .withField(new QFieldMetaData("a", QFieldType.STRING))
+ .withField(new QFieldMetaData("b", QFieldType.STRING))
+ .withField(new QFieldMetaData("c", QFieldType.STRING))
+ .withField(new QFieldMetaData("d", QFieldType.STRING));
+
+ //////////////////////////////////////////////////////
+ // with no security field, only id should come back //
+ //////////////////////////////////////////////////////
+ {
+ QTableMetaData tableMetaData = new CustomizableTableViewsTablePersonalizer().applyViewToTable(
+ new TableView().withFields(List.of()),
+ baseTable.clone(), new QueryInput());
+
+ assertEquals(1, tableMetaData.getFields().size());
+ QFieldMetaDataAssert.assertThat(tableMetaData.getField("id")).isNotNull();
+ }
+
+ ///////////////////////////////////////////////////////
+ // with a simple security field, it should come back //
+ ///////////////////////////////////////////////////////
+ baseTable.setRecordSecurityLocks(List.of(
+ new RecordSecurityLock().withFieldName("a")
+ ));
+ {
+ QTableMetaData tableMetaData = new CustomizableTableViewsTablePersonalizer().applyViewToTable(
+ new TableView().withFields(List.of()),
+ baseTable.clone(), new QueryInput());
+
+ assertEquals(2, tableMetaData.getFields().size());
+ QFieldMetaDataAssert.assertThat(tableMetaData.getField("id")).isNotNull();
+ QFieldMetaDataAssert.assertThat(tableMetaData.getField("a")).isNotNull();
+ }
+
+ /////////////////////////////////////////////////////////////
+ // with a multi-lock, all included fields should come back //
+ /////////////////////////////////////////////////////////////
+ baseTable.setRecordSecurityLocks(List.of(
+ new MultiRecordSecurityLock()
+ .withOperator(MultiRecordSecurityLock.BooleanOperator.OR)
+ .withLocks(List.of(
+ new RecordSecurityLock().withFieldName("b"),
+ new MultiRecordSecurityLock()
+ .withOperator(MultiRecordSecurityLock.BooleanOperator.AND)
+ .withLocks(List.of(
+ new RecordSecurityLock().withFieldName("c"),
+ new RecordSecurityLock().withFieldName("d")
+ ))
+ ))
+ ));
+ {
+ QTableMetaData tableMetaData = new CustomizableTableViewsTablePersonalizer().applyViewToTable(
+ new TableView().withFields(List.of()),
+ baseTable.clone(), new QueryInput());
+
+ assertEquals(4, tableMetaData.getFields().size());
+ QFieldMetaDataAssert.assertThat(tableMetaData.getField("id")).isNotNull();
+ QFieldMetaDataAssert.assertThat(tableMetaData.getField("b")).isNotNull();
+ QFieldMetaDataAssert.assertThat(tableMetaData.getField("c")).isNotNull();
+ QFieldMetaDataAssert.assertThat(tableMetaData.getField("d")).isNotNull();
+ }
+
+ ///////////////////////////////////////////////////////////
+ // make sure lock field from join table doesn't break us //
+ ///////////////////////////////////////////////////////////
+ baseTable.setRecordSecurityLocks(List.of(
+ new RecordSecurityLock().withFieldName("join.a")
+ ));
+ {
+ QTableMetaData tableMetaData = new CustomizableTableViewsTablePersonalizer().applyViewToTable(
+ new TableView().withFields(List.of()),
+ baseTable.clone(), new QueryInput());
+
+ assertEquals(1, tableMetaData.getFields().size());
+ QFieldMetaDataAssert.assertThat(tableMetaData.getField("id")).isNotNull();
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testFieldsWithSupplementalMetaData()
+ {
+ ///////////////////////////
+ // empty with empty case //
+ ///////////////////////////
+ QTableMetaData baseTable = new QTableMetaData()
+ .withPrimaryKeyField("id")
+ .withField(new QFieldMetaData("id", QFieldType.STRING).withIsEditable(false))
+ .withField(new QFieldMetaData("a", QFieldType.STRING))
+ .withField(new QFieldMetaData("b", QFieldType.STRING))
+ .withField(new QFieldMetaData("c", QFieldType.STRING))
+ .withField(new QFieldMetaData("d", QFieldType.STRING));
+
+ //////////////////////////////////
+ // base case, should just be id //
+ //////////////////////////////////
+ {
+ QTableMetaData tableMetaData = new CustomizableTableViewsTablePersonalizer().applyViewToTable(
+ new TableView().withFields(List.of()),
+ baseTable.clone(), new QueryInput());
+
+ assertEquals(1, tableMetaData.getFields().size());
+ QFieldMetaDataAssert.assertThat(tableMetaData.getField("id")).isNotNull();
+ }
+
+ ///////////////////////////////////////////////////////////////////////
+ // supplemental meta data, but without a rule should not be included //
+ ///////////////////////////////////////////////////////////////////////
+ baseTable.getField("a").withSupplementalMetaData(new CustomizableTableViewsFieldMetaData());
+ {
+ QTableMetaData tableMetaData = new CustomizableTableViewsTablePersonalizer().applyViewToTable(
+ new TableView().withFields(List.of()),
+ baseTable.clone(), new QueryInput());
+
+ assertEquals(1, tableMetaData.getFields().size());
+ QFieldMetaDataAssert.assertThat(tableMetaData.getField("id")).isNotNull();
+ }
+
+ //////////////////////////////////////////////////////////////////////
+ // supplemental meta data, with the include rule should be included //
+ //////////////////////////////////////////////////////////////////////
+ baseTable.getField("b").withSupplementalMetaData(new CustomizableTableViewsFieldMetaData()
+ .withRule(CustomizableTableViewsFieldMetaData.Rule.ALWAYS_KEEP_FIELD));
+ {
+ QTableMetaData tableMetaData = new CustomizableTableViewsTablePersonalizer().applyViewToTable(
+ new TableView().withFields(List.of()),
+ baseTable.clone(), new QueryInput());
+
+ assertEquals(2, tableMetaData.getFields().size());
+ QFieldMetaDataAssert.assertThat(tableMetaData.getField("id")).isNotNull();
+ QFieldMetaDataAssert.assertThat(tableMetaData.getField("b")).isNotNull();
+ }
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
@@ -550,6 +846,11 @@ class CustomizableTableViewsTablePersonalizerTest extends BaseTest
new InsertAction().execute(new InsertInput(CustomizableTable.TABLE_NAME).withRecordEntities(List.of(
new CustomizableTable().withTableName(baseTable.getName()).withIsActive(true))));
+ ////////////////////////////////////////////////////////////////
+ // assert system inputSource still gets same as default table //
+ ////////////////////////////////////////////////////////////////
+ assertSame(baseTable, personalizer.execute(new TableMetaDataPersonalizerInput().withTableMetaData(baseTable).withInputSource(QInputSource.SYSTEM)));
+
///////////////////////////////////////////
// assert it's pretty empty with no view //
///////////////////////////////////////////
@@ -571,6 +872,11 @@ class CustomizableTableViewsTablePersonalizerTest extends BaseTest
new InsertAction().execute(new InsertInput(TableViewRoleInt.TABLE_NAME).withRecordEntities(List.of(
new TableViewRoleInt().withRoleId(101).withTableViewId(1))));
+ ////////////////////////////////////////////////////////////////
+ // assert system inputSource still gets same as default table //
+ ////////////////////////////////////////////////////////////////
+ assertSame(baseTable, personalizer.execute(new TableMetaDataPersonalizerInput().withTableMetaData(baseTable).withInputSource(QInputSource.SYSTEM)));
+
QContext.getQSession().setUser(new QUser().withIdReference(UUID.randomUUID().toString()));
QContext.getQSession().setValue("roleIds", "101");
@@ -583,6 +889,54 @@ class CustomizableTableViewsTablePersonalizerTest extends BaseTest
assertEquals(Set.of("id", "optional"), personalizedTable.getFields().values().stream().map(s -> s.getName()).collect(Collectors.toSet()));
assertEquals(3, personalizedTable.getSections().size());
assertEquals(List.of("s0", "s1", "w0"), personalizedTable.getSections().stream().map(s -> s.getName()).toList());
+
+ //////////////////////////////////////////////////////////////////////////
+ // add a join table, and a field from it in the main table's t2 section //
+ //////////////////////////////////////////////////////////////////////////
+ QTableMetaData joinTable = new QTableMetaData()
+ .withName("joinTable")
+ .withPrimaryKeyField("id")
+ .withField(new QFieldMetaData("id", QFieldType.STRING).withIsEditable(false))
+ .withField(new QFieldMetaData("joinField", QFieldType.STRING))
+ .withSection(SectionFactory.defaultT1("id", "joinField").withName("s0"));
+ QContext.getQInstance().addTable(joinTable);
+ QContext.getQInstance().addJoin(new QJoinMetaData().withLeftTable("baseTable").withRightTable("joinTable").withInferredName().withType(JoinType.ONE_TO_ONE).withJoinOn(new JoinOn("id", "joinField")));
+ baseTable.getSection("s1").getFieldNames().add("joinTable.joinField");
+
+ //////////////////////////////////////////
+ // by default, should get the joinField //
+ //////////////////////////////////////////
+ personalizedTable = personalizer.execute(input);
+ assertEquals(List.of("optional", "joinTable.joinField"), personalizedTable.getSection("s1").getFieldNames());
+
+ //////////////////////////////////////////////////////////////////////////////////
+ // activate customization on the join table, which should remove the join field //
+ //////////////////////////////////////////////////////////////////////////////////
+ new InsertAction().execute(new InsertInput(CustomizableTable.TABLE_NAME).withRecordEntities(List.of(
+ new CustomizableTable().withTableName(joinTable.getName()).withIsActive(true))));
+ personalizedTable = personalizer.execute(input);
+ assertEquals(List.of("optional"), personalizedTable.getSection("s1").getFieldNames());
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // add a view to the join table, with a field, and attach it to the view - then the user gets the field back //
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ new InsertAction().execute(new InsertInput(TableView.TABLE_NAME).withRecordEntities(List.of(
+ new TableView().withId(2).withCustomizableTableId(2).withName("a")
+ .withFields(List.of(new TableViewField().withFieldName(joinTable.getName() + ".joinField").withAccessLevel(EDITABLE_OPTIONAL))))));
+ new InsertAction().execute(new InsertInput(TableViewRoleInt.TABLE_NAME).withRecordEntities(List.of(
+ new TableViewRoleInt().withRoleId(101).withTableViewId(2))));
+
+ personalizedTable = personalizer.execute(input);
+ assertEquals(List.of("optional", "joinTable.joinField"), personalizedTable.getSection("s1").getFieldNames());
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////
+ // finally, set user to not have this role, make sure the section with the join field disappears //
+ // (the non-join field will have also gone away by switching to other role, so the section is empty) //
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////
+ QContext.getQSession().setUser(new QUser().withIdReference(UUID.randomUUID().toString()));
+ QContext.getQSession().setValue("roleIds", "102");
+ personalizedTable = personalizer.execute(input);
+ assertNull(personalizedTable.getSection("s1"));
}
diff --git a/src/test/java/com/kingsrook/qbits/customizabletableviews/model/CustomizableTableFieldPVSTest.java b/src/test/java/com/kingsrook/qbits/customizabletableviews/model/CustomizableTableFieldPVSTest.java
index 664d567..3a775fd 100644
--- a/src/test/java/com/kingsrook/qbits/customizabletableviews/model/CustomizableTableFieldPVSTest.java
+++ b/src/test/java/com/kingsrook/qbits/customizabletableviews/model/CustomizableTableFieldPVSTest.java
@@ -32,11 +32,15 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSourceInput;
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.possiblevalues.QPossibleValue;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.SectionFactory;
import org.junit.jupiter.api.Test;
+import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
/*******************************************************************************
@@ -84,4 +88,60 @@ class CustomizableTableFieldPVSTest extends BaseTest
assertEquals("Optional", possibleValues.get(1).getLabel());
}
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testWithVirtualFields() throws QException
+ {
+ QTableMetaData testTable = new QTableMetaData()
+ .withName("testTable")
+ .withPrimaryKeyField("id")
+ .withField(new QFieldMetaData("id", QFieldType.STRING).withIsEditable(false))
+ .withField(new QFieldMetaData("optional", QFieldType.STRING).withLabel("Optional"))
+ .withVirtualField(new QVirtualFieldMetaData("computed", QFieldType.STRING).withLabel("Computed"))
+ .withVirtualField(new QVirtualFieldMetaData("hiddenVirtual", QFieldType.STRING).withIsHidden(true).withLabel("Hidden Virtual"))
+ .withSection(SectionFactory.defaultT1("id").withName("s0"))
+ .withSection(SectionFactory.defaultT2("optional").withName("s1"));
+ QContext.getQInstance().addTable(testTable);
+
+ new InsertAction().execute(new InsertInput(CustomizableTable.TABLE_NAME).withRecordEntities(List.of(
+ new CustomizableTable().withTableName(testTable.getName()).withIsActive(true))));
+
+ Integer tableViewId = 17;
+ new InsertAction().execute(new InsertInput(TableView.TABLE_NAME).withRecordEntities(List.of(
+ new TableView().withId(tableViewId).withCustomizableTableId(1).withName("a"))));
+
+ /////////////////////////////////////////////////////////////////////
+ // search should include the non-hidden virtual field in results //
+ /////////////////////////////////////////////////////////////////////
+ List> possibleValues = new CustomizableTableFieldPVS().search(new SearchPossibleValueSourceInput().withOtherValues(Map.of("tableViewId", tableViewId)));
+ assertEquals(2, possibleValues.size());
+ assertThat(possibleValues)
+ .anyMatch(pv1 -> "testTable.computed".equals(pv1.getId()) && "Computed".equals(pv1.getLabel()))
+ .anyMatch(pv1 -> "testTable.optional".equals(pv1.getId()) && "Optional".equals(pv1.getLabel()));
+
+ ///////////////////////////////////////////////////////////////////
+ // getPossibleValue should resolve a virtual field by table.name //
+ ///////////////////////////////////////////////////////////////////
+ QPossibleValue pv = new CustomizableTableFieldPVS().getPossibleValue("testTable.computed");
+ assertNotNull(pv);
+ assertEquals("testTable.computed", pv.getId());
+ assertEquals("Computed", pv.getLabel());
+
+ /////////////////////////////////////////////////////
+ // getPossibleValue for a normal field still works //
+ /////////////////////////////////////////////////////
+ QPossibleValue pvNormal = new CustomizableTableFieldPVS().getPossibleValue("testTable.optional");
+ assertNotNull(pvNormal);
+ assertEquals("Optional", pvNormal.getLabel());
+
+ /////////////////////////////////////////////////////////
+ // getPossibleValue for a nonexistent field returns null //
+ /////////////////////////////////////////////////////////
+ assertNull(new CustomizableTableFieldPVS().getPossibleValue("testTable.doesNotExist"));
+ }
+
}
\ No newline at end of file
diff --git a/src/test/java/com/kingsrook/qbits/customizabletableviews/model/FieldAccessLevelTest.java b/src/test/java/com/kingsrook/qbits/customizabletableviews/model/FieldAccessLevelTest.java
index 31b2afc..75ad5fa 100644
--- a/src/test/java/com/kingsrook/qbits/customizabletableviews/model/FieldAccessLevelTest.java
+++ b/src/test/java/com/kingsrook/qbits/customizabletableviews/model/FieldAccessLevelTest.java
@@ -25,12 +25,16 @@ package com.kingsrook.qbits.customizabletableviews.model;
import java.util.function.BiFunction;
import com.kingsrook.qbits.customizabletableviews.BaseTest;
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 org.junit.jupiter.api.Test;
import static com.kingsrook.qbits.customizabletableviews.QFieldMetaDataAssert.assertThat;
import static com.kingsrook.qbits.customizabletableviews.model.FieldAccessLevel.EDITABLE_OPTIONAL;
import static com.kingsrook.qbits.customizabletableviews.model.FieldAccessLevel.EDITABLE_REQUIRED;
import static com.kingsrook.qbits.customizabletableviews.model.FieldAccessLevel.READ_ONLY;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
/*******************************************************************************
@@ -122,4 +126,31 @@ class FieldAccessLevelTest extends BaseTest
assertThat(doApply.apply(READ_ONLY, hiddenField))/*....*/.isHidden().isNotRequired().isEditable(); // no changes
}
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testValidateForVirtualField()
+ {
+ ///////////////////////////////////////////////////////////////////////////////////
+ // a virtual field (even if editable) should be treated as read-only by validate //
+ ///////////////////////////////////////////////////////////////////////////////////
+ QVirtualFieldMetaData editableVirtualField = new QVirtualFieldMetaData("vf", QFieldType.STRING).withIsEditable(true);
+
+ assertNull(READ_ONLY.validateForField(editableVirtualField));
+ assertNotNull(EDITABLE_REQUIRED.validateForField(editableVirtualField));
+ assertNotNull(EDITABLE_OPTIONAL.validateForField(editableVirtualField));
+
+ /////////////////////////////////////////////////
+ // a non-editable virtual field - same results //
+ /////////////////////////////////////////////////
+ QVirtualFieldMetaData readOnlyVirtualField = new QVirtualFieldMetaData("vf", QFieldType.STRING).withIsEditable(false);
+
+ assertNull(READ_ONLY.validateForField(readOnlyVirtualField));
+ assertNotNull(EDITABLE_REQUIRED.validateForField(readOnlyVirtualField));
+ assertNotNull(EDITABLE_OPTIONAL.validateForField(readOnlyVirtualField));
+ }
+
}
\ No newline at end of file
diff --git a/src/test/java/com/kingsrook/qbits/customizabletableviews/tables/customizers/TableViewFieldCustomizerTest.java b/src/test/java/com/kingsrook/qbits/customizabletableviews/tables/customizers/TableViewFieldCustomizerTest.java
index 65c06e8..bb0d21d 100644
--- a/src/test/java/com/kingsrook/qbits/customizabletableviews/tables/customizers/TableViewFieldCustomizerTest.java
+++ b/src/test/java/com/kingsrook/qbits/customizabletableviews/tables/customizers/TableViewFieldCustomizerTest.java
@@ -32,6 +32,7 @@ import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
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.tables.QTableMetaData;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
@@ -74,6 +75,27 @@ class TableViewFieldCustomizerTest extends BaseTest
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testVirtualField() throws QException
+ {
+ QContext.getQInstance().addTable(new QTableMetaData()
+ .withName("testTable2")
+ .withField(new QFieldMetaData("id", QFieldType.STRING))
+ .withVirtualField(new QVirtualFieldMetaData("virtualField", QFieldType.STRING).withLabel("Virtual")));
+
+ //////////////////////////////////////////////////////////////////////////////
+ // virtual fields are treated as read-only, so only READ_ONLY should pass. //
+ //////////////////////////////////////////////////////////////////////////////
+ assertNoError(new TableViewField().withFieldName("testTable2.virtualField").withAccessLevel(FieldAccessLevel.READ_ONLY));
+ assertError(new TableViewField().withFieldName("testTable2.virtualField").withAccessLevel(FieldAccessLevel.EDITABLE_REQUIRED));
+ assertError(new TableViewField().withFieldName("testTable2.virtualField").withAccessLevel(FieldAccessLevel.EDITABLE_OPTIONAL));
+ }
+
+
+
/***************************************************************************
*
***************************************************************************/