Compare commits

..
Author SHA1 Message Date
forge-recovery 65340f76e7 ci: enroll shared Gitea lifecycle
QRun-IO/ci-policy: ci / java-maven-7 (push) Skipped
QRun-IO/ci-policy: ci / node-8 (push) Skipped
QRun-IO/ci-policy: ci / node-10 (push) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (push) Skipped
QRun-IO/ci-policy: ci / ci-result (push) Successful in 5s
QRun-IO/ci-policy: ci / repository-policy-12 (push) Skipped
QRun-IO/ci-policy: ci / source-config-13 (push) Skipped
QRun-IO/ci-policy: ci / source-config-14 (push) Skipped
QRun-IO/ci-policy: ci / android-0 (push) Skipped
QRun-IO/ci-policy: ci / original-business (push) Skipped
QRun-IO/ci-policy: ci / original-mcp (push) Skipped
QRun-IO/ci-policy: ci / original-voyage (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Skipped
QRun-IO/ci-policy: native / fork-deny (push) Skipped
QRun-IO/ci-policy: native / dotnet-0 (push) Skipped
Shared original Node npm producer / verify (push) Successful in 1m21s
Shared original Node npm producer / version (push) Skipped
QRun-IO/ci-policy: Shared Node CI / verify (push) Successful in 1m8s
QRun-IO/ci-policy: ci / repository-policy (push) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (push) Skipped
QRun-IO/ci-policy: native / native-result (push) Successful in 4s
QRun-IO/ci-policy: Shared Node CI / secrets (push) Successful in 16s
Shared original Node npm producer / package (push) Failing after 1m35s
QRun original Node release / release (push) Failing after 3m13s
Shared original Node npm producer / cleanup (push) Skipped
QRun original Node release / postpublish (push) Skipped
QRun-IO/ci-policy: Shared Node CI / gate (push) Successful in 12s
QRun-IO/ci-policy: ci / node-9 (push) Successful in 1m38s
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: ci / head-profile (push) Skipped
QRun-IO/ci-policy: ci / application-template-0 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (push) Skipped
QRun-IO/ci-policy: ci / gradle-1 (push) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (push) Skipped
2026-11-03 21:51:41 +00:00
forge-recoveryandCodex 8989aa1812 ci: enroll original shared Node lifecycle
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-business (pull_request) Skipped
QRun-IO/ci-policy: ci / original-mcp (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
QRun-IO/ci-policy: ci / android-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-voyage (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
Shared original Node npm producer / verify (push) Successful in 1m38s
Shared original Node npm producer / version (push) Skipped
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 6s
Shared original Node npm producer / package (push) Successful in 7s
QRun-IO/ci-policy: Shared Node CI / secrets (pull_request) Successful in 21s
QRun original Node release / release (push) Successful in 2m24s
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun original Node release / postpublish (push) Skipped
Shared original Node npm producer / cleanup (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: Shared Node CI / verify (pull_request) Successful in 1m8s
QRun-IO/ci-policy: Shared Node CI / gate (pull_request) Successful in 10s
QRun-IO/ci-policy: ci / node-9 (pull_request) Successful in 1m20s
QRun-IO/ci-policy: ci / ci-result (pull_request) Successful in 5s
Co-Authored-By: Codex <noreply@openai.com>
2026-11-03 21:43:08 +00:00
james.maesandCodex daefcc1ddf ci: use central orb pipeline on main (#56)
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 17:38:58 -05:00
james.maes 8d26150312 chore: migrate license from AGPL-3.0 to Apache-2.0 2026-01-05 10:11:34 -06:00
james.maes 44e3d80ccb ci: Add GitHub release configuration 2025-12-24 12:43:29 -06:00
43 changed files with 595 additions and 2093 deletions

No files matched your search

+10 -67
View File
@@ -1,71 +1,14 @@
---
version: 2.1
setup: true
orbs:
qqq-orb: kingsrook/qqq-orb@0.6
qqq-orb: kingsrook/qqq-orb@0.8
workflows:
test_only:
setup:
jobs:
- 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\/.*/
- qqq-orb/generate_pipeline:
orb_version: '0.8'
filters:
branches:
ignore: automation/qbit-rc-pending
tags:
only: /.*/
+37
View File
@@ -0,0 +1,37 @@
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
@@ -0,0 +1,38 @@
# 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,7 +1,5 @@
*.iml
.env
.yalc
yalc*
node_modules
coverage
@@ -13,7 +11,3 @@ lib
**/.DS_Store
*.iml
# Claude Code local settings
.claude/
+4
View File
@@ -0,0 +1,4 @@
profile: frontend-core
orb_version: '0.8'
default_branch: develop
node_version: '24.21'
-38
View File
@@ -1,38 +0,0 @@
# 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
@@ -1,70 +0,0 @@
# 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"
+465 -104
View File
@@ -1,155 +1,516 @@
# qqq-frontend-core
# QQQ Frontend Core
TypeScript client library for building frontends on QQQ applications.
[![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)
**For:** Developers building custom UIs or integrating QQQ into existing applications
**Status:** Stable
> **Core Frontend Library for QQQ - Low-code Application Framework for Engineers**
## Why This Exists
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.
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:
## 🚀 Overview
- Understands QQQ's metadata-driven architecture
- Handles authentication flows (OAuth2, tokens)
- Provides typed interfaces for records, queries, and processes
- Manages API communication reliably
QQQ Frontend Core is the **foundational library** that enables frontend applications to communicate with QQQ backend servers. It provides:
This library is that client. It's framework-agnostic - use it with React, Vue, Angular, or plain JavaScript.
- **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
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.
### What This Repository Contains
## Features
- **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
- **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
### What This Repository Does NOT Contain
## Quick Start
- **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
**Prerequisites:** Node.js 18+
## 🏗️ 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
```bash
npm install @qrunio/qqq-frontend-core
```
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 api = new QController('https://your-qqq-server.com');
const controller = new QController('https://your-qqq-server.com');
// Load what tables and processes exist
const metadata = await api.loadMetaData();
console.log('Tables:', [...metadata.tables.keys()]);
// Load metadata
const metadata = await controller.loadMetaData();
// Query records
const users = await api.queryRecords('user', {
filters: [{ field: 'isActive', operator: '=', value: true }],
limit: 10
});
const records = await controller.queryRecords('tableName', filters);
// Execute processes
const result = await controller.executeProcess('processName', parameters);
```
## Usage
### Data Models
### Authentication
Complete TypeScript models for all QQQ data structures:
```typescript
// Bearer token (simplest)
api.setAuthorizationHeaderValue('Bearer your-jwt-token');
import {
QInstance,
QTableMetaData,
QProcessMetaData
} from '@qrunio/qqq-frontend-core';
// OAuth2
api.setAuthenticationMetaData({
type: 'OAUTH2',
clientId: 'your-client-id',
tokenUrl: 'https://auth.example.com/token'
});
// Instance metadata
const instance: QInstance = new QInstance(rawData);
// Table metadata
const table: QTableMetaData = new QTableMetaData(tableData);
// Process metadata
const process: QProcessMetaData = new QProcessMetaData(processData);
```
### Working with Records
### Exception Handling
```typescript
// Create
const order = await api.createRecord('order', {
customerId: 42,
status: 'pending'
});
// Read
const record = await api.getRecord('order', order.id);
// Update
await api.updateRecord('order', order.id, { status: 'shipped' });
// Delete
await api.deleteRecord('order', order.id);
// 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
Standardized error handling for QQQ operations:
```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})`);
const result = await controller.executeProcess('processName', params);
} catch (error) {
if (error instanceof QException) {
console.error(`QQQ Error: ${error.message} (Status: ${error.status})`);
}
throw e;
}
```
## Configuration
## 🚀 Getting Started
### Installation
```bash
npm install @qrunio/qqq-frontend-core
```
### Basic Usage
```typescript
// Custom timeout (default: 60s)
api.setStepTimeoutMillis(120000);
import { QController } from '@qrunio/qqq-frontend-core';
// Global error handler
const api = new QController('https://...', (error) => {
logToMonitoring(error);
// Create controller instance
const controller = 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'
});
```
## Project Status
### Advanced Usage
**Maturity:** Stable, used in production
**Breaking changes:** Follow semver; major versions may break API
```typescript
// Custom exception handler
const controller = new QController('https://your-qqq-server.com', (error) => {
console.error('Custom error handling:', error);
// Handle error appropriately
});
**Roadmap:**
- WebSocket support for real-time updates
- Request batching for bulk operations
- Offline queue for intermittent connectivity
// Set custom timeout
controller.setStepTimeoutMillis(120000); // 2 minutes
## Contributing
```bash
git clone https://github.com/QRun-IO/qqq-frontend-core
cd qqq-frontend-core
npm install
npm test
// Handle authentication
controller.setAuthenticationMetaData({
type: 'OAUTH2',
clientId: 'your-client-id',
// ... other auth config
});
```
Submit issues and PRs to the [main QQQ repository](https://github.com/QRun-IO/qqq/issues).
## 🔧 Configuration
## License
### 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);
// Query records
const records = await controller.queryRecords('users', {
filters: [{ field: 'active', operator: '=', value: true }],
sort: [{ field: 'name', direction: 'ASC' }],
limit: 50,
offset: 0
});
```
### Process Execution
```typescript
// Execute process
const result = await controller.executeProcess('sendEmail', {
to: 'user@example.com',
subject: 'Welcome',
body: 'Welcome to our platform!'
});
// Monitor process status
const status = await controller.getProcessStatus('process-id');
// Cancel process
await controller.cancelProcess('process-id');
```
## 🎨 Integration Examples
### React Integration
```typescript
import React, { useEffect, useState } from 'react';
import { QController, QInstance } from '@qrunio/qqq-frontend-core';
function QQQApp() {
const [metadata, setMetadata] = useState<QInstance | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const controller = new QController('https://your-qqq-server.com');
controller.loadMetaData()
.then(setMetadata)
.finally(() => setLoading(false));
}, []);
if (loading) return <div>Loading...</div>;
if (!metadata) return <div>No metadata available</div>;
return (
<div>
<h1>QQQ Application</h1>
<p>Tables: {metadata.tables?.size || 0}</p>
<p>Processes: {metadata.processes?.size || 0}</p>
</div>
);
}
```
### Vue Integration
```typescript
import { defineComponent, ref, onMounted } from 'vue';
import { QController, QInstance } from '@qrunio/qqq-frontend-core';
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 };
}
});
```
### Vanilla JavaScript
```javascript
import { QController } from '@qrunio/qqq-frontend-core';
const controller = new QController('https://your-qqq-server.com');
// Load metadata
controller.loadMetaData()
.then(metadata => {
console.log('Tables:', metadata.tables);
console.log('Processes:', metadata.processes);
})
.catch(error => {
console.error('Error loading metadata:', error);
});
```
## 🚀 Performance
### 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**
AGPL-3.0
-55
View File
@@ -1,55 +0,0 @@
# 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
@@ -1,42 +0,0 @@
# 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": "@qrunio/qqq-frontend-core",
"version": "0.40.6-SNAPSHOT",
"name": "@kingsrook/qqq-frontend-core",
"version": "1.0.127-SNAPSHOT",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@qrunio/qqq-frontend-core",
"version": "0.40.6-SNAPSHOT",
"name": "@kingsrook/qqq-frontend-core",
"version": "1.0.127-SNAPSHOT",
"license": "ISC",
"dependencies": {
"axios": "0.28.1",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@qrunio/qqq-frontend-core",
"version": "0.40.11-SNAPSHOT",
"version": "1.1.0",
"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": "npm run prepublishOnly && yalc push --private",
"yalc-push": "yalc push --private",
"yalc-watch": "yalc-watch"
},
"dependencies": {
+12 -101
View File
@@ -19,7 +19,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {AxiosError, AxiosRequestConfig, AxiosResponse} from "axios";
import {AxiosError, AxiosResponse} from "axios";
import FormData from "form-data";
import {QException} from "../exceptions/QException";
import {QAuthenticationMetaData} from "../model/metaData/QAuthenticationMetaData";
@@ -783,15 +783,13 @@ 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, ...axiosConfigExtras})
.post(url, formDataOrQueryString, {headers: formDataHeaders})
.then((response: AxiosResponse) =>
{
const responseObject = this.parseProcessResponse(response);
@@ -901,7 +899,7 @@ export class QController
/*******************************************************************************
** Fetch the data about variants for a table
** Fetch the data for a specific widget.
*******************************************************************************/
async tableVariants(tableName: string): Promise<QTableVariant[]>
{
@@ -935,48 +933,24 @@ export class QController
/*******************************************************************************
* 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.
** Fetch the data for a specific widget.
*******************************************************************************/
async widget(widgetName: string, params?: string | FormData, overrideAbortName?: string): Promise<any>
async widget(widgetName: string, urlParams?: string): Promise<any>
{
let url = `/widget/${encodeURIComponent(widgetName)}`;
const abortName = overrideAbortName ?? widgetName;
const paramsAreFormData = params instanceof FormData
let postBody: FormData;
if (paramsAreFormData)
if (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));
}
}
url += `?${urlParams}`;
}
/////////////////////////////////////////////////////////////////
// see if an abort controller was created for this widget name //
/////////////////////////////////////////////////////////////////
let controller = QController.widgetAbortControllerMap.get(abortName);
console.log("Looking for controller for widget [" + widgetName + "]");
let controller = QController.widgetAbortControllerMap.get(widgetName);
if (controller)
{
console.log(`Found existing abort controller for widget '${abortName}', aborting...`);
console.log(`Found existing abort controller for widget '${widgetName}', aborting...`);
controller.abort();
}
@@ -985,69 +959,7 @@ export class QController
/////////////////////////////////////////
controller = new AbortController();
const signal = controller.signal;
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);
QController.widgetAbortControllerMap.set(widgetName, controller);
return this.axiosInstance
.get(url, {signal})
@@ -1056,7 +968,7 @@ export class QController
///////////////////////////////////////////////////
// make sure to clear out the request controller //
///////////////////////////////////////////////////
QController.widgetAbortControllerMap.set(abortName, null);
QController.widgetAbortControllerMap.set(widgetName, null);
return response.data;
})
.catch((e: any) =>
@@ -1071,7 +983,6 @@ 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,16 +34,5 @@ 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,7 +35,6 @@ 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 //
///////////////////////////////////////////////////////////////////////////
+10 -12
View File
@@ -23,16 +23,14 @@
** 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",
TABLE_EXPORT = "TABLE_EXPORT"
////////////////////////////////////////////////////////////////////////
// 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"
////////////////////////////////////////////////////////////////////////
// keep these values in sync with Capability.java in qqq-backend-core //
////////////////////////////////////////////////////////////////////////
}
-51
View File
@@ -1,51 +0,0 @@
/*
* 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,23 +53,4 @@ 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);
}
}
+2 -8
View File
@@ -21,7 +21,6 @@
import {QAppSection} from "./QAppSection";
import {QAppTreeNode} from "./QAppTreeNode";
import {QHelpContent} from "./QHelpContent";
/*******************************************************************************
** Meta-Data to define an app in a QQQ instance.
@@ -37,7 +36,6 @@ export class QAppMetaData
widgets?: string[];
sections?: QAppSection[];
supplementalAppMetaData: Map<String, any> = new Map();
helpContent?: Map<string, QHelpContent[]>;
constructor(object: any)
{
@@ -66,17 +64,13 @@ 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,7 +31,6 @@ export class QAppSection
label: string;
icon?: QIcon;
tables?: string[];
apps?: string[];
processes?: string[];
reports?: string[];
@@ -55,9 +54,5 @@ export class QAppSection
{
this.tables = object.tables;
}
if (object.apps)
{
this.apps = object.apps;
}
}
}
-4
View File
@@ -33,10 +33,8 @@ export class QAppTreeNode
label: string;
type: QAppNodeType;
children?: QAppTreeNode[];
hideChildrenFromNavigation?: boolean;
iconName?: string;
icon?: QIcon;
appAffinity?: number;
constructor(object: any)
{
@@ -44,8 +42,6 @@ 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,8 +33,6 @@ export class QBrandingMetaData
logo?: string;
icon?: string;
accentColor?: string;
accentColorLight?: string;
gravatarDefault?: string;
// Deprecated
environmentBannerText?: string;
@@ -52,8 +50,6 @@ 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;
+4 -28
View File
@@ -36,37 +36,13 @@ 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);
}
}
+3 -65
View File
@@ -91,12 +91,11 @@ export class QFieldMetaData
this.helpContents = QHelpContent.buildArray(object.helpContents);
this.behaviors = object.behaviors;
const supplementalFieldMetaData = object.supplementalFieldMetaData || object.supplementalMetaData;
if (supplementalFieldMetaData)
if (object.supplementalFieldMetaData)
{
for (const key in supplementalFieldMetaData)
for (const key in object.supplementalFieldMetaData)
{
this.supplementalFieldMetaData.set(key, supplementalFieldMetaData[key]);
this.supplementalFieldMetaData.set(key, object.supplementalFieldMetaData[key]);
}
}
@@ -136,65 +135,4 @@ 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,26 +105,4 @@ 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,15 +35,4 @@ export class QIcon
this.path = object.path;
this.color = object.color;
}
/***************************************************************************
*
***************************************************************************/
public clone(): QIcon
{
const clone = new QIcon({
...this
});
return (clone);
}
}
+2 -49
View File
@@ -45,8 +45,6 @@ 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)
{
@@ -127,44 +125,6 @@ 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});
}
}
}
@@ -182,7 +142,7 @@ export class QInstance
*******************************************************************************/
getTablePath(table: QTableMetaData): string | null
{
return this.getTablePathByName(table.name);
return QInstance.searchAppTreeForPath(this.appTree, table.name, QAppNodeType.TABLE, 1, "");
}
/*******************************************************************************
@@ -190,14 +150,7 @@ export class QInstance
*******************************************************************************/
getTablePathByName(tableName: string): string | null
{
const value = this.pathsMap[tableName];
if(value)
{
value.sort((a, b) => b.affinity - a.affinity);
return (value[0].path);
}
return (null);
return QInstance.searchAppTreeForPath(this.appTree, tableName, QAppNodeType.TABLE, 1, "");
}
/*******************************************************************************
-11
View File
@@ -37,15 +37,4 @@ export class QJoinMetaData
this.leftTable = object.leftTable;
this.rightTable = object.rightTable;
}
/***************************************************************************
*
***************************************************************************/
public clone(): QJoinMetaData
{
const clone = new QJoinMetaData({
...this
});
return (clone);
}
}
-69
View File
@@ -1,69 +0,0 @@
/*
* 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
@@ -1,75 +0,0 @@
/*
* 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);
}
}
+1 -122
View File
@@ -22,9 +22,7 @@
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.
@@ -37,7 +35,6 @@ export class QTableMetaData
isHidden: boolean = false;
primaryKeyField: string;
fields?: Map<string, QFieldMetaData>;
virtualFields?: Map<string, QVirtualFieldMetaData>;
iconName?: string;
sections?: QTableSection[];
exposedJoins?: QExposedJoin[];
@@ -51,7 +48,6 @@ export class QTableMetaData
supplementalTableMetaData: Map<String, any> = new Map();
shareableTableMetaData: any;
helpContent?: Map<string, QHelpContent[]>;
menus?: QMenu[];
constructor(object: any)
{
@@ -76,15 +72,6 @@ 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 = [];
@@ -106,7 +93,7 @@ export class QTableMetaData
this.capabilities = new Set<string>();
if (object.capabilities)
{
for (let i = 0; i < object.capabilities.length; i++)
for (var i = 0; i < object.capabilities.length; i++)
{
this.capabilities.add(object.capabilities[i]);
}
@@ -123,113 +110,5 @@ 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);
}
}
+1 -57
View File
@@ -19,7 +19,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Collapsible} from "./Collapsible";
import {QHelpContent} from "./QHelpContent";
/*******************************************************************************
@@ -37,8 +36,6 @@ export class QTableSection
isHidden: boolean;
gridColumns?: number;
helpContents?: QHelpContent[];
alternatives?: Map<string, QTableSection>;
collapsible?: Collapsible;
/*******************************************************************************
@@ -49,7 +46,7 @@ export class QTableSection
this.name = object.name;
this.label = object.label;
this.tier = object.tier;
this.iconName = object.icon ? object.icon.name : object.iconName;
this.iconName = object.icon ? object.icon.name : null;
if (object.fieldNames)
{
@@ -61,59 +58,6 @@ 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);
}
}
@@ -1,64 +0,0 @@
/*
* 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,7 +19,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Collapsible} from "./Collapsible";
import {QHelpContent} from "./QHelpContent";
import {QIcon} from "./QIcon";
@@ -52,7 +51,6 @@ export class QWidgetMetaData
helpContent?: Map<string, QHelpContent[]>;
defaultValues?: Map<string, any>;
collapsible?: Collapsible;
constructor(object: any)
{
@@ -91,9 +89,5 @@ export class QWidgetMetaData
}
}
if (object.collapsible)
{
this.collapsible = new Collapsible(object.collapsible);
}
}
}
-54
View File
@@ -1,54 +0,0 @@
/*
* 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,7 +19,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {FieldFunction} from "./FieldFunction";
import {QCriteriaOperator} from "./QCriteriaOperator";
/*******************************************************************************
@@ -32,7 +31,6 @@ export class QFilterCriteria
operator: QCriteriaOperator;
values!: any[];
otherFieldName?: string;
fieldFunction?: FieldFunction;
constructor(fieldName: string, operator: QCriteriaOperator, values: any[])
{
@@ -40,27 +38,4 @@ 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,16 +33,4 @@ 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,47 +107,4 @@ 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
@@ -1,64 +0,0 @@
/*
* 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
@@ -1,89 +0,0 @@
/*
* 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
@@ -1,451 +0,0 @@
/*
* 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
@@ -1,105 +0,0 @@
/*
* 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();
});
});
});
@@ -1,67 +0,0 @@
/*
* 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);
});
});
});