Compare commits

..
5 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
30 changed files with 585 additions and 1850 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.5.3
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:
- "*"
+4
View File
@@ -0,0 +1,4 @@
profile: frontend-core
orb_version: '0.8'
default_branch: develop
node_version: '24.21'
+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
+4 -16
View File
@@ -1,12 +1,12 @@
{
"name": "@qrunio/qqq-frontend-core",
"version": "0.36.0-SNAPSHOT",
"name": "@kingsrook/qqq-frontend-core",
"version": "1.0.127-SNAPSHOT",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@qrunio/qqq-frontend-core",
"version": "0.36.0-SNAPSHOT",
"name": "@kingsrook/qqq-frontend-core",
"version": "1.0.127-SNAPSHOT",
"license": "ISC",
"dependencies": {
"axios": "0.28.1",
@@ -74,7 +74,6 @@
"integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.27.1",
@@ -1129,7 +1128,6 @@
"integrity": "sha512-mpT8LJJ4CMeeahobofYWIjFo0xonRS/HfxnVEPMPFSQdGUt1uHCnoPT7Zhb+sjDU2wz0oKV0OLUR0WzrHNgfeA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"jest-matcher-utils": "^27.0.0",
"pretty-format": "^27.0.0"
@@ -1230,7 +1228,6 @@
"integrity": "sha512-JaNYGkaQVhP6HNF+lkdOr2cAs2wdSZBoalE22uYWq8IEv/OVH0RksSGydk+sW8cLoSeYmC+OHvRyv2i4AQ7Czg==",
"dev": true,
"license": "BSD-2-Clause",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "5.10.2",
"@typescript-eslint/types": "5.10.2",
@@ -1397,7 +1394,6 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -1904,7 +1900,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001735",
"electron-to-chromium": "^1.5.204",
@@ -2719,7 +2714,6 @@
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint/eslintrc": "^1.0.5",
"@humanwhocodes/config-array": "^0.9.2",
@@ -2896,7 +2890,6 @@
"integrity": "sha512-/KJBASVFxpu0xg1kIBn9AUa8hQVnszpwgE7Ld0lKAlx7Ie87yzEzCgSkekt+le/YVhiaosO4Y14GDAOc41nfxA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"array-includes": "^3.1.4",
"array.prototype.flat": "^1.2.5",
@@ -2955,7 +2948,6 @@
"integrity": "sha512-sVCFKX9fllURnXT2JwLN5Qgo24Ug5NF6dxhkmxsMEUZhXRcGg+X3e1JbJ84YePQKBl5E0ZjAH5Q4rkdcGY99+g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.16.3",
"aria-query": "^4.2.2",
@@ -2983,7 +2975,6 @@
"integrity": "sha512-IOlFIRHzWfEQQKcAD4iyYDndHwTQiCMcJVJjxempf203jnNLUnW34AXLrV33+nEXoifJE2ZEGmcjKPL8957eSw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"array-includes": "^3.1.4",
"array.prototype.flatmap": "^1.2.5",
@@ -3013,7 +3004,6 @@
"integrity": "sha512-XslZy0LnMn+84NEG9jSGR6eGqaZB3133L8xewQo3fQagbQuGt7a63gf+P1NGKZavEYEC3UXaWEAA/AqDkuN6xA==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
},
@@ -4547,7 +4537,6 @@
"integrity": "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@jest/core": "^27.5.1",
"import-local": "^3.0.2",
@@ -7224,7 +7213,6 @@
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@qrunio/qqq-frontend-core",
"version": "0.36.0-RC.4",
"version": "1.1.0",
"description": "",
"main": "qqq-frontend-core.js",
"module": "lib/qqq-frontend-core.esm.js",
+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 //
////////////////////////////////////////////////////////////////////////
}
-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
View File
@@ -35,7 +35,6 @@ export class QAppTreeNode
children?: QAppTreeNode[];
iconName?: string;
icon?: QIcon;
appAffinity?: number;
constructor(object: any)
{
@@ -43,7 +42,6 @@ export class QAppTreeNode
this.label = object.label;
this.type = object.type;
this.iconName = object.iconName;
this.appAffinity = object.appAffinity;
if (object.icon)
{
-21
View File
@@ -45,25 +45,4 @@ export class QExposedJoin
this.joinPath.push(new QJoinMetaData(object.joinPath[i]));
}
}
/***************************************************************************
*
***************************************************************************/
public clone(): QExposedJoin
{
const joinTableClone = (this.joinTable as any).clone();
const joinPathClone: QJoinMetaData[] = [];
for (let joinMetaData of this.joinPath)
{
joinPathClone.push((joinMetaData as any).clone());
}
const clone = new QExposedJoin({
...this,
joinTable: joinTableClone,
joinPath: joinPathClone
});
return (clone);
}
}
-61
View File
@@ -135,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 -56
View File
@@ -27,7 +27,6 @@ import {QHelpContent} from "./QHelpContent";
import {QProcessMetaData} from "./QProcessMetaData";
import {QReportMetaData} from "./QReportMetaData";
import {QTableMetaData} from "./QTableMetaData";
import {QThemeMetaData} from "./QThemeMetaData";
import {QWidgetMetaData} from "./QWidgetMetaData";
/*******************************************************************************
@@ -43,12 +42,9 @@ export class QInstance
apps?: Map<string, QAppMetaData>;
appTree?: QAppTreeNode[];
branding?: QBrandingMetaData;
theme?: QThemeMetaData;
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)
{
@@ -126,52 +122,9 @@ export class QInstance
{
this.supplementalInstanceMetaData.set(key, object.supplementalInstanceMetaData[key]);
}
if (object.supplementalInstanceMetaData["theme"])
{
this.theme = new QThemeMetaData(object.supplementalInstanceMetaData["theme"]);
}
}
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});
}
}
}
@@ -189,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, "");
}
/*******************************************************************************
@@ -197,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 -121
View File
@@ -22,7 +22,6 @@
import {QExposedJoin} from "./QExposedJoin";
import {QFieldMetaData} from "./QFieldMetaData";
import {QHelpContent} from "./QHelpContent";
import {QMenu} from "./QMenu";
import {QTableSection} from "./QTableSection";
/*******************************************************************************
@@ -36,7 +35,6 @@ export class QTableMetaData
isHidden: boolean = false;
primaryKeyField: string;
fields?: Map<string, QFieldMetaData>;
virtualFields?: Map<string, QFieldMetaData>;
iconName?: string;
sections?: QTableSection[];
exposedJoins?: QExposedJoin[];
@@ -50,7 +48,6 @@ export class QTableMetaData
supplementalTableMetaData: Map<String, any> = new Map();
shareableTableMetaData: any;
helpContent?: Map<string, QHelpContent[]>;
menus?: QMenu[];
constructor(object: any)
{
@@ -75,15 +72,6 @@ export class QTableMetaData
}
}
if (object.virtualFields)
{
this.virtualFields = new Map<string, QFieldMetaData>();
for (const key in object.virtualFields)
{
this.virtualFields.set(key, new QFieldMetaData(object.virtualFields[key]));
}
}
if (object.sections)
{
this.sections = [];
@@ -105,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]);
}
@@ -122,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 -47
View File
@@ -36,7 +36,6 @@ export class QTableSection
isHidden: boolean;
gridColumns?: number;
helpContents?: QHelpContent[];
alternatives?: Map<string, QTableSection>;
/*******************************************************************************
@@ -47,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)
{
@@ -59,51 +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]));
}
}
}
/***************************************************************************
*
***************************************************************************/
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 clone = new QTableSection({...this});
clone.fieldNames = fieldNamesClone;
clone.helpContents = helpContentsClone;
clone.alternatives = alternativesClone;
return (clone);
}
}
-312
View File
@@ -1,312 +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/>.
*/
/*******************************************************************************
** Meta-Data to define theme configuration in a QQQ instance.
**
*******************************************************************************/
export class QThemeMetaData
{
// Color palette
primaryColor?: string;
secondaryColor?: string;
backgroundColor?: string;
surfaceColor?: string;
textPrimary?: string;
textSecondary?: string;
errorColor?: string;
warningColor?: string;
successColor?: string;
infoColor?: string;
// Typography - Base
fontFamily?: string;
headerFontFamily?: string;
monoFontFamily?: string;
fontSizeBase?: string;
fontWeightLight?: number;
fontWeightRegular?: number;
fontWeightMedium?: number;
fontWeightBold?: number;
// Typography - H1
typographyH1FontSize?: string;
typographyH1FontWeight?: number;
typographyH1LineHeight?: number;
typographyH1LetterSpacing?: string;
typographyH1TextTransform?: string;
// Typography - H2
typographyH2FontSize?: string;
typographyH2FontWeight?: number;
typographyH2LineHeight?: number;
typographyH2LetterSpacing?: string;
typographyH2TextTransform?: string;
// Typography - H3
typographyH3FontSize?: string;
typographyH3FontWeight?: number;
typographyH3LineHeight?: number;
typographyH3LetterSpacing?: string;
typographyH3TextTransform?: string;
// Typography - H4
typographyH4FontSize?: string;
typographyH4FontWeight?: number;
typographyH4LineHeight?: number;
typographyH4LetterSpacing?: string;
typographyH4TextTransform?: string;
// Typography - H5
typographyH5FontSize?: string;
typographyH5FontWeight?: number;
typographyH5LineHeight?: number;
typographyH5LetterSpacing?: string;
typographyH5TextTransform?: string;
// Typography - H6
typographyH6FontSize?: string;
typographyH6FontWeight?: number;
typographyH6LineHeight?: number;
typographyH6LetterSpacing?: string;
typographyH6TextTransform?: string;
// Typography - Body1
typographyBody1FontSize?: string;
typographyBody1FontWeight?: number;
typographyBody1LineHeight?: number;
typographyBody1LetterSpacing?: string;
typographyBody1TextTransform?: string;
// Typography - Body2
typographyBody2FontSize?: string;
typographyBody2FontWeight?: number;
typographyBody2LineHeight?: number;
typographyBody2LetterSpacing?: string;
typographyBody2TextTransform?: string;
// Typography - Button
typographyButtonFontSize?: string;
typographyButtonFontWeight?: number;
typographyButtonLineHeight?: number;
typographyButtonLetterSpacing?: string;
typographyButtonTextTransform?: string;
// Typography - Caption
typographyCaptionFontSize?: string;
typographyCaptionFontWeight?: number;
typographyCaptionLineHeight?: number;
typographyCaptionLetterSpacing?: string;
typographyCaptionTextTransform?: string;
// Sizing
borderRadius?: string;
density?: "compact" | "normal" | "comfortable";
// Asset paths
logoPath?: string;
iconPath?: string;
faviconPath?: string;
// Custom CSS
customCss?: string;
// Icon style
iconStyle?: "filled" | "outlined" | "rounded" | "sharp" | "two-tone";
// Branded Header Bar
brandedHeaderEnabled?: boolean;
brandedHeaderBackgroundColor?: string;
brandedHeaderTextColor?: string;
brandedHeaderLogoPath?: string;
brandedHeaderLogoAltText?: string;
brandedHeaderHeight?: string;
brandedHeaderTagline?: string;
// App Bar
appBarBackgroundColor?: string;
appBarTextColor?: string;
// Sidebar
sidebarBackgroundColor?: string;
sidebarTextColor?: string;
sidebarIconColor?: string;
sidebarSelectedBackgroundColor?: string;
sidebarSelectedTextColor?: string;
sidebarHoverBackgroundColor?: string;
sidebarDividerColor?: string;
// Tables
tableHeaderBackgroundColor?: string;
tableHeaderTextColor?: string;
tableRowHoverColor?: string;
tableRowSelectedColor?: string;
tableBorderColor?: string;
// General
dividerColor?: string;
borderColor?: string;
cardBorderColor?: string;
constructor(object: any)
{
// Color palette
this.primaryColor = object.primaryColor;
this.secondaryColor = object.secondaryColor;
this.backgroundColor = object.backgroundColor;
this.surfaceColor = object.surfaceColor;
this.textPrimary = object.textPrimary;
this.textSecondary = object.textSecondary;
this.errorColor = object.errorColor;
this.warningColor = object.warningColor;
this.successColor = object.successColor;
this.infoColor = object.infoColor;
// Typography - Base
this.fontFamily = object.fontFamily;
this.headerFontFamily = object.headerFontFamily;
this.monoFontFamily = object.monoFontFamily;
this.fontSizeBase = object.fontSizeBase;
this.fontWeightLight = object.fontWeightLight;
this.fontWeightRegular = object.fontWeightRegular;
this.fontWeightMedium = object.fontWeightMedium;
this.fontWeightBold = object.fontWeightBold;
// Typography - H1
this.typographyH1FontSize = object.typographyH1FontSize;
this.typographyH1FontWeight = object.typographyH1FontWeight;
this.typographyH1LineHeight = object.typographyH1LineHeight;
this.typographyH1LetterSpacing = object.typographyH1LetterSpacing;
this.typographyH1TextTransform = object.typographyH1TextTransform;
// Typography - H2
this.typographyH2FontSize = object.typographyH2FontSize;
this.typographyH2FontWeight = object.typographyH2FontWeight;
this.typographyH2LineHeight = object.typographyH2LineHeight;
this.typographyH2LetterSpacing = object.typographyH2LetterSpacing;
this.typographyH2TextTransform = object.typographyH2TextTransform;
// Typography - H3
this.typographyH3FontSize = object.typographyH3FontSize;
this.typographyH3FontWeight = object.typographyH3FontWeight;
this.typographyH3LineHeight = object.typographyH3LineHeight;
this.typographyH3LetterSpacing = object.typographyH3LetterSpacing;
this.typographyH3TextTransform = object.typographyH3TextTransform;
// Typography - H4
this.typographyH4FontSize = object.typographyH4FontSize;
this.typographyH4FontWeight = object.typographyH4FontWeight;
this.typographyH4LineHeight = object.typographyH4LineHeight;
this.typographyH4LetterSpacing = object.typographyH4LetterSpacing;
this.typographyH4TextTransform = object.typographyH4TextTransform;
// Typography - H5
this.typographyH5FontSize = object.typographyH5FontSize;
this.typographyH5FontWeight = object.typographyH5FontWeight;
this.typographyH5LineHeight = object.typographyH5LineHeight;
this.typographyH5LetterSpacing = object.typographyH5LetterSpacing;
this.typographyH5TextTransform = object.typographyH5TextTransform;
// Typography - H6
this.typographyH6FontSize = object.typographyH6FontSize;
this.typographyH6FontWeight = object.typographyH6FontWeight;
this.typographyH6LineHeight = object.typographyH6LineHeight;
this.typographyH6LetterSpacing = object.typographyH6LetterSpacing;
this.typographyH6TextTransform = object.typographyH6TextTransform;
// Typography - Body1
this.typographyBody1FontSize = object.typographyBody1FontSize;
this.typographyBody1FontWeight = object.typographyBody1FontWeight;
this.typographyBody1LineHeight = object.typographyBody1LineHeight;
this.typographyBody1LetterSpacing = object.typographyBody1LetterSpacing;
this.typographyBody1TextTransform = object.typographyBody1TextTransform;
// Typography - Body2
this.typographyBody2FontSize = object.typographyBody2FontSize;
this.typographyBody2FontWeight = object.typographyBody2FontWeight;
this.typographyBody2LineHeight = object.typographyBody2LineHeight;
this.typographyBody2LetterSpacing = object.typographyBody2LetterSpacing;
this.typographyBody2TextTransform = object.typographyBody2TextTransform;
// Typography - Button
this.typographyButtonFontSize = object.typographyButtonFontSize;
this.typographyButtonFontWeight = object.typographyButtonFontWeight;
this.typographyButtonLineHeight = object.typographyButtonLineHeight;
this.typographyButtonLetterSpacing = object.typographyButtonLetterSpacing;
this.typographyButtonTextTransform = object.typographyButtonTextTransform;
// Typography - Caption
this.typographyCaptionFontSize = object.typographyCaptionFontSize;
this.typographyCaptionFontWeight = object.typographyCaptionFontWeight;
this.typographyCaptionLineHeight = object.typographyCaptionLineHeight;
this.typographyCaptionLetterSpacing = object.typographyCaptionLetterSpacing;
this.typographyCaptionTextTransform = object.typographyCaptionTextTransform;
// Sizing
this.borderRadius = object.borderRadius;
this.density = object.density;
// Asset paths
this.logoPath = object.logoPath;
this.iconPath = object.iconPath;
this.faviconPath = object.faviconPath;
// Custom CSS
this.customCss = object.customCss;
// Icon style
this.iconStyle = object.iconStyle;
// Branded Header Bar
this.brandedHeaderEnabled = object.brandedHeaderEnabled;
this.brandedHeaderBackgroundColor = object.brandedHeaderBackgroundColor;
this.brandedHeaderTextColor = object.brandedHeaderTextColor;
this.brandedHeaderLogoPath = object.brandedHeaderLogoPath;
this.brandedHeaderLogoAltText = object.brandedHeaderLogoAltText;
this.brandedHeaderHeight = object.brandedHeaderHeight;
this.brandedHeaderTagline = object.brandedHeaderTagline;
// App Bar
this.appBarBackgroundColor = object.appBarBackgroundColor;
this.appBarTextColor = object.appBarTextColor;
// Sidebar
this.sidebarBackgroundColor = object.sidebarBackgroundColor;
this.sidebarTextColor = object.sidebarTextColor;
this.sidebarIconColor = object.sidebarIconColor;
this.sidebarSelectedBackgroundColor = object.sidebarSelectedBackgroundColor;
this.sidebarSelectedTextColor = object.sidebarSelectedTextColor;
this.sidebarHoverBackgroundColor = object.sidebarHoverBackgroundColor;
this.sidebarDividerColor = object.sidebarDividerColor;
// Tables
this.tableHeaderBackgroundColor = object.tableHeaderBackgroundColor;
this.tableHeaderTextColor = object.tableHeaderTextColor;
this.tableRowHoverColor = object.tableRowHoverColor;
this.tableRowSelectedColor = object.tableRowSelectedColor;
this.tableBorderColor = object.tableBorderColor;
// General
this.dividerColor = object.dividerColor;
this.borderColor = object.borderColor;
this.cardBorderColor = object.cardBorderColor;
}
}
-16
View File
@@ -38,20 +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;
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);
}
}
-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);
}
-216
View File
@@ -1,216 +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 {QThemeMetaData} from "../../../src/model/metaData/QThemeMetaData";
describe("QThemeMetaData", () =>
{
describe("constructor", () =>
{
it("should construct with all properties from object", () =>
{
const theme = new QThemeMetaData({
primaryColor: "#FF0000",
secondaryColor: "#00FF00",
backgroundColor: "#0000FF",
surfaceColor: "#FFFFFF",
textPrimary: "#000000",
textSecondary: "#666666",
errorColor: "#FF0000",
warningColor: "#FFA500",
successColor: "#00FF00",
infoColor: "#0000FF",
fontFamily: "Roboto",
headerFontFamily: "Open Sans",
borderRadius: "8px",
density: "compact",
logoPath: "/assets/logo.svg",
iconPath: "/assets/icon.svg",
faviconPath: "/assets/favicon.ico",
customCss: "body { margin: 0; }",
iconStyle: "outlined"
});
expect(theme.primaryColor).toBe("#FF0000");
expect(theme.secondaryColor).toBe("#00FF00");
expect(theme.backgroundColor).toBe("#0000FF");
expect(theme.surfaceColor).toBe("#FFFFFF");
expect(theme.textPrimary).toBe("#000000");
expect(theme.textSecondary).toBe("#666666");
expect(theme.errorColor).toBe("#FF0000");
expect(theme.warningColor).toBe("#FFA500");
expect(theme.successColor).toBe("#00FF00");
expect(theme.infoColor).toBe("#0000FF");
expect(theme.fontFamily).toBe("Roboto");
expect(theme.headerFontFamily).toBe("Open Sans");
expect(theme.borderRadius).toBe("8px");
expect(theme.density).toBe("compact");
expect(theme.logoPath).toBe("/assets/logo.svg");
expect(theme.iconPath).toBe("/assets/icon.svg");
expect(theme.faviconPath).toBe("/assets/favicon.ico");
expect(theme.customCss).toBe("body { margin: 0; }");
expect(theme.iconStyle).toBe("outlined");
});
it("should handle empty object", () =>
{
const theme = new QThemeMetaData({});
expect(theme.primaryColor).toBeUndefined();
expect(theme.secondaryColor).toBeUndefined();
expect(theme.backgroundColor).toBeUndefined();
expect(theme.surfaceColor).toBeUndefined();
expect(theme.textPrimary).toBeUndefined();
expect(theme.textSecondary).toBeUndefined();
expect(theme.errorColor).toBeUndefined();
expect(theme.warningColor).toBeUndefined();
expect(theme.successColor).toBeUndefined();
expect(theme.infoColor).toBeUndefined();
expect(theme.fontFamily).toBeUndefined();
expect(theme.headerFontFamily).toBeUndefined();
expect(theme.borderRadius).toBeUndefined();
expect(theme.density).toBeUndefined();
expect(theme.logoPath).toBeUndefined();
expect(theme.iconPath).toBeUndefined();
expect(theme.faviconPath).toBeUndefined();
expect(theme.customCss).toBeUndefined();
expect(theme.iconStyle).toBeUndefined();
});
it("should handle partial object", () =>
{
const theme = new QThemeMetaData({
primaryColor: "#FF0000",
density: "normal"
});
expect(theme.primaryColor).toBe("#FF0000");
expect(theme.density).toBe("normal");
expect(theme.secondaryColor).toBeUndefined();
expect(theme.iconStyle).toBeUndefined();
});
});
describe("density type", () =>
{
it("should accept compact density", () =>
{
const theme = new QThemeMetaData({density: "compact"});
expect(theme.density).toBe("compact");
});
it("should accept normal density", () =>
{
const theme = new QThemeMetaData({density: "normal"});
expect(theme.density).toBe("normal");
});
it("should accept comfortable density", () =>
{
const theme = new QThemeMetaData({density: "comfortable"});
expect(theme.density).toBe("comfortable");
});
});
describe("iconStyle type", () =>
{
it("should accept filled iconStyle", () =>
{
const theme = new QThemeMetaData({iconStyle: "filled"});
expect(theme.iconStyle).toBe("filled");
});
it("should accept outlined iconStyle", () =>
{
const theme = new QThemeMetaData({iconStyle: "outlined"});
expect(theme.iconStyle).toBe("outlined");
});
it("should accept rounded iconStyle", () =>
{
const theme = new QThemeMetaData({iconStyle: "rounded"});
expect(theme.iconStyle).toBe("rounded");
});
it("should accept sharp iconStyle", () =>
{
const theme = new QThemeMetaData({iconStyle: "sharp"});
expect(theme.iconStyle).toBe("sharp");
});
it("should accept two-tone iconStyle", () =>
{
const theme = new QThemeMetaData({iconStyle: "two-tone"});
expect(theme.iconStyle).toBe("two-tone");
});
});
describe("color properties", () =>
{
it("should handle hex color formats", () =>
{
const theme = new QThemeMetaData({
primaryColor: "#F00",
secondaryColor: "#00FF00",
errorColor: "#FF0000FF"
});
expect(theme.primaryColor).toBe("#F00");
expect(theme.secondaryColor).toBe("#00FF00");
expect(theme.errorColor).toBe("#FF0000FF");
});
});
describe("asset paths", () =>
{
it("should handle various path formats", () =>
{
const theme = new QThemeMetaData({
logoPath: "/assets/logo.svg",
iconPath: "https://example.com/icon.png",
faviconPath: "./favicon.ico"
});
expect(theme.logoPath).toBe("/assets/logo.svg");
expect(theme.iconPath).toBe("https://example.com/icon.png");
expect(theme.faviconPath).toBe("./favicon.ico");
});
});
describe("customCss", () =>
{
it("should handle multi-line CSS", () =>
{
const css = `
body {
margin: 0;
padding: 0;
}
.custom-class {
color: red;
}
`;
const theme = new QThemeMetaData({customCss: css});
expect(theme.customCss).toBe(css);
});
});
});
-334
View File
@@ -1,334 +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 {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";
describe("QTableMetaData tests", () =>
{
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"
}
],
"supplementalFieldMetaData": {
"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",
"supplementalFieldMetaData": {
"materialDashboard": {
"formAdjusterIdentifier": "table:myTable;field:type",
"onChangeFormAdjuster": {
"name": "com.qqq.MyTableFormAdjuster",
"codeType": "JAVA"
},
"type": "materialDashboard"
}
}
},
},
"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": [],
"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.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);
////////////////////////////////////////////////////////////////////////////////////////////////
// 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);
if (cloneTable.sections)
{
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);
if (cloneTable.helpContent && table.helpContent) // avoid possibly-undef and ?. on left-hand sides below
{
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);
}
else
{
fail("help content wasn't set in either table or cloneTable (or neither)");
}
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);
});
});