Release/0.4 #6

Merged
darinkelkhoff merged 22 commits from release/0.4 into main 2026-03-13 15:03:20 +00:00
12 changed files with 958 additions and 113 deletions

No files matched your search

+1 -1
View File
@@ -13,7 +13,7 @@ workflows:
branches:
ignore: /(develop|main|release/.*|hotfix/.*|integration.*)/
tags:
only: []
only: []
publish_snapshot:
jobs:
+15 -1
View File
@@ -1,4 +1,18 @@
# qbit-customizable-table-views
# QBit: Customizable Table Views
[![Build Status](https://dl.circleci.com/status-badge/img/gh/Kingsrook/qbit-customizable-table-views/tree/develop.svg?style=svg)](https://circleci.com/gh/Kingsrook/qbit-customizable-table-views)
[![Maven Central](https://img.shields.io/maven-central/v/com.kingsrook.qbits/qbit-customizable-table-views.svg)](https://central.sonatype.com/namespace/com.kingsrook.qbits)
[![Java](https://img.shields.io/badge/java-17+-blue.svg)](https://adoptium.net/)
[![License](https://img.shields.io/badge/license-AGPL%203.0-green.svg)](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.
+21 -14
View File
@@ -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>
@@ -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();
@@ -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()))
{
@@ -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);
}
}
@@ -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)
{
@@ -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)
@@ -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"));
}
@@ -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"));
}
}
@@ -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));
}
}
@@ -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));
}
/***************************************************************************
*
***************************************************************************/