Compare commits

..
Author SHA1 Message Date
Darin KelkhoffandClaude Opus 4.6 96862bf6f1 fix: QExposedJoin.clone() preserves joinTable fields — overwrite after constructor to avoid Map loss
The constructor re-wraps joinTable in new QTableMetaData(), which uses
for-in to iterate fields — this doesn't work on Map instances, losing
all fields from the cloned join table.

Also added defensive ?. in constructor and clone, removed unnecessary
'as any' casts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 15:51:57 -05:00
CircleCI 2427dbeeba Bump version to 0.40.11-SNAPSHOT [skip ci] 2026-03-26 18:56:40 +00:00
Tim Chamberlain ce80326e30 fix(metadata): accept supplementalMetaData in field parser 2026-03-26 13:54:53 -05:00
CircleCI 0b35383fd2 Bump version to 0.40.10-SNAPSHOT [skip ci] 2026-03-06 22:09:36 +00:00
Darin Kelkhoff 463da833d2 Merge pull request #42 from QRun-IO/feature/field-functions
feat(fieldFunctions): add support for fieldFunctions in filter criter…
2026-03-06 16:09:04 -06:00
Darin Kelkhoff 3d08dc7f12 feat(fieldFunctions): add support for fieldFunctions in filter criteria, and more explicit handling of virtualFields (e.g., their own type now that they have new attributes coming from backend) 2026-03-02 16:07:27 -06:00
CircleCI 91ea8e6385 Bump version to 0.40.9-SNAPSHOT [skip ci] 2026-02-20 17:19:55 +00:00
Darin Kelkhoff 6aefb654e4 Merge pull request #41 from QRun-IO/feature/fix-visual-regressions-128-v2
Feature/fix visual regressions 128 v2
2026-02-20 11:18:50 -06:00
Darin Kelkhoff 5464627053 feat(themes): Add accentColorLight and gravatarDefault attributes 2026-02-18 11:11:33 -06:00
Darin Kelkhoff 4a3d9dc477 refactor(themes): Moving QThemeMetaData out of core - into qqq-frontend-material-dashboard (as MaterialDashboardThemeMetaData) 2026-02-18 11:11:16 -06:00
CircleCI c5b1a58ca1 Bump version to 0.40.8-SNAPSHOT [skip ci] 2026-02-10 18:28:45 +00:00
Tim Chamberlain 29fd6e11b2 Merge branch 'feature/custom-apps-qbit' into develop 2026-02-10 12:27:59 -06:00
CircleCI 6bb4b3286d Bump version to 0.40.7-SNAPSHOT [skip ci] 2026-02-10 02:26:12 +00:00
Darin Kelkhoff d82fa30a6c chore: update version to 0.4.6-SNAPSHOT 2026-02-09 20:25:20 -06:00
CircleCI f1c646605a Bump version to 0.40.5-SNAPSHOT [skip ci] 2026-02-10 02:22:54 +00:00
Darin Kelkhoff 2513b651e3 Merge pull request #39 from QRun-IO/feature/collapsible-sections-and-widgets
feat(collapsibleElements): Initial checkin of new Collapsible meta-da…
2026-02-09 20:22:20 -06:00
Tim Chamberlain 35a75b166c updated package version 2026-02-06 14:06:26 -06:00
Tim Chamberlain 6c72f2e805 added help text to apps 2026-02-06 13:59:52 -06:00
Tim Chamberlain 7d6428e046 added help text to apps 2026-02-05 19:01:09 -06:00
Darin Kelkhoff 8406d8af57 test(collapsibleElements): add tests for handling collasibles inside QTableSection and QWidgetMetaData 2026-02-05 12:06:24 -06:00
Darin Kelkhoff 86546fa919 test(collapsibleElements): add test for this file (and force booleans to always get set by constructor, and true iff value in source object is true). 2026-02-05 11:47:09 -06:00
Darin Kelkhoff 883dd300c0 feat(collapsibleElements): Initial checkin of new Collapsible meta-data class plus applying it to QTableSection and QWidgetMetaData 2026-02-05 08:38:22 -06:00
Darin Kelkhoff b6f0af7cb3 chore: update to qqq-orb0.6 2026-01-30 10:47:52 -06:00
Tim Chamberlain 71e7daffdf updates to frontend core to support custom app qbit 2026-01-30 10:24:16 -06:00
CircleCI ca72cf2a29 Bump version to 0.40.4-SNAPSHOT [skip ci] 2026-01-23 18:35:29 +00:00
james.maes aa8c53c6f9 chore: merge release/0.36.0 into develop
Brings multi-app tables (#38) and custom menus (#37).
2026-01-23 12:34:02 -06:00
CircleCI 74c9c8e6c5 Bump version to 0.36.0-RC.4 [skip ci] 2026-01-21 16:44:41 +00:00
Darin Kelkhoff 2af455f2ab Merge pull request #37 from QRun-IO/feature/364-customizable-table-action-menus-and-additional-menu-support
feat(menus): Add new QMenu and QMenuItem meta data objects
2026-01-21 10:44:06 -06:00
CircleCI c6252fc1f8 Bump version to 0.36.0-RC.3 [skip ci] 2026-01-21 15:38:20 +00:00
Darin Kelkhoff 6a18604811 Merge pull request #38 from QRun-IO/feature/365-formal-support-for-tables-belonging-to-multiple-apps
feat(tables): improve/formalize support for tables being added to mor…
2026-01-21 09:37:46 -06:00
Darin Kelkhoff 10f91c7adc feat(menus): Add new QMenu and QMenuItem meta data objects, which can be attached to QTableMetaData
- update cloning to handle menus.
2026-01-19 20:53:23 -06:00
Darin Kelkhoff f4b6b5adce feat(tables): improve/formalize support for tables being added to more than 1 app.
- Accept map of redirects to from the backend's metaData action, stored in QInstance. (to redirect a user from links to a table through an app that they don't have access to, instead to an app that they do have access to).
- Add appAffinity field to QAppTreeNode, to specify which app to use for a table in a global context (e.g., dot menu) if it belongs to more than 1.
- Update QInstance.getTablePath to return the path with the highest affinity value for a table in multiple apps
2026-01-19 20:42:15 -06:00
CircleCI 07b6b23111 Bump version to 0.40.3-SNAPSHOT [skip ci] 2026-01-12 20:16:34 +00:00
james.maes d17e4e9f20 docs: update session state with current version 2026-01-12 14:16:06 -06:00
james.maes 030d78f454 docs: add project context and session continuity files
- Add CLAUDE.md with project overview and common issues
- Add docs/version-management.md for version strategy
- Add docs/troubleshooting.md for common issues
- Add .session-state.md for session continuity
- Update .gitignore to exclude .claude/ directory
2026-01-12 14:15:43 -06:00
CircleCI 560b00c109 Bump version to 0.40.2-SNAPSHOT [skip ci] 2026-01-12 16:38:39 +00:00
james.maes f60d45c325 fix(build): ensure yalc-push rebuilds lib before pushing 2026-01-12 08:00:07 -06:00
CircleCI e8ec0e6bbe Bump version to 0.40.1-SNAPSHOT [skip ci] 2026-01-11 02:53:46 +00:00
james.maes e136eb4f52 chore: bump to 0.40.0-SNAPSHOT 2026-01-10 20:52:28 -06:00
CircleCI 7ddf61dfd5 Bump version to 0.36.0-RC.2 [skip ci] 2026-01-11 02:44:13 +00:00
james.maes 74f64e7b17 chore: update qqq-orb to 0.5.3 with RC fixes 2026-01-10 20:43:43 -06:00
CircleCI e0652f4b9a Bump version to 0.36.0-RC.1 [skip ci] 2026-01-11 02:33:37 +00:00
james.maes f6016558b6 chore: update qqq-orb to 0.5.0 for proper RC support 2026-01-10 20:33:05 -06:00
CircleCI 3e69e81d30 Bump version to 0.36.1-SNAPSHOT [skip ci] 2026-01-11 02:18:49 +00:00
james.maes 182b6f5e5a chore: align version to 0.36.0 for RC 2026-01-10 20:16:34 -06:00
CircleCI 1809736416 Bump version to 1.4.4-SNAPSHOT [skip ci] 2026-01-05 16:12:09 +00:00
james.maes be74f6485a chore: migrate license from AGPL-3.0 to Apache-2.0 2026-01-05 10:11:36 -06:00
CircleCI 52ef938cfe Bump version to 1.4.3-SNAPSHOT [skip ci] 2026-01-01 22:40:48 +00:00
CircleCI 1ea9058d63 Bump version to 1.4.2-SNAPSHOT [skip ci] 2026-01-01 22:32:26 +00:00
james.maes 3db2c19a49 feat(theme): add 60+ theme properties to QThemeMetaData interface
Mirrors Java QThemeMetaData class with typography, branded header,
sidebar, app bar, table, and general UI customization properties.
2026-01-01 16:31:09 -06:00
CircleCI 405583ea63 Bump version to 1.4.1-SNAPSHOT [skip ci] 2025-12-29 16:46:44 +00:00
james.maes 83630fe536 build: bump version to 1.4.0-SNAPSHOT for next release 2025-12-29 10:45:34 -06:00
CircleCI caade04633 Bump version to 1.3.13-SNAPSHOT [skip ci] 2025-12-29 14:45:02 +00:00
CircleCI e980f2bded Bump version to 1.3.12-SNAPSHOT [skip ci] 2025-12-29 14:38:38 +00:00
CircleCI 3a97a808dc Bump version to 1.3.11-SNAPSHOT [skip ci] 2025-12-29 14:34:16 +00:00
james.maes aa2e9adf68 feat: add QThemeMetaData interface for pluggable themes support
- Add QThemeMetaData TypeScript interface
- Add theme property to QInstance
- Add iconStyle support
- Include unit tests with 100% coverage
2025-12-29 08:33:19 -06:00
james.maes a93e513847 fix(ci): use node_publish for publish_feature workflow 2025-12-29 08:31:03 -06:00
james.maes 8de50fbf59 test(theme): add QThemeMetaData.test.ts with 14 tests (100% coverage)
Tests constructor, density types, iconStyle types, color formats
2025-12-28 21:34:41 -06:00
james.maes e323b332b4 feat: add iconStyle to QThemeMetaData
Support for Material Icons style variants (filled, outlined, rounded,
sharp, two-tone).
2025-12-28 21:02:25 -06:00
james.maes 80dedc6c20 feat: add QThemeMetaData interface and theme support in QInstance 2025-12-28 20:34:42 -06:00
CircleCI 642c6ed223 Bump version to 1.3.10-SNAPSHOT [skip ci] 2025-12-26 01:39:43 +00:00
james.maes 51c59958d6 chore(license): update to AGPL-3.0 with QRun-IO copyright 2025-12-25 19:39:05 -06:00
CircleCI 215856dc4d Bump version to 1.3.9-SNAPSHOT [skip ci] 2025-12-24 20:13:55 +00:00
james.maes 13f4116dd4 docs: Standardize README 2025-12-24 14:12:43 -06:00
CircleCI 55b1ba4ff5 Bump version to 1.3.8-SNAPSHOT [skip ci] 2025-12-09 17:33:14 +00:00
Darin Kelkhoff e39a67dd1b feat: add tableSection.alternatives and table.virtualFields 2025-12-09 11:32:25 -06:00
CircleCI 8ff1ae642b Bump version to 1.3.7-SNAPSHOT [skip ci] 2025-11-21 21:16:53 +00:00
Darin Kelkhoff 948bccaace feat(widgets): add optional overrideAbortName param to widget() - for use-cases where the same widget is on the screen multiple times, so rendering it once shouldn't cancel others 2025-11-21 15:16:16 -06:00
Darin Kelkhoff db0b0e3e24 feat(widgetAdornments): Add widget as a new field AdornmentType 2025-11-21 15:15:17 -06:00
CircleCI a97e05e2ce Bump version to 1.3.6-SNAPSHOT [skip ci] 2025-10-28 13:43:01 +00:00
Darin Kelkhoff 2bc7719b0f Merge pull request #36 from QRun-IO/feature/table-meta-data-clone
fix(clone): avoid null pointer when cloning criteria with null values
2025-10-28 08:42:27 -05:00
Darin Kelkhoff 9576463f42 fix(clone): avoid null pointer when cloning criteria with null values 2025-10-28 08:38:25 -05:00
CircleCI 65becd0425 Bump version to 1.3.5-SNAPSHOT [skip ci] 2025-10-20 18:41:19 +00:00
Darin Kelkhoff cf932e8655 Merge pull request #35 from QRun-IO/feature/table-meta-data-clone
fix(model): More clone fixing (this time with a full table deep equal…
2025-10-20 07:47:18 -05:00
Darin Kelkhoff fec6c55333 fix(model): More clone fixing (this time with a full table deep equals test, so hopefully last time) 2025-10-20 07:12:29 -05:00
CircleCI d9587dbcae Bump version to 1.3.4-SNAPSHOT [skip ci] 2025-10-16 21:03:59 +00:00
Darin Kelkhoff 552546fd9e Merge pull request #34 from QRun-IO/feature/table-meta-data-clone
fix(model): Fix more clone methods, usages of Map this time...
2025-10-16 16:02:32 -05:00
Darin Kelkhoff 750d9e82cd fix(model): Fix more clone methods, usages of Map this time... 2025-10-16 15:59:14 -05:00
CircleCI c838d378f8 Bump version to 1.3.3-SNAPSHOT [skip ci] 2025-10-16 17:08:00 +00:00
Darin Kelkhoff 49c9eaf8a9 Merge pull request #33 from QRun-IO/feature/table-meta-data-clone
fix(model): Fix clone method in QTableMetaData and QHelpContent, whic…
2025-10-16 12:06:48 -05:00
Darin Kelkhoff 359912252b fix(model): Fix clone method in QTableMetaData and QHelpContent, which use Set, which didn't quite follow same pattern as the rest 2025-10-16 12:03:05 -05:00
CircleCI 31ba2d7780 Bump version to 1.3.2-SNAPSHOT [skip ci] 2025-10-16 01:32:22 +00:00
Darin Kelkhoff a2bb2fe577 Merge pull request #32 from QRun-IO/feature/table-meta-data-clone
feat(model): add clone method to QTableMEtaData and all composed classes
2025-10-15 20:30:51 -05:00
Darin Kelkhoff e162bbc127 feat(model): add clone method to QTableMEtaData and all composed classes 2025-10-14 15:00:06 -05:00
CircleCI 6d1d175de9 Bump version to 1.3.1-SNAPSHOT [skip ci] 2025-10-09 19:27:34 +00:00
Darin Kelkhoff 1ea3bc54ec chore: try to be able to deploy a change by bumping version to 1.3.0 2025-10-09 14:26:20 -05:00
CircleCI 739c78f52f Bump version to 1.2.7-SNAPSHOT [skip ci] 2025-10-09 17:44:45 +00:00
Darin Kelkhoff 1b4e9c1bc7 fix(widgets): before doing a wiget-post, when building post-body from query-string params, decode them (caller should have passed them encoded, but for the POST they should not be) 2025-10-09 12:42:39 -05:00
CircleCI 5d76e1544d Bump version to 1.2.6-SNAPSHOT [skip ci] 2025-10-04 00:21:35 +00:00
Darin Kelkhoff 4f2e66ffd1 feat(processes): Update processStepOrInit to set axios timeout to 5 minutes (up from 1) if requested dontGoAsyncOnBackend and setting _qStepTimeoutMillis 2025-10-03 19:20:12 -05:00
CircleCI d4cb33db3b Bump version to 1.2.5-SNAPSHOT [skip ci] 2025-10-02 16:35:00 +00:00
Darin Kelkhoff a3084f10ca Merge pull request #31 from QRun-IO/feature/migrate-widget-to-post
feat(widget): issue requests for widgets as POST (for larger param se…
2025-10-02 11:33:29 -05:00
Darin Kelkhoff b6eec5df5e feat(widget): issue requests for widgets as POST (for larger param sets), with a fallback as a GET upon 404 (e.g., for backward-compatibility while qqq javalin endpoints are updated) 2025-10-02 10:55:27 -05:00
CircleCI 1a32c3a4fd Bump version to 1.2.4-SNAPSHOT [skip ci] 2025-09-30 20:28:50 +00:00
Tim Chamberlain 144f057fa6 added TABLE_EXPORT as a capability 2025-09-30 15:27:29 -05:00
CircleCI 29f748c1b9 Bump version to 1.2.3-SNAPSHOT [skip ci] 2025-09-29 20:42:16 +00:00
james.maes efd60358af Merge branch 'develop' of github.com:Kingsrook/qqq-frontend-core into develop 2025-09-29 15:41:22 -05:00
james.maes a62adc460d ci(circleci): updated to latest production qqq orb - 0.3.8, added workflow for features 2025-09-29 15:41:13 -05:00
CircleCI f9ca2b13c2 Bump version to 1.2.2-SNAPSHOT [skip ci] 2025-09-18 17:40:55 +00:00
james.maes 9bead086d6 chore(deps): Updated qqq-orb to latest published versions 2025-09-18 12:40:03 -05:00
CircleCI afce853921 Bump version to 1.2.1-SNAPSHOT [skip ci] 2025-09-15 14:45:41 +00:00
james.maes 2aba630a78 fix: updated to next snapshot version 2025-09-15 09:44:53 -05:00
CircleCI 1a3c51a90e Bump version to 1.0.133-SNAPSHOT [skip ci] 2025-09-15 14:19:19 +00:00
43 changed files with 2092 additions and 594 deletions

No files matched your search

+67 -10
View File
@@ -1,14 +1,71 @@
---
version: 2.1
setup: true
orbs:
qqq-orb: kingsrook/qqq-orb@0.8
qqq-orb: kingsrook/qqq-orb@0.6
workflows:
setup:
test_only:
jobs:
- qqq-orb/generate_pipeline:
orb_version: '0.8'
filters:
branches:
ignore: automation/qbit-rc-pending
tags:
only: /.*/
- qqq-orb/node_test_only:
context: [qqq-maven-registry-credentials, build-qqq-sample-app]
filters:
branches:
ignore: /(develop|main|release/.*|hotfix/.*|integration.*)/
tags:
only: []
publish_snapshot:
jobs:
- qqq-orb/node_publish:
context: [qqq-maven-registry-credentials, build-qqq-sample-app]
branch_type: snapshot
filters:
branches:
only:
- develop
publish_feature:
jobs:
- qqq-orb/node_publish:
context: [qqq-maven-registry-credentials, build-qqq-sample-app]
branch_type: feature
filters:
branches:
only:
- feature/.*
tags:
only: [/publish.*/]
publish_release_candidate:
jobs:
- qqq-orb/node_publish:
context: [qqq-maven-registry-credentials, build-qqq-sample-app]
branch_type: release_candidate
filters:
branches:
only:
- /release\/.*/
publish_release:
jobs:
- qqq-orb/node_publish:
context: [qqq-maven-registry-credentials, build-qqq-sample-app]
branch_type: release
filters:
branches:
only: [main]
tags:
only: [/v.*/]
publish_hotfix_release:
jobs:
- qqq-orb/node_publish:
context: [qqq-maven-registry-credentials, build-qqq-sample-app]
branch_type: hotfix
filters:
branches:
only:
- /hotfix\/.*/
-37
View File
@@ -1,37 +0,0 @@
name: QRun original Node release
on:
push:
branches: ['**']
create:
workflow_dispatch:
# Thin caller default stays READ.
permissions:
contents: read
actions: read
jobs:
release:
# Tag creation bypasses the forge's [skip ci] push suppression. Push is branch-only, so a tag has one route.
if: ${{ github.event_name != 'create' || (github.event.ref_type == 'tag' && github.event.ref == github.ref && (startsWith(github.ref, 'refs/tags/publish') || startsWith(github.ref, 'refs/tags/v'))) }}
# Ceiling only: the callee's isolated version and cleanup jobs declare their own contents: write; every other callee job is READ.
# No NPM_TOKEN, no OIDC, no registry secret is passed. Not rolled out to any caller.
permissions:
contents: write
actions: read
uses: self:KofTwentyTwo/platform-ci/.gitea/workflows/qrun-node-release.yaml@qrun-release-v1
with:
runner: linux-small
postpublish:
needs: release
if: ${{ success() && needs.release.outputs.computed-version != '' }}
permissions:
contents: write
actions: read
uses: self:KofTwentyTwo/platform-ci/.gitea/workflows/qrun-node-finalize.yaml@qrun-release-v1
with:
runner: linux-small
mode: ${{ needs.release.outputs.mode }}
version: ${{ needs.release.outputs.computed-version }}
dist-tag: ${{ needs.release.outputs.dist-tag }}
built-commit: ${{ needs.release.outputs.built-commit }}
build-job-id: ${{ needs.release.outputs.build-job-id }}
run-attempt: ${{ needs.release.outputs.run-attempt }}
-38
View File
@@ -1,38 +0,0 @@
# GitHub Release Configuration
# Auto-generates release notes from PR labels
changelog:
exclude:
labels:
- ignore-for-release
- duplicate
- invalid
authors:
- dependabot
- dependabot[bot]
categories:
- title: Breaking Changes
labels:
- breaking-change
- breaking
- title: New Features
labels:
- enhancement
- feature
- title: Bug Fixes
labels:
- bug
- fix
- title: Performance Improvements
labels:
- performance
- title: Documentation
labels:
- documentation
- docs
- title: Dependencies
labels:
- dependencies
- title: Other Changes
labels:
- "*"
+6
View File
@@ -1,5 +1,7 @@
*.iml
.env
.yalc
yalc*
node_modules
coverage
@@ -11,3 +13,7 @@ lib
**/.DS_Store
*.iml
# Claude Code local settings
.claude/
-4
View File
@@ -1,4 +0,0 @@
profile: frontend-core
orb_version: '0.8'
default_branch: develop
node_version: '24.21'
+38
View File
@@ -0,0 +1,38 @@
# Session State - qqq-frontend-core
## Last Session
- **Date:** 2026-01-12
- **Branch:** develop
- **Version:** 0.40.2-SNAPSHOT (auto-bumped by CI)
- **Status:** All tasks complete, ready to continue
## Completed This Session
### 1. qqq-frontend-core (npm)
- Fixed stale `lib/` build issue (theme properties were missing)
- Committed `f60d45c`: fix(build): ensure yalc-push rebuilds lib before pushing
- Pipeline #341 succeeded
- Published `@qrunio/qqq-frontend-core@0.40.1-SNAPSHOT` to npm
- Verified 90+ QThemeMetaData properties in published package
### 2. qqq-frontend-material-dashboard (Maven)
- Bumped to `0.40.1-SNAPSHOT` (commit `aadcb599`)
- Fixed BOM dependency: `qqq.version=0.40.0-SNAPSHOT` (commit `cb6331a`)
- Pipeline #1370 succeeded (21 min, includes Playwright tests)
- Published to Maven Central snapshots
### 3. Views Dropdown Investigation
- **Not a bug** - permission-based visibility
- Requires `querySavedView`, `storeSavedView`, `deleteSavedView` process permissions
- Documented in `docs/troubleshooting.md`
## Current State
- All frontend artifacts at 0.40.1-SNAPSHOT and published
- No pending code changes
- Documentation created in `docs/` and `CLAUDE.md`
## To Continue
Say "continue from last session" - Claude will read this file and CLAUDE.md.
## Open Items
None - all tasks complete.
+70
View File
@@ -0,0 +1,70 @@
# QQQ Frontend Core - Project Context
## Overview
TypeScript client library for QQQ frontends. Framework-agnostic (React, Vue, Angular, plain JS).
- **Package:** `@qrunio/qqq-frontend-core`
- **Registry:** npm
- **Current Version:** 0.40.1-SNAPSHOT
## Build System
### Key Scripts
```bash
npm run prepublishOnly # Clean rebuild: rm -rf lib/ && tsc -p ./ --outDir lib/
npm run yalc-push # Local dev: rebuild + yalc push
```
### Build Guarantee
The `prepublishOnly` npm lifecycle hook ensures `lib/` is always rebuilt before publishing. This prevents stale build issues.
## Related Repositories
| Repo | Type | Relationship |
|------|------|--------------|
| `qqq-frontend-material-dashboard` | Maven JAR | React frontend, depends on this library |
| `qqq` | Maven multi-module | Backend framework |
## Version Strategy
Frontend repos can version independently from backend:
- `qqq-frontend-core` and `qqq-frontend-material-dashboard` share version (currently 0.40.1-SNAPSHOT)
- `qqq-frontend-material-dashboard` can use different `qqq.version` for backend deps (currently 0.40.0-SNAPSHOT)
## Key Classes
### QInstance (`src/model/metaData/QInstance.ts`)
Main metaData container. Contains:
- `tables: Map<string, QTableMetaData>`
- `processes: Map<string, QProcessMetaData>`
- `reports`, `widgets`, `apps`, `branding`, `theme`, etc.
### QThemeMetaData (`src/model/metaData/QThemeMetaData.ts`)
90+ theme properties for UI customization. Includes colors, typography, component-specific styling.
## CI/CD
- **Platform:** CircleCI
- **Orb:** `kingsrook/qqq-orb`
- **Workflows:**
- `test_only` - feature branches
- `publish_snapshot` - develop branch
- `publish_release` - main branch + tags
## Common Issues
### Stale lib/ Build
**Symptom:** Published package missing recent changes
**Cause:** `lib/` not rebuilt before publish
**Fix:** `prepublishOnly` hook handles this automatically
### Views Dropdown Missing (Frontend)
**Symptom:** "Views" dropdown not showing on table pages
**Cause:** Permission-based visibility - requires process permissions
**Fix:** Grant permissions for:
- `querySavedView` (required to show dropdown)
- `storeSavedView` (save functionality)
- `deleteSavedView` (delete functionality)
## Session Continuity
Session state is stored in `.session-state.md` (local to this repo).
To continue: say "continue from last session"
+103 -464
View File
@@ -1,516 +1,155 @@
# QQQ Frontend Core
# qqq-frontend-core
[![Version](https://img.shields.io/badge/version-1.0.128-blue.svg)](https://github.com/Kingsrook/qqq-frontend-core)
[![License](https://img.shields.io/badge/license-GNU%20Affero%20GPL%20v3-green.svg)](https://www.gnu.org/licenses/agpl-3.0.en.html)
[![TypeScript](https://img.shields.io/badge/TypeScript-4.4.4-blue.svg)](https://www.typescriptlang.org/)
[![NPM Package](https://img.shields.io/badge/npm-%40qrunio%2Fqqq--frontend--core-red.svg)](https://www.npmjs.com/package/@qrunio/qqq-frontend-core)
TypeScript client library for building frontends on QQQ applications.
> **Core Frontend Library for QQQ - Low-code Application Framework for Engineers**
**For:** Developers building custom UIs or integrating QQQ into existing applications
**Status:** Stable
A TypeScript/JavaScript library that provides the core client-side functionality for interacting with QQQ backend servers. This library is framework-agnostic and can be used in any JavaScript environment, including React, Vue, Angular, or vanilla JavaScript applications.
## Why This Exists
## 🚀 Overview
QQQ applications expose a backend API for metadata, records, and processes. If you're building a custom frontend instead of using the standard dashboard, you need a client that:
QQQ Frontend Core is the **foundational library** that enables frontend applications to communicate with QQQ backend servers. It provides:
- Understands QQQ's metadata-driven architecture
- Handles authentication flows (OAuth2, tokens)
- Provides typed interfaces for records, queries, and processes
- Manages API communication reliably
- **HTTP Client**: Axios-based HTTP client for QQQ API interactions
- **Data Models**: TypeScript interfaces and classes for QQQ data structures
- **Controllers**: High-level abstractions for common QQQ operations
- **Exception Handling**: Standardized error handling for QQQ operations
- **Authentication**: Built-in authentication management and token handling
This library is that client. It's framework-agnostic - use it with React, Vue, Angular, or plain JavaScript.
### What This Repository Contains
If you want a ready-to-use admin UI, use [qqq-frontend-material-dashboard](https://github.com/QRun-IO/qqq-frontend-material-dashboard) instead. It's built on this library.
- **Core Library**: TypeScript/JavaScript library for QQQ client operations
- **Data Models**: Complete type definitions for QQQ metadata and data structures
- **HTTP Controllers**: Controllers for interacting with QQQ backend APIs
- **Exception Classes**: Standardized error handling for QQQ operations
- **Type Definitions**: Full TypeScript support with strict typing
## Features
### What This Repository Does NOT Contain
- **Typed API client** - Full TypeScript support for QQQ operations
- **Metadata loading** - Fetch table, process, and app definitions from QQQ
- **Record CRUD** - Create, read, update, delete with query filters
- **Process execution** - Run backend processes with parameter passing
- **Auth handling** - OAuth2, bearer tokens, and custom auth flows
- **Error normalization** - Consistent exception handling across operations
- **QQQ Server**: The actual low-code engine and backend
- **UI Components**: Visual components or user interface elements
- **Framework Bindings**: React, Vue, or Angular specific implementations
- **Business Logic**: Application-specific business rules or processes
## Quick Start
## 🏗️ Architecture
### Technology Stack
- **Language**: TypeScript with strict typing
- **HTTP Client**: Axios for HTTP requests
- **Build Tool**: TypeScript compiler with ES modules
- **Testing**: Jest with TypeScript support
- **Linting**: ESLint with TypeScript rules
### Core Dependencies
- **axios**: HTTP client for API communication
- **form-data**: Form data handling for file uploads
- **TypeScript**: Type safety and modern JavaScript features
### Development Dependencies
- **jest**: Testing framework
- **eslint**: Code quality and style enforcement
- **typescript**: TypeScript compiler and tools
## 📁 Project Structure
```
src/
├── controllers/ # HTTP controllers for QQQ operations
│ ├── QController.ts # Main controller for QQQ interactions
│ └── QControllerV1.ts # Version 1 controller implementation
├── model/ # Data models and type definitions
│ ├── metaData/ # QQQ metadata models
│ │ ├── QInstance.ts # QQQ instance metadata
│ │ ├── QTableMetaData.ts # Table structure definitions
│ │ ├── QProcessMetaData.ts # Process definitions
│ │ ├── QAppMetaData.ts # Application metadata
│ │ └── ... # Other metadata models
│ ├── query/ # Query and filter models
│ ├── processes/ # Process execution models
│ ├── QRecord.ts # Data record model
│ └── QPossibleValue.ts # Possible value model
├── exceptions/ # Exception handling
│ └── QException.ts # Standard QQQ exception class
└── qqq-frontend-core.ts # Main library entry point
```
## 🎯 Core Functionality
### HTTP Controllers
The library provides high-level controllers for common QQQ operations:
```typescript
import { QController } from '@qrunio/qqq-frontend-core';
const controller = new QController('https://your-qqq-server.com');
// Load metadata
const metadata = await controller.loadMetaData();
// Query records
const records = await controller.queryRecords('tableName', filters);
// Execute processes
const result = await controller.executeProcess('processName', parameters);
```
### Data Models
Complete TypeScript models for all QQQ data structures:
```typescript
import {
QInstance,
QTableMetaData,
QProcessMetaData
} from '@qrunio/qqq-frontend-core';
// Instance metadata
const instance: QInstance = new QInstance(rawData);
// Table metadata
const table: QTableMetaData = new QTableMetaData(tableData);
// Process metadata
const process: QProcessMetaData = new QProcessMetaData(processData);
```
### Exception Handling
Standardized error handling for QQQ operations:
```typescript
import { QException } from '@qrunio/qqq-frontend-core';
try {
const result = await controller.executeProcess('processName', params);
} catch (error) {
if (error instanceof QException) {
console.error(`QQQ Error: ${error.message} (Status: ${error.status})`);
}
}
```
## 🚀 Getting Started
### Installation
**Prerequisites:** Node.js 18+
```bash
npm install @qrunio/qqq-frontend-core
```
### Basic Usage
```typescript
import { QController } from '@qrunio/qqq-frontend-core';
// Create controller instance
const controller = new QController('https://your-qqq-server.com');
const api = new QController('https://your-qqq-server.com');
// Set authentication (if required)
controller.setAuthorizationHeaderValue('Bearer your-token');
// Load QQQ instance metadata
const metadata = await controller.loadMetaData();
// Query records from a table
const records = await controller.queryRecords('users', {
filters: [{ field: 'active', operator: '=', value: true }],
limit: 100
});
// Execute a process
const result = await controller.executeProcess('sendNotification', {
userId: 123,
message: 'Hello World'
});
```
### Advanced Usage
```typescript
// Custom exception handler
const controller = new QController('https://your-qqq-server.com', (error) => {
console.error('Custom error handling:', error);
// Handle error appropriately
});
// Set custom timeout
controller.setStepTimeoutMillis(120000); // 2 minutes
// Handle authentication
controller.setAuthenticationMetaData({
type: 'OAUTH2',
clientId: 'your-client-id',
// ... other auth config
});
```
## 🔧 Configuration
### Environment Setup
```typescript
// Development
const controller = new QController('http://localhost:8080');
// Production
const controller = new QController('https://api.yourdomain.com');
// With custom configuration
const controller = new QController('https://api.yourdomain.com', {
timeout: 30000,
headers: {
'X-Custom-Header': 'value'
}
});
```
### Authentication Configuration
```typescript
// OAuth2
controller.setAuthenticationMetaData({
type: 'OAUTH2',
clientId: 'your-client-id',
clientSecret: 'your-client-secret',
authorizationUrl: 'https://auth.yourdomain.com/oauth2/authorize',
tokenUrl: 'https://auth.yourdomain.com/oauth2/token'
});
// Custom token
controller.setAuthorizationHeaderValue('Bearer your-jwt-token');
```
## 🧪 Testing
### Running Tests
```bash
# Run all tests
npm test
# Run tests in watch mode
npm test -- --watch
# Run tests with coverage
npm test -- --coverage
```
### Test Structure
- **Unit Tests**: Individual component testing
- **Integration Tests**: Controller and model testing
- **Mock Data**: Comprehensive test fixtures
## 📦 Building for Production
### Build Process
```bash
# Clean and build
npm run clean-and-install
npm run prepublishOnly
```
The build process:
- Compiles TypeScript to JavaScript
- Generates ES modules and CommonJS outputs
- Creates optimized production bundles
- Generates type definitions
### Output Files
- **lib/**: Compiled JavaScript files
- **qqq-frontend-core.js**: CommonJS bundle
- **qqq-frontend-core.esm.js**: ES module bundle
- **Type definitions**: Full TypeScript support
## 🔐 Authentication
### Supported Authentication Methods
1. **OAuth2**: Standard OAuth2 flow
2. **Custom Tokens**: JWT or other token-based auth
3. **API Keys**: Simple API key authentication
### Authentication Flow
```typescript
// OAuth2 flow
await controller.authenticateOAuth2({
username: 'user@example.com',
password: 'password'
});
// Custom token
controller.setAuthorizationHeaderValue('Bearer token');
// Check authentication status
const isAuthenticated = controller.isAuthenticated();
```
## 📊 Data Management
### Record Operations
```typescript
// Create record
const newRecord = await controller.createRecord('users', {
name: 'John Doe',
email: 'john@example.com'
});
// Update record
const updatedRecord = await controller.updateRecord('users', 123, {
name: 'John Smith'
});
// Delete record
await controller.deleteRecord('users', 123);
// Load what tables and processes exist
const metadata = await api.loadMetaData();
console.log('Tables:', [...metadata.tables.keys()]);
// Query records
const records = await controller.queryRecords('users', {
filters: [{ field: 'active', operator: '=', value: true }],
sort: [{ field: 'name', direction: 'ASC' }],
limit: 50,
offset: 0
const users = await api.queryRecords('user', {
filters: [{ field: 'isActive', operator: '=', value: true }],
limit: 10
});
```
### Process Execution
## Usage
### Authentication
```typescript
// Execute process
const result = await controller.executeProcess('sendEmail', {
to: 'user@example.com',
subject: 'Welcome',
body: 'Welcome to our platform!'
// Bearer token (simplest)
api.setAuthorizationHeaderValue('Bearer your-jwt-token');
// OAuth2
api.setAuthenticationMetaData({
type: 'OAUTH2',
clientId: 'your-client-id',
tokenUrl: 'https://auth.example.com/token'
});
// Monitor process status
const status = await controller.getProcessStatus('process-id');
// Cancel process
await controller.cancelProcess('process-id');
```
## 🎨 Integration Examples
### React Integration
### Working with Records
```typescript
import React, { useEffect, useState } from 'react';
import { QController, QInstance } from '@qrunio/qqq-frontend-core';
// Create
const order = await api.createRecord('order', {
customerId: 42,
status: 'pending'
});
function QQQApp() {
const [metadata, setMetadata] = useState<QInstance | null>(null);
const [loading, setLoading] = useState(true);
// Read
const record = await api.getRecord('order', order.id);
useEffect(() => {
const controller = new QController('https://your-qqq-server.com');
controller.loadMetaData()
.then(setMetadata)
.finally(() => setLoading(false));
}, []);
// Update
await api.updateRecord('order', order.id, { status: 'shipped' });
if (loading) return <div>Loading...</div>;
if (!metadata) return <div>No metadata available</div>;
// Delete
await api.deleteRecord('order', order.id);
return (
<div>
<h1>QQQ Application</h1>
<p>Tables: {metadata.tables?.size || 0}</p>
<p>Processes: {metadata.processes?.size || 0}</p>
</div>
);
// Query with filters
const pending = await api.queryRecords('order', {
filters: [{ field: 'status', operator: '=', value: 'pending' }],
sort: [{ field: 'createdAt', direction: 'DESC' }],
limit: 50
});
```
### Running Processes
```typescript
const result = await api.executeProcess('generateInvoice', {
orderId: 123,
format: 'pdf'
});
```
### Error Handling
```typescript
import { QException } from '@qrunio/qqq-frontend-core';
try {
await api.executeProcess('riskyOperation', params);
} catch (e) {
if (e instanceof QException) {
console.error(`QQQ error: ${e.message} (${e.status})`);
}
throw e;
}
```
### Vue Integration
## Configuration
```typescript
import { defineComponent, ref, onMounted } from 'vue';
import { QController, QInstance } from '@qrunio/qqq-frontend-core';
// Custom timeout (default: 60s)
api.setStepTimeoutMillis(120000);
export default defineComponent({
setup() {
const metadata = ref<QInstance | null>(null);
const loading = ref(true);
onMounted(async () => {
const controller = new QController('https://your-qqq-server.com');
try {
metadata.value = await controller.loadMetaData();
} finally {
loading.value = false;
}
});
return { metadata, loading };
}
// Global error handler
const api = new QController('https://...', (error) => {
logToMonitoring(error);
});
```
### Vanilla JavaScript
## Project Status
```javascript
import { QController } from '@qrunio/qqq-frontend-core';
**Maturity:** Stable, used in production
**Breaking changes:** Follow semver; major versions may break API
const controller = new QController('https://your-qqq-server.com');
**Roadmap:**
- WebSocket support for real-time updates
- Request batching for bulk operations
- Offline queue for intermittent connectivity
// Load metadata
controller.loadMetaData()
.then(metadata => {
console.log('Tables:', metadata.tables);
console.log('Processes:', metadata.processes);
})
.catch(error => {
console.error('Error loading metadata:', error);
});
## Contributing
```bash
git clone https://github.com/QRun-IO/qqq-frontend-core
cd qqq-frontend-core
npm install
npm test
```
## 🚀 Performance
Submit issues and PRs to the [main QQQ repository](https://github.com/QRun-IO/qqq/issues).
### Optimization Features
- **Memoization**: Cached metadata promises
- **Connection Reuse**: Persistent HTTP connections
- **Batch Operations**: Efficient bulk operations
- **Lazy Loading**: On-demand metadata loading
### Best Practices
- **Reuse Controllers**: Don't create new controllers unnecessarily
- **Cache Metadata**: Use cached metadata when possible
- **Handle Errors**: Implement proper error handling
- **Monitor Performance**: Track API response times
## 🤝 Contributing
**Important**: This repository is a core component of the QQQ framework. All contributions, issues, and discussions should go through the main QQQ repository.
### Development Workflow
1. **Fork the main QQQ repository**: https://github.com/Kingsrook/qqq
2. **Create a feature branch**: `git checkout -b feature/amazing-feature`
3. **Make your changes** (including core library changes if applicable)
4. **Run tests**: `npm test`
5. **Commit your changes**: `git commit -m 'Add amazing feature'`
6. **Push to the branch**: `git push origin feature/amazing-feature`
7. **Open a Pull Request** to the main QQQ repository
### Code Standards
- **TypeScript**: Strict type checking enabled
- **ESLint**: Code quality and style enforcement
- **Testing**: Comprehensive test coverage
- **Documentation**: Clear API documentation
## 📄 License
This project is licensed under the GNU Affero General Public License v3.0 - see the [LICENSE](LICENSE) file for details.
```
QQQ Frontend Core
Copyright (C) 2021-2022 Kingsrook, LLC
651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
contact@kingsrook.com | https://github.com/Kingsrook/
```
**Note**: This is a core component of the QQQ framework. For the complete license and more information, see the main QQQ repository: https://github.com/Kingsrook/qqq
## 🆘 Support & Community
### ⚠️ Important: Use Main QQQ Repository
**All support, issues, discussions, and community interactions should go through the main QQQ repository:**
- **Main Repository**: https://github.com/Kingsrook/qqq
- **Issues**: https://github.com/Kingsrook/qqq/issues
- **Discussions**: https://github.com/Kingsrook/qqq/discussions
- **Wiki**: https://github.com/Kingsrook/qqq.wiki
### Why This Repository Exists
This repository is maintained separately from the main QQQ repository to:
- **Enable independent library development** and versioning
- **Allow library-specific CI/CD** and deployment pipelines
- **Provide clear separation** between core library and framework concerns
- **Support different release cycles** for library vs. framework updates
### Getting Help
- **Documentation**: Check the [QQQ Wiki](https://github.com/Kingsrook/qqq.wiki)
- **Issues**: Report bugs and feature requests on [Main QQQ Issues](https://github.com/Kingsrook/qqq/issues)
- **Discussions**: Join community discussions on [Main QQQ Discussions](https://github.com/Kingsrook/qqq/discussions)
- **Questions**: Ask questions in the main QQQ repository
### Contact Information
- **Company**: Kingsrook, LLC
- **Email**: contact@kingsrook.com
- **Website**: https://kingsrook.com
- **Main GitHub**: https://github.com/Kingsrook/qqq
## 🙏 Acknowledgments
- **TypeScript Team**: For the powerful type system
- **Axios Team**: For the excellent HTTP client library
- **QQQ Framework Team**: For the underlying low-code platform
- **Open Source Community**: For the tools and libraries that make this possible
---
**Built with ❤️ by the Kingsrook Team**
**This is a core library component of the QQQ framework. For complete information, support, and community, visit: https://github.com/Kingsrook/qqq**
## License
AGPL-3.0
+55
View File
@@ -0,0 +1,55 @@
# Troubleshooting
## Views Dropdown Missing on Table Pages
### Symptom
The "Views" dropdown (for saving/loading filter presets) is not visible on table pages.
### Root Cause
The SavedViews component visibility is **permission-based**. It checks:
```typescript
const hasQueryPermission = metaData?.processes.has("querySavedView");
// Only renders if hasQueryPermission && tableMetaData
```
### Solution
Grant users permission to these processes:
| Process | Purpose |
|---------|---------|
| `querySavedView` | **Required** - show dropdown, load views |
| `storeSavedView` | Save/update views |
| `deleteSavedView` | Delete views |
In your QQQ app's permission configuration:
```java
.withProcessPermission("querySavedView", true)
.withProcessPermission("storeSavedView", true)
.withProcessPermission("deleteSavedView", true)
```
### Related Files
- Frontend: `SavedViews.tsx` in qqq-frontend-material-dashboard
- Backend: `SavedViewsMetaDataProvider.java` in qqq-backend-core
---
## Stale lib/ Build
### Symptom
Published npm package is missing recent TypeScript changes.
### Root Cause
The `lib/` directory wasn't rebuilt before publishing.
### Solution
The `prepublishOnly` hook in `package.json` now handles this:
```json
"prepublishOnly": "rm -rf lib/ && tsc -p ./ --outDir lib/"
```
For local development with yalc:
```bash
npm run yalc-push # Runs prepublishOnly first
```
+42
View File
@@ -0,0 +1,42 @@
# Version Management
## Current Versions (as of 2026-01-12)
| Package | Version | Registry |
|---------|---------|----------|
| `@qrunio/qqq-frontend-core` | 0.40.1-SNAPSHOT | npm |
| `qqq-frontend-material-dashboard` | 0.40.1-SNAPSHOT | Maven Central |
| `qqq` (backend) | 0.40.0-SNAPSHOT | Maven Central |
## Frontend vs Backend Versioning
Frontend repos can version independently from the backend. In `qqq-frontend-material-dashboard`:
```xml
<properties>
<revision>0.40.1-SNAPSHOT</revision> <!-- This artifact's version -->
<qqq.version>0.40.0-SNAPSHOT</qqq.version> <!-- Backend dependency version -->
</properties>
```
This allows frontend-only releases without requiring a full backend release.
## Version Bump Process
### qqq-frontend-core (npm)
1. Update `version` in `package.json`
2. Push to develop branch
3. CI automatically publishes to npm
### qqq-frontend-material-dashboard (Maven)
1. Update `revision` property in `pom.xml`
2. Optionally update `qqq.version` if backend changes needed
3. Push to develop branch
4. CI runs Playwright tests + publishes to Maven Central
## CI/CD Orb Behavior
The `kingsrook/qqq-orb` `calculate_version.sh` script:
- For develop branch with existing SNAPSHOT: keeps current version
- Only bumps version on release merges or RC versions
- Each repo manages its own version independently
+4 -4
View File
@@ -1,12 +1,12 @@
{
"name": "@kingsrook/qqq-frontend-core",
"version": "1.0.127-SNAPSHOT",
"name": "@qrunio/qqq-frontend-core",
"version": "0.40.6-SNAPSHOT",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@kingsrook/qqq-frontend-core",
"version": "1.0.127-SNAPSHOT",
"name": "@qrunio/qqq-frontend-core",
"version": "0.40.6-SNAPSHOT",
"license": "ISC",
"dependencies": {
"axios": "0.28.1",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@qrunio/qqq-frontend-core",
"version": "1.1.0",
"version": "0.40.11-SNAPSHOT",
"description": "",
"main": "qqq-frontend-core.js",
"module": "lib/qqq-frontend-core.esm.js",
@@ -11,7 +11,7 @@
"clean-and-install": "rm -rf node_modules/ && rm -rf package-lock.json && rm -rf lib/ && npm install",
"prepublishOnly": "rm -rf lib/ && tsc -p ./ --outDir lib/",
"test": "jest",
"yalc-push": "yalc push --private",
"yalc-push": "npm run prepublishOnly && yalc push --private",
"yalc-watch": "yalc-watch"
},
"dependencies": {
+101 -12
View File
@@ -19,7 +19,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {AxiosError, AxiosResponse} from "axios";
import {AxiosError, AxiosRequestConfig, AxiosResponse} from "axios";
import FormData from "form-data";
import {QException} from "../exceptions/QException";
import {QAuthenticationMetaData} from "../model/metaData/QAuthenticationMetaData";
@@ -783,13 +783,15 @@ export class QController
}
}
const axiosConfigExtras: AxiosRequestConfig = {};
if (dontGoAsyncOnBackend)
{
formDataOrQueryString.append("_qStepTimeoutMillis", 300 * 1000);
axiosConfigExtras.timeout = 300 * 1000;
}
return this.axiosInstance
.post(url, formDataOrQueryString, {headers: formDataHeaders})
.post(url, formDataOrQueryString, {headers: formDataHeaders, ...axiosConfigExtras})
.then((response: AxiosResponse) =>
{
const responseObject = this.parseProcessResponse(response);
@@ -899,7 +901,7 @@ export class QController
/*******************************************************************************
** Fetch the data for a specific widget.
** Fetch the data about variants for a table
*******************************************************************************/
async tableVariants(tableName: string): Promise<QTableVariant[]>
{
@@ -933,24 +935,48 @@ export class QController
/*******************************************************************************
** Fetch the data for a specific widget.
* Fetch the data for a specific widget.
*
* Originally, this was a GET request, with params passed on the query string.
* However, that fails if params get too long. So, in 10/2025, qqq-javalin
* is being updated to serve /widget/{widgetName} as a POST.
*
* This method, for some attempted backward compatibility, will first *try* a
* POST. If it fails with a 404 (which an older version (w/o the POST endpoint)
* would return), then, assuming the params are a string (not FormData), then
* this method will automatically re-try the request as a GET.
*******************************************************************************/
async widget(widgetName: string, urlParams?: string): Promise<any>
async widget(widgetName: string, params?: string | FormData, overrideAbortName?: string): Promise<any>
{
let url = `/widget/${encodeURIComponent(widgetName)}`;
if (urlParams)
const abortName = overrideAbortName ?? widgetName;
const paramsAreFormData = params instanceof FormData
let postBody: FormData;
if (paramsAreFormData)
{
url += `?${urlParams}`;
postBody = params;
}
else
{
postBody = new FormData();
if (params)
{
for (let pair of params.split("&"))
{
let [name, value] = pair.split("=");
postBody.append(name, decodeURIComponent(value));
}
}
}
/////////////////////////////////////////////////////////////////
// see if an abort controller was created for this widget name //
/////////////////////////////////////////////////////////////////
console.log("Looking for controller for widget [" + widgetName + "]");
let controller = QController.widgetAbortControllerMap.get(widgetName);
let controller = QController.widgetAbortControllerMap.get(abortName);
if (controller)
{
console.log(`Found existing abort controller for widget '${widgetName}', aborting...`);
console.log(`Found existing abort controller for widget '${abortName}', aborting...`);
controller.abort();
}
@@ -959,7 +985,69 @@ export class QController
/////////////////////////////////////////
controller = new AbortController();
const signal = controller.signal;
QController.widgetAbortControllerMap.set(widgetName, controller);
QController.widgetAbortControllerMap.set(abortName, controller);
return this.axiosInstance
.post(url, postBody, {signal})
.then((response: AxiosResponse) =>
{
///////////////////////////////////////////////////
// make sure to clear out the request controller //
///////////////////////////////////////////////////
QController.widgetAbortControllerMap.set(abortName, null);
return response.data;
})
.catch(async (e: any) =>
{
///////////////////////////////////////////////////////////////////////////////////////////////////////
// if the server responded with 404, assume this could be due to the backend not supporting POST for //
// the widget endpoint - so if we took in params as a string (not FormData), then try to do a GET //
///////////////////////////////////////////////////////////////////////////////////////////////////////
if(e?.response?.status == 404)
{
if(!paramsAreFormData)
{
return await this.getWidget(widgetName, params, overrideAbortName);
}
else
{
console.log("widget POST request failed with 404 - but params are FormData, so we cannot attempt a fallback-GET...");
}
}
if (e.code && e.code === "ERR_CANCELED")
{
console.log("Controller request cancellation successful!");
return;
}
this.handleException(e);
});
}
/*******************************************************************************
* Fetch the data for a specific widget, doing a GET.
* This HTTP method is being deprecated for rendering widgets, due to query
* string being too limited in size, compared to the relative unlimited size of
* POST body - but this method is here for a backward-compatibility later - in
* case hitting a backend server that 404'ed for the POST call.
*******************************************************************************/
private async getWidget(widgetName: string, urlParams?: string, overrideAbortName?: string): Promise<any>
{
const abortName = overrideAbortName ?? widgetName;
let url = `/widget/${encodeURIComponent(widgetName)}`;
if(urlParams)
{
url += `?${urlParams}`;
}
/////////////////////////////////////////
// keep track of this widget's request //
/////////////////////////////////////////
const controller = new AbortController();
const signal = controller.signal;
QController.widgetAbortControllerMap.set(abortName, controller);
return this.axiosInstance
.get(url, {signal})
@@ -968,7 +1056,7 @@ export class QController
///////////////////////////////////////////////////
// make sure to clear out the request controller //
///////////////////////////////////////////////////
QController.widgetAbortControllerMap.set(widgetName, null);
QController.widgetAbortControllerMap.set(abortName, null);
return response.data;
})
.catch((e: any) =>
@@ -983,6 +1071,7 @@ export class QController
}
/***************************************************************************
* overload 1 of possibleValues function - this is its "original" signature
* that took several params, many optional, and kept evolving and getting
+11
View File
@@ -34,5 +34,16 @@ export class QPossibleValue
this.id = object.id;
this.label = object.label;
}
/***************************************************************************
*
***************************************************************************/
public clone(): QPossibleValue
{
const clone = new QPossibleValue({
...this
});
return (clone);
}
}
+1
View File
@@ -35,6 +35,7 @@ export enum AdornmentType
FILE_DOWNLOAD = "FILE_DOWNLOAD",
FILE_UPLOAD = "FILE_UPLOAD",
TOOLTIP = "TOOLTIP",
WIDGET = "WIDGET",
///////////////////////////////////////////////////////////////////////////
// keep these values in sync with AdornmentType.java in qqq-backend-core //
///////////////////////////////////////////////////////////////////////////
+12 -10
View File
@@ -23,14 +23,16 @@
** Things that can be done to tables, fields.
**
*******************************************************************************/
export enum Capability {
TABLE_QUERY = "TABLE_QUERY",
TABLE_GET = "TABLE_GET",
TABLE_COUNT = "TABLE_COUNT",
TABLE_INSERT = "TABLE_INSERT",
TABLE_UPDATE = "TABLE_UPDATE",
TABLE_DELETE = "TABLE_DELETE"
////////////////////////////////////////////////////////////////////////
// keep these values in sync with Capability.java in qqq-backend-core //
////////////////////////////////////////////////////////////////////////
export enum Capability
{
TABLE_QUERY = "TABLE_QUERY",
TABLE_GET = "TABLE_GET",
TABLE_COUNT = "TABLE_COUNT",
TABLE_INSERT = "TABLE_INSERT",
TABLE_UPDATE = "TABLE_UPDATE",
TABLE_DELETE = "TABLE_DELETE",
TABLE_EXPORT = "TABLE_EXPORT"
////////////////////////////////////////////////////////////////////////
// keep these values in sync with Capability.java in qqq-backend-core //
////////////////////////////////////////////////////////////////////////
}
+51
View File
@@ -0,0 +1,51 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. 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/>.
*/
/*******************************************************************************
** Meta-Data to define Collapsible state, e.g., for a section or widget
**
*******************************************************************************/
export class Collapsible
{
isCollapsible: boolean;
initiallyOpen: boolean;
constructor(object: any)
{
////////////////////////////////////////////////////////////////////////////
// use value === true to let undefined (or any other) values become false //
////////////////////////////////////////////////////////////////////////////
this.isCollapsible = object.isCollapsible === true;
this.initiallyOpen = object.initiallyOpen === true;
}
/***************************************************************************
*
***************************************************************************/
public clone(): Collapsible
{
const clone = new Collapsible({
...this
});
return (clone);
}
}
+19
View File
@@ -53,4 +53,23 @@ export class FieldAdornment
}
return (null);
}
/***************************************************************************
*
***************************************************************************/
public clone(): FieldAdornment
{
const valuesClone = this.values ? new Map<string, any>() : undefined;
if(this.values && valuesClone)
{
this.values.forEach((value, key) =>
{
valuesClone.set(key, value);
});
}
const clone = new FieldAdornment({type: this.type});
clone.values = valuesClone;
return (clone);
}
}
+8 -2
View File
@@ -21,6 +21,7 @@
import {QAppSection} from "./QAppSection";
import {QAppTreeNode} from "./QAppTreeNode";
import {QHelpContent} from "./QHelpContent";
/*******************************************************************************
** Meta-Data to define an app in a QQQ instance.
@@ -36,6 +37,7 @@ export class QAppMetaData
widgets?: string[];
sections?: QAppSection[];
supplementalAppMetaData: Map<String, any> = new Map();
helpContent?: Map<string, QHelpContent[]>;
constructor(object: any)
{
@@ -64,13 +66,17 @@ export class QAppMetaData
}
}
if(object.supplementalAppMetaData)
if (object.supplementalAppMetaData)
{
for (const key in object.supplementalAppMetaData)
{
this.supplementalAppMetaData.set(key, object.supplementalAppMetaData[key]);
}
}
}
if (object.helpContents)
{
this.helpContent = QHelpContent.buildMap(object.helpContents);
}
}
}
+5
View File
@@ -31,6 +31,7 @@ export class QAppSection
label: string;
icon?: QIcon;
tables?: string[];
apps?: string[];
processes?: string[];
reports?: string[];
@@ -54,5 +55,9 @@ export class QAppSection
{
this.tables = object.tables;
}
if (object.apps)
{
this.apps = object.apps;
}
}
}
+4
View File
@@ -33,8 +33,10 @@ export class QAppTreeNode
label: string;
type: QAppNodeType;
children?: QAppTreeNode[];
hideChildrenFromNavigation?: boolean;
iconName?: string;
icon?: QIcon;
appAffinity?: number;
constructor(object: any)
{
@@ -42,6 +44,8 @@ export class QAppTreeNode
this.label = object.label;
this.type = object.type;
this.iconName = object.iconName;
this.appAffinity = object.appAffinity;
this.hideChildrenFromNavigation = object.hideChildrenFromNavigation;
if (object.icon)
{
+4
View File
@@ -33,6 +33,8 @@ export class QBrandingMetaData
logo?: string;
icon?: string;
accentColor?: string;
accentColorLight?: string;
gravatarDefault?: string;
// Deprecated
environmentBannerText?: string;
@@ -50,6 +52,8 @@ export class QBrandingMetaData
this.logo = object.logo;
this.icon = object.icon;
this.accentColor = object.accentColor;
this.accentColorLight = object.accentColorLight;
this.gravatarDefault = object.gravatarDefault;
this.environmentBannerText = object.environmentBannerText;
this.environmentBannerColor = object.environmentBannerColor;
+28 -4
View File
@@ -36,13 +36,37 @@ export class QExposedJoin
constructor(object: any)
{
this.label = object.label;
this.isMany = object.isMany;
this.joinTable = new QTableMetaData(object.joinTable);
this.label = object?.label;
this.isMany = object?.isMany;
this.joinTable = new QTableMetaData(object?.joinTable);
this.joinPath = [];
for (let i = 0; i < object.joinPath.length; i++)
for (let i = 0; i < object?.joinPath?.length; i++)
{
this.joinPath.push(new QJoinMetaData(object.joinPath[i]));
}
}
/***************************************************************************
*
***************************************************************************/
public clone(): QExposedJoin
{
const joinTableClone = this.joinTable?.clone();
const joinPathClone: QJoinMetaData[] = [];
for (let joinMetaData of this.joinPath ?? [])
{
joinPathClone.push(joinMetaData.clone());
}
const clone = new QExposedJoin({
...this,
joinTable: joinTableClone,
joinPath: joinPathClone
});
// overwrite joinTable — the constructor re-wraps it in new QTableMetaData(),
// which loses Map-based fields/virtualFields from the already-cloned object
clone.joinTable = joinTableClone;
return (clone);
}
}
+65 -3
View File
@@ -91,11 +91,12 @@ export class QFieldMetaData
this.helpContents = QHelpContent.buildArray(object.helpContents);
this.behaviors = object.behaviors;
if (object.supplementalFieldMetaData)
const supplementalFieldMetaData = object.supplementalFieldMetaData || object.supplementalMetaData;
if (supplementalFieldMetaData)
{
for (const key in object.supplementalFieldMetaData)
for (const key in supplementalFieldMetaData)
{
this.supplementalFieldMetaData.set(key, object.supplementalFieldMetaData[key]);
this.supplementalFieldMetaData.set(key, supplementalFieldMetaData[key]);
}
}
@@ -135,4 +136,65 @@ export class QFieldMetaData
return (null);
}
/***************************************************************************
*
***************************************************************************/
public clone(): QFieldMetaData
{
const inlinePossibleValueSourceClone: QPossibleValue[] | undefined = this.inlinePossibleValueSource ? [] : undefined;
if(this.inlinePossibleValueSource && inlinePossibleValueSourceClone)
{
for (let qPossibleValue of this.inlinePossibleValueSource)
{
inlinePossibleValueSourceClone.push((qPossibleValue as any).clone());
}
}
const adornmentsClone: FieldAdornment[] | undefined = this.adornments ? [] : undefined;
if(this.adornments && adornmentsClone)
{
for (let adornment of this.adornments)
{
adornmentsClone.push((adornment as any).clone());
}
}
const helpContentsClone: QHelpContent[] | undefined = this.helpContents ? [] : undefined;
if(this.helpContents && helpContentsClone)
{
for (let helpContent of this.helpContents)
{
helpContentsClone.push((helpContent as any).clone());
}
}
const behaviorsClone: any[] | undefined = this.behaviors ? [...this.behaviors] : undefined;
const supplementalFieldMetaDataClone: Map<String, any> = new Map();
this.supplementalFieldMetaData.forEach((value, key) =>
{
supplementalFieldMetaDataClone.set(key, value);
});
const possibleValueSourceFilterClone = this.possibleValueSourceFilter ? (this.possibleValueSourceFilter as any).clone() : undefined;
const clone = new QFieldMetaData({
...this,
inlinePossibleValueSource: inlinePossibleValueSourceClone,
behaviors: behaviorsClone,
possibleValueSourceFilter: possibleValueSourceFilterClone
});
if(adornmentsClone)
{
clone.adornments = adornmentsClone;
}
clone.helpContents = helpContentsClone;
clone.supplementalFieldMetaData = supplementalFieldMetaDataClone;
return (clone);
}
}
+22
View File
@@ -105,4 +105,26 @@ export class QHelpContent
return (rs);
}
/***************************************************************************
*
***************************************************************************/
public clone(): QHelpContent
{
const rolesClone = new Set<string>();
this.roles.forEach((role) =>
{
rolesClone.add(role);
});
const clone = new QHelpContent({...this});
////////////////////////////////////////////////////////////////////////////////
// sets don't quite work as expected for this kind of clone, so copy manually //
////////////////////////////////////////////////////////////////////////////////
clone.roles = rolesClone;
return (clone);
}
}
+11
View File
@@ -35,4 +35,15 @@ export class QIcon
this.path = object.path;
this.color = object.color;
}
/***************************************************************************
*
***************************************************************************/
public clone(): QIcon
{
const clone = new QIcon({
...this
});
return (clone);
}
}
+49 -2
View File
@@ -45,6 +45,8 @@ export class QInstance
environmentValues?: Map<string, string>;
helpContent?: Map<string, QHelpContent[]>;
supplementalInstanceMetaData: Map<String, any> = new Map();
redirects: Map<string, string> = new Map();
private pathsMap: Record<string, {path: string, affinity: number}[]> = {};
constructor(object: any)
{
@@ -125,6 +127,44 @@ export class QInstance
}
this.helpContent = QHelpContent.buildMap(object.helpContents);
if (object.redirects)
{
for (const key in object.redirects)
{
this.redirects.set(key, object.redirects[key]);
}
}
this.buildPathsMap(this.appTree || []);
}
/***************************************************************************
* populate the private/internal "pathsMap" - with lists of all available
* paths to tables, processes, etc (where a table in more than 1 app will
* have more than 1 entry). This is used to help determine which app to
* open when a user clicks on a table name. To select a path, they should
* be sorted by affinity, so that the most preferred app is first.
***************************************************************************/
buildPathsMap(nodes: QAppTreeNode[], path: string = ""): void
{
for (let i = 0; i < nodes.length; i++)
{
const node = nodes[i];
if(node.type === QAppNodeType.APP && node.children)
{
this.buildPathsMap(node.children, path + "/" + node.name);
}
else
{
if(!this.pathsMap[node.name])
{
this.pathsMap[node.name] = [];
}
this.pathsMap[node.name].push({path: path + "/" + node.name, affinity: node.appAffinity ?? Number.MIN_SAFE_INTEGER});
}
}
}
@@ -142,7 +182,7 @@ export class QInstance
*******************************************************************************/
getTablePath(table: QTableMetaData): string | null
{
return QInstance.searchAppTreeForPath(this.appTree, table.name, QAppNodeType.TABLE, 1, "");
return this.getTablePathByName(table.name);
}
/*******************************************************************************
@@ -150,7 +190,14 @@ export class QInstance
*******************************************************************************/
getTablePathByName(tableName: string): string | null
{
return QInstance.searchAppTreeForPath(this.appTree, tableName, QAppNodeType.TABLE, 1, "");
const value = this.pathsMap[tableName];
if(value)
{
value.sort((a, b) => b.affinity - a.affinity);
return (value[0].path);
}
return (null);
}
/*******************************************************************************
+11
View File
@@ -37,4 +37,15 @@ export class QJoinMetaData
this.leftTable = object.leftTable;
this.rightTable = object.rightTable;
}
/***************************************************************************
*
***************************************************************************/
public clone(): QJoinMetaData
{
const clone = new QJoinMetaData({
...this
});
return (clone);
}
}
+69
View File
@@ -0,0 +1,69 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. 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/>.
*/
import {QIcon} from "./QIcon";
import {QMenuItem} from "./QMenuItem";
/*******************************************************************************
* Meta-Data that defines a Menu in a QQQ Instance
*
*******************************************************************************/
export class QMenu
{
label: string;
icon?: QIcon;
slot: string;
items?: QMenuItem[];
constructor(object: any)
{
this.label = object.label;
if (object.icon)
{
this.icon = new QIcon(object.icon);
}
this.slot = object.slot;
if (object.items)
{
this.items = object.items.map((item: any) => new QMenuItem(item));
}
}
/***************************************************************************
*
***************************************************************************/
public clone(): QMenu
{
const clone = new QMenu({
label: this.label,
slot: this.slot
});
clone.icon = this.icon?.clone();
if (this.items)
{
clone.items = this.items.map((item: QMenuItem) => item.clone());
}
return (clone);
}
}
+75
View File
@@ -0,0 +1,75 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. 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/>.
*/
import {QIcon} from "./QIcon";
/*******************************************************************************
* Meta-Data that defines a MenuItem in a QQQ Instance
*
*******************************************************************************/
export class QMenuItem
{
label: string;
icon?: QIcon;
itemType: string;
values?: Map<string, any>;
constructor(object: any)
{
this.label = object.label;
if (object.icon)
{
this.icon = new QIcon(object.icon);
}
this.itemType = object.itemType;
if (object.values)
{
this.values = new Map<string, any>();
for (let key in object.values)
{
this.values?.set(key, object.values[key]);
}
}
}
/***************************************************************************
*
***************************************************************************/
public clone(): QMenuItem
{
const clone = new QMenuItem({
label: this.label,
itemType: this.itemType
});
clone.icon = this.icon?.clone();
if (this.values)
{
clone.values = new Map<string, any>();
this.values.forEach((value: any, key: string) => clone.values?.set(key, value));
}
return (clone);
}
}
+122 -1
View File
@@ -22,7 +22,9 @@
import {QExposedJoin} from "./QExposedJoin";
import {QFieldMetaData} from "./QFieldMetaData";
import {QHelpContent} from "./QHelpContent";
import {QMenu} from "./QMenu";
import {QTableSection} from "./QTableSection";
import {QVirtualFieldMetaData} from "./QVirtualFieldMetaData";
/*******************************************************************************
** Meta-Data to define a table in a QQQ instance.
@@ -35,6 +37,7 @@ export class QTableMetaData
isHidden: boolean = false;
primaryKeyField: string;
fields?: Map<string, QFieldMetaData>;
virtualFields?: Map<string, QVirtualFieldMetaData>;
iconName?: string;
sections?: QTableSection[];
exposedJoins?: QExposedJoin[];
@@ -48,6 +51,7 @@ export class QTableMetaData
supplementalTableMetaData: Map<String, any> = new Map();
shareableTableMetaData: any;
helpContent?: Map<string, QHelpContent[]>;
menus?: QMenu[];
constructor(object: any)
{
@@ -72,6 +76,15 @@ export class QTableMetaData
}
}
if (object.virtualFields)
{
this.virtualFields = new Map<string, QVirtualFieldMetaData>();
for (const key in object.virtualFields)
{
this.virtualFields.set(key, new QVirtualFieldMetaData(object.virtualFields[key]));
}
}
if (object.sections)
{
this.sections = [];
@@ -93,7 +106,7 @@ export class QTableMetaData
this.capabilities = new Set<string>();
if (object.capabilities)
{
for (var i = 0; i < object.capabilities.length; i++)
for (let i = 0; i < object.capabilities.length; i++)
{
this.capabilities.add(object.capabilities[i]);
}
@@ -110,5 +123,113 @@ export class QTableMetaData
this.shareableTableMetaData = object.shareableTableMetaData;
this.helpContent = QHelpContent.buildMap(object.helpContents);
if(object.menus)
{
this.menus = [];
for(let i = 0; i < object.menus.length; i++)
{
this.menus.push(new QMenu(object.menus[i]));
}
}
}
/***************************************************************************
*
***************************************************************************/
public clone(): QTableMetaData
{
const fieldsClone = this.fields ? new Map() : undefined;
if (this.fields && fieldsClone)
{
this.fields.forEach((field, name) =>
{
fieldsClone.set(name, field.clone());
});
}
const virtualFieldsClone = this.virtualFields ? new Map() : undefined;
if (this.virtualFields && virtualFieldsClone)
{
this.virtualFields.forEach((field, name) =>
{
virtualFieldsClone.set(name, field.clone());
});
}
const sectionsClone: QTableSection[] | undefined = this.sections ? [] : undefined;
if (this.sections && sectionsClone)
{
for (let section of this.sections)
{
sectionsClone.push((section as any).clone());
}
}
const exposedJoinsClone: QExposedJoin[] | undefined = this.exposedJoins ? [] : undefined;
if (this.exposedJoins && exposedJoinsClone)
{
for (let exposedJoin of this.exposedJoins)
{
exposedJoinsClone.push((exposedJoin as any).clone());
}
}
const capabilitiesClone = new Set<string>();
if (this.capabilities && capabilitiesClone)
{
this.capabilities.forEach((capability) =>
{
capabilitiesClone.add(capability);
});
}
const helpContentsClone = this.helpContent ? new Map() : undefined;
if (this.helpContent && helpContentsClone)
{
this.helpContent.forEach((helpContentArray, name) =>
{
if (helpContentArray)
{
const helpContentArrayClone: QHelpContent[] = [];
for (let qHelpContent of helpContentArray)
{
helpContentArrayClone.push((qHelpContent as any).clone());
}
helpContentsClone.set(name, helpContentArrayClone);
}
});
}
const supplementalTableMetaDataClone = new Map<String, any>();
this.supplementalTableMetaData.forEach((value, name) =>
{
supplementalTableMetaDataClone.set(name, Object.assign({}, value));
});
const menusClone: QMenu[] | undefined = this.menus ? [] : undefined;
if (this.menus && menusClone)
{
for (let i = 0; i < this.menus.length; i++)
{
menusClone.push(this.menus[i].clone());
}
}
const clone = new QTableMetaData({...this});
clone.fields = fieldsClone;
clone.virtualFields = virtualFieldsClone;
clone.sections = sectionsClone;
clone.exposedJoins = exposedJoinsClone;
clone.capabilities = capabilitiesClone;
clone.supplementalTableMetaData = supplementalTableMetaDataClone;
clone.helpContent = helpContentsClone;
clone.menus = menusClone;
return (clone);
}
}
+57 -1
View File
@@ -19,6 +19,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Collapsible} from "./Collapsible";
import {QHelpContent} from "./QHelpContent";
/*******************************************************************************
@@ -36,6 +37,8 @@ export class QTableSection
isHidden: boolean;
gridColumns?: number;
helpContents?: QHelpContent[];
alternatives?: Map<string, QTableSection>;
collapsible?: Collapsible;
/*******************************************************************************
@@ -46,7 +49,7 @@ export class QTableSection
this.name = object.name;
this.label = object.label;
this.tier = object.tier;
this.iconName = object.icon ? object.icon.name : null;
this.iconName = object.icon ? object.icon.name : object.iconName;
if (object.fieldNames)
{
@@ -58,6 +61,59 @@ export class QTableSection
this.gridColumns = object.gridColumns;
this.helpContents = QHelpContent.buildArray(object.helpContents)
if (object.alternatives)
{
this.alternatives = new Map<string, QTableSection>();
for (let type in object.alternatives)
{
this.alternatives.set(type, new QTableSection(object.alternatives[type]));
}
}
if (object.collapsible)
{
this.collapsible = new Collapsible(object.collapsible);
}
}
/***************************************************************************
*
***************************************************************************/
public clone(): QTableSection
{
let fieldNamesClone: string[] | undefined = undefined;
if(this.fieldNames)
{
fieldNamesClone = [...this.fieldNames];
}
const helpContentsClone: QHelpContent[] = (this.helpContents ? [] : undefined) as QHelpContent[];
if(this.helpContents && helpContentsClone)
{
for (let helpContent of this.helpContents)
{
helpContentsClone.push((helpContent as any).clone());
}
}
const alternativesClone: Map<string, QTableSection> | undefined = (this.alternatives ? new Map<string, QTableSection> : undefined)
if(this.alternatives && alternativesClone)
{
this.alternatives.forEach((value: QTableSection, key: string) =>
alternativesClone.set(key, value.clone()));
}
const collapsibleClone: Collapsible | undefined = (this.collapsible ? this.collapsible.clone() : undefined);
const clone = new QTableSection({...this});
clone.fieldNames = fieldNamesClone;
clone.helpContents = helpContentsClone;
clone.alternatives = alternativesClone;
clone.collapsible = collapsibleClone;
return (clone);
}
}
@@ -0,0 +1,64 @@
/*
* 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/>.
*/
import {QFieldMetaData} from "./QFieldMetaData";
/***************************************************************************
* Meta-data to represent a virtual field in a table.
*
* This is a subclass of QFieldMetaData, as it is on the QQQ backend.
***************************************************************************/
export class QVirtualFieldMetaData extends QFieldMetaData
{
isQueryCriteria: boolean = false;
isQuerySelectable: boolean = false;
/***************************************************************************
*
***************************************************************************/
constructor(object: any)
{
super(object);
this.isQueryCriteria = object.isQueryCriteria;
this.isQuerySelectable = object.isQuerySelectable;
}
/***************************************************************************
*
***************************************************************************/
public clone(): QVirtualFieldMetaData
{
const clone = super.clone() as QVirtualFieldMetaData;
///////////////////////////////////////////////////////////////////////////////
// any time we do a super clone, we want to set the prototype to our subtype //
///////////////////////////////////////////////////////////////////////////////
Object.setPrototypeOf(clone, QVirtualFieldMetaData.prototype)
clone.isQueryCriteria = this.isQueryCriteria;
clone.isQuerySelectable = this.isQuerySelectable;
return (clone);
}
}
+6
View File
@@ -19,6 +19,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Collapsible} from "./Collapsible";
import {QHelpContent} from "./QHelpContent";
import {QIcon} from "./QIcon";
@@ -51,6 +52,7 @@ export class QWidgetMetaData
helpContent?: Map<string, QHelpContent[]>;
defaultValues?: Map<string, any>;
collapsible?: Collapsible;
constructor(object: any)
{
@@ -89,5 +91,9 @@ export class QWidgetMetaData
}
}
if (object.collapsible)
{
this.collapsible = new Collapsible(object.collapsible);
}
}
}
+54
View File
@@ -0,0 +1,54 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. 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/>.
*/
/*******************************************************************************
** Define a field function being used in a QQQ application
**
*******************************************************************************/
export class FieldFunction
{
fieldName: string;
functionTypeIdentifierName: string;
arguments: Record<string, any>
constructor(fieldName: string, functionTypeIdentifierName: string, _arguments: Record<string, any>)
{
this.fieldName = fieldName;
this.functionTypeIdentifierName = functionTypeIdentifierName;
this.arguments = _arguments;
}
/***************************************************************************
*
***************************************************************************/
public clone(): FieldFunction
{
const cloneArguments = (this.arguments === null ? null : this.arguments === undefined ? undefined : {...this.arguments}) as Record<string, any>;
const clone = new FieldFunction(
this.fieldName,
this.functionTypeIdentifierName,
cloneArguments
);
return (clone);
}
}
+25
View File
@@ -19,6 +19,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {FieldFunction} from "./FieldFunction";
import {QCriteriaOperator} from "./QCriteriaOperator";
/*******************************************************************************
@@ -31,6 +32,7 @@ export class QFilterCriteria
operator: QCriteriaOperator;
values!: any[];
otherFieldName?: string;
fieldFunction?: FieldFunction;
constructor(fieldName: string, operator: QCriteriaOperator, values: any[])
{
@@ -38,4 +40,27 @@ export class QFilterCriteria
this.operator = operator;
this.values = values;
}
/***************************************************************************
*
***************************************************************************/
public clone(): QFilterCriteria
{
const cloneValues = (this.values === null ? null : this.values === undefined ? undefined : [...this.values]) as any[];
const clone = new QFilterCriteria(
this.fieldName,
this.operator,
cloneValues
);
clone.otherFieldName = this.otherFieldName;
if(this.fieldFunction)
{
clone.fieldFunction = this.fieldFunction.clone();
}
return (clone);
}
}
+12
View File
@@ -33,4 +33,16 @@ export class QFilterOrderBy
this.fieldName = fieldName;
this.isAscending = isAscending;
}
/***************************************************************************
*
***************************************************************************/
public clone(): QFilterOrderBy
{
const clone = new QFilterOrderBy(
this.fieldName,
this.isAscending
);
return (clone);
}
}
+43
View File
@@ -107,4 +107,47 @@ export class QQueryFilter
}
this.subFilters.push(subFilter);
}
/***************************************************************************
*
***************************************************************************/
public clone(): QQueryFilter
{
const criteriaClone: QFilterCriteria[] | undefined = this.criteria ? [] : undefined;
if(this.criteria && criteriaClone)
{
for (let criteria of this.criteria)
{
criteriaClone.push((criteria as any).clone());
}
}
const orderBysClone: QFilterOrderBy[] | undefined = this.orderBys ? [] : undefined;
if(this.orderBys && orderBysClone)
{
for (let orderBy of this.orderBys)
{
orderBysClone.push((orderBy as any).clone());
}
}
const subFiltersClone: QQueryFilter[] | undefined = this.subFilters ? [] : undefined;
if(this.subFilters && subFiltersClone)
{
for (let subFilter of this.subFilters)
{
subFiltersClone.push((subFilter as any).clone());
}
}
const clone = new QQueryFilter(
criteriaClone,
orderBysClone,
subFiltersClone,
this.booleanOperator,
this.skip,
this.limit
);
return (clone);
}
}
+64
View File
@@ -0,0 +1,64 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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/>.
*/
import {Collapsible} from "../../../src/model/metaData/Collapsible";
describe("Collapsible", () =>
{
describe("constructor", () =>
{
it("should construct with all properties from object", () =>
{
const collapsible = new Collapsible({
isCollapsible: true,
initiallyOpen: true
});
expect(collapsible.isCollapsible).toBe(true);
expect(collapsible.initiallyOpen).toBe(true);
});
it("should handle empty object", () =>
{
const collapsible = new Collapsible({});
//////////////////////////////////////
// both attributes default to false //
//////////////////////////////////////
expect(collapsible.isCollapsible).toBe(false);
expect(collapsible.initiallyOpen).toBe(false);
});
it("should treat non-true values as false", () =>
{
const collapsible = new Collapsible({
isCollapsible: 1,
initiallyOpen: "true"
});
//////////////////////////////////////
// both attributes default to false //
//////////////////////////////////////
expect(collapsible.isCollapsible).toBe(false);
expect(collapsible.initiallyOpen).toBe(false);
});
});
});
+89
View File
@@ -0,0 +1,89 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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/>.
*/
import {QAppMetaData} from "../../../src/model/metaData/QAppMetaData";
import {QAppNodeType} from "../../../src/model/metaData/QAppNodeType";
import {QAppTreeNode} from "../../../src/model/metaData/QAppTreeNode";
import {QInstance} from "../../../src/model/metaData/QInstance";
import {QTableMetaData} from "../../../src/model/metaData/QTableMetaData";
describe("QInstance", () =>
{
describe("getTablePath", () =>
{
it("should work for basic case (tables in only 1 app)", () =>
{
const qInstance = buildTestQInstance();
expect(qInstance.getTablePathByName("order")).toBe("/oms/order");
expect(qInstance.getTablePathByName("group")).toBe("/admin/usermgr/group");
expect(qInstance.getTablePathByName("role")).toBe("/setup/role");
});
it("should use app affinity when table is in two apps", () =>
{
const qInstance1 = buildTestQInstance({user: {admin: 50, setup: 100}});
expect(qInstance1.getTablePathByName("user")).toBe("/setup/user");
const qInstance2 = buildTestQInstance({user: {admin: 1}});
expect(qInstance2.getTablePathByName("user")).toBe("/admin/usermgr/user");
});
});
});
function buildTestQInstance(affinityValues: Record<string, Record<string, number>> = {}): QInstance
{
const qInstance = new QInstance({});
qInstance.tables = new Map<string, QTableMetaData>();
qInstance.tables.set("order", new QTableMetaData({name: "order"}));
qInstance.tables.set("user", new QTableMetaData({name: "user"})); // will be under both /admin/usermgr and /setup
qInstance.tables.set("group", new QTableMetaData({name: "group"})); // will just be under /admin/usermgr
qInstance.tables.set("role", new QTableMetaData({name: "role"})); // will just be under /setup
qInstance.apps = new Map<string, QAppMetaData>();
qInstance.apps.set("oms", new QAppMetaData({name: "oms"}));
qInstance.apps.set("usermgr", new QAppMetaData({name: "usermgr"})); // will go under setup
qInstance.apps.set("admin", new QAppMetaData({name: "admin"}));
qInstance.apps.set("setup", new QAppMetaData({name: "setup"}));
const orderTableNode = new QAppTreeNode({name: "order", type: QAppNodeType.TABLE});
const userTableNodeForAdminUsermgr = new QAppTreeNode({name: "user", type: QAppNodeType.TABLE, appAffinity: affinityValues["user"]?.["admin"] ?? undefined});
const userTableNodeForSetup = new QAppTreeNode({name: "user", type: QAppNodeType.TABLE, appAffinity: affinityValues["user"]?.["setup"] ?? undefined});
const groupTableNode = new QAppTreeNode({name: "group", type: QAppNodeType.TABLE});
const roleTableNode = new QAppTreeNode({name: "role", type: QAppNodeType.TABLE});
const omsAppNode = new QAppTreeNode({name: "oms", type: QAppNodeType.APP, children: [orderTableNode]});
const usermgrAppNode = new QAppTreeNode({name: "usermgr", type: QAppNodeType.APP, children: [userTableNodeForAdminUsermgr, groupTableNode]});
const adminAppNode = new QAppTreeNode({name: "admin", type: QAppNodeType.APP, children: [usermgrAppNode]});
const setupAppNode = new QAppTreeNode({name: "setup", type: QAppNodeType.APP, children: [userTableNodeForSetup, roleTableNode]});
qInstance.appTree = [];
qInstance.appTree.push(omsAppNode);
qInstance.appTree.push(adminAppNode);
qInstance.appTree.push(setupAppNode);
qInstance.buildPathsMap(qInstance.appTree);
return (qInstance);
}
+451
View File
@@ -0,0 +1,451 @@
/*
* 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/>.
*/
import {QHelpContent} from "../../../src/model/metaData/QHelpContent";
import {QMenu} from "../../../src/model/metaData/QMenu";
import {QMenuItem} from "../../../src/model/metaData/QMenuItem";
import {QTableMetaData} from "../../../src/model/metaData/QTableMetaData";
import {QVirtualFieldMetaData} from "../../../src/model/metaData/QVirtualFieldMetaData";
describe("QTableMetaData tests", () =>
{
it("it should read field supplementalFieldMetaData", () =>
{
const table = new QTableMetaData({
"name": "myTable",
"label": "My Table",
"primaryKeyField": "id",
"fields": {
"warehouseId": {
"name": "warehouseId",
"label": "Warehouse",
"type": "INTEGER",
"displayFormat": "%s",
"supplementalFieldMetaData": {
"materialDashboard": {
"formAdjusterIdentifier": "process:myProcess;field:warehouseId",
"type": "materialDashboard"
}
}
}
}
});
expect(table.fields?.get("warehouseId")?.supplementalFieldMetaData.get("materialDashboard")).toEqual(
expect.objectContaining({
formAdjusterIdentifier: "process:myProcess;field:warehouseId",
type: "materialDashboard"
})
);
});
it("it should read field supplementalMetaData", () =>
{
const table = new QTableMetaData({
"name": "myTable",
"label": "My Table",
"primaryKeyField": "id",
"fields": {
"warehouseId": {
"name": "warehouseId",
"label": "Warehouse",
"type": "INTEGER",
"displayFormat": "%s",
"supplementalMetaData": {
"materialDashboard": {
"formAdjusterIdentifier": "process:myProcess;field:warehouseId",
"type": "materialDashboard"
}
}
}
}
});
expect(table.fields?.get("warehouseId")?.supplementalFieldMetaData.get("materialDashboard")).toEqual(
expect.objectContaining({
formAdjusterIdentifier: "process:myProcess;field:warehouseId",
type: "materialDashboard"
})
);
});
it("it should clone", () =>
{
/////////////////////////////////////
// build a table with lots of data //
/////////////////////////////////////
const table = new QTableMetaData(
{
"name": "myTable",
"label": "My Table",
"isHidden": false,
"primaryKeyField": "id",
"icon": {"name": "link"},
"fields": {
"id": {
"name": "id",
"label": "Id",
"type": "INTEGER",
"isRequired": false,
"isEditable": true,
"isHeavy": false,
"isHidden": false,
"gridColumns": -1,
"displayFormat": "%s"
},
"name": {
"name": "name",
"label": "Name",
"type": "STRING",
"isRequired": false,
"isEditable": true,
"isHeavy": false,
"isHidden": false,
"gridColumns": -1,
"displayFormat": "%s",
"maxLength": 100
},
"clientId": {
"name": "clientId",
"label": "Client",
"type": "INTEGER",
"isRequired": false,
"isEditable": true,
"isHeavy": false,
"isHidden": false,
"gridColumns": -1,
"possibleValueSourceName": "client",
"displayFormat": "%s",
"adornments": [
{
"type": "LINK",
"values": {
"toRecordFromTable": "client"
}
}
],
"possibleValueSourceFilter": {
"criteria": [
{
"fieldName": "isActive",
"operator": "EQUALS",
"values": [
true
]
}
],
"booleanOperator": "AND",
"subFilters": [
{
"criteria": [
{
"fieldName": "clientId",
"operator": "EQUALS",
"values": [
"${input.clientId}"
]
},
{
"fieldName": "clientId",
"operator": "IS_BLANK"
}
],
"booleanOperator": "OR"
}
]
}
},
"apiKey": {
"name": "apiKey",
"label": "API Key",
"type": "PASSWORD",
"isRequired": false,
"isEditable": true,
"isHeavy": false,
"isHidden": false,
"gridColumns": -1,
"displayFormat": "%s",
"helpContents": [
{
"content": "Field Help",
"format": "MARKDOWN",
"roles": ["READ_SCREENS"],
"contentAsHtml": "Field Help"
}
],
"supplementalMetaData": {
"materialDashboard": {
"formAdjusterIdentifier": "table:connection;field:externalSystemId",
"onChangeFormAdjuster": {
"name": "com.coldtrack.live.tables.setup.customizers.ConnectionTableMetaDataAdjuster",
"codeType": "JAVA"
},
"type": "materialDashboard"
}
},
"adornments": [
{
"type": "LINK",
"values": {"toRecordFromTable": "foreignTable"}
}
]
},
"type": {
"name": "type",
"label": "Type",
"type": "BOOLEAN",
"isRequired": false,
"isEditable": true,
"isHeavy": false,
"isHidden": false,
"gridColumns": -1,
"displayFormat": "%s",
"possibleValueSourceName": "type",
"supplementalMetaData": {
"materialDashboard": {
"formAdjusterIdentifier": "table:myTable;field:type",
"onChangeFormAdjuster": {
"name": "com.qqq.MyTableFormAdjuster",
"codeType": "JAVA"
},
"type": "materialDashboard"
}
}
},
},
"virtualFields": {
"nameLength": {
"name": "nameLength",
"label": "Name Length",
"type": "INTEGER",
"isRequired": false,
"isEditable": false,
"isHeavy": false,
"isHidden": false,
"gridColumns": -1,
"isQueryCriteria": true,
"isQuerySelectable": false
},
"createDateWeekday": {
"name": "createDateWeekday",
"label": "Create Date Weekday",
"type": "DATE",
"isRequired": false,
"isEditable": false,
"isHeavy": false,
"isHidden": false,
"gridColumns": -1,
"isQueryCriteria": false,
"isQuerySelectable": true
},
},
"sections": [
{
"name": "identity",
"label": "Identity",
"tier": "T1",
"fieldNames": ["id", "name"],
"icon": {"name": "badge"},
"isHidden": false,
"alternatives": {
"RECORD_VIEW": {
"name": "identityView",
"label": "Identity",
"fieldNames": ["id", "specialName"]
}
}
},
{
"name": "authentication",
"label": "Authentication",
"tier": "T2",
"fieldNames": ["apiKey",],
"icon": {"name": "edit_attributes"},
"isHidden": false
},
],
"exposedJoins": [
{
"label": "Parcel",
"isMany": false,
"joinTable": {
"name": "parcel",
"label": "Parcel",
"primaryKeyField": "id",
"fields": {
"id": {"name": "id", "label": "Id", "type": "INTEGER", "isEditable": false, "isHidden": false},
"trackingNumber": {"name": "trackingNumber", "label": "Tracking Number", "type": "STRING", "isEditable": true, "isHidden": false},
"status": {"name": "status", "label": "Status", "type": "STRING", "isEditable": true, "isHidden": false}
},
"sections": [],
"capabilities": []
},
"joinPath": [
{"name": "orderShipmentJoinParcel", "leftTable": "orderShipment", "rightTable": "parcel", "type": "ONE_TO_ONE", "joinOns": []}
]
}
],
"supplementalTableMetaData": {
"materialDashboard": {
"onLoadFormAdjuster": {
"name": "com.qqq.MyTableFormAdjuster",
"codeType": "JAVA"
},
"type": "materialDashboard"
}
},
"capabilities": [
"TABLE_COUNT",
"TABLE_GET",
"TABLE_QUERY",
"QUERY_STATS",
"TABLE_INSERT",
"TABLE_UPDATE",
"TABLE_DELETE",
"TABLE_EXPORT"
],
"readPermission": true,
"insertPermission": true,
"editPermission": true,
"deletePermission": true,
"usesVariants": false,
"iconName": "link",
"helpContents": {
"key": [
{
"content": "Table Help",
"format": "MARKDOWN",
"roles": ["WRITE_SCREENS"],
"contentAsHtml": "Table Help"
}
]
},
"menus": [
{
label: "My Menu",
icon: {name: "hotdog"},
slot: "VIEW_SCREEN_ADDITIONAL",
items: [
{label: "Cars", icon: {name: "car"}, itemType: "RUN_PROCESS", values: {processName: "runCarsProcess"}},
{label: "Apples", icon: {name: "download"}, itemType: "DOWNLOAD_FILE", values: {fieldName: "appleFileUrl"}},
]
}
]
});
////////////////////////////////////////////////////////////////
// clone it - make a few assertions about data being the same //
////////////////////////////////////////////////////////////////
const cloneTable = table.clone();
expect(cloneTable.name).toEqual(table.name);
expect(cloneTable.virtualFields?.size).toEqual(2);
expect(cloneTable.virtualFields?.get("nameLength") instanceof QVirtualFieldMetaData).toBeTruthy();
expect(cloneTable.sections?.length).toEqual(2);
expect(cloneTable.sections?.length).toEqual(table.sections?.length);
expect(cloneTable.sections?.[0].name).toEqual(table.sections?.[0].name);
expect(cloneTable.sections?.[0].iconName).toEqual(table.sections?.[0].iconName);
expect(cloneTable.sections?.[0].alternatives?.size).toEqual(1);
expect(cloneTable.capabilities.size).toEqual(table.capabilities.size);
expect(cloneTable.helpContent?.size).toEqual(table.helpContent?.size);
expect(cloneTable.helpContent?.get("key")?.[0].roles.size).not.toEqual(0);
expect(cloneTable.helpContent?.get("key")?.[0].roles.size).toEqual(table.helpContent?.get("key")?.[0].roles.size);
expect(cloneTable.supplementalTableMetaData.size).not.toEqual(0);
expect(cloneTable.supplementalTableMetaData.size).toEqual(table.supplementalTableMetaData.size);
const apiKeyField = table.fields?.get("apiKey");
const cloneApiKeyField = cloneTable.fields?.get("apiKey");
expect(cloneApiKeyField?.supplementalFieldMetaData.size).not.toEqual(0);
expect(cloneApiKeyField?.supplementalFieldMetaData.size).toEqual(apiKeyField?.supplementalFieldMetaData.size);
expect(cloneApiKeyField?.adornments?.length).not.toEqual(0);
expect(cloneApiKeyField?.adornments?.length).toEqual(apiKeyField?.adornments?.length);
expect(cloneApiKeyField?.adornments?.[0].values?.size).not.toEqual(0);
expect(cloneApiKeyField?.adornments?.[0].values?.size).toEqual(apiKeyField?.adornments?.[0].values?.size);
expect(cloneTable.menus?.length).toEqual(1);
expect(cloneTable.menus?.[0].items?.length).toEqual(2);
expect(cloneTable.exposedJoins?.length).toEqual(1);
expect(cloneTable.exposedJoins?.[0].joinTable.name).toEqual("parcel");
expect(cloneTable.exposedJoins?.[0].joinTable.fields?.size).toEqual(3);
expect(cloneTable.exposedJoins?.[0].joinTable.fields?.get("trackingNumber")?.name).toEqual("trackingNumber");
////////////////////////////////////////////////////////////////////////////////////////////////
// turns out toEqual will do a deep equality test, so, we could have just done this all along //
////////////////////////////////////////////////////////////////////////////////////////////////
expect(cloneTable).toEqual(table);
///////////////////////////////////////////////////////////////////////////////////////////
// change some things in the clone - assert that they are then not equal to the original //
///////////////////////////////////////////////////////////////////////////////////////////
cloneTable.name = "changed";
expect(cloneTable.name).not.toEqual(table.name);
cloneTable.virtualFields?.set("new", new QVirtualFieldMetaData({}));
expect(cloneTable.virtualFields?.size).toEqual(3);
expect(table.virtualFields?.size).toEqual(2);
cloneTable.virtualFields!.get("nameLength")!.gridColumns = 6;
cloneTable.virtualFields!.get("nameLength")!.isQuerySelectable = !cloneTable.virtualFields!.get("nameLength")!.isQuerySelectable;
cloneTable.virtualFields!.get("nameLength")!.isQueryCriteria = !cloneTable.virtualFields!.get("nameLength")!.isQueryCriteria;
expect(cloneTable.virtualFields?.get("nameLength")?.gridColumns).not.toEqual(table.virtualFields?.get("nameLength")?.gridColumns);
expect(cloneTable.virtualFields?.get("nameLength")?.isQuerySelectable).not.toEqual(table.virtualFields?.get("nameLength")?.isQuerySelectable);
expect(cloneTable.virtualFields?.get("nameLength")?.isQueryCriteria).not.toEqual(table.virtualFields?.get("nameLength")?.isQueryCriteria);
cloneTable.sections![0]!.name = "changed";
cloneTable.sections![0]!.isHidden = true;
expect(cloneTable.sections?.[0].name).not.toEqual(table.sections?.[0].name);
expect(cloneTable.sections?.[0].isHidden).not.toEqual(table.sections?.[0].isHidden);
cloneTable.capabilities.delete("TABLE_COUNT");
expect(cloneTable.capabilities.size).not.toEqual(table.capabilities.size);
cloneTable.helpContent?.set("key2", []);
expect(cloneTable.helpContent?.size).not.toEqual(table.helpContent?.size);
cloneTable.helpContent?.get("key")?.push(new QHelpContent({}));
expect(cloneTable.helpContent?.get("key")?.length).not.toEqual(table.helpContent?.get("key")?.length);
cloneTable.helpContent?.get("key")?.[0].roles.add("READ_SCREENS");
expect(cloneTable.helpContent?.get("key")?.[0].roles.size).not.toEqual(table.helpContent?.get("key")?.[0].roles.size);
cloneTable.sections?.[0]?.fieldNames?.push("foobar");
expect(cloneTable.sections?.[0]?.fieldNames?.length).not.toEqual(table.sections?.[0]?.fieldNames?.length);
cloneTable.sections?.[0]?.alternatives?.get("RECORD_VIEW")?.fieldNames?.push("another");
expect(table.sections?.[0]?.alternatives?.get("RECORD_VIEW")?.fieldNames?.length).toEqual(2);
expect(cloneTable.sections?.[0]?.alternatives?.get("RECORD_VIEW")?.fieldNames?.length).toEqual(3);
cloneTable.menus?.[0]?.icon && (cloneTable.menus[0].icon.name = "hamburger");
cloneTable.menus?.[0]?.items && (cloneTable.menus[0].items.push(new QMenuItem({itemType: "DIVIDER"})));
cloneTable.menus && (cloneTable.menus.push(new QMenu({})));
expect(cloneTable.menus?.[0]?.icon?.name).not.toEqual(table.menus?.[0]?.icon?.name);
expect(cloneTable.menus?.[0]?.items?.length).not.toEqual(table.menus?.[0]?.items?.length);
expect(cloneTable.menus?.length).not.toEqual(table.menus?.length);
});
});
+105
View File
@@ -0,0 +1,105 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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/>.
*/
import {QTableSection} from "../../../src/model/metaData/QTableSection";
describe("QTableSection", () =>
{
describe("constructor", () =>
{
it("should construct with collapsible object if set in source object", () =>
{
const section = new QTableSection({
name: "section1",
collapsible: {
isCollapsible: true,
initiallyOpen: false
}
});
expect(section.name).toBe("section1");
expect(section.collapsible?.isCollapsible).toBe(true);
expect(section.collapsible?.initiallyOpen).toBe(false);
});
it("should construct with undefined collapsible object if not present in source object", () =>
{
const section = new QTableSection({
name: "section2"
});
expect(section.name).toBe("section2");
expect(section.collapsible).toBeUndefined();
});
it("should construct with collapsible object with null values if empty collapsible object is in source", () =>
{
const section = new QTableSection({
name: "section3",
collapsible: {}
});
expect(section.name).toBe("section3");
expect(section.collapsible?.isCollapsible).toBe(false);
expect(section.collapsible?.initiallyOpen).toBe(false);
});
it("should deeply clone a collapsible", () =>
{
const section = new QTableSection({
name: "section4",
collapsible: {
isCollapsible: true,
initiallyOpen: true
}
});
expect(section.name).toBe("section4");
expect(section.collapsible?.isCollapsible).toBe(true);
expect(section.collapsible?.initiallyOpen).toBe(true);
const clone = section.clone();
expect(clone.collapsible?.isCollapsible).toBe(true);
expect(clone.collapsible?.initiallyOpen).toBe(true);
if(clone.collapsible)
{
clone.collapsible.initiallyOpen = false;
}
expect(section.collapsible?.initiallyOpen).toBe(true);
expect(clone.collapsible?.initiallyOpen).toBe(false);
});
it("should handle an undefined collapsible during cloning", () =>
{
const section = new QTableSection({
name: "section5"
});
expect(section.name).toBe("section5");
expect(section.collapsible).toBeUndefined();
const clone = section.clone();
expect(clone.collapsible).toBeUndefined();
});
});
});
@@ -0,0 +1,67 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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/>.
*/
import {QWidgetMetaData} from "../../../src/model/metaData/QWidgetMetaData";
describe("QWidgetMetaData", () =>
{
describe("constructor", () =>
{
it("should construct with collapsible object if set in source object", () =>
{
const widget = new QWidgetMetaData({
name: "widget1",
collapsible: {
isCollapsible: true,
initiallyOpen: false
}
});
expect(widget.name).toBe("widget1");
expect(widget.collapsible?.isCollapsible).toBe(true);
expect(widget.collapsible?.initiallyOpen).toBe(false);
});
it("should construct with undefined collapsible object if not present in source object", () =>
{
const widget = new QWidgetMetaData({
name: "widget2"
});
expect(widget.name).toBe("widget2");
expect(widget.collapsible).toBeUndefined();
});
it("should construct with collapsible object with null values if empty collapsible object is in source", () =>
{
const widget = new QWidgetMetaData({
name: "widget3",
collapsible: {}
});
expect(widget.name).toBe("widget3");
expect(widget.collapsible?.isCollapsible).toBe(false);
expect(widget.collapsible?.initiallyOpen).toBe(false);
});
});
});