Compare commits

...
Author SHA1 Message Date
james.maesandCodex 70e3c31de1 ci: centralize QBit source scanning
Co-Authored-By: Codex <noreply@openai.com>
2026-10-21 10:17:33 -05:00
james.maesandCodex 74939c3b7c ci: adopt central orb 0.7.12 and stable gates (#19)
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 08:53:16 -05:00
james.maesandCodex 55c89ec2b3 ci: centralize rc release completion in qqq orb (#18)
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 02:24:32 -05:00
james.maesandCodex dbdedf7d00 ci: publish GA artifacts only from stable tags (#17)
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 01:09:03 -05:00
james.maesandCodex 914aac2cbb ci: adopt shared orb 0.7.11 (#16)
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 00:57:35 -05:00
james.maes b04cc0b0fa Merge pull request #15 from QRun-IO/feature/central-ci
ci: adopt shared QQQ orb standard
2026-10-20 19:20:20 -05:00
james.maesandCodex a42e85d1c2 ci: adopt verified central QQQ orb 0.7.3
Co-Authored-By: Codex <noreply@openai.com>
2026-10-20 19:07:54 -05:00
james.maes 0be1fc54a4 Merge pull request #13 from QRun-IO/ci/release-rc-secrets-inherit
ci(release-rc): pass secrets so the shared workflow can sign the RC tag
2026-10-16 08:05:21 -05:00
james.maesandClaude Opus 5.5 e43515a92e ci(release-rc): pass secrets to the shared workflow so it can sign the RC tag
QRun-IO/.github#2 adds a tag-and-release job that signs vX with the
QRUN_TAG_SIGNING_KEY secret after Maven Central confirms the artifact.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 16:22:50 -05:00
james.maes bce290d6b4 Merge pull request #12 from QRun-IO/feature/release-rc-automation
ci: automate RC releases; hotfix/* pushes no longer publish
2026-10-13 15:00:39 -05:00
james.maesandClaude Opus 5.5 ee20dfef2a ci: automate RC releases; hotfix/* pushes no longer publish
Removes publish_hotfix_release: a hotfix/* branch push published a GA
release to Maven Central with no tag or review (QRun-IO/qbit-cdn#12,
QRun-IO/qqq#1121). hotfix/* branches now run test_only; a hotfix
release publishes from a reviewed vX.Y.Z tag through publish_release.
Adds .github/workflows/release-rc.yml (reusable QRun-IO/.github
qbit-release-rc): a manual dispatch, or merging a develop PR labelled
release:rc, merges develop into the release line and the release/* push
runs the existing CircleCI RC publish.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:37:56 -05:00
james.maes ad11a13e62 Merge pull request #11 from QRun-IO/feature/ga-only-release-tags
ci: publish only GA tags (vX.Y.Z), never RC tags
2026-10-12 16:07:53 -05:00
james.maesandClaude Opus 5.5 8cebae2f2c ci: publish only GA tags (vX.Y.Z), never RC tags
An RC tag matched /v.*/ and the orb republished it as the next RC.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 15:59:43 -05:00
james.maes 2f1ff80065 build: re-pin to qbit-build-parent 2.0.0 (#8)
Closes QRun-IO/qqq#765. Part of QRun-IO/qqq-all#1.
2026-09-25 20:44:34 -05:00
james.maes dbfbc59f0c build: re-pin to qbit-build-parent 2.0.0 (QRun-IO/qqq#765)
Migrates the removed qqq 4.0 APIs with a sparse-update test, adds an opt-in qqq-snapshot profile, and keeps CircleCI package installs unattended.

Part of QRun-IO/qqq-all#1
2026-09-25 20:41:08 -05:00
james.maes 512defb9cc chore: back-merge main into develop
Records main's 0.4.0 release commits on develop, keeping develop's 0.5.0-SNAPSHOT revision.
2026-09-25 20:40:13 -05:00
James MaesandClaude Opus 4.8 b3ac14a0fd docs: add CLAUDE.md pointer to QQQ knowledge-base dossier + hub
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 05:31:05 -05:00
james.maesandClaude Opus 4.7 6a0f548afa chore: add CODEOWNERS pointing to sole maintainer
Architecture audit pass 2026-05-21. Minimal CODEOWNERS file
making the implicit ownership explicit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 17:14:07 -05:00
Darin Kelkhoff 523fd34095 chore: fix ot use revision property in <version> tag 2026-03-13 10:10:36 -05:00
Darin Kelkhoff 8f92567c93 chore: update revision/version to 0.5.0-SNAPSHOT 2026-03-13 10:05:15 -05:00
CircleCI 9a6eca0189 Bump version to 0.4.0 [skip ci] 2026-03-13 15:05:08 +00:00
Darin Kelkhoff 67252ae1f3 Merge pull request #6 from QRun-IO/release/0.4
Release/0.4
2026-03-13 10:03:20 -05:00
CircleCI 5323d4ed25 Bump version to 0.4.0-RC.1 [skip ci] 2026-03-13 14:57:53 +00:00
Darin Kelkhoff b0ec372885 Merge pull request #4 from QRun-IO/feature/virtual-field-support
feat(virtualFields): add baseline support for virtualFields
2026-03-09 12:05:44 -05:00
Darin Kelkhoff 5c97a017dc fix: avoid NPE if table has no virtual fields 2026-03-09 11:59:59 -05:00
Darin Kelkhoff 2f3938929b feat(virtualFields): add baseline support for virtualFields 2026-03-09 09:00:03 -05:00
16 changed files with 476 additions and 80 deletions

No files matched your search

+10 -53
View File
@@ -1,57 +1,14 @@
version: 2.1
setup: true
orbs:
qqq-orb: kingsrook/qqq-orb@0.5.0
qqq-orb: kingsrook/qqq-orb@0.7.12
workflows:
test_only:
setup:
jobs:
- qqq-orb/mvn_test_only:
context: [ qqq-maven-registry-credentials ]
filters:
branches:
ignore: /(develop|main|release/.*|hotfix/.*|integration.*)/
tags:
only: []
publish_snapshot:
jobs:
- qqq-orb/mvn_publish:
context: [ qqq-maven-registry-credentials ]
branch_type: snapshot
filters:
branches:
only:
- develop
publish_release_candidate:
jobs:
- qqq-orb/mvn_publish:
context: [ qqq-maven-registry-credentials ]
branch_type: release_candidate
filters:
branches:
only:
- /release\/.*/
publish_release:
jobs:
- qqq-orb/mvn_publish:
context: [ qqq-maven-registry-credentials ]
branch_type: release
filters:
branches:
only: [ main ]
tags:
only: [ /v.*/ ]
publish_hotfix_release:
jobs:
- qqq-orb/mvn_publish:
context: [ qqq-maven-registry-credentials ]
branch_type: hotfix
filters:
branches:
only:
- /hotfix\/.*/
- qqq-orb/generate_pipeline:
orb_version: 0.7.12
filters:
branches:
ignore: automation/qbit-rc-pending
tags:
only: /^v\d+\.\d+\.\d+$/
+1
View File
@@ -0,0 +1 @@
* @KofTwentyTwo
+23
View File
@@ -0,0 +1,23 @@
name: CodeQL
'on':
push:
branches:
- main
- develop
- release/**
- hotfix/**
pull_request: {}
schedule:
- cron: 41 16 * * 0
workflow_dispatch: {}
permissions:
contents: read
actions: read
security-events: write
packages: read
concurrency:
group: codeql-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
analyze:
uses: QRun-IO/qqq-orb/.github/workflows/qbit-codeql.yml@c288a8864fe9054f68410e1d78f5863aea4c4d22
+74
View File
@@ -0,0 +1,74 @@
name: Release candidate
'on':
workflow_dispatch:
inputs:
dry_run:
description: Plan a cut without changing release state
type: boolean
default: false
finalize:
description: Retry finalization of an existing cut; do not cut another RC
type: boolean
default: false
recover:
description: Recover the same published RC after a deployment timeout; never
reupload
type: boolean
default: false
circle_build:
description: Original Circle publisher build number (recovery only)
type: string
default: ''
merge_sha:
description: Existing cut merge_sha (finalize retry only)
type: string
default: ''
bump_sha:
description: Existing cut bump_sha (finalize retry only)
type: string
default: ''
branch:
description: Existing cut branch (finalize retry only)
type: string
default: ''
version:
description: Existing cut version (finalize retry only)
type: string
default: ''
pull_request:
types:
- closed
branches:
- develop
repository_dispatch:
types:
- qbit-rc-published
permissions:
contents: write
actions: read
checks: read
jobs:
release-rc:
if: github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch'
|| (github.event_name == 'pull_request' && github.event.pull_request.merged
== true && github.event.pull_request.base.ref == 'develop' && contains(github.event.pull_request.labels.*.name,
'release:rc'))
uses: QRun-IO/qqq-orb/.github/workflows/qbit-release-rc.yml@cc30f599e63c8e41629c99d23395bac1adb8bc44
with:
dry_run: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run || false
}}
finalize: ${{ github.event_name == 'repository_dispatch' || (github.event_name
== 'workflow_dispatch' && inputs.finalize) }}
recover: ${{ github.event_name == 'workflow_dispatch' && inputs.recover || false
}}
circle_build: ${{ github.event_name == 'workflow_dispatch' && inputs.circle_build
|| '' }}
merge_sha: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.merge_sha
|| inputs.merge_sha || '' }}
bump_sha: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.bump_sha
|| inputs.bump_sha || '' }}
branch: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.branch
|| inputs.branch || '' }}
version: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.version
|| inputs.version || '' }}
secrets: inherit
+16
View File
@@ -0,0 +1,16 @@
profile: qbit
orb_version: 0.7.12
default_branch: develop
java_version: '21'
browsers: true
api_checks: false
docs_only: false
release_on_main: false
release_tag_policy: stable
test_branch_policy: non-release
publish:
snapshot: true
release_candidate: true
release: true
asciidoc: false
release_completion: true
+23
View File
@@ -0,0 +1,23 @@
# qbit-customizable-table-views
## Knowledge base
A reviewed dossier for this repo lives in the second-brain vault:
- Hub / start here: `R:/Git.Local/KofTwentyTwo/second-brain/knowledge/qqq/qqq-hub.md`
- This repo's dossier: `R:/Git.Local/KofTwentyTwo/second-brain/knowledge/qqq/repos/qbit-customizable-table-views.md`
- QBit mechanics refresher: `R:/Git.Local/KofTwentyTwo/second-brain/knowledge/qqq/architecture/metadata-model.md`
Reviewed at commit `6a0f548afa8e` (branch `develop`, 2026-05-21). If HEAD has moved
significantly past that commit, treat the dossier as potentially stale.
Notes for agents working here:
- The README describes a "saved views" API (`QSavedViewMetaData`, `saved_view` tables) that
does NOT exist in this code — trust the source, not the README. CHANGELOG.md and
CONTRIBUTING.md are copy-pasted from qbit-user-role-permissions.
- Licensing metadata is contradictory (LICENSE/NOTICE = Apache-2.0; pom, file headers,
checkstyle template = AGPL-3.0; README footer says proprietary). Open PR #5 fixes the pom.
- Default builds take the qqq version only from `qbit-build-parent` (2.0.0 = qqq 4.0.0);
do not add an always-on `qqq-bom-pom` import (ADR-0007). To check the next qqq line, run
`mvn -B verify -Pqqq-snapshot` (4.1.0-SNAPSHOT; override with `-Dqqq.snapshot.version`).
+37 -21
View File
@@ -25,12 +25,12 @@
<parent>
<groupId>com.kingsrook</groupId>
<artifactId>qbit-build-parent</artifactId>
<version>1.5.1</version>
<version>2.0.0</version>
</parent>
<groupId>com.kingsrook.qbits</groupId>
<artifactId>qbit-customizable-table-views</artifactId>
<version>0.4.0-SNAPSHOT</version>
<version>${revision}</version>
<packaging>jar</packaging>
<name>QBit Customizable Table Views</name>
@@ -65,21 +65,9 @@
</scm>
<properties>
<revision>0.4.0-SNAPSHOT</revision>
<revision>0.5.0-SNAPSHOT</revision>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-bom-pom</artifactId>
<version>0.40.0-SNAPSHOT</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.kingsrook.qqq</groupId>
@@ -110,11 +98,39 @@
</dependency>
</dependencies>
<repositories>
<repository>
<id>sonatype-snapshots</id>
<url>https://central.sonatype.com/repository/maven-snapshots/</url>
</repository>
</repositories>
<profiles>
<!-- Opt-in check against the next qqq line: mvn -B verify -Pqqq-snapshot -->
<!-- (override the version with -Dqqq.snapshot.version=...). Default builds -->
<!-- take the qqq version only from qbit-build-parent. -->
<profile>
<id>qqq-snapshot</id>
<properties>
<qqq.snapshot.version>4.1.0-SNAPSHOT</qqq.snapshot.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-bom-pom</artifactId>
<version>${qqq.snapshot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<repositories>
<repository>
<id>sonatype-snapshots</id>
<url>https://central.sonatype.com/repository/maven-snapshots/</url>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
</profile>
</profiles>
</project>
@@ -72,6 +72,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.DynamicDefaultValueB
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;
@@ -178,6 +179,9 @@ public class CustomizableTableViewsTablePersonalizer implements TableMetaDataPer
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 //
///////////////////////////////////////////////////////////
@@ -360,6 +364,55 @@ public class CustomizableTableViewsTablePersonalizer implements TableMetaDataPer
/***************************************************************************
*
***************************************************************************/
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.
@@ -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()))
{
@@ -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)
{
@@ -36,6 +36,7 @@ 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.statusmessages.BadInputStatusMessage;
import com.kingsrook.qqq.backend.core.model.statusmessages.SystemErrorStatusMessage;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
/*******************************************************************************
@@ -57,14 +58,14 @@ public class TableViewFieldCustomizer implements TableCustomizerInterface
try
{
Integer id = record.getValueInteger("id");
String fieldName = RecordCustomizerUtilityInterface.getValueFromRecordOrOldRecord("fieldName", record, id, oldRecordMap);
String accessLevelId = RecordCustomizerUtilityInterface.getValueFromRecordOrOldRecord("accessLevel", record, id, oldRecordMap);
String fieldName = ValueUtils.getValueAsString(RecordCustomizerUtilityInterface.getValueFromRecordElseFromOldRecord("fieldName", record, id, oldRecordMap));
String accessLevelId = ValueUtils.getValueAsString(RecordCustomizerUtilityInterface.getValueFromRecordElseFromOldRecord("accessLevel", record, id, oldRecordMap));
String[] parts = fieldName.split("\\.");
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)
@@ -78,7 +78,7 @@ public class BaseTest
// basic definition of an instance //
/////////////////////////////////////
QInstance qInstance = new QInstance();
qInstance.setAuthentication(new QAuthenticationMetaData().withType(QAuthenticationType.FULLY_ANONYMOUS));
qInstance.withInstanceDefaultAuthentication(new QAuthenticationMetaData().withType(QAuthenticationType.FULLY_ANONYMOUS));
qInstance.addBackend(new RDBMSBackendMetaData()
.withName(RDBMS_BACKEND_NAME)
.withVendor("h2")
@@ -59,6 +59,7 @@ 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;
@@ -499,6 +500,85 @@ 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());
}
/*******************************************************************************
**
*******************************************************************************/
@@ -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,53 @@ 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));
}
/*******************************************************************************
** a sparse update (e.g., only accessLevel set) must validate against the
** fieldName from the old record.
*******************************************************************************/
@Test
void testSparseUpdateUsesOldRecordValues() throws QException
{
QContext.getQInstance().addTable(new QTableMetaData()
.withName("testTable3")
.withField(new QFieldMetaData("mandatory", QFieldType.STRING).withIsRequired(true))
.withField(new QFieldMetaData("optional", QFieldType.STRING)));
Optional<List<QRecord>> oldRecordList = Optional.of(List.of(
new TableViewField().withId(1).withFieldName("testTable3.mandatory").withAccessLevel(FieldAccessLevel.EDITABLE_REQUIRED).toQRecord(),
new TableViewField().withId(2).withFieldName("testTable3.optional").withAccessLevel(FieldAccessLevel.EDITABLE_OPTIONAL).toQRecord()));
QRecord mandatoryToReadOnly = new QRecord().withValue("id", 1).withValue("accessLevel", FieldAccessLevel.READ_ONLY.getId());
QRecord optionalToReadOnly = new QRecord().withValue("id", 2).withValue("accessLevel", FieldAccessLevel.READ_ONLY.getId());
new TableViewFieldCustomizer().preInsertOrUpdate(null, List.of(mandatoryToReadOnly, optionalToReadOnly), false, oldRecordList);
assertThat(mandatoryToReadOnly.getErrors()).hasSizeGreaterThan(0);
assertThat(optionalToReadOnly.getErrors()).isNullOrEmpty();
}
/***************************************************************************
*
***************************************************************************/