Release/0.4 #6
No files matched your search
@@ -13,7 +13,7 @@ workflows:
|
||||
branches:
|
||||
ignore: /(develop|main|release/.*|hotfix/.*|integration.*)/
|
||||
tags:
|
||||
only: []
|
||||
only: []
|
||||
|
||||
publish_snapshot:
|
||||
jobs:
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -20,8 +20,7 @@
|
||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<parent>
|
||||
<groupId>com.kingsrook</groupId>
|
||||
@@ -31,13 +30,15 @@
|
||||
|
||||
<groupId>com.kingsrook.qbits</groupId>
|
||||
<artifactId>qbit-customizable-table-views</artifactId>
|
||||
<version>${revision}</version>
|
||||
<packaging>jar</packaging>
|
||||
<version>0.4.0-SNAPSHOT</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>QBit Customizable Table Views</name>
|
||||
<description>Customizable table views module for the QBit framework, providing table personalization and field/widget access control</description>
|
||||
<url>https://github.com/Kingsrook/qbit-customizable-table-views</url>
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<licenses>
|
||||
<license>
|
||||
<name>GNU Affero General Public License v3.0</name>
|
||||
@@ -64,23 +65,15 @@
|
||||
</scm>
|
||||
|
||||
<properties>
|
||||
<revision>0.3.0</revision>
|
||||
<revision>0.4.0-RC.1</revision>
|
||||
</properties>
|
||||
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
<artifactId>qqq-bom-pom</artifactId>
|
||||
<version>0.27.9</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit</groupId>
|
||||
<artifactId>junit-bom</artifactId>
|
||||
<version>5.10.0</version>
|
||||
<version>0.40.0-SNAPSHOT</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
@@ -95,6 +88,13 @@
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
<artifactId>qqq-backend-module-rdbms</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<version>2.2.220</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -110,4 +110,11 @@
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>sonatype-snapshots</id>
|
||||
<url>https://central.sonatype.com/repository/maven-snapshots/</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
</project>
|
||||
+312
-57
@@ -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<String, QFieldMetaData> cloneFields = cloneTable.getFields();
|
||||
if(cloneFields == null)
|
||||
{
|
||||
cloneFields = new LinkedHashMap<>();
|
||||
cloneTable.setFields(cloneFields);
|
||||
}
|
||||
|
||||
Map<String, QFieldMetaData> 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<String> 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<String, QFieldMetaData> fieldsToKeep = getFieldsToKeepForTable(tableView, cloneTable, tableActionInput);
|
||||
cloneTable.setFields(fieldsToKeep);
|
||||
|
||||
Map<String, QVirtualFieldMetaData> 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<String, Set<String>> getFieldsToKeepFromJoinTableCache = null;
|
||||
|
||||
Iterator<String> 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<String> 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<String> getFieldsToKeepFromJoinTable(String joinTableName, AbstractTableActionInput tableActionInput, Map<String, Set<String>> 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<String, QFieldMetaData> 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<String, QFieldMetaData> getFieldsToKeepForTable(TableView tableView, QTableMetaData cloneTable, AbstractTableActionInput tableActionInput)
|
||||
{
|
||||
Map<String, QFieldMetaData> cloneFields = cloneTable.getFields();
|
||||
if(cloneFields == null)
|
||||
{
|
||||
cloneFields = new LinkedHashMap<>();
|
||||
cloneTable.setFields(cloneFields);
|
||||
}
|
||||
|
||||
Map<String, QFieldMetaData> 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<String, QVirtualFieldMetaData> getVirtualFieldsToKeepForTable(TableView tableView, QTableMetaData cloneTable, AbstractTableActionInput tableActionInput)
|
||||
{
|
||||
Map<String, QVirtualFieldMetaData> cloneFields = cloneTable.getVirtualFields();
|
||||
if(cloneFields == null)
|
||||
{
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
Map<String, QVirtualFieldMetaData> 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.
|
||||
*
|
||||
* <p>built-in rules are:</p>
|
||||
* <ul>
|
||||
* <li>field is required</li>
|
||||
* <li>field is primary key</li>
|
||||
* <li>field is part of table's security locks</li>
|
||||
* <li>there's supplemental metadata from this qbit that says to keep the field</li>
|
||||
* <li>this is an insert or update, and there's a {@link DynamicDefaultValueBehavior}</li>
|
||||
* </ul>
|
||||
*
|
||||
* 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<RecordSecurityLock> 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<Integer> 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();
|
||||
|
||||
+13
-1
@@ -96,6 +96,12 @@ public class CustomizableTableFieldPVS implements QCustomPossibleValueProvider<S
|
||||
{
|
||||
return (new QPossibleValue<>(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<S
|
||||
|
||||
if(table != null)
|
||||
{
|
||||
for(QFieldMetaData field : table.getFields().values())
|
||||
List<QFieldMetaData> 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()))
|
||||
{
|
||||
|
||||
+89
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+2
-1
@@ -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<String>
|
||||
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)
|
||||
{
|
||||
|
||||
+1
-1
@@ -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)
|
||||
|
||||
+391
-37
@@ -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"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
+60
@@ -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<QPossibleValue<String>> 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<String> 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<String> 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"));
|
||||
}
|
||||
|
||||
}
|
||||
+31
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
+22
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
|
||||
Reference in new issue
Block a user