Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db005be598
|
||
|
|
707a75a4c9
|
||
|
|
367e188393
|
||
|
|
990803ab33
|
||
|
|
280b112340
|
||
|
|
50bac3dd3d
|
||
|
|
8e293eac08
|
||
|
|
e8c31da3d6 | ||
|
|
f0f5f1ced1
|
||
|
|
9f6396831c
|
||
|
|
439a289f12
|
||
|
|
0c5e7c283d
|
||
|
|
edfc385dcb
|
||
|
|
e95da041c7
|
||
|
|
b84e4a2811 | ||
|
|
8786c9a868 | ||
|
|
ba1b26d1e4 | ||
|
|
a90fdc9363 | ||
|
|
74939c3b7c | ||
|
|
55c89ec2b3 | ||
|
|
dbdedf7d00 | ||
|
|
914aac2cbb | ||
|
|
b04cc0b0fa | ||
|
|
a42e85d1c2
|
||
|
|
0be1fc54a4 | ||
|
|
e43515a92e
|
||
|
|
bce290d6b4 | ||
|
|
ee20dfef2a
|
||
|
|
ad11a13e62 | ||
|
|
8cebae2f2c | ||
|
|
2f1ff80065 | ||
|
|
dbfbc59f0c | ||
|
|
512defb9cc | ||
|
|
b3ac14a0fd | ||
|
|
6a0f548afa | ||
|
|
523fd34095 | ||
|
|
8f92567c93 | ||
|
|
9a6eca0189 | ||
|
|
67252ae1f3 | ||
|
|
5323d4ed25 | ||
|
|
b0ec372885 | ||
|
|
5c97a017dc | ||
|
|
2f3938929b | ||
|
|
0c406e9a4b | ||
|
|
aed9b45c7b | ||
|
|
4213148582 | ||
|
|
ba98f2872d | ||
|
|
8d4ecce8b4 | ||
|
|
c2d648736a | ||
|
|
8b94ecedf3 | ||
|
|
983bde7713 | ||
|
|
3e9453d36e | ||
|
|
b0ebc121eb | ||
|
|
8db9359d78 | ||
|
|
a08a3b424f | ||
|
|
ab00b186fe | ||
|
|
735cb27ee4 | ||
|
|
bda8753e4c | ||
|
|
6b9a6cbcef | ||
|
|
11a9b782f1 | ||
|
|
afa221be9d | ||
|
|
2a82f4ea36 | ||
|
|
d29b008edc | ||
|
|
b7837d7585 |
No files matched your search
+10
-53
@@ -1,57 +1,14 @@
|
||||
version: 2.1
|
||||
|
||||
setup: true
|
||||
orbs:
|
||||
qqq-orb: kingsrook/qqq-orb@0.5.0
|
||||
|
||||
qqq-orb: kingsrook/qqq-orb@0.8
|
||||
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.8'
|
||||
filters:
|
||||
branches:
|
||||
ignore: automation/qbit-rc-pending
|
||||
tags:
|
||||
only: /^v\d+\.\d+\.\d+$/
|
||||
@@ -0,0 +1,7 @@
|
||||
name: build
|
||||
on:
|
||||
push:
|
||||
jobs:
|
||||
maven:
|
||||
uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/maven.yaml@v1
|
||||
secrets: inherit
|
||||
@@ -0,0 +1 @@
|
||||
* @KofTwentyTwo
|
||||
@@ -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
|
||||
@@ -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
@@ -0,0 +1,16 @@
|
||||
profile: qbit
|
||||
orb_version: '0.8'
|
||||
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
|
||||
@@ -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`).
|
||||
@@ -0,0 +1,202 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to the Licensor for inclusion in the Work by the copyright
|
||||
owner or by an individual or Legal Entity authorized to submit on
|
||||
behalf of the copyright owner. For the purposes of this definition,
|
||||
"submitted" means any form of electronic, verbal, or written
|
||||
communication sent to the Licensor or its representatives, including
|
||||
but not limited to communication on electronic mailing lists, source
|
||||
code control systems, and issue tracking systems that are managed by,
|
||||
or on behalf of, the Licensor for the purpose of discussing and
|
||||
improving the Work, but excluding communication that is conspicuously
|
||||
marked or otherwise designated in writing by the copyright owner as
|
||||
"Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2022-2025 QRunIO, LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,15 @@
|
||||
qBit: Customizable Table Views
|
||||
Copyright 2022-2025 QRunIO, LLC
|
||||
|
||||
This product includes software developed at QRunIO, LLC (https://qrun.io/).
|
||||
|
||||
Licensed under the Apache License, Version 2.0.
|
||||
|
||||
================================================================================
|
||||
|
||||
Trademarks:
|
||||
|
||||
QQQ, qBits, qRun, qStudio, and the QRunIO logo are trademarks of QRunIO, LLC.
|
||||
Use of these trademarks must comply with QRunIO's Trademark Guidelines.
|
||||
|
||||
================================================================================
|
||||
@@ -1,4 +1,18 @@
|
||||
# qbit-customizable-table-views
|
||||
# QBit: Customizable Table Views
|
||||
[](https://circleci.com/gh/Kingsrook/qbit-customizable-table-views)
|
||||
[](https://central.sonatype.com/namespace/com.kingsrook.qbits)
|
||||
[](https://adoptium.net/)
|
||||
[](LICENSE.txt)
|
||||
|
||||
**QBit Customizable Table Views** provides table personalization capabilities for QQQ applications, allowing users to customize which fields and widgets they see based on their roles and permissions.
|
||||
|
||||
## 🚀 What Makes This QBit Different?
|
||||
|
||||
**Role-Based Table Customization** - This QBit enables fine-grained control over table presentation without modifying application code. Users see different fields and widgets based on their assigned roles, providing a personalized experience while maintaining security and data integrity.
|
||||
|
||||
## 🔓 Open Source & Full Control
|
||||
|
||||
**This QBit is 100% open source** - you have complete ownership and control:
|
||||
|
||||
User-defined saved views for QQQ dashboard tables.
|
||||
|
||||
@@ -24,7 +38,7 @@ This QBit lets users save these configurations as named views. Create a view, ad
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- QQQ application (v0.20+)
|
||||
- QQQ application (v4.1.0+)
|
||||
- QQQ dashboard deployed
|
||||
- Database backend configured
|
||||
|
||||
@@ -34,9 +48,9 @@ Add to your `pom.xml`:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>io.qrun</groupId>
|
||||
<groupId>com.kingsrook.qbits</groupId>
|
||||
<artifactId>qbit-customizable-table-views</artifactId>
|
||||
<version>0.2.0</version>
|
||||
<version>0.3.0</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
|
||||
@@ -20,24 +20,25 @@
|
||||
~ 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>
|
||||
<artifactId>qbit-build-parent</artifactId>
|
||||
<version>1.5.1</version>
|
||||
<version>2.2.1</version>
|
||||
</parent>
|
||||
|
||||
<groupId>com.kingsrook.qbits</groupId>
|
||||
<artifactId>qbit-customizable-table-views</artifactId>
|
||||
<version>${revision}</version>
|
||||
<packaging>jar</packaging>
|
||||
<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,17 @@
|
||||
</scm>
|
||||
|
||||
<properties>
|
||||
<revision>0.3.0-SNAPSHOT</revision>
|
||||
<revision>0.5.0-SNAPSHOT</revision>
|
||||
<qqq.version>4.1.0-RC.26</qqq.version>
|
||||
</properties>
|
||||
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<!-- Published QQQ 4.1 core (minimum QQQ 4.1), imported ahead of the parent's BOM. -->
|
||||
<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>${qqq.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
@@ -95,6 +90,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>
|
||||
|
||||
+312
-57
@@ -25,8 +25,10 @@ package com.kingsrook.qbits.customizabletableviews.logic;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -35,6 +37,7 @@ import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import com.kingsrook.qbits.customizabletableviews.model.CustomizableTable;
|
||||
import com.kingsrook.qbits.customizabletableviews.model.CustomizableTableViewsFieldMetaData;
|
||||
import com.kingsrook.qbits.customizabletableviews.model.FieldAccessLevel;
|
||||
import com.kingsrook.qbits.customizabletableviews.model.TableView;
|
||||
import com.kingsrook.qbits.customizabletableviews.model.TableViewField;
|
||||
@@ -56,14 +59,22 @@ import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInpu
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.QInputSource;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.DynamicDefaultValueBehavior;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAndJoinTable;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QSupplementalFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QVirtualFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.MultiRecordSecurityLock;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QUser;
|
||||
@@ -103,7 +114,6 @@ public class CustomizableTableViewsTablePersonalizer implements TableMetaDataPer
|
||||
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
@@ -153,7 +163,7 @@ public class CustomizableTableViewsTablePersonalizer implements TableMetaDataPer
|
||||
|
||||
if(tableView != null)
|
||||
{
|
||||
return applyViewToTable(tableView, tableActionInput.getTable().clone());
|
||||
return applyViewToTable(tableView, tableActionInput.getTable().clone(), tableActionInput);
|
||||
}
|
||||
|
||||
return tableActionInput.getTable();
|
||||
@@ -164,69 +174,60 @@ public class CustomizableTableViewsTablePersonalizer implements TableMetaDataPer
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
QTableMetaData applyViewToTable(TableView tableView, QTableMetaData cloneTable)
|
||||
QTableMetaData applyViewToTable(TableView tableView, QTableMetaData cloneTable, AbstractTableActionInput tableActionInput)
|
||||
{
|
||||
Map<String, QFieldMetaData> cloneFields = cloneTable.getFields();
|
||||
if(cloneFields == null)
|
||||
{
|
||||
cloneFields = new LinkedHashMap<>();
|
||||
cloneTable.setFields(cloneFields);
|
||||
}
|
||||
|
||||
Map<String, QFieldMetaData> fieldsToKeep = new LinkedHashMap<>();
|
||||
|
||||
///////////////////////////////////////////
|
||||
// figure out which fields the user gets //
|
||||
///////////////////////////////////////////
|
||||
for(TableViewField tableViewField : CollectionUtils.nonNullList(tableView.getFields()))
|
||||
{
|
||||
try
|
||||
{
|
||||
String fieldName = tableViewField.getFieldName().split("\\.")[1];
|
||||
QFieldMetaData fieldMetaData = cloneFields.get(fieldName);
|
||||
if(fieldMetaData != null)
|
||||
{
|
||||
FieldAccessLevel fieldAccessLevel = FieldAccessLevel.getById(tableViewField.getAccessLevel());
|
||||
if(fieldAccessLevel != null)
|
||||
{
|
||||
fieldAccessLevel.apply(fieldMetaData);
|
||||
}
|
||||
|
||||
fieldsToKeep.put(fieldName, fieldMetaData);
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.warn("Error processing tableViewField", e, logPair("fieldName", tableViewField.getFieldName()));
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////
|
||||
// always keep the primary key field and required fields //
|
||||
///////////////////////////////////////////////////////////
|
||||
Set<String> requiredAndPrimaryKeyFields = new HashSet<>();
|
||||
for(QFieldMetaData field : cloneTable.getFields().values())
|
||||
{
|
||||
String fieldName = field.getName();
|
||||
if(field.getIsRequired() || Objects.equals(fieldName, cloneTable.getPrimaryKeyField()))
|
||||
{
|
||||
if(!fieldsToKeep.containsKey(fieldName))
|
||||
{
|
||||
fieldsToKeep.put(fieldName, cloneFields.get(fieldName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, QFieldMetaData> fieldsToKeep = getFieldsToKeepForTable(tableView, cloneTable, tableActionInput);
|
||||
cloneTable.setFields(fieldsToKeep);
|
||||
|
||||
Map<String, QVirtualFieldMetaData> virtualFieldsToKeep = getVirtualFieldsToKeepForTable(tableView, cloneTable, tableActionInput);
|
||||
cloneTable.setVirtualFields(virtualFieldsToKeep);
|
||||
|
||||
///////////////////////////////////////////////////////////
|
||||
// remove field names which aren't present from sections //
|
||||
///////////////////////////////////////////////////////////
|
||||
for(QFieldSection section : CollectionUtils.nonNullList(cloneTable.getSections()))
|
||||
{
|
||||
if(section.getFieldNames() != null)
|
||||
Map<String, Set<String>> getFieldsToKeepFromJoinTableCache = null;
|
||||
|
||||
Iterator<String> fieldListIterator = CollectionUtils.nonNullList(section.getFieldNames()).iterator();
|
||||
while(fieldListIterator.hasNext())
|
||||
{
|
||||
section.getFieldNames().removeIf(next -> !fieldsToKeep.containsKey(next));
|
||||
try
|
||||
{
|
||||
String fieldName = fieldListIterator.next();
|
||||
FieldAndJoinTable fieldAndJoinTable = FieldAndJoinTable.get(cloneTable, fieldName);
|
||||
|
||||
if(fieldAndJoinTable.joinTable().getName().equals(cloneTable.getName()))
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if the field is from this table, then remove it unless it's in the set of ones to keep //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(!fieldsToKeep.containsKey(fieldName))
|
||||
{
|
||||
fieldListIterator.remove();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
getFieldsToKeepFromJoinTableCache = Objects.requireNonNullElseGet(getFieldsToKeepFromJoinTableCache, HashMap::new);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// else the field is from a join table - get that join table's allowed fields for the user, and keep or remove based on that //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
Set<String> fieldsToKeepFromJoinTable = getFieldsToKeepFromJoinTable(fieldAndJoinTable.joinTable().getName(), tableActionInput, getFieldsToKeepFromJoinTableCache);
|
||||
if(!fieldsToKeepFromJoinTable.contains(fieldAndJoinTable.field().getName()))
|
||||
{
|
||||
fieldListIterator.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
/////////////////////////////////////////////////////////
|
||||
// an unknown field, let's assume it should be removed //
|
||||
/////////////////////////////////////////////////////////
|
||||
fieldListIterator.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,6 +267,258 @@ public class CustomizableTableViewsTablePersonalizer implements TableMetaDataPer
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
protected Set<String> getFieldsToKeepFromJoinTable(String joinTableName, AbstractTableActionInput tableActionInput, Map<String, Set<String>> getFieldsToKeepFromJoinTableCache)
|
||||
{
|
||||
return (getFieldsToKeepFromJoinTableCache.computeIfAbsent(joinTableName, k ->
|
||||
{
|
||||
try
|
||||
{
|
||||
if(isTableCustomizable(joinTableName))
|
||||
{
|
||||
////////////////////////////////////////////////////////////////
|
||||
// if the table is customizable, return set of fields to keep //
|
||||
////////////////////////////////////////////////////////////////
|
||||
QTableMetaData joinTableClone = QContext.getQInstance().getTable(joinTableName).clone();
|
||||
TableView joinTableView = getEffectiveTableViewForCurrentSession(joinTableName);
|
||||
Map<String, QFieldMetaData> joinTableFieldsToKeep = getFieldsToKeepForTable(joinTableView, joinTableClone, tableActionInput);
|
||||
return joinTableFieldsToKeep.keySet();
|
||||
}
|
||||
else
|
||||
{
|
||||
///////////////////////////////////////////////
|
||||
// else, not customizable, return all fields //
|
||||
///////////////////////////////////////////////
|
||||
return (QContext.getQInstance().getTable(joinTableName).getFields().keySet());
|
||||
}
|
||||
}
|
||||
catch(QException e)
|
||||
{
|
||||
return Collections.emptySet();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
protected Map<String, QFieldMetaData> getFieldsToKeepForTable(TableView tableView, QTableMetaData cloneTable, AbstractTableActionInput tableActionInput)
|
||||
{
|
||||
Map<String, QFieldMetaData> cloneFields = cloneTable.getFields();
|
||||
if(cloneFields == null)
|
||||
{
|
||||
cloneFields = new LinkedHashMap<>();
|
||||
cloneTable.setFields(cloneFields);
|
||||
}
|
||||
|
||||
Map<String, QFieldMetaData> fieldsToKeep = new LinkedHashMap<>();
|
||||
|
||||
/////////////////////////////////////////////
|
||||
// figure out which fields the user gets //
|
||||
// start with ones in the user's TableView //
|
||||
/////////////////////////////////////////////
|
||||
for(TableViewField tableViewField : CollectionUtils.nonNullList(tableView.getFields()))
|
||||
{
|
||||
try
|
||||
{
|
||||
String fieldName = tableViewField.getFieldName().split("\\.")[1];
|
||||
QFieldMetaData fieldMetaData = cloneFields.get(fieldName);
|
||||
if(fieldMetaData != null)
|
||||
{
|
||||
FieldAccessLevel fieldAccessLevel = FieldAccessLevel.getById(tableViewField.getAccessLevel());
|
||||
if(fieldAccessLevel != null)
|
||||
{
|
||||
fieldAccessLevel.apply(fieldMetaData);
|
||||
}
|
||||
|
||||
fieldsToKeep.put(fieldName, fieldMetaData);
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.warn("Error processing tableViewField", e, logPair("fieldName", tableViewField.getFieldName()));
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////
|
||||
// next look for ones that always get kept //
|
||||
/////////////////////////////////////////////
|
||||
for(QFieldMetaData field : cloneTable.getFields().values())
|
||||
{
|
||||
String fieldName = field.getName();
|
||||
if(!fieldsToKeep.containsKey(fieldName))
|
||||
{
|
||||
if(shouldKeepFieldDueToRules(cloneTable, field, tableActionInput))
|
||||
{
|
||||
fieldsToKeep.put(fieldName, cloneFields.get(fieldName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fieldsToKeep;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
protected Map<String, QVirtualFieldMetaData> getVirtualFieldsToKeepForTable(TableView tableView, QTableMetaData cloneTable, AbstractTableActionInput tableActionInput)
|
||||
{
|
||||
Map<String, QVirtualFieldMetaData> cloneFields = cloneTable.getVirtualFields();
|
||||
if(cloneFields == null)
|
||||
{
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
Map<String, QVirtualFieldMetaData> fieldsToKeep = new LinkedHashMap<>();
|
||||
|
||||
/////////////////////////////////////////////
|
||||
// figure out which fields the user gets //
|
||||
// start with ones in the user's TableView //
|
||||
/////////////////////////////////////////////
|
||||
for(TableViewField tableViewField : CollectionUtils.nonNullList(tableView.getFields()))
|
||||
{
|
||||
try
|
||||
{
|
||||
String fieldName = tableViewField.getFieldName().split("\\.")[1];
|
||||
QVirtualFieldMetaData fieldMetaData = cloneFields.get(fieldName);
|
||||
if(fieldMetaData != null)
|
||||
{
|
||||
FieldAccessLevel fieldAccessLevel = FieldAccessLevel.getById(tableViewField.getAccessLevel());
|
||||
if(fieldAccessLevel != null)
|
||||
{
|
||||
fieldAccessLevel.apply(fieldMetaData);
|
||||
}
|
||||
|
||||
fieldsToKeep.put(fieldName, fieldMetaData);
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.warn("Error processing tableViewField", e, logPair("fieldName", tableViewField.getFieldName()));
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// unlike normal fields, there are no 'always keep' rules for virtual fields //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
return fieldsToKeep;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* check if any system-defined rules make it so that a field should always
|
||||
* be kept.
|
||||
*
|
||||
* <p>built-in rules are:</p>
|
||||
* <ul>
|
||||
* <li>field is required</li>
|
||||
* <li>field is primary key</li>
|
||||
* <li>field is part of table's security locks</li>
|
||||
* <li>there's supplemental metadata from this qbit that says to keep the field</li>
|
||||
* <li>this is an insert or update, and there's a {@link DynamicDefaultValueBehavior}</li>
|
||||
* </ul>
|
||||
*
|
||||
* also - an application could override this method in a subclass to add
|
||||
* additional rules.
|
||||
***************************************************************************/
|
||||
protected boolean shouldKeepFieldDueToRules(QTableMetaData cloneTable, QFieldMetaData field, AbstractTableActionInput tableActionInput)
|
||||
{
|
||||
if(field.getIsRequired() || Objects.equals(field.getName(), cloneTable.getPrimaryKeyField()))
|
||||
{
|
||||
///////////////////////////////////////////////////////////
|
||||
// always keep the primary key field and required fields //
|
||||
///////////////////////////////////////////////////////////
|
||||
return (true);
|
||||
}
|
||||
|
||||
if(doRecordSecurityLocksContainField(cloneTable.getRecordSecurityLocks(), field))
|
||||
{
|
||||
/////////////////////////////////////////////////////////////
|
||||
// keep fields that are part of the table's security locks //
|
||||
/////////////////////////////////////////////////////////////
|
||||
return (true);
|
||||
}
|
||||
|
||||
if(doesSupplementalMetaDataSayToKeepField(field))
|
||||
{
|
||||
//////////////////////////////////////////////////////////
|
||||
// keep a field if it's marked as being always-included //
|
||||
//////////////////////////////////////////////////////////
|
||||
return (true);
|
||||
}
|
||||
|
||||
boolean isInsertOrUpdate = (tableActionInput instanceof InsertInput) || (tableActionInput instanceof UpdateInput);
|
||||
if(isInsertOrUpdate && field.getBehaviorOnlyIfSet(DynamicDefaultValueBehavior.class) != null)
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if this is for an insert or update, then keep any fields that have a DynamicDefaultValueBehavior //
|
||||
// e.g., createDates, modifyDates, things that capture userId //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
return (true);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////
|
||||
// by default, no rule matched, so say no //
|
||||
////////////////////////////////////////////
|
||||
return (false);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Check if the supplemental meta data from this qbit for a field says
|
||||
* that the field should always be kept.
|
||||
***************************************************************************/
|
||||
protected boolean doesSupplementalMetaDataSayToKeepField(QFieldMetaData field)
|
||||
{
|
||||
QSupplementalFieldMetaData supplementalMetaData = field.getSupplementalMetaData(CustomizableTableViewsFieldMetaData.TYPE);
|
||||
if(supplementalMetaData instanceof CustomizableTableViewsFieldMetaData customizableTableViewsFieldMetaData)
|
||||
{
|
||||
if(CustomizableTableViewsFieldMetaData.Rule.ALWAYS_KEEP_FIELD.equals(customizableTableViewsFieldMetaData.getRule()))
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
}
|
||||
|
||||
return (false);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Check if a list of record security locks include a field, recursively
|
||||
* processing {@link MultiRecordSecurityLock}s
|
||||
***************************************************************************/
|
||||
protected boolean doRecordSecurityLocksContainField(List<RecordSecurityLock> locks, QFieldMetaData field)
|
||||
{
|
||||
for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(locks))
|
||||
{
|
||||
if(recordSecurityLock instanceof MultiRecordSecurityLock multiRecordSecurityLock)
|
||||
{
|
||||
if(doRecordSecurityLocksContainField(multiRecordSecurityLock.getLocks(), field))
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
}
|
||||
else if(field.getName().equals(recordSecurityLock.getFieldName()))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* the assumption is, that this method is called AFTER we've identified that
|
||||
* a table is customizable (and customizaztion is active) - so - if we fail
|
||||
@@ -294,7 +547,9 @@ public class CustomizableTableViewsTablePersonalizer implements TableMetaDataPer
|
||||
{
|
||||
Set<Integer> roleIds = Arrays.stream(roleIdsString.split(",")).map(Integer::parseInt).collect(Collectors.toSet());
|
||||
tableViews = new QueryAction().execute(new QueryInput(TableView.TABLE_NAME)
|
||||
.withFilter(new QQueryFilter().withCriteria(new QFilterCriteria(TableViewRoleInt.TABLE_NAME + ".roleId", QCriteriaOperator.IN, roleIds)))
|
||||
.withFilter(new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria(TableViewRoleInt.TABLE_NAME + ".roleId", QCriteriaOperator.IN, roleIds))
|
||||
.withCriteria(new QFilterCriteria(CustomizableTable.TABLE_NAME + ".tableName", QCriteriaOperator.EQUALS, tableName)))
|
||||
.withQueryJoin(new QueryJoin(TableViewRoleInt.TABLE_NAME))
|
||||
.withIncludeAssociations(true))
|
||||
.getRecords();
|
||||
|
||||
+13
-1
@@ -96,6 +96,12 @@ public class CustomizableTableFieldPVS implements QCustomPossibleValueProvider<S
|
||||
{
|
||||
return (new QPossibleValue<>(idString, fieldMetaData.getLabel()));
|
||||
}
|
||||
|
||||
fieldMetaData = table.getVirtualFields() == null ? null : table.getVirtualFields().get(fieldName);
|
||||
if(fieldMetaData != null)
|
||||
{
|
||||
return (new QPossibleValue<>(idString, fieldMetaData.getLabel()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +134,13 @@ public class CustomizableTableFieldPVS implements QCustomPossibleValueProvider<S
|
||||
|
||||
if(table != null)
|
||||
{
|
||||
for(QFieldMetaData field : table.getFields().values())
|
||||
List<QFieldMetaData> fields = new ArrayList<>(table.getFields().values());
|
||||
if(table.getVirtualFields() != null)
|
||||
{
|
||||
fields.addAll(table.getVirtualFields().values());
|
||||
}
|
||||
|
||||
for(QFieldMetaData field : fields)
|
||||
{
|
||||
if(field.getName().equals(table.getPrimaryKeyField()))
|
||||
{
|
||||
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.customizabletableviews.model;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QSupplementalFieldMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class CustomizableTableViewsFieldMetaData extends QSupplementalFieldMetaData
|
||||
{
|
||||
public static final String TYPE = CustomizableTableViewsFieldMetaData.class.getName();
|
||||
|
||||
private Rule rule;
|
||||
|
||||
public enum Rule
|
||||
{
|
||||
ALWAYS_KEEP_FIELD
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public String getType()
|
||||
{
|
||||
return TYPE;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
* Getter for rule
|
||||
* @see #withRule(Rule)
|
||||
*******************************************************************************/
|
||||
public Rule getRule()
|
||||
{
|
||||
return (this.rule);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
* Setter for rule
|
||||
* @see #withRule(Rule)
|
||||
*******************************************************************************/
|
||||
public void setRule(Rule rule)
|
||||
{
|
||||
this.rule = rule;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
* Fluent setter for rule
|
||||
*
|
||||
* @param rule
|
||||
* TODO document this property
|
||||
*
|
||||
* @return this
|
||||
*******************************************************************************/
|
||||
public CustomizableTableViewsFieldMetaData withRule(Rule rule)
|
||||
{
|
||||
this.rule = rule;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+2
-1
@@ -24,6 +24,7 @@ package com.kingsrook.qbits.customizabletableviews.model;
|
||||
|
||||
import java.util.Objects;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QVirtualFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PossibleValueEnum;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.QMetaDataProducingPossibleValueEnum;
|
||||
|
||||
@@ -204,7 +205,7 @@ public enum FieldAccessLevel implements PossibleValueEnum<String>
|
||||
return ("This field is defined as hidden by the system - you may not make it " + getLabel());
|
||||
}
|
||||
|
||||
if(!fieldMetaData.getIsEditable())
|
||||
if(!fieldMetaData.getIsEditable() || fieldMetaData instanceof QVirtualFieldMetaData)
|
||||
{
|
||||
if(this == READ_ONLY)
|
||||
{
|
||||
|
||||
+4
-3
@@ -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")
|
||||
|
||||
+391
-37
@@ -34,6 +34,7 @@ import java.util.stream.Collectors;
|
||||
import com.kingsrook.qbits.customizabletableviews.BaseTest;
|
||||
import com.kingsrook.qbits.customizabletableviews.QFieldMetaDataAssert;
|
||||
import com.kingsrook.qbits.customizabletableviews.model.CustomizableTable;
|
||||
import com.kingsrook.qbits.customizabletableviews.model.CustomizableTableViewsFieldMetaData;
|
||||
import com.kingsrook.qbits.customizabletableviews.model.FieldAccessLevel;
|
||||
import com.kingsrook.qbits.customizabletableviews.model.TableView;
|
||||
import com.kingsrook.qbits.customizabletableviews.model.TableViewField;
|
||||
@@ -44,6 +45,8 @@ import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.metadata.personalization.TableMetaDataPersonalizerInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.QInputSource;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
|
||||
@@ -51,8 +54,17 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.DynamicDefaultValueBehavior;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QVirtualFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.MultiRecordSecurityLock;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.SectionFactory;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QUser;
|
||||
@@ -262,8 +274,10 @@ class CustomizableTableViewsTablePersonalizerTest extends BaseTest
|
||||
{
|
||||
CustomizableTableViewsTablePersonalizer personalizer = new CustomizableTableViewsTablePersonalizer();
|
||||
|
||||
String tableWithoutDefaultView = "tableWithoutDefaultView";
|
||||
String tableWithDefaultView = "tableWithDefaultView";
|
||||
String tableWithoutDefaultView = "tableWithoutDefaultView";
|
||||
String tableWithDefaultView = "tableWithDefaultView";
|
||||
Integer tableWithoutDefaultViewId = 1;
|
||||
Integer tableWithDefaultViewId = 2;
|
||||
|
||||
QContext.getQInstance().addTable(new QTableMetaData()
|
||||
.withName(tableWithDefaultView)
|
||||
@@ -272,8 +286,8 @@ class CustomizableTableViewsTablePersonalizerTest extends BaseTest
|
||||
.withField(new QFieldMetaData("c", QFieldType.STRING)));
|
||||
|
||||
new InsertAction().execute(new InsertInput(CustomizableTable.TABLE_NAME).withRecordEntities(List.of(
|
||||
new CustomizableTable().withId(1).withTableName(tableWithoutDefaultView).withDefaultTableViewId(null),
|
||||
new CustomizableTable().withId(2).withTableName(tableWithDefaultView).withDefaultTableViewId(1)
|
||||
new CustomizableTable().withId(tableWithoutDefaultViewId).withTableName(tableWithoutDefaultView).withDefaultTableViewId(null),
|
||||
new CustomizableTable().withId(tableWithDefaultViewId).withTableName(tableWithDefaultView).withDefaultTableViewId(1)
|
||||
)));
|
||||
|
||||
//////////////////////
|
||||
@@ -293,6 +307,13 @@ class CustomizableTableViewsTablePersonalizerTest extends BaseTest
|
||||
QContext.getQSession().setValue("roleIds", "");
|
||||
assertEmptyView(personalizer.getEffectiveTableViewForCurrentSession(tableWithoutDefaultView));
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// put roles in session - there still aren't any view-role-int's for this table, so empty view //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
QContext.getQSession().setUser(new QUser().withIdReference(UUID.randomUUID().toString()));
|
||||
QContext.getQSession().setValue("roleIds", "1,2");
|
||||
assertEmptyView(personalizer.getEffectiveTableViewForCurrentSession(tableWithoutDefaultView));
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// this table has a defaultViewId, but that view doesn't exist, so get null for now //
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -300,31 +321,24 @@ class CustomizableTableViewsTablePersonalizerTest extends BaseTest
|
||||
QContext.getQSession().setValue("roleIds", null);
|
||||
assertEmptyView(personalizer.getEffectiveTableViewForCurrentSession(tableWithDefaultView));
|
||||
|
||||
/////////////////////
|
||||
// insert the view //
|
||||
/////////////////////
|
||||
////////////////////////////////////////////////
|
||||
// insert that default view, and a few others //
|
||||
////////////////////////////////////////////////
|
||||
new InsertAction().execute(new InsertInput(TableView.TABLE_NAME).withRecordEntities(List.of(
|
||||
new TableView().withId(1).withCustomizableTableId(2).withName("a").withFields(List.of(new TableViewField().withFieldName(tableWithDefaultView + ".a").withAccessLevel(EDITABLE_OPTIONAL))),
|
||||
new TableView().withId(2).withCustomizableTableId(2).withName("b").withFields(List.of(new TableViewField().withFieldName(tableWithDefaultView + ".b").withAccessLevel(EDITABLE_OPTIONAL))),
|
||||
new TableView().withId(3).withCustomizableTableId(2).withName("c").withFields(List.of(new TableViewField().withFieldName(tableWithDefaultView + ".c").withAccessLevel(EDITABLE_OPTIONAL)))
|
||||
new TableView().withId(1).withCustomizableTableId(tableWithDefaultViewId).withName("a").withFields(List.of(new TableViewField().withFieldName(tableWithDefaultView + ".a").withAccessLevel(EDITABLE_OPTIONAL))),
|
||||
new TableView().withId(2).withCustomizableTableId(tableWithDefaultViewId).withName("b").withFields(List.of(new TableViewField().withFieldName(tableWithDefaultView + ".b").withAccessLevel(EDITABLE_OPTIONAL))),
|
||||
new TableView().withId(3).withCustomizableTableId(tableWithDefaultViewId).withName("c").withFields(List.of(new TableViewField().withFieldName(tableWithDefaultView + ".c").withAccessLevel(EDITABLE_OPTIONAL)))
|
||||
)));
|
||||
|
||||
///////////////////////
|
||||
// now we can get it //
|
||||
///////////////////////
|
||||
////////////////////////////////////////////////////
|
||||
// now we can get the default view for this table //
|
||||
////////////////////////////////////////////////////
|
||||
QContext.getQSession().setUser(new QUser().withIdReference(UUID.randomUUID().toString()));
|
||||
QContext.getQSession().setValue("roleIds", null);
|
||||
assertThat(personalizer.getEffectiveTableViewForCurrentSession(tableWithDefaultView))
|
||||
.isNotNull()
|
||||
.extracting("name").isEqualTo("a");
|
||||
|
||||
/////////////////////////////////////////////////////////////
|
||||
// put roles in session - but fail to find a view-role-int //
|
||||
/////////////////////////////////////////////////////////////
|
||||
QContext.getQSession().setUser(new QUser().withIdReference(UUID.randomUUID().toString()));
|
||||
QContext.getQSession().setValue("roleIds", "1,2");
|
||||
assertEmptyView(personalizer.getEffectiveTableViewForCurrentSession(tableWithoutDefaultView));
|
||||
|
||||
///////////////////////////////
|
||||
// build some view-role ints //
|
||||
///////////////////////////////
|
||||
@@ -339,7 +353,7 @@ class CustomizableTableViewsTablePersonalizerTest extends BaseTest
|
||||
//////////////////////////////////////////
|
||||
QContext.getQSession().setUser(new QUser().withIdReference(UUID.randomUUID().toString()));
|
||||
QContext.getQSession().setValue("roleIds", "1");
|
||||
assertThat(personalizer.getEffectiveTableViewForCurrentSession(tableWithoutDefaultView))
|
||||
assertThat(personalizer.getEffectiveTableViewForCurrentSession(tableWithDefaultView))
|
||||
.isNotNull()
|
||||
.extracting("name").isEqualTo("a");
|
||||
|
||||
@@ -348,23 +362,20 @@ class CustomizableTableViewsTablePersonalizerTest extends BaseTest
|
||||
////////////////////////////////////////////////////
|
||||
QContext.getQSession().setUser(new QUser().withIdReference(UUID.randomUUID().toString()));
|
||||
QContext.getQSession().setValue("roleIds", "2");
|
||||
assertThat(personalizer.getEffectiveTableViewForCurrentSession(tableWithoutDefaultView))
|
||||
assertThat(personalizer.getEffectiveTableViewForCurrentSession(tableWithDefaultView))
|
||||
.isNotNull()
|
||||
.extracting("name").isEqualTo("b");
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// find 2 views (and when they merge, their name is lost) through role int //
|
||||
// do this last lookup twice, clearing memory record store //
|
||||
// in between to demonstrate memoization being used. //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
QContext.getQSession().setUser(new QUser().withIdReference(UUID.randomUUID().toString()));
|
||||
QContext.getQSession().setValue("roleIds", "2,3");
|
||||
|
||||
/////////////////////////////////////////////////////////////
|
||||
// do this last lookup twice, clearing memory record store //
|
||||
// in between to demonstrate memoization being used. //
|
||||
/////////////////////////////////////////////////////////////
|
||||
for(int i = 0; i < 2; i++)
|
||||
{
|
||||
TableView effectiveTableViewForCurrentSession = personalizer.getEffectiveTableViewForCurrentSession(tableWithoutDefaultView);
|
||||
TableView effectiveTableViewForCurrentSession = personalizer.getEffectiveTableViewForCurrentSession(tableWithDefaultView);
|
||||
assertThat(effectiveTableViewForCurrentSession)
|
||||
.isNotNull()
|
||||
.extracting("name").isNull();
|
||||
@@ -400,17 +411,20 @@ class CustomizableTableViewsTablePersonalizerTest extends BaseTest
|
||||
CustomizableTableViewsTablePersonalizer personalizer = new CustomizableTableViewsTablePersonalizer();
|
||||
QTableMetaData tableMetaData;
|
||||
|
||||
TableMetaDataInput tableActionInput = new TableMetaDataInput();
|
||||
|
||||
///////////////////////////
|
||||
// empty with empty case //
|
||||
///////////////////////////
|
||||
tableMetaData = personalizer.applyViewToTable(
|
||||
new TableView(),
|
||||
new QTableMetaData()
|
||||
);
|
||||
new QTableMetaData(),
|
||||
tableActionInput);
|
||||
assertThat(tableMetaData.getFields()).isNullOrEmpty();
|
||||
assertThat(tableMetaData.getSections()).isNullOrEmpty();
|
||||
|
||||
QTableMetaData baseTable = new QTableMetaData()
|
||||
.withName("baseTable")
|
||||
.withPrimaryKeyField("id")
|
||||
.withField(new QFieldMetaData("id", QFieldType.STRING).withIsEditable(false))
|
||||
.withField(new QFieldMetaData("secret", QFieldType.STRING).withIsHidden(true))
|
||||
@@ -426,7 +440,7 @@ class CustomizableTableViewsTablePersonalizerTest extends BaseTest
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
tableMetaData = personalizer.applyViewToTable(
|
||||
new TableView().withFields(List.of()),
|
||||
baseTable.clone());
|
||||
baseTable.clone(), tableActionInput);
|
||||
|
||||
assertEquals(2, tableMetaData.getFields().size());
|
||||
QFieldMetaDataAssert.assertThat(tableMetaData.getField("id")).isNotHidden().isNotEditable().isNotRequired();
|
||||
@@ -444,12 +458,12 @@ class CustomizableTableViewsTablePersonalizerTest extends BaseTest
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
tableMetaData = personalizer.applyViewToTable(
|
||||
new TableView().withFields(List.of(
|
||||
new TableViewField().withFieldName("x.id").withAccessLevel(EDITABLE_OPTIONAL),
|
||||
new TableViewField().withFieldName("x.secret").withAccessLevel(EDITABLE_OPTIONAL),
|
||||
new TableViewField().withFieldName("x.mandatory").withAccessLevel(EDITABLE_OPTIONAL),
|
||||
new TableViewField().withFieldName("x.optional").withAccessLevel(EDITABLE_OPTIONAL)
|
||||
new TableViewField().withFieldName("baseTable.id").withAccessLevel(EDITABLE_OPTIONAL),
|
||||
new TableViewField().withFieldName("baseTable.secret").withAccessLevel(EDITABLE_OPTIONAL),
|
||||
new TableViewField().withFieldName("baseTable.mandatory").withAccessLevel(EDITABLE_OPTIONAL),
|
||||
new TableViewField().withFieldName("baseTable.optional").withAccessLevel(EDITABLE_OPTIONAL)
|
||||
)),
|
||||
baseTable.clone());
|
||||
baseTable.clone(), tableActionInput);
|
||||
|
||||
assertEquals(4, tableMetaData.getFields().size());
|
||||
QFieldMetaDataAssert.assertThat(tableMetaData.getField("id")).isNotHidden().isNotEditable().isNotRequired();
|
||||
@@ -478,7 +492,7 @@ class CustomizableTableViewsTablePersonalizerTest extends BaseTest
|
||||
new TableView().withWidgets(List.of(
|
||||
new TableViewWidget().withWidgetName("lilWidgy").withAccessLevel(WidgetAccessLevel.HAS_ACCESS)
|
||||
)),
|
||||
baseTable.clone());
|
||||
baseTable.clone(), tableActionInput);
|
||||
assertEquals(2, tableMetaData.getFields().size());
|
||||
assertEquals(3, tableMetaData.getSections().size()); // one for id, one for mandatory, and 1 for our widget
|
||||
assertEquals(List.of("s0", "s1", "w0"), tableMetaData.getSections().stream().map(s -> s.getName()).toList());
|
||||
@@ -486,6 +500,288 @@ class CustomizableTableViewsTablePersonalizerTest extends BaseTest
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testApplyViewToTableWithVirtualFields()
|
||||
{
|
||||
CustomizableTableViewsTablePersonalizer personalizer = new CustomizableTableViewsTablePersonalizer();
|
||||
TableMetaDataInput tableActionInput = new TableMetaDataInput();
|
||||
|
||||
QTableMetaData baseTable = new QTableMetaData()
|
||||
.withName("vfTable")
|
||||
.withPrimaryKeyField("id")
|
||||
.withField(new QFieldMetaData("id", QFieldType.STRING).withIsEditable(false))
|
||||
.withField(new QFieldMetaData("optional", QFieldType.STRING))
|
||||
.withVirtualField(new QVirtualFieldMetaData("computed", QFieldType.STRING).withLabel("Computed"))
|
||||
.withVirtualField(new QVirtualFieldMetaData("derived", QFieldType.STRING).withLabel("Derived"))
|
||||
.withSection(SectionFactory.defaultT1("id").withName("s0"))
|
||||
.withSection(SectionFactory.defaultT2("optional").withName("s1"));
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
// empty view - no virtual fields kept (no "always keep" rules for virtuals) //
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
QTableMetaData tableMetaData = personalizer.applyViewToTable(
|
||||
new TableView().withFields(List.of()),
|
||||
baseTable.clone(), tableActionInput);
|
||||
|
||||
assertEquals(1, tableMetaData.getFields().size());
|
||||
assertNotNull(tableMetaData.getField("id"));
|
||||
assertTrue(tableMetaData.getVirtualFields().isEmpty());
|
||||
|
||||
///////////////////////////////////////////////////////////////
|
||||
// view references one virtual field - only that one is kept //
|
||||
///////////////////////////////////////////////////////////////
|
||||
tableMetaData = personalizer.applyViewToTable(
|
||||
new TableView().withFields(List.of(
|
||||
new TableViewField().withFieldName("vfTable.computed").withAccessLevel(READ_ONLY)
|
||||
)),
|
||||
baseTable.clone(), tableActionInput);
|
||||
|
||||
assertEquals(1, tableMetaData.getFields().size());
|
||||
assertEquals(1, tableMetaData.getVirtualFields().size());
|
||||
assertNotNull(tableMetaData.getVirtualFields().get("computed"));
|
||||
assertNull(tableMetaData.getVirtualFields().get("derived"));
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// view references both virtual fields and a regular field - all are kept //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
tableMetaData = personalizer.applyViewToTable(
|
||||
new TableView().withFields(List.of(
|
||||
new TableViewField().withFieldName("vfTable.optional").withAccessLevel(EDITABLE_OPTIONAL),
|
||||
new TableViewField().withFieldName("vfTable.computed").withAccessLevel(READ_ONLY),
|
||||
new TableViewField().withFieldName("vfTable.derived").withAccessLevel(READ_ONLY)
|
||||
)),
|
||||
baseTable.clone(), tableActionInput);
|
||||
|
||||
assertEquals(2, tableMetaData.getFields().size()); // id + optional
|
||||
assertEquals(2, tableMetaData.getVirtualFields().size()); // computed + derived
|
||||
|
||||
assertEquals(List.of("optional"), tableMetaData.getSection("s1").getFieldNames());
|
||||
|
||||
//////////////////////////////////////////////////////////////////
|
||||
// table with no virtual fields at all - should not break //
|
||||
//////////////////////////////////////////////////////////////////
|
||||
QTableMetaData noVfTable = new QTableMetaData()
|
||||
.withName("noVfTable")
|
||||
.withPrimaryKeyField("id")
|
||||
.withField(new QFieldMetaData("id", QFieldType.STRING).withIsEditable(false))
|
||||
.withSection(SectionFactory.defaultT1("id").withName("s0"));
|
||||
|
||||
tableMetaData = personalizer.applyViewToTable(
|
||||
new TableView().withFields(List.of()),
|
||||
noVfTable.clone(), tableActionInput);
|
||||
|
||||
assertEquals(1, tableMetaData.getFields().size());
|
||||
assertTrue(tableMetaData.getVirtualFields() == null || tableMetaData.getVirtualFields().isEmpty());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testDynamicDefaultValueFields()
|
||||
{
|
||||
///////////////////////////
|
||||
// empty with empty case //
|
||||
///////////////////////////
|
||||
QTableMetaData baseTable = new QTableMetaData()
|
||||
.withPrimaryKeyField("id")
|
||||
.withField(new QFieldMetaData("id", QFieldType.STRING).withIsEditable(false))
|
||||
.withField(new QFieldMetaData("secret", QFieldType.STRING).withIsHidden(true))
|
||||
.withField(new QFieldMetaData("mandatory", QFieldType.STRING).withIsRequired(true))
|
||||
.withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withBehavior(DynamicDefaultValueBehavior.CREATE_DATE))
|
||||
.withField(new QFieldMetaData("optional", QFieldType.STRING))
|
||||
.withSection(SectionFactory.defaultT1("id").withName("s0"))
|
||||
.withSection(SectionFactory.defaultT2("secret", "mandatory").withName("s1"))
|
||||
.withSection(SectionFactory.defaultT2("optional", "createDate").withName("s2"));
|
||||
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
// for actions that aren't insert or update, just get pkey & required //
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
for(AbstractTableActionInput actionInput : List.of(new QueryInput(), new DeleteInput(), new TableMetaDataInput()))
|
||||
{
|
||||
QTableMetaData tableMetaData = new CustomizableTableViewsTablePersonalizer().applyViewToTable(
|
||||
new TableView().withFields(List.of()),
|
||||
baseTable.clone(), actionInput);
|
||||
|
||||
assertEquals(2, tableMetaData.getFields().size());
|
||||
QFieldMetaDataAssert.assertThat(tableMetaData.getField("id")).isNotNull();
|
||||
QFieldMetaDataAssert.assertThat(tableMetaData.getField("mandatory")).isNotNull();
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
// for insert or update action, get createDate too //
|
||||
/////////////////////////////////////////////////////
|
||||
for(AbstractTableActionInput actionInput : List.of(new InsertInput(), new UpdateInput()))
|
||||
{
|
||||
QTableMetaData tableMetaData = new CustomizableTableViewsTablePersonalizer().applyViewToTable(
|
||||
new TableView().withFields(List.of()),
|
||||
baseTable.clone(), actionInput);
|
||||
|
||||
assertEquals(3, tableMetaData.getFields().size());
|
||||
QFieldMetaDataAssert.assertThat(tableMetaData.getField("id")).isNotNull();
|
||||
QFieldMetaDataAssert.assertThat(tableMetaData.getField("mandatory")).isNotNull();
|
||||
QFieldMetaDataAssert.assertThat(tableMetaData.getField("createDate")).isNotNull();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testFieldsInSecurityLocks()
|
||||
{
|
||||
///////////////////////////
|
||||
// empty with empty case //
|
||||
///////////////////////////
|
||||
QTableMetaData baseTable = new QTableMetaData()
|
||||
.withPrimaryKeyField("id")
|
||||
.withField(new QFieldMetaData("id", QFieldType.STRING).withIsEditable(false))
|
||||
.withField(new QFieldMetaData("a", QFieldType.STRING))
|
||||
.withField(new QFieldMetaData("b", QFieldType.STRING))
|
||||
.withField(new QFieldMetaData("c", QFieldType.STRING))
|
||||
.withField(new QFieldMetaData("d", QFieldType.STRING));
|
||||
|
||||
//////////////////////////////////////////////////////
|
||||
// with no security field, only id should come back //
|
||||
//////////////////////////////////////////////////////
|
||||
{
|
||||
QTableMetaData tableMetaData = new CustomizableTableViewsTablePersonalizer().applyViewToTable(
|
||||
new TableView().withFields(List.of()),
|
||||
baseTable.clone(), new QueryInput());
|
||||
|
||||
assertEquals(1, tableMetaData.getFields().size());
|
||||
QFieldMetaDataAssert.assertThat(tableMetaData.getField("id")).isNotNull();
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////
|
||||
// with a simple security field, it should come back //
|
||||
///////////////////////////////////////////////////////
|
||||
baseTable.setRecordSecurityLocks(List.of(
|
||||
new RecordSecurityLock().withFieldName("a")
|
||||
));
|
||||
{
|
||||
QTableMetaData tableMetaData = new CustomizableTableViewsTablePersonalizer().applyViewToTable(
|
||||
new TableView().withFields(List.of()),
|
||||
baseTable.clone(), new QueryInput());
|
||||
|
||||
assertEquals(2, tableMetaData.getFields().size());
|
||||
QFieldMetaDataAssert.assertThat(tableMetaData.getField("id")).isNotNull();
|
||||
QFieldMetaDataAssert.assertThat(tableMetaData.getField("a")).isNotNull();
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////
|
||||
// with a multi-lock, all included fields should come back //
|
||||
/////////////////////////////////////////////////////////////
|
||||
baseTable.setRecordSecurityLocks(List.of(
|
||||
new MultiRecordSecurityLock()
|
||||
.withOperator(MultiRecordSecurityLock.BooleanOperator.OR)
|
||||
.withLocks(List.of(
|
||||
new RecordSecurityLock().withFieldName("b"),
|
||||
new MultiRecordSecurityLock()
|
||||
.withOperator(MultiRecordSecurityLock.BooleanOperator.AND)
|
||||
.withLocks(List.of(
|
||||
new RecordSecurityLock().withFieldName("c"),
|
||||
new RecordSecurityLock().withFieldName("d")
|
||||
))
|
||||
))
|
||||
));
|
||||
{
|
||||
QTableMetaData tableMetaData = new CustomizableTableViewsTablePersonalizer().applyViewToTable(
|
||||
new TableView().withFields(List.of()),
|
||||
baseTable.clone(), new QueryInput());
|
||||
|
||||
assertEquals(4, tableMetaData.getFields().size());
|
||||
QFieldMetaDataAssert.assertThat(tableMetaData.getField("id")).isNotNull();
|
||||
QFieldMetaDataAssert.assertThat(tableMetaData.getField("b")).isNotNull();
|
||||
QFieldMetaDataAssert.assertThat(tableMetaData.getField("c")).isNotNull();
|
||||
QFieldMetaDataAssert.assertThat(tableMetaData.getField("d")).isNotNull();
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////
|
||||
// make sure lock field from join table doesn't break us //
|
||||
///////////////////////////////////////////////////////////
|
||||
baseTable.setRecordSecurityLocks(List.of(
|
||||
new RecordSecurityLock().withFieldName("join.a")
|
||||
));
|
||||
{
|
||||
QTableMetaData tableMetaData = new CustomizableTableViewsTablePersonalizer().applyViewToTable(
|
||||
new TableView().withFields(List.of()),
|
||||
baseTable.clone(), new QueryInput());
|
||||
|
||||
assertEquals(1, tableMetaData.getFields().size());
|
||||
QFieldMetaDataAssert.assertThat(tableMetaData.getField("id")).isNotNull();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testFieldsWithSupplementalMetaData()
|
||||
{
|
||||
///////////////////////////
|
||||
// empty with empty case //
|
||||
///////////////////////////
|
||||
QTableMetaData baseTable = new QTableMetaData()
|
||||
.withPrimaryKeyField("id")
|
||||
.withField(new QFieldMetaData("id", QFieldType.STRING).withIsEditable(false))
|
||||
.withField(new QFieldMetaData("a", QFieldType.STRING))
|
||||
.withField(new QFieldMetaData("b", QFieldType.STRING))
|
||||
.withField(new QFieldMetaData("c", QFieldType.STRING))
|
||||
.withField(new QFieldMetaData("d", QFieldType.STRING));
|
||||
|
||||
//////////////////////////////////
|
||||
// base case, should just be id //
|
||||
//////////////////////////////////
|
||||
{
|
||||
QTableMetaData tableMetaData = new CustomizableTableViewsTablePersonalizer().applyViewToTable(
|
||||
new TableView().withFields(List.of()),
|
||||
baseTable.clone(), new QueryInput());
|
||||
|
||||
assertEquals(1, tableMetaData.getFields().size());
|
||||
QFieldMetaDataAssert.assertThat(tableMetaData.getField("id")).isNotNull();
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
// supplemental meta data, but without a rule should not be included //
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
baseTable.getField("a").withSupplementalMetaData(new CustomizableTableViewsFieldMetaData());
|
||||
{
|
||||
QTableMetaData tableMetaData = new CustomizableTableViewsTablePersonalizer().applyViewToTable(
|
||||
new TableView().withFields(List.of()),
|
||||
baseTable.clone(), new QueryInput());
|
||||
|
||||
assertEquals(1, tableMetaData.getFields().size());
|
||||
QFieldMetaDataAssert.assertThat(tableMetaData.getField("id")).isNotNull();
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// supplemental meta data, with the include rule should be included //
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
baseTable.getField("b").withSupplementalMetaData(new CustomizableTableViewsFieldMetaData()
|
||||
.withRule(CustomizableTableViewsFieldMetaData.Rule.ALWAYS_KEEP_FIELD));
|
||||
{
|
||||
QTableMetaData tableMetaData = new CustomizableTableViewsTablePersonalizer().applyViewToTable(
|
||||
new TableView().withFields(List.of()),
|
||||
baseTable.clone(), new QueryInput());
|
||||
|
||||
assertEquals(2, tableMetaData.getFields().size());
|
||||
QFieldMetaDataAssert.assertThat(tableMetaData.getField("id")).isNotNull();
|
||||
QFieldMetaDataAssert.assertThat(tableMetaData.getField("b")).isNotNull();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@@ -550,6 +846,11 @@ class CustomizableTableViewsTablePersonalizerTest extends BaseTest
|
||||
new InsertAction().execute(new InsertInput(CustomizableTable.TABLE_NAME).withRecordEntities(List.of(
|
||||
new CustomizableTable().withTableName(baseTable.getName()).withIsActive(true))));
|
||||
|
||||
////////////////////////////////////////////////////////////////
|
||||
// assert system inputSource still gets same as default table //
|
||||
////////////////////////////////////////////////////////////////
|
||||
assertSame(baseTable, personalizer.execute(new TableMetaDataPersonalizerInput().withTableMetaData(baseTable).withInputSource(QInputSource.SYSTEM)));
|
||||
|
||||
///////////////////////////////////////////
|
||||
// assert it's pretty empty with no view //
|
||||
///////////////////////////////////////////
|
||||
@@ -571,6 +872,11 @@ class CustomizableTableViewsTablePersonalizerTest extends BaseTest
|
||||
new InsertAction().execute(new InsertInput(TableViewRoleInt.TABLE_NAME).withRecordEntities(List.of(
|
||||
new TableViewRoleInt().withRoleId(101).withTableViewId(1))));
|
||||
|
||||
////////////////////////////////////////////////////////////////
|
||||
// assert system inputSource still gets same as default table //
|
||||
////////////////////////////////////////////////////////////////
|
||||
assertSame(baseTable, personalizer.execute(new TableMetaDataPersonalizerInput().withTableMetaData(baseTable).withInputSource(QInputSource.SYSTEM)));
|
||||
|
||||
QContext.getQSession().setUser(new QUser().withIdReference(UUID.randomUUID().toString()));
|
||||
QContext.getQSession().setValue("roleIds", "101");
|
||||
|
||||
@@ -583,6 +889,54 @@ class CustomizableTableViewsTablePersonalizerTest extends BaseTest
|
||||
assertEquals(Set.of("id", "optional"), personalizedTable.getFields().values().stream().map(s -> s.getName()).collect(Collectors.toSet()));
|
||||
assertEquals(3, personalizedTable.getSections().size());
|
||||
assertEquals(List.of("s0", "s1", "w0"), personalizedTable.getSections().stream().map(s -> s.getName()).toList());
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// add a join table, and a field from it in the main table's t2 section //
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
QTableMetaData joinTable = new QTableMetaData()
|
||||
.withName("joinTable")
|
||||
.withPrimaryKeyField("id")
|
||||
.withField(new QFieldMetaData("id", QFieldType.STRING).withIsEditable(false))
|
||||
.withField(new QFieldMetaData("joinField", QFieldType.STRING))
|
||||
.withSection(SectionFactory.defaultT1("id", "joinField").withName("s0"));
|
||||
QContext.getQInstance().addTable(joinTable);
|
||||
QContext.getQInstance().addJoin(new QJoinMetaData().withLeftTable("baseTable").withRightTable("joinTable").withInferredName().withType(JoinType.ONE_TO_ONE).withJoinOn(new JoinOn("id", "joinField")));
|
||||
baseTable.getSection("s1").getFieldNames().add("joinTable.joinField");
|
||||
|
||||
//////////////////////////////////////////
|
||||
// by default, should get the joinField //
|
||||
//////////////////////////////////////////
|
||||
personalizedTable = personalizer.execute(input);
|
||||
assertEquals(List.of("optional", "joinTable.joinField"), personalizedTable.getSection("s1").getFieldNames());
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
// activate customization on the join table, which should remove the join field //
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
new InsertAction().execute(new InsertInput(CustomizableTable.TABLE_NAME).withRecordEntities(List.of(
|
||||
new CustomizableTable().withTableName(joinTable.getName()).withIsActive(true))));
|
||||
personalizedTable = personalizer.execute(input);
|
||||
assertEquals(List.of("optional"), personalizedTable.getSection("s1").getFieldNames());
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// add a view to the join table, with a field, and attach it to the view - then the user gets the field back //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
new InsertAction().execute(new InsertInput(TableView.TABLE_NAME).withRecordEntities(List.of(
|
||||
new TableView().withId(2).withCustomizableTableId(2).withName("a")
|
||||
.withFields(List.of(new TableViewField().withFieldName(joinTable.getName() + ".joinField").withAccessLevel(EDITABLE_OPTIONAL))))));
|
||||
new InsertAction().execute(new InsertInput(TableViewRoleInt.TABLE_NAME).withRecordEntities(List.of(
|
||||
new TableViewRoleInt().withRoleId(101).withTableViewId(2))));
|
||||
|
||||
personalizedTable = personalizer.execute(input);
|
||||
assertEquals(List.of("optional", "joinTable.joinField"), personalizedTable.getSection("s1").getFieldNames());
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// finally, set user to not have this role, make sure the section with the join field disappears //
|
||||
// (the non-join field will have also gone away by switching to other role, so the section is empty) //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
QContext.getQSession().setUser(new QUser().withIdReference(UUID.randomUUID().toString()));
|
||||
QContext.getQSession().setValue("roleIds", "102");
|
||||
personalizedTable = personalizer.execute(input);
|
||||
assertNull(personalizedTable.getSection("s1"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
+60
@@ -32,11 +32,15 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSourceInput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QVirtualFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.SectionFactory;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@@ -84,4 +88,60 @@ class CustomizableTableFieldPVSTest extends BaseTest
|
||||
assertEquals("Optional", possibleValues.get(1).getLabel());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testWithVirtualFields() throws QException
|
||||
{
|
||||
QTableMetaData testTable = new QTableMetaData()
|
||||
.withName("testTable")
|
||||
.withPrimaryKeyField("id")
|
||||
.withField(new QFieldMetaData("id", QFieldType.STRING).withIsEditable(false))
|
||||
.withField(new QFieldMetaData("optional", QFieldType.STRING).withLabel("Optional"))
|
||||
.withVirtualField(new QVirtualFieldMetaData("computed", QFieldType.STRING).withLabel("Computed"))
|
||||
.withVirtualField(new QVirtualFieldMetaData("hiddenVirtual", QFieldType.STRING).withIsHidden(true).withLabel("Hidden Virtual"))
|
||||
.withSection(SectionFactory.defaultT1("id").withName("s0"))
|
||||
.withSection(SectionFactory.defaultT2("optional").withName("s1"));
|
||||
QContext.getQInstance().addTable(testTable);
|
||||
|
||||
new InsertAction().execute(new InsertInput(CustomizableTable.TABLE_NAME).withRecordEntities(List.of(
|
||||
new CustomizableTable().withTableName(testTable.getName()).withIsActive(true))));
|
||||
|
||||
Integer tableViewId = 17;
|
||||
new InsertAction().execute(new InsertInput(TableView.TABLE_NAME).withRecordEntities(List.of(
|
||||
new TableView().withId(tableViewId).withCustomizableTableId(1).withName("a"))));
|
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// search should include the non-hidden virtual field in results //
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
List<QPossibleValue<String>> possibleValues = new CustomizableTableFieldPVS().search(new SearchPossibleValueSourceInput().withOtherValues(Map.of("tableViewId", tableViewId)));
|
||||
assertEquals(2, possibleValues.size());
|
||||
assertThat(possibleValues)
|
||||
.anyMatch(pv1 -> "testTable.computed".equals(pv1.getId()) && "Computed".equals(pv1.getLabel()))
|
||||
.anyMatch(pv1 -> "testTable.optional".equals(pv1.getId()) && "Optional".equals(pv1.getLabel()));
|
||||
|
||||
///////////////////////////////////////////////////////////////////
|
||||
// getPossibleValue should resolve a virtual field by table.name //
|
||||
///////////////////////////////////////////////////////////////////
|
||||
QPossibleValue<String> pv = new CustomizableTableFieldPVS().getPossibleValue("testTable.computed");
|
||||
assertNotNull(pv);
|
||||
assertEquals("testTable.computed", pv.getId());
|
||||
assertEquals("Computed", pv.getLabel());
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
// getPossibleValue for a normal field still works //
|
||||
/////////////////////////////////////////////////////
|
||||
QPossibleValue<String> pvNormal = new CustomizableTableFieldPVS().getPossibleValue("testTable.optional");
|
||||
assertNotNull(pvNormal);
|
||||
assertEquals("Optional", pvNormal.getLabel());
|
||||
|
||||
/////////////////////////////////////////////////////////
|
||||
// getPossibleValue for a nonexistent field returns null //
|
||||
/////////////////////////////////////////////////////////
|
||||
assertNull(new CustomizableTableFieldPVS().getPossibleValue("testTable.doesNotExist"));
|
||||
}
|
||||
|
||||
}
|
||||
+31
@@ -25,12 +25,16 @@ package com.kingsrook.qbits.customizabletableviews.model;
|
||||
import java.util.function.BiFunction;
|
||||
import com.kingsrook.qbits.customizabletableviews.BaseTest;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QVirtualFieldMetaData;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static com.kingsrook.qbits.customizabletableviews.QFieldMetaDataAssert.assertThat;
|
||||
import static com.kingsrook.qbits.customizabletableviews.model.FieldAccessLevel.EDITABLE_OPTIONAL;
|
||||
import static com.kingsrook.qbits.customizabletableviews.model.FieldAccessLevel.EDITABLE_REQUIRED;
|
||||
import static com.kingsrook.qbits.customizabletableviews.model.FieldAccessLevel.READ_ONLY;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@@ -122,4 +126,31 @@ class FieldAccessLevelTest extends BaseTest
|
||||
assertThat(doApply.apply(READ_ONLY, hiddenField))/*....*/.isHidden().isNotRequired().isEditable(); // no changes
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testValidateForVirtualField()
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
// a virtual field (even if editable) should be treated as read-only by validate //
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
QVirtualFieldMetaData editableVirtualField = new QVirtualFieldMetaData("vf", QFieldType.STRING).withIsEditable(true);
|
||||
|
||||
assertNull(READ_ONLY.validateForField(editableVirtualField));
|
||||
assertNotNull(EDITABLE_REQUIRED.validateForField(editableVirtualField));
|
||||
assertNotNull(EDITABLE_OPTIONAL.validateForField(editableVirtualField));
|
||||
|
||||
/////////////////////////////////////////////////
|
||||
// a non-editable virtual field - same results //
|
||||
/////////////////////////////////////////////////
|
||||
QVirtualFieldMetaData readOnlyVirtualField = new QVirtualFieldMetaData("vf", QFieldType.STRING).withIsEditable(false);
|
||||
|
||||
assertNull(READ_ONLY.validateForField(readOnlyVirtualField));
|
||||
assertNotNull(EDITABLE_REQUIRED.validateForField(readOnlyVirtualField));
|
||||
assertNotNull(EDITABLE_OPTIONAL.validateForField(readOnlyVirtualField));
|
||||
}
|
||||
|
||||
}
|
||||
+48
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
|
||||
Reference in new issue
Block a user