Compare commits

...

10 Commits

Author SHA1 Message Date
Matt 3a8e601a37 Refactor nginx setup to use host-level configuration
- Remove main nginx ingress container from docker-compose
- Add minimal api-nginx container for FastCGI to HTTP conversion
- Expose services directly on ports 7654 (API) and 7655 (UI)
- Add comprehensive NGINX_SETUP.md documentation
- Include example host nginx configuration
- Update docker setup script for new architecture

This change allows OpnForm to integrate better with existing host nginx
setups by removing the containerized ingress and exposing services
directly to the host for reverse proxy configuration.
2025-06-05 17:42:01 +02:00
Chirag Chhatrala a11fb01bef
ESC-590 - Fix UUID and Auto Increment ID Generation Logic in StoreFormSubmissio… (#774)
* Fix UUID and Auto Increment ID Generation Logic in StoreFormSubmissionJob

- Updated the conditions for generating UUID and auto-increment IDs in the `StoreFormSubmissionJob` to ensure they only trigger when the answer value is not provided. This change enhances the logic for handling form submissions, particularly for users with non-pro subscriptions, by preventing unnecessary ID generation when an answer is already present.

These modifications aim to improve the accuracy of form submissions and ensure proper handling of ID generation based on user subscription status.

* Enhance ID Generation Logic in StoreFormSubmissionJob

- Updated conditions for UUID and auto-increment ID generation in `StoreFormSubmissionJob` to ensure they only trigger when the answer value is either not provided or invalid. This change improves the accuracy of ID generation based on user subscription status, preventing unnecessary ID creation when valid input is present.

These modifications aim to enhance the reliability of form submissions and ensure proper handling of ID generation features.

* Test  case
2025-06-05 16:58:24 +02:00
Chirag Chhatrala a140f789c2
Add Tabler Icons and Refactor Form Components (#771)
- Updated `package.json` and `package-lock.json` to include `@iconify-json/tabler` for additional icon support.
- Refactored `ImageInput.vue` to utilize `nuxt/icon` for icon rendering, enhancing consistency across components.
- Introduced `OptionSelectorInput.vue` as a new form component for selecting options in a grid layout, integrating with the form system.
- Updated `FormCustomization.vue` and `FormEditorPreview.vue` to utilize the new `OptionSelectorInput` for improved user experience in form settings.
- Enhanced `HiddenRequiredDisabled.vue` to replace manual button rendering with `OptionSelectorInput`, streamlining the component structure.

These changes aim to improve the iconography and form component functionality, providing a more cohesive and user-friendly interface.
2025-06-02 15:54:38 +02:00
Julien Nahum 9a42aacc3a Enhance Barcode Input and File Input Components with Client-Only Wrapper
- Updated `nuxt.config.ts` to include `@zxing/library` in the transpile list, ensuring compatibility with the new barcode scanning functionality.
- Modified `BarcodeInput.vue` and `FileInput.vue` components to wrap the `CameraUpload` component in a `<ClientOnly>` tag, preventing server-side rendering issues and improving the user experience during barcode scanning.

These changes aim to enhance the functionality of the barcode scanning feature while ensuring proper rendering in client-side environments.
2025-05-30 11:49:13 +02:00
Chirag Chhatrala b47a528075
Update dependencies and refactor CameraUpload component to use ZXing … (#760)
* Update dependencies and refactor CameraUpload component to use ZXing for barcode scanning

- Removed `@gtm-support/vue-gtm` from dependencies in `package-lock.json` and `package.json`.
- Added `@zxing/library` to dependencies for improved barcode scanning functionality.
- Refactored `CameraUpload.vue` to replace Quagga with ZXing for barcode detection, enhancing performance and reliability.
- Updated `FieldOptions.vue` to include QR Code as a selectable option for barcode scanning.

These changes aim to streamline the barcode scanning process and ensure the application utilizes the latest libraries for optimal performance.

* Self hosted domain redirect (#756)

* Integration pages from Notion

* Self hosted domain redirect

* Refactor root-redirect middleware and update 404 page layout

- Simplified the `root-redirect.js` middleware by removing the specific route checks, allowing for a more general redirect based on the `self_hosted` feature flag.
- Updated the 404 error page in `[...all].vue` by removing the unnecessary `NuxtLayout` wrapper and replacing `NuxtLink` with a custom `UButton` for navigation, enhancing the overall layout and user experience.

These changes aim to streamline the redirect logic and improve the presentation of the 404 error page.

* Refactor feature flag handling and update middleware

- Updated the condition in `Navbar.vue` to correctly evaluate the feature flags for rendering the AI form builder link.
- Removed the `feature-flags.global.js` middleware as it was no longer needed, streamlining the middleware structure.
- Enhanced the `root-redirect.js` middleware to utilize `h3`'s `sendRedirect` for server-side redirection, improving the redirect logic.
- Modified the `ai-form-builder.vue` page to include the new `root-redirect` middleware, ensuring proper redirection based on feature flags.

These changes aim to improve the handling of feature flags and redirection logic, enhancing the overall application flow.

---------

Co-authored-by: Julien Nahum <julien@nahum.net>

* Remove Quagga dependency from package.json and package-lock.json to streamline barcode scanning functionality. This change is part of the ongoing effort to enhance performance by utilizing more efficient libraries, following the recent integration of ZXing for barcode detection.

* Enhance Docker Configuration and Health Checks (#761)

* Enhance Docker Configuration and Health Checks

- Added PHP configuration settings in `docker-compose.dev.yml` and `docker-compose.yml` to improve memory management and execution limits, ensuring better performance for PHP applications.
- Introduced health checks for various services including `api`, `api-worker`, `api-scheduler`, `ui`, `redis`, and `db` to ensure service availability and reliability.
- Updated environment variables in `.env.docker` and `client/.env.docker` to include new keys for H-Captcha and reCAPTCHA, enhancing security features.
- Refactored the PHP-FPM entrypoint script to apply PHP configurations dynamically based on environment variables, improving flexibility in deployment.
- Removed outdated PHP configuration files to streamline the Docker setup.

These changes aim to enhance the overall stability, performance, and security of the application in a Dockerized environment.

* Refactor Dockerfile for Improved Build Process

- Changed the Dockerfile to utilize a multi-stage build approach, separating the build and runtime environments for better efficiency.
- Introduced a builder stage using the PHP CLI image to install dependencies and extensions, optimizing the final image size.
- Removed unnecessary installation steps and combined related commands to streamline the Dockerfile, enhancing readability and maintainability.
- Updated the runtime stage to use the PHP FPM Alpine image, ensuring a smaller and more secure production environment.

These changes aim to improve the build process, reduce image size, and enhance the overall performance of the Dockerized application.

* Refactor Environment Variables Documentation

- Consolidated the documentation for environment variables in `environment-variables.mdx` by removing duplicate entries for `JWT_TTL` and `JWT_SECRET`, ensuring clarity and reducing redundancy.
- Introduced a new section for PHP configuration environment variables, detailing important settings such as `PHP_MEMORY_LIMIT`, `PHP_MAX_EXECUTION_TIME`, and others, to aid users in optimizing their PHP applications.
- Added a section for database configuration environment variables, providing essential details for connecting the Laravel backend to the database.

These changes aim to enhance the documentation's clarity and comprehensiveness, making it easier for users to configure their environments effectively.

* Update Environment Configuration for Production Deployment

- Changed the `APP_ENV` variable in the `.env.docker` file from `local` to `production`, aligning the environment settings with the deployment requirements for the production environment. This change is essential for ensuring that the application runs with the appropriate configurations and optimizations for a live setting.

This update aims to facilitate a smoother transition to production by ensuring the correct environment variables are set.

* Update Environment Variables in .env.docker

- Removed the `BROADCAST_CONNECTION` and `SESSION_LIFETIME` variables from the `.env.docker` file, streamlining the environment configuration for better clarity and relevance.
- Retained essential Redis configurations for caching and session management, ensuring that the application maintains its performance and functionality.

This change aims to simplify the environment setup by eliminating unnecessary variables that are no longer needed.

* Refactor Docker Entrypoint and Environment Variables

- Removed the `IS_API_WORKER` environment variable from `docker-compose.yml`, simplifying the configuration for API services and aligning with the new role-based command handling.
- Updated the `php-fpm-entrypoint` script to determine the role (API, worker, or scheduler) based on the command being executed, enhancing flexibility and clarity in service initialization.
- Streamlined the setup process for different roles, ensuring that appropriate commands are executed based on the determined role, which improves maintainability and reduces potential errors.

These changes aim to enhance the Docker configuration by adopting a more dynamic approach to service roles, improving the overall deployment process.

* Enhance PHP Configuration Defaults in Docker Entrypoint

- Updated the `php-fpm-entrypoint` script to set default values for PHP configuration settings, including `memory_limit`, `max_execution_time`, `upload_max_filesize`, and `post_max_size`, when not explicitly provided through environment variables. This change ensures that the application has sensible defaults, improving reliability and reducing potential misconfigurations during deployment.

These modifications aim to enhance the Docker setup by providing fallback values for critical PHP settings, thereby streamlining the configuration process and ensuring better performance in various environments.

* Update Docker Compose Configuration for Storage Volume

- Changed the storage volume mapping in `docker-compose.yml` from a relative path (`./api/storage`) to a named volume (`opnform_storage`). This modification enhances portability and consistency across different environments by utilizing Docker's volume management, ensuring that the storage is properly managed and isolated.

This change aims to improve the Docker setup by leveraging named volumes for better data persistence and management.

* Update Documentation and Configuration for Subdomain Redirect

- Added a link to the new "Subdomain Redirect Configuration" page in `environment-variables.mdx` to enhance user guidance on setting up subdomain redirects.
- Updated the `mint.json` file to include "configuration/subdomain-redirect" in the configuration paths, ensuring it is recognized in the documentation structure.

These changes aim to improve the documentation's comprehensiveness and provide users with clear instructions for configuring subdomain redirects, thereby enhancing the overall setup experience.

* Enhance Form Management with Dynamic Configuration Updates

- Added a new watcher in `OpenCompleteForm.vue` to monitor changes to the form prop and update the form manager accordingly, ensuring that the form manager reflects the latest configuration.
- Introduced an `updateConfig` method in `useFormManager.js` to handle updates to the form configuration, resetting the form state and reinitializing with the new config. This improves the flexibility and responsiveness of the form management system.

These changes aim to enhance the user experience by ensuring that form updates are seamlessly integrated into the form management workflow, allowing for more dynamic interactions.

* Refactor FlatSelectInput Component to Simplify Template Structure

- Removed the conditional rendering of the selected options slot in `FlatSelectInput.vue`, streamlining the template for better readability and maintainability. This change focuses on enhancing the component's clarity by eliminating unnecessary complexity in the markup.

These modifications aim to improve the overall structure of the FlatSelectInput component, making it easier to understand and work with in future development.

* Enhance Application Optimization in Docker Entrypoint

- Added a cache clearing step in the `optimize_application` function of the `php-fpm-entrypoint` script. This new line executes `php ./artisan optimize:clear` before optimizing the application, ensuring that any stale cache is removed, which can lead to improved performance and reliability during application optimization.

This change aims to enhance the application's responsiveness by ensuring that the optimization process starts with a clean state, thereby reducing potential issues related to outdated cached data.

* Refactored UI

* Refactor CameraUpload Component for Improved Webcam Handling

- Updated the CameraUpload.vue component to replace element IDs with Vue refs for better reactivity and maintainability. This change enhances the component's structure by utilizing Vue's reference system, allowing for more efficient DOM manipulation.
- Modified the webcam initialization logic to accommodate both regular and barcode modes, ensuring that the appropriate setup is executed based on the current mode.
- Improved error handling and cleanup processes for the webcam stream, enhancing the robustness of the component during camera operations.
- Streamlined the barcode scanning logic by using ZXing's decodeFromConstraints method, which simplifies the handling of video streams and improves performance.

These modifications aim to enhance the overall functionality and reliability of the CameraUpload component, providing a better user experience during webcam interactions.

* Enhance Barcode Decoder Options and Add QR Code Reader Configuration

- Added 'QR Code' to the barcode decoders options in `FieldOptions.vue`, ensuring users can select this option for scanning.
- Updated `blocks_types.json` to include a new configuration for the QR Code Reader, providing default values and settings for its integration.
- Refactored the `working_form.js` store to accommodate changes in block definitions, ensuring that the effective type and settings are applied correctly when adding blocks.

These modifications aim to improve the functionality and user experience of barcode scanning within the application.

---------

Co-authored-by: Julien Nahum <julien@nahum.net>
2025-05-30 10:30:19 +02:00
Chirag Chhatrala cac88e7a3c
ESC-575 If operator has no format and no expected_type, it means it doesn't n… (#767)
* If operator has no format and no expected_type, it means it doesn't need input

* Add testcase for integration logic with checkbox

---------

Co-authored-by: Julien Nahum <julien@nahum.net>
2025-05-28 10:05:24 +02:00
Julien Nahum 360b116062 Enhance Form Manager with Reactive Mode Handling
- Introduced a reactive reference for the `mode` prop in `OpenCompleteForm.vue`, allowing for dynamic updates to the form mode.
- Updated the `useFormManager` function to accept a reactive `mode` reference, ensuring that the form management logic adapts to changes in the mode dynamically.
- Modified the strategy computation in `useFormManager` to utilize the reactive `mode`, improving the responsiveness of the form management system.

These changes aim to enhance the flexibility and adaptability of the form management system by allowing real-time updates to the form mode, thereby improving user interactions and overall experience.
2025-05-27 20:51:53 +02:00
JhumanJ 72a87f1de8 Revert docker-compose.yml to use pre-built images - switches back from local builds to published Docker images 2025-05-27 18:17:18 +02:00
JhumanJ 61fc30b95c Enhance Pinia History Plugin with State Filtering and Configuration Options
- Added an `ignoreKeys` option in the `mergeOptions` function of `pinia-history.js` to specify keys that should be excluded from history tracking, improving the plugin's flexibility.
- Introduced a new `filterState` function to filter out ignored keys from the state object, ensuring that only relevant state changes are tracked.
- Updated the history management logic to utilize the filtered state, enhancing the accuracy of undo/redo functionality.
- Modified the `working_form.js` store to include default ignored keys for history tracking, streamlining the configuration process.

These changes aim to improve the usability and performance of the Pinia history plugin by allowing developers to customize which state properties are tracked, thereby reducing unnecessary history entries.
2025-05-27 17:45:16 +02:00
Julien Nahum f3a02df80e
Version in self hosted (#770)
* Enhance Application Version Management in Docker and Feature Flags

- Added a new build argument `APP_VERSION` in the Docker configuration files to facilitate version tracking during builds.
- Introduced a private method `getAppVersion` in `FeatureFlagsController` to retrieve the application version from the Docker environment, enhancing the feature flags response with version information.
- Updated the `app.php` configuration file to include a new entry for `docker_version`, allowing for better version management and fallback when the Docker build version is unavailable.

These changes aim to improve the application's versioning capabilities, ensuring that the version is consistently available across different components and environments.

* Refactor OpenFormFooter and Update Feature Flags Handling

- Modified `OpenFormFooter.vue` to include a version display when available, enhancing user awareness of the application version.
- Refactored the script section to use the `<script setup>` syntax, improving readability and reactivity by utilizing `ref` and `computed` for reactive properties.
- Removed the deprecated `feature-flags.js` plugin and updated `featureFlags.js` to ensure proper SSR compatibility and error handling during feature flag fetching.

These changes aim to improve the user interface by providing version information and enhance the overall code structure for better maintainability and performance.
2025-05-27 17:44:42 +02:00
35 changed files with 1189 additions and 1037 deletions

View File

@ -60,6 +60,7 @@ jobs:
build-args: | build-args: |
APP_ENV=${{ env.VERSION == 'dev' && 'local' || 'production' }} APP_ENV=${{ env.VERSION == 'dev' && 'local' || 'production' }}
COMPOSER_FLAGS=${{ env.VERSION == 'dev' && '--optimize-autoloader --no-interaction' || '--no-dev --optimize-autoloader --no-interaction' }} COMPOSER_FLAGS=${{ env.VERSION == 'dev' && '--optimize-autoloader --no-interaction' || '--no-dev --optimize-autoloader --no-interaction' }}
APP_VERSION=${{ env.VERSION }}
tags: ${{ env.API_TAGS }} tags: ${{ env.API_TAGS }}
cache-from: type=registry,ref=${{secrets.DOCKER_API_REPO}}:dev cache-from: type=registry,ref=${{secrets.DOCKER_API_REPO}}:dev
cache-to: type=inline cache-to: type=inline
@ -71,6 +72,8 @@ jobs:
file: docker/Dockerfile.client file: docker/Dockerfile.client
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
push: true push: true
build-args: |
APP_VERSION=${{ env.VERSION }}
tags: ${{ env.UI_TAGS }} tags: ${{ env.UI_TAGS }}
cache-from: type=registry,ref=${{secrets.DOCKER_UI_REPO}}:dev cache-from: type=registry,ref=${{secrets.DOCKER_UI_REPO}}:dev
cache-to: type=inline cache-to: type=inline

131
NGINX_SETUP.md Normal file
View File

@ -0,0 +1,131 @@
# OpnForm Nginx Setup Guide
This guide explains how to set up OpnForm with a host-level nginx configuration.
## Architecture Overview
The modified setup removes the main nginx ingress container and exposes services directly:
- **UI Service**: Exposed on port 7655 (HTTP)
- **API Service**: Exposed on port 7654 (HTTP via minimal nginx container)
- **Database**: PostgreSQL (internal only)
- **Redis**: Cache service (internal only)
## Key Changes from Default Setup
1. **Removed YAML anchors** - Each container now has its own explicit configuration to avoid conflicts
2. **Removed main ingress container** - Your host nginx handles all routing
3. **Added minimal api-nginx** - Small nginx container just to convert FastCGI to HTTP for the API
4. **Custom ports** - Using 7654-7655 range to avoid conflicts
## Setup Steps
### 1. Stop any existing containers
```bash
docker compose down
docker compose -f docker-compose.dev.yml down
```
### 2. Run the setup script
```bash
./scripts/docker-setup.sh
```
### 3. Verify services are running
```bash
docker compose ps
```
You should see:
- opnform-api (healthy)
- opnform-api-nginx (healthy)
- opnform-api-worker (running)
- opnform-api-scheduler (running)
- opnform-client (healthy)
- opnform-redis (healthy)
- opnform-db (healthy)
### 4. Configure your host nginx
Copy the example configuration:
```bash
sudo cp nginx-host-example.conf /etc/nginx/sites-available/forms.portnimara.dev
sudo ln -s /etc/nginx/sites-available/forms.portnimara.dev /etc/nginx/sites-enabled/
```
Edit the file to adjust:
- SSL certificate paths
- Server name if different
- Any other site-specific settings
### 5. Test nginx configuration
```bash
sudo nginx -t
```
### 6. Reload nginx
```bash
sudo systemctl reload nginx
```
## Troubleshooting
### Port already in use
If you get "port already allocated" errors:
1. Check what's using the ports:
```bash
sudo lsof -i :7654
sudo lsof -i :7655
```
2. Stop conflicting services or change the ports in docker-compose.yml
### API not responding
1. Check the api-nginx logs:
```bash
docker logs opnform-api-nginx
```
2. Verify the API container is running:
```bash
docker logs opnform-api
```
### UI not loading
1. Check the client logs:
```bash
docker logs opnform-client
```
2. Ensure the client/.env file has correct API URL settings
## Port Reference
- **7654**: API (HTTP) - proxied through api-nginx to PHP-FPM
- **7655**: UI (HTTP) - Nuxt.js frontend
- **9000**: PHP-FPM (internal only, FastCGI protocol)
- **5432**: PostgreSQL (internal only)
- **6379**: Redis (internal only)
## Security Notes
1. Ports are bound to 127.0.0.1 only, not exposed to external network
2. All traffic should go through your host nginx with SSL
3. The minimal api-nginx container only handles FastCGI conversion, no SSL termination
## Default Credentials
- Email: admin@opnform.com
- Password: password
**Important**: Change these immediately after first login!

View File

@ -14,6 +14,7 @@ class FeatureFlagsController extends Controller
'self_hosted' => config('app.self_hosted', true), 'self_hosted' => config('app.self_hosted', true),
'custom_domains' => config('custom-domains.enabled', false), 'custom_domains' => config('custom-domains.enabled', false),
'ai_features' => !empty(config('services.openai.api_key')), 'ai_features' => !empty(config('services.openai.api_key')),
'version' => $this->getAppVersion(),
'billing' => [ 'billing' => [
'enabled' => !empty(config('cashier.key')) && !empty(config('cashier.secret')), 'enabled' => !empty(config('cashier.key')) && !empty(config('cashier.secret')),
@ -44,4 +45,17 @@ class FeatureFlagsController extends Controller
return response()->json($featureFlags); return response()->json($featureFlags);
} }
/**
* Get the application version from Docker environment or fallback
*/
private function getAppVersion(): ?string
{
// Only return version for self-hosted installations
if (!config('app.self_hosted', true)) {
return null;
}
return config('app.docker_version');
}
} }

View File

@ -179,9 +179,9 @@ class StoreFormSubmissionJob implements ShouldQueue
} }
} else { } else {
// Standard field processing (text, ID generation, etc.) // Standard field processing (text, ID generation, etc.)
if ($field['type'] == 'text' && isset($field['generates_uuid']) && $field['generates_uuid']) { if ((!$answerValue || !Str::isUuid($answerValue)) && $field['type'] == 'text' && isset($field['generates_uuid']) && $field['generates_uuid']) {
$finalData[$field['id']] = ($this->form->is_pro) ? Str::uuid()->toString() : 'Please upgrade your OpenForm subscription to use our ID generation features'; $finalData[$field['id']] = ($this->form->is_pro) ? Str::uuid()->toString() : 'Please upgrade your OpenForm subscription to use our ID generation features';
} elseif ($field['type'] == 'text' && isset($field['generates_auto_increment_id']) && $field['generates_auto_increment_id']) { } elseif ((!$answerValue || !is_int($answerValue)) && $field['type'] == 'text' && isset($field['generates_auto_increment_id']) && $field['generates_auto_increment_id']) {
$finalData[$field['id']] = ($this->form->is_pro) ? (string) ($this->form->submissions_count + 1) : 'Please upgrade your OpenForm subscription to use our ID generation features'; $finalData[$field['id']] = ($this->form->is_pro) ? (string) ($this->form->submissions_count + 1) : 'Please upgrade your OpenForm subscription to use our ID generation features';
} else { } else {
$finalData[$field['id']] = $answerValue; $finalData[$field['id']] = $answerValue;

View File

@ -47,6 +47,14 @@ class IntegrationLogicRule implements DataAwareRule, ValidationRule
return; return;
} }
$typeField = $condition['value']['property_meta']['type'];
$operator = $condition['value']['operator'];
// If operator has no format and no expected_type, it means it doesn't need input
if (!isset(FormPropertyLogicRule::getConditionMapping()[$typeField]['comparators'][$operator]['expected_type'])) {
return;
}
if (!isset($condition['value']['value'])) { if (!isset($condition['value']['value'])) {
$this->isConditionCorrect = false; $this->isConditionCorrect = false;
$this->conditionErrors[] = 'missing condition value'; $this->conditionErrors[] = 'missing condition value';
@ -54,8 +62,6 @@ class IntegrationLogicRule implements DataAwareRule, ValidationRule
return; return;
} }
$typeField = $condition['value']['property_meta']['type'];
$operator = $condition['value']['operator'];
$value = $condition['value']['value']; $value = $condition['value']['value'];
if (!isset(FormPropertyLogicRule::getConditionMapping()[$typeField])) { if (!isset(FormPropertyLogicRule::getConditionMapping()[$typeField])) {

View File

@ -15,6 +15,17 @@ return [
'name' => env('APP_NAME', 'OpnForm'), 'name' => env('APP_NAME', 'OpnForm'),
/*
|--------------------------------------------------------------------------
| Application Version
|--------------------------------------------------------------------------
|
| This value is the version of your application. Used for display purposes
| and fallback when Docker build version is not available.
|
*/
'docker_version' => env('APP_VERSION_DOCKER'),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Application Environment | Application Environment

View File

@ -45,3 +45,58 @@ it('can CRUD form integration', function () {
'message' => 'Form Integration was deleted.' 'message' => 'Form Integration was deleted.'
]); ]);
}); });
it('can create form integration with checkbox logic', function () {
$user = $this->actingAsProUser();
$workspace = $this->createUserWorkspace($user);
$form = $this->createForm($user, $workspace, [
'properties' => [
[
'id' => 'checkbox_field',
'name' => 'Checkbox Field',
'type' => 'checkbox'
],
[
'id' => 'text_field',
'name' => 'Text Field',
'type' => 'text',
],
],
]);
$data = [
'status' => true,
'integration_id' => 'email',
'logic' => [
'operatorIdentifier' => 'and',
'children' => [
[
'identifier' => 'checkbox_field',
'value' => [
'operator' => 'is_checked',
'property_meta' => [
'id' => 'checkbox_field',
'type' => 'checkbox',
]
],
],
],
],
'settings' => [
'send_to' => 'test@test.com',
'sender_name' => 'OpnForm',
'subject' => 'New form submission with checkbox logic',
'email_content' => 'Checkbox logic triggered.',
'include_submission_data' => true,
'include_hidden_fields_submission_data' => false,
'reply_to' => null
]
];
$this->postJson(route('open.forms.integration.create', $form->id), $data)
->assertSuccessful()
->assertJson([
'type' => 'success',
'message' => 'Form Integration was created.'
]);
});

View File

@ -1,6 +1,6 @@
<?php <?php
use Illuminate\Support\Str;
it('can update form with existing record', function () { it('can update form with existing record', function () {
$user = $this->actingAsProUser(); $user = $this->actingAsProUser();
@ -39,3 +39,56 @@ it('can update form with existing record', function () {
expect($response->json('data.' . $nameProperty['id']))->toBe('Testing Updated'); expect($response->json('data.' . $nameProperty['id']))->toBe('Testing Updated');
} }
}); });
it('can update form with existing record but generates_uuid field is not update', function () {
$user = $this->actingAsProUser();
$workspace = $this->createUserWorkspace($user);
$form = $this->createForm($user, $workspace, [
'editable_submissions' => true,
'properties' => [
[
'id' => 'uuid_field',
'type' => 'text',
'generates_uuid' => true,
'name' => 'UUID Field'
],
[
'id' => 'name',
'type' => 'text',
'name' => 'Name'
]
]
]);
$response = $this->postJson(route('forms.answer', $form->slug), ['name' => 'Testing', 'uuid_field' => null])
->assertSuccessful()
->assertJson([
'type' => 'success',
'message' => 'Form submission saved.',
]);
$submissionId = $response->json('submission_id');
expect($submissionId)->toBeString();
$response = $this->getJson(route('forms.fetchSubmission', [$form->slug, $submissionId]))
->assertSuccessful();
$uuid = $response->json('data.uuid_field');
expect(Str::isUuid($uuid))->toBeTrue();
if ($submissionId) {
$formData = $this->generateFormSubmissionData($form, ['submission_id' => $submissionId, 'name' => 'Testing Updated', 'uuid_field' => $uuid]);
$response = $this->postJson(route('forms.answer', $form->slug), $formData)
->assertSuccessful()
->assertJson([
'type' => 'success',
'message' => 'Form submission saved.',
]);
$submissionId2 = $response->json('submission_id');
expect($submissionId2)->toBeString();
expect($submissionId2)->toBe($submissionId);
$response = $this->getJson(route('forms.fetchSubmission', [$form->slug, $submissionId]))
->assertSuccessful();
expect($response->json('data.name'))->toBe('Testing Updated');
$uuid2 = $response->json('data.uuid_field');
expect($uuid2)->toBe($uuid);
}
});

View File

@ -8,12 +8,14 @@
v-if="isScanning" v-if="isScanning"
class="relative w-full" class="relative w-full"
> >
<ClientOnly>
<CameraUpload <CameraUpload
:is-barcode-mode="true" :is-barcode-mode="true"
:decoders="decoders" :decoders="decoders"
@stop-webcam="stopScanning" @stop-webcam="stopScanning"
@barcode-detected="handleBarcodeDetected" @barcode-detected="handleBarcodeDetected"
/> />
</ClientOnly>
</div> </div>
<div <div

View File

@ -10,12 +10,14 @@
theme.fileInput.minHeight theme.fileInput.minHeight
]" ]"
> >
<camera-upload <ClientOnly>
<CameraUpload
v-if="cameraUpload" v-if="cameraUpload"
:theme="theme" :theme="theme"
@upload-image="cameraFileUpload" @upload-image="cameraFileUpload"
@stop-webcam="isInWebcam=false" @stop-webcam="isInWebcam=false"
/> />
</ClientOnly>
</div> </div>
<div <div
v-else v-else

View File

@ -1,10 +1,10 @@
<template> <template>
<input-wrapper v-bind="inputWrapperProps"> <InputWrapper v-bind="inputWrapperProps">
<template #label> <template #label>
<slot name="label" /> <slot name="label" />
</template> </template>
<span class="inline-block w-full rounded-md shadow-sm"> <span class="inline-block w-full rounded-md shadow-xs">
<button <button
type="button" type="button"
aria-haspopup="listbox" aria-haspopup="listbox"
@ -12,7 +12,7 @@
aria-labelledby="listbox-label" aria-labelledby="listbox-label"
class="cursor-pointer relative w-full" class="cursor-pointer relative w-full"
:class="[ :class="[
theme.default.input, theme.default.input,
theme.default.spacing.horizontal, theme.default.spacing.horizontal,
theme.default.spacing.vertical, theme.default.spacing.vertical,
theme.default.fontSize, theme.default.fontSize,
@ -24,52 +24,37 @@
> >
<div <div
v-if="currentUrl == null" v-if="currentUrl == null"
class="text-gray-600 dark:text-gray-400" class="text-gray-600 dark:text-gray-400 flex justify-center"
> >
<svg <Icon
xmlns="http://www.w3.org/2000/svg" name="heroicons:cloud-arrow-up"
class="h-5 w-5 inline" class="h-5 w-5"
fill="none" />
viewBox="0 0 24 24" <span class="ml-2">
stroke="currentColor" Upload
> </span>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
/>
</svg>
Upload image
</div> </div>
<div <div
v-else v-else
class="h-6 text-gray-600 dark:text-gray-400 flex" class=" text-gray-600 dark:text-gray-400 flex"
> >
<div class="flex-grow"> <div class="flex-grow">
<img <img
:src="currentUrl" :src="tmpFile ?? currentUrl"
class="h-6 rounded shadow-md" class="h-5 rounded shadow-md border"
> >
</div> </div>
<a <a
href="#" href="#"
class="hover:text-nt-blue flex" class="text-gray-500 hover:text-red-500 flex items-center"
@click.prevent="clearUrl" @click.prevent="clearUrl"
> >
<svg <Icon
xmlns="http://www.w3.org/2000/svg" name="heroicons:trash"
class="h-6 w-6" class="h-5 w-5"
fill="none" />
viewBox="0 0 24 24" </a>
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/></svg></a>
</div> </div>
</button> </button>
</span> </span>
@ -105,7 +90,7 @@
v-if="loading" v-if="loading"
class="text-gray-600 dark:text-gray-400" class="text-gray-600 dark:text-gray-400"
> >
<Loader class="h-6 w-6 mx-auto m-10" /> <loader class="h-5 w-5 mx-auto m-10" />
<p class="text-center mt-6"> <p class="text-center mt-6">
Uploading your file... Uploading your file...
</p> </p>
@ -127,20 +112,10 @@
accept="image/png, image/gif, image/jpeg, image/bmp, image/svg+xml" accept="image/png, image/gif, image/jpeg, image/bmp, image/svg+xml"
@change="manualFileUpload" @change="manualFileUpload"
> >
<svg <Icon
xmlns="http://www.w3.org/2000/svg" name="heroicons:cloud-arrow-up"
class="mx-auto h-24 w-24 text-gray-200" class="x-auto h-24 w-24 text-gray-200"
fill="none" />
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
/>
</svg>
<p class="mt-5 text-sm text-gray-600"> <p class="mt-5 text-sm text-gray-600">
<button <button
type="button" type="button"
@ -161,7 +136,7 @@
</div> </div>
</div> </div>
</modal> </modal>
</input-wrapper> </InputWrapper>
</template> </template>
<script> <script>

View File

@ -0,0 +1,199 @@
<template>
<input-wrapper v-bind="inputWrapperProps">
<template #label>
<slot name="label" />
</template>
<div
class="grid"
:class="[gridClass, { 'gap-2': !seamless }]"
:style="optionStyle"
role="listbox"
:aria-multiselectable="multiple ? 'true' : 'false'"
:tabindex="disabled ? -1 : 0"
@keydown="onKeydown"
ref="root"
>
<button
v-for="(option, idx) in options"
:key="option[optionKey]"
class="flex flex-col items-center justify-center p-1.5 border transition-colors text-gray-500 focus:outline-none"
:class="[
option.class ? (typeof option.class === 'function' ? option.class(isSelected(option)) : option.class) : {},
{
'border-form-color text-form-color bg-form-color/10': isSelected(option),
'hover:bg-gray-100 border-gray-300': !isSelected(option),
'opacity-50 pointer-events-none': disabled || option.disabled,
// Seamless mode: only first and last have radius
'rounded-lg': !seamless,
'rounded-l-lg': seamless && idx === 0,
'rounded-r-lg': seamless && idx === options.length - 1,
// Seamless mode: overlap borders with negative margin, keep all borders
'-ml-px': seamless && idx > 0,
// Seamless mode: z-index hierarchy - selected > hovered/focused > default
'relative z-20': seamless && isSelected(option),
'relative z-10': seamless && !isSelected(option) && focusedIdx === idx,
'relative z-0': seamless && !isSelected(option) && focusedIdx !== idx,
// Add hover z-index for seamless mode (but lower than selected)
'hover:z-10': seamless && !isSelected(option)
}
]"
:aria-selected="isSelected(option) ? 'true' : 'false'"
:tabindex="disabled || option.disabled ? -1 : 0"
:disabled="disabled || option.disabled"
@click="selectOption(option)"
@focus="focusedIdx = idx"
@mouseenter="focusedIdx = idx"
:title="option.tooltip || ''"
role="option"
>
<slot name="icon" :option="option" :selected="isSelected(option)">
<Icon
v-if="option.icon"
:name="isSelected(option) && option.selectedIcon ? option.selectedIcon : option.icon"
:class="[
'w-4 h-4',
option.label ? 'mb-1' : '',
isSelected(option) ? 'text-form-color' : 'text-inherit',
option.iconClass ? (typeof option.iconClass === 'function' ? option.iconClass(isSelected(option)) : option.iconClass) : {}
]"
/>
</slot>
<span
v-if="option.label || !option.icon"
class="text-xs"
:class="{
'text-form-color': isSelected(option),
'text-inherit': !isSelected(option),
}"
>{{ isSelected(option) ? option.selectedLabel ?? option.label : option.label }}</span>
</button>
</div>
<template #help>
<slot name="help" />
</template>
<template #error>
<slot name="error" />
</template>
</input-wrapper>
</template>
<script setup>
import { ref, computed, watch, nextTick } from 'vue'
import { inputProps, useFormInput } from './useFormInput.js'
import InputWrapper from './components/InputWrapper.vue'
/**
* OptionSelectorInput.vue
*
* A form input component for selecting options in a grid layout with icons.
* Integrates with the form system using InputWrapper and useFormInput.
*
* Props:
* - options: Array<{ name, label, icon, selectedIcon?, iconClass?, tooltip?, disabled? }>
* - multiple: Boolean (default: false)
* - optionKey: String (default: 'name')
* - columns: Number (default: 3, for grid layout)
* - seamless: Boolean (default: false, removes gaps and only applies radius to first/last items)
*
* Features:
* - Keyboard navigation (arrow keys, enter/space to select)
* - Focus management
* - Optional tooltips per option
* - Form validation integration
* - Notion-style look by default
* - Seamless mode for connected button appearance
*/
const props = defineProps({
...inputProps,
options: { type: Array, required: true },
multiple: { type: Boolean, default: false },
optionKey: { type: String, default: 'name' },
columns: { type: Number, default: 3 },
seamless: { type: Boolean, default: false }
})
const emit = defineEmits(['update:modelValue', 'focus', 'blur'])
// Use form input composable
const {
compVal,
inputWrapperProps
} = useFormInput(props, { emit })
// Local state
const focusedIdx = ref(-1)
const root = ref(null)
// Computed properties
const gridClass = computed(() => `grid-cols-${props.columns}`)
const optionStyle = computed(() => ({
'--bg-form-color': props.color
}))
// Methods
function isSelected(option) {
if (props.multiple) {
return Array.isArray(compVal.value) && compVal.value.includes(option[props.optionKey])
}
return compVal.value === option[props.optionKey]
}
function selectOption(option) {
if (props.disabled || option.disabled) return
if (props.multiple) {
let newValue = Array.isArray(compVal.value) ? [...compVal.value] : []
const idx = newValue.indexOf(option[props.optionKey])
if (idx > -1) {
newValue.splice(idx, 1)
} else {
newValue.push(option[props.optionKey])
}
compVal.value = newValue
} else {
compVal.value = isSelected(option) ? null : option[props.optionKey]
}
}
function onKeydown(e) {
if (props.disabled) return
const len = props.options.length
if (len === 0) return
if (["ArrowRight", "ArrowDown"].includes(e.key)) {
e.preventDefault()
focusedIdx.value = (focusedIdx.value + 1) % len
focusButton(focusedIdx.value)
} else if (["ArrowLeft", "ArrowUp"].includes(e.key)) {
e.preventDefault()
focusedIdx.value = (focusedIdx.value - 1 + len) % len
focusButton(focusedIdx.value)
} else if (["Enter", " ", "Spacebar"].includes(e.key)) {
e.preventDefault()
if (focusedIdx.value >= 0 && focusedIdx.value < len) {
selectOption(props.options[focusedIdx.value])
}
}
}
function focusButton(idx) {
nextTick(() => {
const btns = root.value?.querySelectorAll('button')
if (btns && btns[idx]) btns[idx].focus()
})
}
// Watchers
watch(compVal, (val) => {
// Keep focus on selected
if (!props.multiple && val != null) {
const idx = props.options.findIndex(opt => opt[props.optionKey] === val)
if (idx !== -1) focusedIdx.value = idx
}
})
</script>

View File

@ -4,7 +4,7 @@
:class="[theme.fileInput.borderRadius]" :class="[theme.fileInput.borderRadius]"
> >
<video <video
id="webcam" ref="webcamRef"
autoplay autoplay
playsinline playsinline
muted muted
@ -12,17 +12,17 @@
{ hidden: !isCapturing }, { hidden: !isCapturing },
theme.fileInput.minHeight, theme.fileInput.minHeight,
theme.fileInput.borderRadius, theme.fileInput.borderRadius,
'w-full h-full object-cover border border-gray-400/30' 'w-full h-full object-cover bg-gray-500'
]" ]"
webkit-playsinline webkit-playsinline
/> />
<canvas <canvas
id="canvas" ref="canvasRef"
:class="[ :class="[
{ hidden: !capturedImage }, { hidden: !capturedImage },
theme.fileInput.borderRadius, theme.fileInput.borderRadius,
theme.fileInput.minHeight, theme.fileInput.minHeight,
'w-full h-full object-cover border border-gray-400/30' 'w-full h-full object-cover'
]" ]"
/> />
@ -31,23 +31,16 @@
v-if="isCapturing && isBarcodeMode" v-if="isCapturing && isBarcodeMode"
class="absolute inset-0 pointer-events-none" class="absolute inset-0 pointer-events-none"
> >
<!-- Semi-transparent overlay -->
<div class="absolute inset-0 bg-black/30" />
<!-- Scanning area (transparent window) --> <!-- Scanning area (transparent window) -->
<div <div
class="absolute inset-0 flex items-center justify-center" class="absolute inset-0 flex items-strech justify-center px-8 py-12"
style="padding-bottom: 60px;"
> >
<div class="relative w-4/5 h-3/5"> <div class="flex-grow w-full relative">
<!-- Transparent window -->
<div class="absolute inset-0 bg-transparent border-0" />
<!-- Corner indicators --> <!-- Corner indicators -->
<div class="absolute top-0 left-0 w-8 h-8 border-t-2 border-l-2 border-white" /> <div class="absolute top-0 left-0 w-8 h-8 border-t-2 border-l-2 rounded-tl-md border-white" />
<div class="absolute top-0 right-0 w-8 h-8 border-t-2 border-r-2 border-white" /> <div class="absolute top-0 right-0 w-8 h-8 border-t-2 border-r-2 rounded-tr-md border-white" />
<div class="absolute bottom-0 left-0 w-8 h-8 border-b-2 border-l-2 border-white" /> <div class="absolute bottom-0 left-0 w-8 h-8 border-b-2 border-l-2 rounded-bl-md border-white" />
<div class="absolute bottom-0 right-0 w-8 h-8 border-b-2 border-r-2 border-white" /> <div class="absolute bottom-0 right-0 w-8 h-8 border-b-2 border-r-2 rounded-br-md border-white" />
</div> </div>
</div> </div>
</div> </div>
@ -152,7 +145,7 @@
<script> <script>
import Webcam from "webcam-easy" import Webcam from "webcam-easy"
import CachedDefaultTheme from "~/lib/forms/themes/CachedDefaultTheme.js" import CachedDefaultTheme from "~/lib/forms/themes/CachedDefaultTheme.js"
import Quagga from 'quagga' import { BrowserMultiFormatReader, DecodeHintType, BarcodeFormat } from '@zxing/library'
export default { export default {
name: "CameraUpload", name: "CameraUpload",
@ -181,7 +174,7 @@ export default {
isCapturing: false, isCapturing: false,
capturedImage: null, capturedImage: null,
cameraPermissionStatus: "loading", cameraPermissionStatus: "loading",
quaggaInitialized: false, zxingReader: null,
currentFacingMode: 'user', currentFacingMode: 'user',
mediaStream: null mediaStream: null
}), }),
@ -197,9 +190,10 @@ export default {
} }
}, },
mounted() { mounted() {
const webcamElement = document.getElementById("webcam") // For regular camera mode, we still need the webcam.js setup
const canvasElement = document.getElementById("canvas") if (!this.isBarcodeMode) {
this.webcam = new Webcam(webcamElement, "user", canvasElement) this.webcam = new Webcam(this.$refs.webcamRef, "user", this.$refs.canvasRef)
}
this.openCameraUpload() this.openCameraUpload()
}, },
@ -209,29 +203,27 @@ export default {
methods: { methods: {
async cleanupCurrentStream() { async cleanupCurrentStream() {
if (this.quaggaInitialized) { if (this.zxingReader) {
Quagga.stop() this.zxingReader.reset()
this.quaggaInitialized = false this.zxingReader = null
}
if (this.mediaStream) {
this.mediaStream.getTracks().forEach(track => track.stop())
this.mediaStream = null
} }
if (this.webcam) { if (this.webcam) {
this.webcam.stop() this.webcam.stop()
this.webcam = null
} }
const webcamElement = document.getElementById("webcam") // Clean up video element if needed
if (webcamElement && webcamElement.srcObject) { if (this.$refs.webcamRef && this.$refs.webcamRef.srcObject) {
const tracks = webcamElement.srcObject.getTracks() const tracks = this.$refs.webcamRef.srcObject.getTracks()
tracks.forEach(track => track.stop()) tracks.forEach(track => track.stop())
webcamElement.srcObject = null this.$refs.webcamRef.srcObject = null
} }
}, },
async switchCamera() { async switchCamera() {
if (!this.isMobileDevice) return
try { try {
// Stop current camera and clean up resources // Stop current camera and clean up resources
this.cleanupCurrentStream() this.cleanupCurrentStream()
@ -240,7 +232,13 @@ export default {
this.currentFacingMode = this.currentFacingMode === 'user' ? 'environment' : 'user' this.currentFacingMode = this.currentFacingMode === 'user' ? 'environment' : 'user'
// Restart camera // Restart camera
await this.openCameraUpload() if (this.isBarcodeMode) {
setTimeout(() => {
this.initZxingDirect()
}, 500)
} else {
await this.openCameraUpload()
}
} catch (error) { } catch (error) {
console.error('Error switching camera:', error) console.error('Error switching camera:', error)
this.cameraPermissionStatus = "unknown" this.cameraPermissionStatus = "unknown"
@ -252,15 +250,18 @@ export default {
this.capturedImage = null this.capturedImage = null
try { try {
const webcamElement = document.getElementById("webcam") if (this.isBarcodeMode) {
const canvasElement = document.getElementById("canvas") // For barcode mode, let ZXing handle everything
this.cameraPermissionStatus = "allowed"
setTimeout(() => {
this.initZxingDirect()
}, 500)
return
}
// Regular camera mode - use existing logic
// Determine the facing mode to use // Determine the facing mode to use
let facingMode = this.currentFacingMode let facingMode = this.currentFacingMode
if (this.isBarcodeMode && this.currentFacingMode === 'user') {
// Force environment mode for barcode scanning
facingMode = 'environment'
}
// Create constraints based on device capabilities // Create constraints based on device capabilities
const constraints = { const constraints = {
@ -293,25 +294,26 @@ export default {
} }
this.mediaStream = stream // Store the stream reference this.mediaStream = stream // Store the stream reference
webcamElement.srcObject = stream this.$refs.webcamRef.srcObject = stream
this.webcam = new Webcam( this.webcam = new Webcam(
webcamElement, this.$refs.webcamRef,
facingMode, facingMode,
canvasElement this.$refs.canvasRef
) )
await new Promise((resolve) => { await new Promise((resolve) => {
webcamElement.onloadedmetadata = () => { this.$refs.webcamRef.onloadedmetadata = () => {
webcamElement.play() this.$refs.webcamRef.play().then(() => {
resolve() resolve()
}).catch(err => {
console.error('Error playing video:', err)
resolve() // Continue anyway
})
} }
}) })
this.cameraPermissionStatus = "allowed" this.cameraPermissionStatus = "allowed"
if (this.isBarcodeMode) {
this.initQuagga()
}
} catch (err) { } catch (err) {
console.error('Camera error:', err) console.error('Camera error:', err)
if (err.name === 'NotAllowedError' || err.toString().includes('Permission denied')) { if (err.name === 'NotAllowedError' || err.toString().includes('Permission denied')) {
@ -321,55 +323,92 @@ export default {
} }
} }
}, },
initQuagga() { initZxingDirect() {
if (!this.quaggaInitialized) { if (this.zxingReader) {
Quagga.init({ this.zxingReader.reset()
inputStream: { this.zxingReader = null
name: "Live",
type: "LiveStream",
target: document.getElementById("webcam"),
constraints: {
facingMode: this.currentFacingMode,
width: { min: 640 },
height: { min: 480 },
aspectRatio: { min: 1, max: 2 }
},
},
locator: {
patchSize: "medium",
halfSample: true
},
numOfWorkers: navigator.hardwareConcurrency || 4,
frequency: 10,
decoder: {
readers: this.decoders || []
},
locate: true
}, (err) => {
if (err) {
console.error('Quagga initialization failed:', err)
return
}
this.quaggaInitialized = true
Quagga.start()
Quagga.onDetected((result) => {
if (result.codeResult) {
this.$emit('barcodeDetected', result.codeResult.code)
this.cancelCamera()
}
})
})
} }
const hints = new Map()
const formats = (this.decoders || []).map(decoder => {
// Map decoder strings to BarcodeFormat enum values
// Remove _reader suffix for mapping
const cleanDecoder = decoder.replace('_reader', '').toLowerCase()
switch(cleanDecoder) {
case 'ean_8': return BarcodeFormat.EAN_8
case 'ean':
case 'ean_13': return BarcodeFormat.EAN_13
case 'upc':
case 'upc_a': return BarcodeFormat.UPC_A
case 'upc_e': return BarcodeFormat.UPC_E
case 'code_39': return BarcodeFormat.CODE_39
case 'code_93': return BarcodeFormat.CODE_93
case 'code_128': return BarcodeFormat.CODE_128
case 'codabar': return BarcodeFormat.CODABAR
case 'itf': return BarcodeFormat.ITF
case 'qr':
case 'qr_code': return BarcodeFormat.QR_CODE
case 'data_matrix': return BarcodeFormat.DATA_MATRIX
case 'aztec': return BarcodeFormat.AZTEC
case 'pdf_417': return BarcodeFormat.PDF_417
default:
console.warn('Unsupported barcode format:', decoder)
return null
}
}).filter(format => format !== null)
if (formats.length > 0) {
hints.set(DecodeHintType.POSSIBLE_FORMATS, formats)
}
this.zxingReader = new BrowserMultiFormatReader(hints)
// Use simple constraints approach instead of device enumeration
const facingMode = this.isMobileDevice && this.currentFacingMode === 'user' ? 'environment' : this.currentFacingMode
const constraints = {
audio: false,
video: {
facingMode: facingMode,
width: { ideal: 1280 },
height: { ideal: 720 }
}
}
// Use ZXing's decodeFromConstraints method
this.zxingReader.decodeFromConstraints(constraints, this.$refs.webcamRef, (result, error) => {
if (result) {
this.$emit('barcodeDetected', result.text)
}
// Don't log NotFoundException errors - they're expected during scanning
// Only log other types of errors
else if (error && error.name && !error.name.includes('NotFoundException') && !error.message?.includes('No MultiFormat Readers')) {
console.error('ZXing decoding error:', error.name, error.message)
}
})
.then(() => {
this.cameraPermissionStatus = "allowed"
})
.catch(err => {
console.error('Camera error in ZXing Direct:', err)
if (err.name === 'NotAllowedError' || err.toString().includes('Permission denied')) {
this.cameraPermissionStatus = "blocked"
} else {
this.cameraPermissionStatus = "unknown"
}
})
}, },
cancelCamera() { cancelCamera() {
this.isCapturing = false this.isCapturing = false
this.capturedImage = null this.capturedImage = null
this.cleanupCurrentStream() // Use the cleanup method this.cleanupCurrentStream() // Use the cleanup method
this.$emit("stopWebcam") this.$emit("stopWebcam")
}, },
processCapturedImage() { processCapturedImage() {
if (!this.webcam) {
return
}
this.capturedImage = this.webcam.snap() this.capturedImage = this.webcam.snap()
this.isCapturing = false this.isCapturing = false
this.webcam.stop() this.webcam.stop()

View File

@ -246,6 +246,8 @@ const isAutoSubmit = ref(import.meta.client && window.location.href.includes('au
// Create a reactive reference directly from the prop // Create a reactive reference directly from the prop
const darkModeRef = toRef(props, 'darkMode') const darkModeRef = toRef(props, 'darkMode')
// Create a reactive reference for the mode prop
const modeRef = toRef(props, 'mode')
// Add back the local theme computation // Add back the local theme computation
const theme = computed(() => { const theme = computed(() => {
@ -258,7 +260,8 @@ const theme = computed(() => {
let formManager = null let formManager = null
if (props.form) { if (props.form) {
formManager = useFormManager(props.form, props.mode, { formManager = useFormManager(props.form, props.mode, {
darkMode: darkModeRef darkMode: darkModeRef,
mode: modeRef
}) })
formManager.initialize({ formManager.initialize({
submissionId: submissionId.value, submissionId: submissionId.value,

View File

@ -17,70 +17,84 @@
:form="form" :form="form"
label="Form Theme" label="Form Theme"
/> />
<color-input <color-input
name="color" name="color"
:form="form" :form="form"
label="Accent Color"
class="my-4"
> >
<template #help> <template #label>
<InputHelp> <InputLabel>Accent Color - <a
<span class="text-gray-500"> href="#" class="text-blue-500"
Color (for buttons & inputs border) - <a @click.prevent="form.color = DEFAULT_COLOR"
class="text-blue-500" >Reset</a></InputLabel>
href="#"
@click.prevent="form.color = DEFAULT_COLOR"
>Reset</a>
</span>
</InputHelp>
</template> </template>
</color-input> </color-input>
<select-input
name="dark_mode" <OptionSelectorInput
:options="[ v-model="form.dark_mode"
{ name: 'Auto', value: 'auto' },
{ name: 'Light Mode', value: 'light' },
{ name: 'Dark Mode', value: 'dark' },
]"
:form="form" :form="form"
name="dark_mode"
label="Color Mode" label="Color Mode"
help="Use Auto to use device system preferences" :options="[
{ name: 'auto', label: 'System', icon: 'i-heroicons-computer-desktop' },
{ name: 'light', label: 'Light', icon: 'i-heroicons-sun' },
{ name: 'dark', label: 'Dark', icon: 'i-heroicons-moon' },
]"
:multiple="false"
:columns="3"
class="mb-4"
/> />
<EditorSectionHeader <EditorSectionHeader
icon="octicon:typography-16" icon="octicon:typography-16"
title="Typography" title="Text & Language"
/> />
<template v-if="useFeatureFlag('services.google.fonts')"> <div class="grid grid-cols-2 gap-4">
<label class="text-gray-700 font-medium text-sm">Font Style</label> <div class="flex-grow my-1" v-if="useFeatureFlag('services.google.fonts')">
<v-button <label class="text-gray-700 font-semibold text-sm mb-1 block">Font Family</label>
color="white" <v-button
class="w-full mb-4" color="white"
size="small" class="w-full py-1.5"
@click="showGoogleFontPicker = true" size="small"
> @click="showGoogleFontPicker = true"
<span :style="{ 'font-family': (form.font_family?form.font_family+' !important':null) }"> >
{{ form.font_family || 'Default' }} <span :style="{ 'font-family': (form.font_family ? form.font_family + ' !important' : null) }">
</span> {{ form.font_family || 'Default' }}
</v-button> </span>
<GoogleFontPicker </v-button>
:show="showGoogleFontPicker" <GoogleFontPicker
:font="form.font_family || null" :show="showGoogleFontPicker"
@close="showGoogleFontPicker=false" :font="form.font_family || null"
@apply="onApplyFont" @close="showGoogleFontPicker = false"
/> @apply="onApplyFont"
</template> />
</div>
<div class="flex-grow">
<select-input
name="language"
searchable
:options="availableLocales"
:form="form"
label="Language"
/>
</div>
</div>
<ToggleSwitchInput
name="layout_rtl"
:form="form"
class="mt-4"
label="Right-to-Left Layout"
/>
<toggle-switch-input <toggle-switch-input
name="uppercase_labels" name="uppercase_labels"
:form="form" :form="form"
label="Uppercase Input Labels"
/>
<select-input
name="language"
class="mt-4" class="mt-4"
searchable label="Uppercase Input Labels"
:options="availableLocales"
:form="form"
label="Form Language"
/> />
<EditorSectionHeader <EditorSectionHeader
@ -88,74 +102,87 @@
title="Layout & Sizing" title="Layout & Sizing"
/> />
<div class="flex space-x-4 justify-stretch"> <div class="flex space-x-4 justify-stretch">
<select-input <div class="flex-grow">
name="size" <OptionSelectorInput
class="flex-grow" seamless
:options="[ label="Input Size"
{ name: 'Small', value: 'sm' }, v-model="form.size"
{ name: 'Medium', value: 'md' }, :form="form"
{ name: 'Large', value: 'lg' }, name="size"
]" :options="[
:form="form" { name: 'sm', label:'S'},
label="Input Size" { name: 'md', label:'M' },
/> { name: 'lg', label:'L' },
]"
:multiple="false"
:columns="3"
class="mb-4"
/>
</div>
<select-input <div class="flex-grow">
name="border_radius" <OptionSelectorInput
class="flex-grow" label="Input Roundness"
:options="[ v-model="form.border_radius"
{ name: 'None', value: 'none' }, seamless
{ name: 'Small', value: 'small' }, :form="form"
{ name: 'Full', value: 'full' }, name="border_radius"
]" :options="[
:form="form" { name: 'none', icon: 'i-tabler-border-corner-square' },
label="Input Roundness" { name: 'small', icon: 'i-tabler-border-corner-rounded' },
/> { name: 'full', icon: 'i-tabler-border-corner-pill' },
]"
:multiple="false"
:columns="3"
class="mb-4"
/>
</div>
</div> </div>
<select-input
name="width" <OptionSelectorInput
:options="[ v-model="form.width"
{ name: 'Centered', value: 'centered' },
{ name: 'Full Width', value: 'full' },
]"
:form="form"
label="Form Width" label="Form Width"
help="Useful when embedding your form"
/>
<ToggleSwitchInput
name="layout_rtl"
:form="form" :form="form"
class="mt-4" name="width"
label="Right-to-Left Layout" seamless
help="Adjusts layout for RTL languages" :options="[
{ name: 'centered', label: 'Centered' },
{ name: 'full', label: 'Full Width' },
]"
:multiple="false"
:columns="2"
class="mb-4 w-2/3"
/> />
<EditorSectionHeader <EditorSectionHeader
icon="heroicons:tag-16-solid" icon="heroicons:tag-16-solid"
title="Branding & Media" title="Branding & Media"
/> />
<image-input <div class="grid grid-cols-2 gap-4">
name="logo_picture" <image-input
:form="form" name="logo_picture"
label="Logo" :form="form"
help="Not visible when form is embedded" label="Logo"
:required="false" :required="false"
/> />
<image-input
name="cover_picture" <image-input
:form="form" name="cover_picture"
label="Cover image" :form="form"
help="Not visible when form is embedded" label="Cover (~1500px)"
/> :required="false"
/>
</div>
<toggle-switch-input <toggle-switch-input
name="no_branding" name="no_branding"
:form="form" :form="form"
class="mt-4"
@update:model-value="onChangeNoBranding" @update:model-value="onChangeNoBranding"
> >
<template #label> <template #label>
<span class="text-sm"> <span class="text-sm">
Remove OpnForm Branding Hide OpnForm Branding
</span> </span>
<pro-tag <pro-tag
upgrade-modal-title="Upgrade today to remove OpnForm branding" upgrade-modal-title="Upgrade today to remove OpnForm branding"
@ -182,7 +209,7 @@
name="transparent_background" name="transparent_background"
:form="form" :form="form"
label="Transparent Background" label="Transparent Background"
help="Only applies when form is embedded" help="When form is embedded"
/> />
<toggle-switch-input <toggle-switch-input
name="confetti_on_submission" name="confetti_on_submission"

View File

@ -18,7 +18,7 @@
class="border rounded-lg bg-white dark:bg-notion-dark w-full block shadow-sm transition-all flex flex-col" class="border rounded-lg bg-white dark:bg-notion-dark w-full block shadow-sm transition-all flex flex-col"
:class="{ 'max-w-5xl': !isExpanded, 'h-full': isExpanded }" :class="{ 'max-w-5xl': !isExpanded, 'h-full': isExpanded }"
> >
<div class="w-full bg-white dark:bg-gray-950 border-b border-gray-300 dark:border-blue-900 dark:border-gray-700 rounded-t-lg p-1.5 px-4 flex items-center gap-x-1.5"> <div class="w-full bg-white dark:bg-gray-950 border-b border-gray-300 dark:border-blue-900 dark:border-gray-700 rounded-t-lg p-1.5 pl-4 pr-1.5 flex items-center gap-x-1.5">
<div class="bg-red-500 rounded-full w-2.5 h-2.5" /> <div class="bg-red-500 rounded-full w-2.5 h-2.5" />
<div class="bg-yellow-500 rounded-full w-2.5 h-2.5" /> <div class="bg-yellow-500 rounded-full w-2.5 h-2.5" />
<div class="bg-green-500 rounded-full w-2.5 h-2.5" /> <div class="bg-green-500 rounded-full w-2.5 h-2.5" />

View File

@ -641,12 +641,13 @@ export default {
], ],
allCountries: countryCodes, allCountries: countryCodes,
barcodeDecodersOptions: [ barcodeDecodersOptions: [
{ name: 'QR Code', value: 'qr_reader' },
{ name: 'EAN-13 (European Article Number)', value: 'ean_reader' }, { name: 'EAN-13 (European Article Number)', value: 'ean_reader' },
{ name: 'EAN-8 (European Article Number)', value: 'ean_8_reader' }, { name: 'EAN-8 (European Article Number)', value: 'ean_8_reader' },
{ name: 'UPC-A (Universal Product Code)', value: 'upc_reader' }, { name: 'UPC-A (Universal Product Code)', value: 'upc_reader' },
{ name: 'UPC-E (Universal Product Code)', value: 'upc_e_reader' }, { name: 'UPC-E (Universal Product Code)', value: 'upc_e_reader' },
{ name: 'Code 128', value: 'code_128_reader' }, { name: 'Code 128', value: 'code_128_reader' },
{ name: 'Code 39', value: 'code_39_reader' }, { name: 'Code 39', value: 'code_39_reader' }
] ]
} }
}, },

View File

@ -1,87 +1,61 @@
<template> <template>
<div class="grid grid-cols-3 gap-2"> <OptionSelectorInput
<button :options="availableOptions"
v-for="option in availableOptions" v-model="selectedOption"
:key="option.name" :multiple="false"
class="flex flex-col items-center justify-center p-1.5 border rounded-lg transition-colors text-gray-500" :disabled="false"
:class="[ :columns="3"
option.class ? (typeof option.class === 'function' ? option.class(isSelected(option.name)) : option.class) : {}, name="field_state"
{ />
'border-blue-500 bg-blue-50': isSelected(option.name),
'hover:bg-gray-100 border-gray-300': !isSelected(option.name)
}
]"
@click="toggleOption(option.name)"
>
<Icon
:name="isSelected(option.name) && option.selectedIcon ? option.selectedIcon : option.icon"
:class="[
'w-4 h-4 mb-1',
{
'text-blue-500': isSelected(option.name),
'text-inherit': !isSelected(option.name),
},
option.iconClass ? (typeof option.iconClass === 'function' ? option.iconClass(isSelected(option.name)) : option.iconClass) : {}
]"
/>
<span
class="text-xs"
:class="{
'text-blue-500': isSelected(option.name),
'text-inherit': !isSelected(option.name),
}"
>{{ isSelected(option.name) ? option.selectedLabel ?? option.label : option.label }}</span>
</button>
</div>
</template> </template>
<script setup> <script setup>
import { computed } from 'vue'
const props = defineProps({ const props = defineProps({
field: { field: {
type: Object, type: Object,
required: true required: true
}, },
canBeDisabled: { canBeDisabled: {
type: Boolean, type: Boolean,
default: true default: true
}, },
canBeRequired: { canBeRequired: {
type: Boolean, type: Boolean,
default: true default: true
}, },
canBeHidden: { canBeHidden: {
type: Boolean, type: Boolean,
default: true default: true
} }
}) })
const emit = defineEmits(['update:field'])
defineEmits(['update:field']) const options = [
{
const options = ref([ name: 'required',
{ label: 'Required',
name: 'required', icon: 'ph:asterisk-bold',
label: 'Required', selectedIcon: 'ph:asterisk-bold',
icon: 'i-ph-asterisk-bold',
selectedIcon: 'i-ph-asterisk-bold',
iconClass: (isActive) => isActive ? 'text-red-500' : '', iconClass: (isActive) => isActive ? 'text-red-500' : '',
}, },
{ {
name: 'hidden', name: 'hidden',
label: 'Hidden', label: 'Hidden',
icon: 'i-heroicons-eye', icon: 'heroicons:eye',
selectedIcon: 'i-heroicons-eye-slash-solid', selectedIcon: 'heroicons:eye-slash-solid',
}, },
{ {
name: 'disabled', name: 'disabled',
label: 'Disabled', label: 'Disabled',
icon: 'i-heroicons-lock-open', icon: 'heroicons:lock-open',
selectedIcon: 'i-heroicons-lock-closed-solid', selectedIcon: 'heroicons:lock-closed-solid',
} }
]) ]
const availableOptions = computed(() => { const availableOptions = computed(() => {
return options.value.filter(option => { return options.filter(option => {
if (option.name === 'disabled') return props.canBeDisabled if (option.name === 'disabled') return props.canBeDisabled
if (option.name === 'required') return props.canBeRequired if (option.name === 'required') return props.canBeRequired
if (option.name === 'hidden') return props.canBeHidden if (option.name === 'hidden') return props.canBeHidden
@ -89,28 +63,34 @@ const availableOptions = computed(() => {
}) })
}) })
const isSelected = (optionName) => { const selectedOption = computed({
return props.field[optionName] get() {
} // Only one can be true at a time, priority: required > hidden > disabled
if (props.field.required) return 'required'
const toggleOption = (optionName) => { if (props.field.hidden) return 'hidden'
const newValue = !props.field[optionName] if (props.field.disabled) return 'disabled'
return null
if (optionName === 'required' && newValue) { },
props.field.hidden = false set(optionName) {
} else if (optionName === 'hidden' && newValue) { // Reset all
props.field.required = false props.field.required = false
props.field.disabled = false
props.field.generates_uuid = false
props.field.generates_auto_increment_id = false
} else if (optionName === 'disabled' && newValue) {
props.field.hidden = false props.field.hidden = false
props.field.disabled = false
// Apply business logic
if (optionName === 'required') {
props.field.required = true
props.field.hidden = false
} else if (optionName === 'hidden') {
props.field.hidden = true
props.field.required = false
props.field.disabled = false
props.field.generates_uuid = false
props.field.generates_auto_increment_id = false
} else if (optionName === 'disabled') {
props.field.disabled = true
props.field.hidden = false
}
emit('update:field', { ...props.field })
} }
})
if ((optionName === 'disabled' && props.canBeDisabled) ||
(optionName === 'required' && props.canBeRequired) ||
(optionName === 'hidden' && props.canBeHidden)) {
props.field[optionName] = newValue
}
}
</script> </script>

View File

@ -4,6 +4,9 @@
<div class="flex mt-2 items-center"> <div class="flex mt-2 items-center">
<p class="text-sm text-gray-600 dark:text-gray-400 text-center w-full"> <p class="text-sm text-gray-600 dark:text-gray-400 text-center w-full">
© Copyright {{ currYear }}. All Rights Reserved © Copyright {{ currYear }}. All Rights Reserved
<span v-if="version">
<br>Version {{ version }}
</span>
</p> </p>
</div> </div>
<div class="flex justify-center mt-5 md:mt-0"> <div class="flex justify-center mt-5 md:mt-0">
@ -82,22 +85,14 @@
</div> </div>
</template> </template>
<script> <script setup>
import { computed } from "vue"
import opnformConfig from "~/opnform.config.js" import opnformConfig from "~/opnform.config.js"
export default { const authStore = useAuthStore()
setup() {
const authStore = useAuthStore()
return {
user: computed(() => authStore.user),
appStore: useAppStore(),
opnformConfig,
}
},
data: () => ({ const user = computed(() => authStore.user)
currYear: new Date().getFullYear(), const currYear = ref(new Date().getFullYear())
}),
} // Use the reactive version for proper template reactivity
const version = computed(() => useFeatureFlag('version'))
</script> </script>

View File

@ -207,9 +207,22 @@
"text_class": "text-pink-900", "text_class": "text-pink-900",
"is_input": true, "is_input": true,
"default_values": { "default_values": {
"decoders": ["ean_reader", "ean_8_reader"] "decoders": ["qr_reader", "ean_reader", "ean_8_reader"]
} }
}, },
"qrcode": {
"name": "qrcode",
"title": "QR Code Reader",
"icon": "i-material-symbols-qr-code-scanner-rounded",
"default_block_name": "Scan QR Code",
"bg_class": "bg-pink-100",
"text_class": "text-pink-900",
"is_input": true,
"default_values": {
"decoders": ["qr_reader", "ean_reader", "ean_8_reader"]
},
"actual_input": "barcode"
},
"payment": { "payment": {
"name": "payment", "name": "payment",
"title": "Payment", "title": "Payment",

View File

@ -19,11 +19,17 @@ import { cloneDeep } from 'lodash'
* Initializes and coordinates various form composables (Structure, Init, Validation, etc.) * Initializes and coordinates various form composables (Structure, Init, Validation, etc.)
* based on the provided form configuration and mode. * based on the provided form configuration and mode.
*/ */
export function useFormManager(initialFormConfig, mode = FormMode.LIVE, options = {}) { export function useFormManager(initialFormConfig, initialMode = FormMode.LIVE, options = {}) {
// --- Reactive State --- // --- Reactive State ---
const config = ref(initialFormConfig) // Use ref for potentially replaceable config const config = ref(initialFormConfig) // Use ref for potentially replaceable config
const form = useForm() // Core vForm instance const form = useForm() // Core vForm instance
const strategy = computed(() => createFormModeStrategy(mode)) // Strategy based on mode
// Make mode reactive - accept either a ref or a static value
const mode = options.mode && typeof options.mode === 'object' && 'value' in options.mode
? options.mode
: ref(initialMode)
const strategy = computed(() => createFormModeStrategy(mode.value)) // Strategy based on reactive mode
// Use the passed darkMode ref if it's a ref, otherwise create a new ref // Use the passed darkMode ref if it's a ref, otherwise create a new ref
const darkMode = options.darkMode && typeof options.darkMode === 'object' && 'value' in options.darkMode const darkMode = options.darkMode && typeof options.darkMode === 'object' && 'value' in options.darkMode
@ -117,7 +123,7 @@ export function useFormManager(initialFormConfig, mode = FormMode.LIVE, options
const paymentBlock = structure.currentPagePaymentBlock.value const paymentBlock = structure.currentPagePaymentBlock.value
if (paymentBlock) { if (paymentBlock) {
// In editor/test mode (not LIVE), skip payment validation // In editor/test mode (not LIVE), skip payment validation
const isPaymentRequired = mode === FormMode.LIVE ? !!paymentBlock.required : false const isPaymentRequired = mode.value === FormMode.LIVE ? !!paymentBlock.required : false
// Pass required refs if Stripe needs them now (unlikely for just intent creation) // Pass required refs if Stripe needs them now (unlikely for just intent creation)
const paymentResult = await payment.processPayment(paymentBlock, isPaymentRequired) const paymentResult = await payment.processPayment(paymentBlock, isPaymentRequired)
@ -179,7 +185,7 @@ export function useFormManager(initialFormConfig, mode = FormMode.LIVE, options
if (paymentBlock) { if (paymentBlock) {
// In editor/test mode (not LIVE), skip payment validation // In editor/test mode (not LIVE), skip payment validation
const isPaymentRequired = mode === FormMode.LIVE ? !!paymentBlock.required : false const isPaymentRequired = mode.value === FormMode.LIVE ? !!paymentBlock.required : false
const paymentResult = await payment.processPayment(paymentBlock, isPaymentRequired) const paymentResult = await payment.processPayment(paymentBlock, isPaymentRequired)
@ -316,6 +322,8 @@ export function useFormManager(initialFormConfig, mode = FormMode.LIVE, options
// UI-related properties // UI-related properties
darkMode, // Dark mode setting darkMode, // Dark mode setting
setDarkMode: (isDark) => { darkMode.value = isDark }, // Method to update dark mode setDarkMode: (isDark) => { darkMode.value = isDark }, // Method to update dark mode
mode, // Form mode setting (ref)
setMode: (newMode) => { mode.value = newMode }, // Method to update form mode
// Composables (Expose if direct access needed, often not necessary) // Composables (Expose if direct access needed, often not necessary)
structure, structure,

View File

@ -1,11 +1,5 @@
export default defineNuxtRouteMiddleware(async () => { export default defineNuxtRouteMiddleware(async () => {
const authStore = useAuthStore() const authStore = useAuthStore()
const featureFlagsStore = useFeatureFlagsStore()
// Ensure feature flags are loaded
if (!featureFlagsStore.isLoaded) {
await featureFlagsStore.fetchFlags()
}
if (useFeatureFlag('self_hosted')) { if (useFeatureFlag('self_hosted')) {
if (authStore.check && authStore.user?.email === 'admin@opnform.com') { if (authStore.check && authStore.user?.email === 'admin@opnform.com') {

View File

@ -22,7 +22,7 @@ export default defineNuxtConfig({
], ],
build: { build: {
transpile: ["vue-notion", "query-builder-vue-3", "vue-signature-pad"], transpile: ["vue-notion", "query-builder-vue-3", "vue-signature-pad", "@zxing/library"],
}, },
i18n: { i18n: {

643
client/package-lock.json generated
View File

@ -21,6 +21,7 @@
"@vueuse/integrations": "^11.2.0", "@vueuse/integrations": "^11.2.0",
"@vueuse/motion": "^2.2.6", "@vueuse/motion": "^2.2.6",
"@vueuse/nuxt": "^11.2.0", "@vueuse/nuxt": "^11.2.0",
"@zxing/library": "^0.21.3",
"amplitude-js": "^8.21.9", "amplitude-js": "^8.21.9",
"chart.js": "^4.4.5", "chart.js": "^4.4.5",
"clone-deep": "^4.0.1", "clone-deep": "^4.0.1",
@ -36,7 +37,6 @@
"pinia": "^3.0.2", "pinia": "^3.0.2",
"prismjs": "^1.29.0", "prismjs": "^1.29.0",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"quagga": "^0.12.1",
"query-builder-vue-3": "^1.0.1", "query-builder-vue-3": "^1.0.1",
"quill": "^2.0.2", "quill": "^2.0.2",
"tailwind-merge": "^2.5.4", "tailwind-merge": "^2.5.4",
@ -57,6 +57,7 @@
"@iconify-json/clarity": "^1.2.1", "@iconify-json/clarity": "^1.2.1",
"@iconify-json/ic": "^1.2.1", "@iconify-json/ic": "^1.2.1",
"@iconify-json/octicon": "^1.2.1", "@iconify-json/octicon": "^1.2.1",
"@iconify-json/tabler": "^1.2.1",
"@nuxt/eslint-config": "^1.3.0", "@nuxt/eslint-config": "^1.3.0",
"@nuxt/icon": "^1.12.0", "@nuxt/icon": "^1.12.0",
"@nuxtjs/i18n": "^9.0.0", "@nuxtjs/i18n": "^9.0.0",
@ -1588,6 +1589,15 @@
"@iconify/types": "*" "@iconify/types": "*"
} }
}, },
"node_modules/@iconify-json/tabler": {
"version": "1.2.18",
"resolved": "https://registry.npmjs.org/@iconify-json/tabler/-/tabler-1.2.18.tgz",
"integrity": "sha512-W+8qiJhJpb4dmBw3P7JSM9QhGsFG8GIS3BJWAmrJ/92rzK6NPGUOPfGmswoO+/MuPzQV96ColY9lcUktUKv0pg==",
"dev": true,
"dependencies": {
"@iconify/types": "*"
}
},
"node_modules/@iconify/collections": { "node_modules/@iconify/collections": {
"version": "1.0.546", "version": "1.0.546",
"resolved": "https://registry.npmjs.org/@iconify/collections/-/collections-1.0.546.tgz", "resolved": "https://registry.npmjs.org/@iconify/collections/-/collections-1.0.546.tgz",
@ -9132,6 +9142,26 @@
"node": ">= 10" "node": ">= 10"
} }
}, },
"node_modules/@zxing/library": {
"version": "0.21.3",
"resolved": "https://registry.npmjs.org/@zxing/library/-/library-0.21.3.tgz",
"integrity": "sha512-hZHqFe2JyH/ZxviJZosZjV+2s6EDSY0O24R+FQmlWZBZXP9IqMo7S3nb3+2LBWxodJQkSurdQGnqE7KXqrYgow==",
"dependencies": {
"ts-custom-error": "^3.2.1"
},
"engines": {
"node": ">= 10.4.0"
},
"optionalDependencies": {
"@zxing/text-encoding": "~0.9.0"
}
},
"node_modules/@zxing/text-encoding": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz",
"integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==",
"optional": true
},
"node_modules/abbrev": { "node_modules/abbrev": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz",
@ -9214,6 +9244,7 @@
"version": "6.12.6", "version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"devOptional": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"fast-deep-equal": "^3.1.1", "fast-deep-equal": "^3.1.1",
@ -9428,24 +9459,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/asn1": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
"license": "MIT",
"dependencies": {
"safer-buffer": "~2.1.0"
}
},
"node_modules/assert-plus": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
"integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==",
"license": "MIT",
"engines": {
"node": ">=0.8"
}
},
"node_modules/ast-kit": { "node_modules/ast-kit": {
"version": "1.4.3", "version": "1.4.3",
"resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-1.4.3.tgz", "resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-1.4.3.tgz",
@ -9496,12 +9509,6 @@
"integrity": "sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==", "integrity": "sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/at-least-node": { "node_modules/at-least-node": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
@ -9548,21 +9555,6 @@
"postcss": "^8.1.0" "postcss": "^8.1.0"
} }
}, },
"node_modules/aws-sign2": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
"integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==",
"license": "Apache-2.0",
"engines": {
"node": "*"
}
},
"node_modules/aws4": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz",
"integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==",
"license": "MIT"
},
"node_modules/b4a": { "node_modules/b4a": {
"version": "1.6.7", "version": "1.6.7",
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz",
@ -9602,15 +9594,6 @@
], ],
"license": "MIT" "license": "MIT"
}, },
"node_modules/bcrypt-pbkdf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
"integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
"license": "BSD-3-Clause",
"dependencies": {
"tweetnacl": "^0.14.3"
}
},
"node_modules/binary-extensions": { "node_modules/binary-extensions": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@ -10021,12 +10004,6 @@
], ],
"license": "CC-BY-4.0" "license": "CC-BY-4.0"
}, },
"node_modules/caseless": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
"integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==",
"license": "Apache-2.0"
},
"node_modules/chalk": { "node_modules/chalk": {
"version": "5.4.1", "version": "5.4.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
@ -10287,18 +10264,6 @@
"text-hex": "1.0.x" "text-hex": "1.0.x"
} }
}, },
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/commander": { "node_modules/commander": {
"version": "8.3.0", "version": "8.3.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
@ -10780,33 +10745,6 @@
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/cwise-compiler": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/cwise-compiler/-/cwise-compiler-1.1.3.tgz",
"integrity": "sha512-WXlK/m+Di8DMMcCjcWr4i+XzcQra9eCdXIJrgh4TUgh0pIS/yJduLxS9JgefsHJ/YVLdgPtXm9r62W92MvanEQ==",
"license": "MIT",
"dependencies": {
"uniq": "^1.0.0"
}
},
"node_modules/dashdash": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
"integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==",
"license": "MIT",
"dependencies": {
"assert-plus": "^1.0.0"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/data-uri-to-buffer": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-0.0.3.tgz",
"integrity": "sha512-Cp+jOa8QJef5nXS5hU7M1DWzXPEIoVR3kbV0dQuVGwROZg8bGf1DcCnkmajBTnvghTtSNMUdRrPjgaT6ZQucbw==",
"license": "MIT"
},
"node_modules/date-fns": { "node_modules/date-fns": {
"version": "2.30.0", "version": "2.30.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
@ -10981,15 +10919,6 @@
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/delegates": { "node_modules/delegates": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
@ -11406,16 +11335,6 @@
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/ecc-jsbn": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
"integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==",
"license": "MIT",
"dependencies": {
"jsbn": "~0.1.0",
"safer-buffer": "^2.1.0"
}
},
"node_modules/ee-first": { "node_modules/ee-first": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@ -12313,12 +12232,6 @@
"integrity": "sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg==", "integrity": "sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
"license": "MIT"
},
"node_modules/externality": { "node_modules/externality": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/externality/-/externality-1.0.2.tgz", "resolved": "https://registry.npmjs.org/externality/-/externality-1.0.2.tgz",
@ -12372,19 +12285,11 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/extsprintf": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
"integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==",
"engines": [
"node >=0.6.0"
],
"license": "MIT"
},
"node_modules/fast-deep-equal": { "node_modules/fast-deep-equal": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-diff": { "node_modules/fast-diff": {
@ -12431,6 +12336,7 @@
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
"devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-levenshtein": { "node_modules/fast-levenshtein": {
@ -12642,50 +12548,6 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/forever-agent": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
"integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==",
"license": "Apache-2.0",
"engines": {
"node": "*"
}
},
"node_modules/form-data": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
"integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.6",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 0.12"
}
},
"node_modules/form-data/node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/form-data/node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/formdata-polyfill": { "node_modules/formdata-polyfill": {
"version": "4.0.10", "version": "4.0.10",
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
@ -12865,55 +12727,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/get-pixels": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/get-pixels/-/get-pixels-3.3.3.tgz",
"integrity": "sha512-5kyGBn90i9tSMUVHTqkgCHsoWoR+/lGbl4yC83Gefyr0HLIhgSWEx/2F/3YgsZ7UpYNuM6pDhDK7zebrUJ5nXg==",
"license": "MIT",
"dependencies": {
"data-uri-to-buffer": "0.0.3",
"jpeg-js": "^0.4.1",
"mime-types": "^2.0.1",
"ndarray": "^1.0.13",
"ndarray-pack": "^1.1.1",
"node-bitmap": "0.0.1",
"omggif": "^1.0.5",
"parse-data-uri": "^0.2.0",
"pngjs": "^3.3.3",
"request": "^2.44.0",
"through": "^2.3.4"
}
},
"node_modules/get-pixels/node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/get-pixels/node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/get-pixels/node_modules/pngjs": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz",
"integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==",
"license": "MIT",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/get-port-please": { "node_modules/get-port-please": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.1.2.tgz", "resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.1.2.tgz",
@ -12958,15 +12771,6 @@
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
} }
}, },
"node_modules/getpass": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
"integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==",
"license": "MIT",
"dependencies": {
"assert-plus": "^1.0.0"
}
},
"node_modules/giget": { "node_modules/giget": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz",
@ -13003,24 +12807,6 @@
"git-up": "^8.1.0" "git-up": "^8.1.0"
} }
}, },
"node_modules/gl-mat2": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gl-mat2/-/gl-mat2-1.0.1.tgz",
"integrity": "sha512-oHgZ3DalAo9qAhMZM9QigXosqotcUCsgxarwrinipaqfSHvacI79Dzs72gY+oT4Td1kDQKEsG0RyX6mb02VVHA==",
"license": "zlib"
},
"node_modules/gl-vec2": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/gl-vec2/-/gl-vec2-1.3.0.tgz",
"integrity": "sha512-YiqaAuNsheWmUV0Sa8k94kBB0D6RWjwZztyO+trEYS8KzJ6OQB/4686gdrf59wld4hHFIvaxynO3nRxpk1Ij/A==",
"license": "zlib"
},
"node_modules/gl-vec3": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/gl-vec3/-/gl-vec3-1.1.3.tgz",
"integrity": "sha512-jduKUqT0SGH02l8Yl+mV1yVsDfYgQAJyXGxkJQGyxPLHRiW25DwVIRPt6uvhrEMHftJfqhqKthRcyZqNEl9Xdw==",
"license": "zlib"
},
"node_modules/glob": { "node_modules/glob": {
"version": "9.3.5", "version": "9.3.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-9.3.5.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-9.3.5.tgz",
@ -13199,29 +12985,6 @@
"h3": "^1.6.0" "h3": "^1.6.0"
} }
}, },
"node_modules/har-schema": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
"integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==",
"license": "ISC",
"engines": {
"node": ">=4"
}
},
"node_modules/har-validator": {
"version": "5.1.5",
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz",
"integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==",
"deprecated": "this library is no longer supported",
"license": "MIT",
"dependencies": {
"ajv": "^6.12.3",
"har-schema": "^2.0.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/has-flag": { "node_modules/has-flag": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@ -13373,21 +13136,6 @@
"node": ">= 0.12.0" "node": ">= 0.12.0"
} }
}, },
"node_modules/http-signature": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
"integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==",
"license": "MIT",
"dependencies": {
"assert-plus": "^1.0.0",
"jsprim": "^1.2.2",
"sshpk": "^1.7.0"
},
"engines": {
"node": ">=0.8",
"npm": ">=1.3.7"
}
},
"node_modules/https-proxy-agent": { "node_modules/https-proxy-agent": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
@ -13597,12 +13345,6 @@
"url": "https://opencollective.com/ioredis" "url": "https://opencollective.com/ioredis"
} }
}, },
"node_modules/iota-array": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/iota-array/-/iota-array-1.0.0.tgz",
"integrity": "sha512-pZ2xT+LOHckCatGQ3DcG/a+QuEqvoxqkiL7tvE8nn3uuu+f6i1TtpB5/FtWFbxUuVr5PZCx8KskuGatbJDXOWA==",
"license": "MIT"
},
"node_modules/ipaddr.js": { "node_modules/ipaddr.js": {
"version": "1.9.1", "version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@ -13640,12 +13382,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/is-buffer": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
"integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
"license": "MIT"
},
"node_modules/is-builtin-module": { "node_modules/is-builtin-module": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-5.0.0.tgz", "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-5.0.0.tgz",
@ -13877,12 +13613,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/is-typedarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
"integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==",
"license": "MIT"
},
"node_modules/is-url": { "node_modules/is-url": {
"version": "1.2.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz",
@ -13964,12 +13694,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/isstream": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
"integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==",
"license": "MIT"
},
"node_modules/jackspeak": { "node_modules/jackspeak": {
"version": "3.4.3", "version": "3.4.3",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
@ -13994,12 +13718,6 @@
"jiti": "lib/jiti-cli.mjs" "jiti": "lib/jiti-cli.mjs"
} }
}, },
"node_modules/jpeg-js": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz",
"integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==",
"license": "BSD-3-Clause"
},
"node_modules/js-sha256": { "node_modules/js-sha256": {
"version": "0.10.1", "version": "0.10.1",
"resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.10.1.tgz", "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.10.1.tgz",
@ -14025,12 +13743,6 @@
"js-yaml": "bin/js-yaml.js" "js-yaml": "bin/js-yaml.js"
} }
}, },
"node_modules/jsbn": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
"integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==",
"license": "MIT"
},
"node_modules/jsdoc-type-pratt-parser": { "node_modules/jsdoc-type-pratt-parser": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.1.0.tgz", "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.1.0.tgz",
@ -14060,16 +13772,11 @@
"devOptional": true, "devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/json-schema": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
"integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
"license": "(AFL-2.1 OR BSD-3-Clause)"
},
"node_modules/json-schema-traverse": { "node_modules/json-schema-traverse": {
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/json-stable-stringify-without-jsonify": { "node_modules/json-stable-stringify-without-jsonify": {
@ -14079,12 +13786,6 @@
"devOptional": true, "devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/json-stringify-safe": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
"license": "ISC"
},
"node_modules/json5": { "node_modules/json5": {
"version": "2.2.3", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
@ -14146,21 +13847,6 @@
"graceful-fs": "^4.1.6" "graceful-fs": "^4.1.6"
} }
}, },
"node_modules/jsprim": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz",
"integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==",
"license": "MIT",
"dependencies": {
"assert-plus": "1.0.0",
"extsprintf": "1.3.0",
"json-schema": "0.4.0",
"verror": "1.10.0"
},
"engines": {
"node": ">=0.6.0"
}
},
"node_modules/junk": { "node_modules/junk": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/junk/-/junk-4.0.1.tgz", "resolved": "https://registry.npmjs.org/junk/-/junk-4.0.1.tgz",
@ -15246,32 +14932,6 @@
"devOptional": true, "devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/ndarray": {
"version": "1.0.19",
"resolved": "https://registry.npmjs.org/ndarray/-/ndarray-1.0.19.tgz",
"integrity": "sha512-B4JHA4vdyZU30ELBw3g7/p9bZupyew5a7tX1Y/gGeF2hafrPaQZhgrGQfsvgfYbgdFZjYwuEcnaobeM/WMW+HQ==",
"license": "MIT",
"dependencies": {
"iota-array": "^1.0.0",
"is-buffer": "^1.0.2"
}
},
"node_modules/ndarray-linear-interpolate": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/ndarray-linear-interpolate/-/ndarray-linear-interpolate-1.0.0.tgz",
"integrity": "sha512-UN0f4+6XWsQzJ2pP5gVp+kKn5tJed6mA3K/L50uO619+7LKrjcSNdcerhpqxYaSkbxNJuEN76N05yBBJySnZDw==",
"license": "MIT"
},
"node_modules/ndarray-pack": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ndarray-pack/-/ndarray-pack-1.2.1.tgz",
"integrity": "sha512-51cECUJMT0rUZNQa09EoKsnFeDL4x2dHRT0VR5U2H5ZgEcm95ZDWcMA5JShroXjHOejmAD/fg8+H+OvUnVXz2g==",
"license": "MIT",
"dependencies": {
"cwise-compiler": "^1.1.2",
"ndarray": "^1.0.13"
}
},
"node_modules/negotiator": { "node_modules/negotiator": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
@ -15883,14 +15543,6 @@
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/node-bitmap": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/node-bitmap/-/node-bitmap-0.0.1.tgz",
"integrity": "sha512-Jx5lPaaLdIaOsj2mVLWMWulXF6GQVdyLvNSxmiYCvZ8Ma2hfKX0POoR2kgKOqz+oFsRreq0yYZjQ2wjE9VNzCA==",
"engines": {
"node": ">=v0.6.5"
}
},
"node_modules/node-domexception": { "node_modules/node-domexception": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
@ -16865,15 +16517,6 @@
"integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/oauth-sign": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
"integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==",
"license": "Apache-2.0",
"engines": {
"node": "*"
}
},
"node_modules/object-assign": { "node_modules/object-assign": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@ -16927,12 +16570,6 @@
"integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/omggif": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.10.tgz",
"integrity": "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==",
"license": "MIT"
},
"node_modules/on-change": { "node_modules/on-change": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/on-change/-/on-change-5.0.1.tgz", "resolved": "https://registry.npmjs.org/on-change/-/on-change-5.0.1.tgz",
@ -17231,15 +16868,6 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/parse-data-uri": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/parse-data-uri/-/parse-data-uri-0.2.0.tgz",
"integrity": "sha512-uOtts8NqDcaCt1rIsO3VFDRsAfgE4c6osG4d9z3l4dCBlxYFzni6Di/oNU270SDrjkfZuUvLZx1rxMyqh46Y9w==",
"license": "ISC",
"dependencies": {
"data-uri-to-buffer": "0.0.3"
}
},
"node_modules/parse-gitignore": { "node_modules/parse-gitignore": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/parse-gitignore/-/parse-gitignore-2.0.0.tgz", "resolved": "https://registry.npmjs.org/parse-gitignore/-/parse-gitignore-2.0.0.tgz",
@ -17418,12 +17046,6 @@
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
"license": "MIT"
},
"node_modules/pg-int8": { "node_modules/pg-int8": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
@ -18531,18 +18153,6 @@
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/psl": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
"integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==",
"license": "MIT",
"dependencies": {
"punycode": "^2.3.1"
},
"funding": {
"url": "https://github.com/sponsors/lupomontero"
}
},
"node_modules/pump": { "node_modules/pump": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
@ -18557,6 +18167,7 @@
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"devOptional": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=6" "node": ">=6"
@ -18733,24 +18344,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/quagga": {
"version": "0.12.1",
"resolved": "https://registry.npmjs.org/quagga/-/quagga-0.12.1.tgz",
"integrity": "sha512-bb2N6eT7ss6Bg27sxQgv/CT96KQBkXa+4YeS1W8bhsaXxoWp8zOQbrOwFWEPxPDTNaWEl7hTs3ZB7OC4k3EY3Q==",
"license": "MIT",
"dependencies": {
"get-pixels": "^3.2.3",
"gl-mat2": "^1.0.0",
"gl-vec2": "^1.0.0",
"gl-vec3": "^1.0.3",
"lodash": "^4.17.4",
"ndarray": "^1.0.18",
"ndarray-linear-interpolate": "^1.0.0"
},
"engines": {
"node": ">= 4.0"
}
},
"node_modules/quansync": { "node_modules/quansync": {
"version": "0.2.10", "version": "0.2.10",
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.10.tgz", "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.10.tgz",
@ -19188,78 +18781,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/request": {
"version": "2.88.2",
"resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz",
"integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==",
"deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142",
"license": "Apache-2.0",
"dependencies": {
"aws-sign2": "~0.7.0",
"aws4": "^1.8.0",
"caseless": "~0.12.0",
"combined-stream": "~1.0.6",
"extend": "~3.0.2",
"forever-agent": "~0.6.1",
"form-data": "~2.3.2",
"har-validator": "~5.1.3",
"http-signature": "~1.2.0",
"is-typedarray": "~1.0.0",
"isstream": "~0.1.2",
"json-stringify-safe": "~5.0.1",
"mime-types": "~2.1.19",
"oauth-sign": "~0.9.0",
"performance-now": "^2.1.0",
"qs": "~6.5.2",
"safe-buffer": "^5.1.2",
"tough-cookie": "~2.5.0",
"tunnel-agent": "^0.6.0",
"uuid": "^3.3.2"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/request/node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/request/node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/request/node_modules/qs": {
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz",
"integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.6"
}
},
"node_modules/request/node_modules/uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.",
"license": "MIT",
"bin": {
"uuid": "bin/uuid"
}
},
"node_modules/require-directory": { "node_modules/require-directory": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@ -19589,6 +19110,7 @@
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/sass": { "node_modules/sass": {
@ -20029,31 +19551,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/sshpk": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz",
"integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==",
"license": "MIT",
"dependencies": {
"asn1": "~0.2.3",
"assert-plus": "^1.0.0",
"bcrypt-pbkdf": "^1.0.0",
"dashdash": "^1.12.0",
"ecc-jsbn": "~0.1.1",
"getpass": "^0.1.1",
"jsbn": "~0.1.0",
"safer-buffer": "^2.0.2",
"tweetnacl": "~0.14.0"
},
"bin": {
"sshpk-conv": "bin/sshpk-conv",
"sshpk-sign": "bin/sshpk-sign",
"sshpk-verify": "bin/sshpk-verify"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/stable-hash": { "node_modules/stable-hash": {
"version": "0.0.5", "version": "0.0.5",
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz",
@ -20818,12 +20315,6 @@
"node": ">=0.8" "node": ">=0.8"
} }
}, },
"node_modules/through": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
"integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
"license": "MIT"
},
"node_modules/tiny-invariant": { "node_modules/tiny-invariant": {
"version": "1.3.3", "version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
@ -20924,19 +20415,6 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/tough-cookie": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
"integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
"license": "BSD-3-Clause",
"dependencies": {
"psl": "^1.1.28",
"punycode": "^2.1.1"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/tr46": { "node_modules/tr46": {
"version": "0.0.3", "version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
@ -20965,6 +20443,14 @@
"typescript": ">=4.8.4" "typescript": ">=4.8.4"
} }
}, },
"node_modules/ts-custom-error": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/ts-custom-error/-/ts-custom-error-3.3.1.tgz",
"integrity": "sha512-5OX1tzOjxWEgsr/YEUWSuPrQ00deKLh6D7OTWcvNHm12/7QPyRh8SYpyWvA4IZv8H/+GQWQEh/kwo95Q9OVW1A==",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/ts-interface-checker": { "node_modules/ts-interface-checker": {
"version": "0.1.13", "version": "0.1.13",
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
@ -21007,24 +20493,6 @@
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
"license": "0BSD" "license": "0BSD"
}, },
"node_modules/tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
},
"engines": {
"node": "*"
}
},
"node_modules/tweetnacl": {
"version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
"integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==",
"license": "Unlicense"
},
"node_modules/type-check": { "node_modules/type-check": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@ -21200,12 +20668,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/uniq": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz",
"integrity": "sha512-Gw+zz50YNKPDKXs+9d+aKAjVwpjNwqzvNpLigIruT4HA9lMZNdMqs9x07kKHB/L9WRzqp4+DlTU5s4wG2esdoA==",
"license": "MIT"
},
"node_modules/universalify": { "node_modules/universalify": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
@ -21573,6 +21035,7 @@
"version": "4.4.1", "version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
"devOptional": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"punycode": "^2.1.0" "punycode": "^2.1.0"
@ -21650,20 +21113,6 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/verror": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
"integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==",
"engines": [
"node >=0.6.0"
],
"license": "MIT",
"dependencies": {
"assert-plus": "^1.0.0",
"core-util-is": "1.0.2",
"extsprintf": "^1.2.0"
}
},
"node_modules/vite": { "node_modules/vite": {
"version": "6.3.5", "version": "6.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",

View File

@ -16,6 +16,7 @@
"@iconify-json/clarity": "^1.2.1", "@iconify-json/clarity": "^1.2.1",
"@iconify-json/ic": "^1.2.1", "@iconify-json/ic": "^1.2.1",
"@iconify-json/octicon": "^1.2.1", "@iconify-json/octicon": "^1.2.1",
"@iconify-json/tabler": "^1.2.1",
"@nuxt/eslint-config": "^1.3.0", "@nuxt/eslint-config": "^1.3.0",
"@nuxt/icon": "^1.12.0", "@nuxt/icon": "^1.12.0",
"@nuxtjs/i18n": "^9.0.0", "@nuxtjs/i18n": "^9.0.0",
@ -49,6 +50,7 @@
"@vueuse/integrations": "^11.2.0", "@vueuse/integrations": "^11.2.0",
"@vueuse/motion": "^2.2.6", "@vueuse/motion": "^2.2.6",
"@vueuse/nuxt": "^11.2.0", "@vueuse/nuxt": "^11.2.0",
"@zxing/library": "^0.21.3",
"amplitude-js": "^8.21.9", "amplitude-js": "^8.21.9",
"chart.js": "^4.4.5", "chart.js": "^4.4.5",
"clone-deep": "^4.0.1", "clone-deep": "^4.0.1",
@ -64,7 +66,6 @@
"pinia": "^3.0.2", "pinia": "^3.0.2",
"prismjs": "^1.29.0", "prismjs": "^1.29.0",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"quagga": "^0.12.1",
"query-builder-vue-3": "^1.0.1", "query-builder-vue-3": "^1.0.1",
"quill": "^2.0.2", "quill": "^2.0.2",
"tailwind-merge": "^2.5.4", "tailwind-merge": "^2.5.4",

View File

@ -1,10 +0,0 @@
import { useFeatureFlagsStore } from '~/stores/featureFlags'
export default defineNuxtPlugin(async () => {
const featureFlagsStore = useFeatureFlagsStore()
// Load flags if they haven't been loaded yet
if (!featureFlagsStore.isLoaded) {
await featureFlagsStore.fetchFlags()
}
})

View File

@ -1,9 +1,18 @@
import { useFeatureFlagsStore } from '~/stores/featureFlags' import { useFeatureFlagsStore } from '~/stores/featureFlags'
export default defineNuxtPlugin((nuxtApp) => { export default defineNuxtPlugin(async (nuxtApp) => {
const featureFlagsStore = useFeatureFlagsStore() // Get the pinia instance for SSR compatibility
const { $pinia } = nuxtApp
nuxtApp.provide('featureFlag', (key, defaultValue = false) => { try {
return featureFlagsStore.getFlag(key, defaultValue) // Pass pinia instance for SSR compatibility
}) const featureFlagsStore = useFeatureFlagsStore($pinia)
// Fetch flags during SSR to prevent hydration mismatches
if (!featureFlagsStore.isLoaded) {
await featureFlagsStore.fetchFlags()
}
} catch (error) {
console.error('Feature flags plugin failed:', error)
}
}) })

View File

@ -25,7 +25,8 @@ function mergeOptions(options) {
} }
} }
}, },
debounceWait: 300 debounceWait: 300,
ignoreKeys: [] // Keys to ignore in history tracking
} }
return { return {
@ -34,6 +35,25 @@ function mergeOptions(options) {
} }
} }
/**
* Filters out ignored keys from the state object
* @param {Object} state - The state object to filter
* @param {Array} ignoreKeys - Array of keys to ignore
* @returns {Object} Filtered state object
*/
function filterState(state, ignoreKeys) {
if (!ignoreKeys || ignoreKeys.length === 0) {
return state
}
const filteredState = { ...state }
ignoreKeys.forEach(key => {
delete filteredState[key]
})
return filteredState
}
/** /**
* Adds undo/redo functionality to a Pinia store. * Adds undo/redo functionality to a Pinia store.
* @param {PiniaPluginContext} context - The context provided by Pinia. * @param {PiniaPluginContext} context - The context provided by Pinia.
@ -46,7 +66,7 @@ const PiniaHistory = (context) => {
return return
} }
const mergedOptions = mergeOptions(history) const mergedOptions = mergeOptions(history)
const {max, persistent, persistentStrategy} = mergedOptions const {max, persistent, persistentStrategy, ignoreKeys} = mergedOptions
const $history = reactive({ const $history = reactive({
max, max,
@ -54,19 +74,23 @@ const PiniaHistory = (context) => {
persistentStrategy, persistentStrategy,
done: [], done: [],
undone: [], undone: [],
current: JSON.stringify(store.$state), current: JSON.stringify(filterState(store.$state, ignoreKeys)),
trigger: true, trigger: true,
}) })
const debouncedStoreUpdate = debounce((state) => { const debouncedStoreUpdate = debounce((state) => {
if (hash($history.current) === hash(JSON.stringify(state))) { // Not a real change here const filteredState = filterState(state, ignoreKeys)
const currentStateHash = hash($history.current)
const newStateHash = hash(JSON.stringify(filteredState))
if (currentStateHash === newStateHash) { // Not a real change here
return return
} }
if ($history.done.length >= max) $history.done.shift() // Remove oldest state if needed if ($history.done.length >= max) $history.done.shift() // Remove oldest state if needed
$history.done.push($history.current) $history.done.push($history.current)
$history.undone = [] // Clear redo history on new action $history.undone = [] // Clear redo history on new action
$history.current = JSON.stringify(state) $history.current = JSON.stringify(filteredState)
if (persistent) { if (persistent) {
persistentStrategy.set(store, 'undo', $history.done) persistentStrategy.set(store, 'undo', $history.done)
@ -90,7 +114,9 @@ const PiniaHistory = (context) => {
$history.undone.push($history.current) // Save current state for redo $history.undone.push($history.current) // Save current state for redo
$history.trigger = false $history.trigger = false
store.$patch(JSON.parse(state)) // Only patch the state that was tracked (filtered state)
const stateToRestore = JSON.parse(state)
store.$patch(stateToRestore)
nextTick(() => { nextTick(() => {
$history.current = state $history.current = state
$history.trigger = true $history.trigger = true
@ -114,7 +140,9 @@ const PiniaHistory = (context) => {
$history.done.push($history.current) // Save current state for undo $history.done.push($history.current) // Save current state for undo
$history.trigger = false $history.trigger = false
store.$patch(JSON.parse(state)) // Only patch the state that was tracked (filtered state)
const stateToRestore = JSON.parse(state)
store.$patch(stateToRestore)
nextTick(() => { nextTick(() => {
$history.current = state $history.current = state
$history.trigger = true $history.trigger = true

View File

@ -175,35 +175,42 @@ export const useWorkingFormStore = defineStore("working_form", {
} }
if (!this.content) return if (!this.content) return
const block = blocksTypes[type] const originalBlockDefinition = blocksTypes[type]
if (block?.self_hosted !== undefined && !block.self_hosted && useFeatureFlag('self_hosted')) { const effectiveType = originalBlockDefinition?.actual_input || type
const effectiveBlockDefinition = blocksTypes[effectiveType]
if (originalBlockDefinition?.self_hosted !== undefined && !originalBlockDefinition.self_hosted && useFeatureFlag('self_hosted')) {
useAlert().error(block?.title + ' is not allowed on self hosted. Please use our hosted version.') useAlert().error(block?.title + ' is not allowed on self hosted. Please use our hosted version.')
return return
} }
if (originalBlockDefinition?.auth_required && !useAuthStore().check) {
if (block?.auth_required && !useAuthStore().check) {
useAlert().error('Please login first to add this block') useAlert().error('Please login first to add this block')
return return
} }
if (block?.max_count !== undefined) { if (originalBlockDefinition?.max_count !== undefined) {
const currentCount = this.content.properties.filter(prop => prop && prop.type === type).length const currentCount = this.content.properties.filter(prop => prop && prop.type === type).length
if (currentCount >= block.max_count) { if (currentCount >= originalBlockDefinition.max_count) {
useAlert().error(`Only ${block.max_count} '${block.title}' block(s) allowed per form.`) useAlert().error(`Only ${originalBlockDefinition.max_count} '${originalBlockDefinition.title}' block(s) allowed per form.`)
return return
} }
openSettings = true openSettings = true
} }
this.blockForm.type = type this.blockForm.type = effectiveType
this.blockForm.name = blocksTypes[type]?.default_block_name || 'New Block' this.blockForm.name = effectiveBlockDefinition?.default_block_name || 'New Block'
const newBlock = this.prefillDefault({ ...this.blockForm.data() }) const newBlock = this.prefillDefault({ ...this.blockForm.data() })
newBlock.id = generateUUID() newBlock.id = generateUUID()
newBlock.hidden = false newBlock.hidden = false
newBlock.help_position = "below_input" newBlock.help_position = "below_input"
if (blocksTypes[type]?.default_values) { // If the type was changed due to actual_input, apply original type's change settings
Object.assign(newBlock, blocksTypes[type].default_values) if (originalBlockDefinition?.actual_input && originalBlockDefinition?.type_change_settings) {
Object.assign(newBlock, originalBlockDefinition.type_change_settings)
}
if (effectiveBlockDefinition?.default_values) {
Object.assign(newBlock, effectiveBlockDefinition.default_values)
} }
const insertIndex = this.determineInsertIndex(index) const insertIndex = this.determineInsertIndex(index)
@ -253,5 +260,7 @@ export const useWorkingFormStore = defineStore("working_form", {
this.setProperties(newFields) this.setProperties(newFields)
} }
}, },
history: {} history: {
ignoreKeys: ['structureService', 'blockForm']
}
}) })

View File

@ -79,7 +79,7 @@ module.exports = {
border: 'rgba(15, 15, 15, 0.1)', border: 'rgba(15, 15, 15, 0.1)',
borderDark: 'rgba(255, 255, 255, 0.1)' borderDark: 'rgba(255, 255, 255, 0.1)'
}, },
"form-color": "var(--bg-form-color)", 'form-color': 'rgb(from var(--form-color, var(--bg-form-color)) r g b / <alpha-value>)'
}, },
transitionProperty: { transitionProperty: {
height: "height", height: "height",

View File

@ -1,11 +1,10 @@
---
services: services:
api: &api-environment api:
image: jhumanj/opnform-api:latest image: jhumanj/opnform-api:latest
container_name: opnform-api container_name: opnform-api
volumes: &api-environment-volumes volumes:
- opnform_storage:/usr/share/nginx/html/storage:rw - opnform_storage:/usr/share/nginx/html/storage:rw
environment: &api-env environment:
APP_ENV: production APP_ENV: production
# Database settings # Database settings
DB_HOST: db DB_HOST: db
@ -25,7 +24,7 @@ services:
db: db:
condition: service_healthy condition: service_healthy
redis: redis:
condition: service_healthy # Depend on redis being healthy too condition: service_healthy
healthcheck: healthcheck:
test: ["CMD-SHELL", "php /usr/share/nginx/html/artisan about || exit 1"] test: ["CMD-SHELL", "php /usr/share/nginx/html/artisan about || exit 1"]
interval: 30s interval: 30s
@ -33,13 +32,49 @@ services:
retries: 3 retries: 3
start_period: 60s start_period: 60s
api-nginx:
image: nginx:alpine
container_name: opnform-api-nginx
volumes:
- ./docker/api-nginx.conf:/etc/nginx/nginx.conf:ro
- opnform_storage:/usr/share/nginx/html/storage:ro
ports:
- "127.0.0.1:7654:80" # API on port 7654
depends_on:
- api
healthcheck:
test: ["CMD-SHELL", "wget --spider -q http://localhost/ || exit 1"]
interval: 30s
timeout: 10s
retries: 3
api-worker: api-worker:
<<: *api-environment image: jhumanj/opnform-api:latest
container_name: opnform-api-worker container_name: opnform-api-worker
command: ["php", "artisan", "queue:work"] command: ["php", "artisan", "queue:work"]
volumes:
- opnform_storage:/usr/share/nginx/html/storage:rw
environment: environment:
<<: *api-env
APP_ENV: production APP_ENV: production
# Database settings
DB_HOST: db
REDIS_HOST: redis
DB_DATABASE: ${DB_DATABASE:-forge}
DB_USERNAME: ${DB_USERNAME:-forge}
DB_PASSWORD: ${DB_PASSWORD:-forge}
DB_CONNECTION: ${DB_CONNECTION:-pgsql}
# PHP Configuration
PHP_MEMORY_LIMIT: "1G"
PHP_MAX_EXECUTION_TIME: "600"
PHP_UPLOAD_MAX_FILESIZE: "64M"
PHP_POST_MAX_SIZE: "64M"
env_file:
- ./api/.env
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
healthcheck: healthcheck:
test: ["CMD-SHELL", "pgrep -f 'php artisan queue:work' > /dev/null || exit 1"] test: ["CMD-SHELL", "pgrep -f 'php artisan queue:work' > /dev/null || exit 1"]
interval: 60s interval: 60s
@ -48,22 +83,44 @@ services:
start_period: 30s start_period: 30s
api-scheduler: api-scheduler:
<<: *api-environment image: jhumanj/opnform-api:latest
container_name: opnform-api-scheduler container_name: opnform-api-scheduler
command: ["php", "artisan", "schedule:work"] command: ["php", "artisan", "schedule:work"]
volumes:
- opnform_storage:/usr/share/nginx/html/storage:rw
environment: environment:
<<: *api-env
APP_ENV: production APP_ENV: production
# Database settings
DB_HOST: db
REDIS_HOST: redis
DB_DATABASE: ${DB_DATABASE:-forge}
DB_USERNAME: ${DB_USERNAME:-forge}
DB_PASSWORD: ${DB_PASSWORD:-forge}
DB_CONNECTION: ${DB_CONNECTION:-pgsql}
# PHP Configuration
PHP_MEMORY_LIMIT: "1G"
PHP_MAX_EXECUTION_TIME: "600"
PHP_UPLOAD_MAX_FILESIZE: "64M"
PHP_POST_MAX_SIZE: "64M"
env_file:
- ./api/.env
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
healthcheck: healthcheck:
test: ["CMD-SHELL", "php /usr/share/nginx/html/artisan app:scheduler-status --mode=check --max-minutes=3 || exit 1"] test: ["CMD-SHELL", "php /usr/share/nginx/html/artisan app:scheduler-status --mode=check --max-minutes=3 || exit 1"]
interval: 60s interval: 60s
timeout: 30s timeout: 30s
retries: 3 retries: 3
start_period: 70s # Allow time for first scheduled run and cache write start_period: 70s
ui: ui:
image: jhumanj/opnform-client:latest image: jhumanj/opnform-client:latest
container_name: opnform-client container_name: opnform-client
ports:
- "127.0.0.1:7655:3000" # UI on port 7655
env_file: env_file:
- ./client/.env - ./client/.env
healthcheck: healthcheck:
@ -95,27 +152,6 @@ services:
volumes: volumes:
- postgres-data:/var/lib/postgresql/data - postgres-data:/var/lib/postgresql/data
ingress:
image: nginx:1
container_name: opnform-ingress
volumes:
- ./docker/nginx.conf:/etc/nginx/templates/default.conf.template
ports:
- 80:80
environment:
- NGINX_MAX_BODY_SIZE=64m
depends_on:
api:
condition: service_started
ui:
condition: service_started
healthcheck:
test: ["CMD-SHELL", "nginx -t && curl -f http://localhost/ || exit 1"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
volumes: volumes:
postgres-data: postgres-data:
opnform_storage: opnform_storage:

View File

@ -27,6 +27,9 @@ RUN composer install --optimize-autoloader --no-interaction \
# Final stage - smaller runtime image # Final stage - smaller runtime image
FROM php:8.3-fpm-alpine FROM php:8.3-fpm-alpine
# Accept version build argument
ARG APP_VERSION=unknown
# Install runtime dependencies # Install runtime dependencies
RUN apk add --no-cache \ RUN apk add --no-cache \
libzip \ libzip \
@ -75,6 +78,9 @@ RUN mkdir -p storage/framework/sessions \
# Copy the entire application from the builder stage # Copy the entire application from the builder stage
COPY --from=builder /app/ ./ COPY --from=builder /app/ ./
# Set version as environment variable (more reliable than file approach)
ENV APP_VERSION_DOCKER=$APP_VERSION
# Setup entrypoint # Setup entrypoint
COPY docker/php-fpm-entrypoint /usr/local/bin/opnform-entrypoint COPY docker/php-fpm-entrypoint /usr/local/bin/opnform-entrypoint
RUN chmod a+x /usr/local/bin/* RUN chmod a+x /usr/local/bin/*

43
docker/api-nginx.conf Normal file
View File

@ -0,0 +1,43 @@
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
server {
listen 80;
server_name _;
root /usr/share/nginx/html/public;
index index.php;
client_max_body_size 64M;
# Logging
access_log /dev/stdout;
error_log /dev/stderr;
# Handle all requests through PHP
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
# PHP-FPM configuration
location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass opnform-api:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME /usr/share/nginx/html/public/index.php;
fastcgi_param DOCUMENT_ROOT /usr/share/nginx/html/public;
fastcgi_param REQUEST_URI $request_uri;
fastcgi_read_timeout 300;
}
# Deny access to . files
location ~ /\. {
deny all;
}
}
}

67
nginx-host-example.conf Normal file
View File

@ -0,0 +1,67 @@
# Example nginx configuration for forms.portnimara.dev
# Place this in /etc/nginx/sites-available/forms.portnimara.dev
# Then create a symlink: ln -s /etc/nginx/sites-available/forms.portnimara.dev /etc/nginx/sites-enabled/
server {
listen 80;
server_name forms.portnimara.dev;
# Redirect HTTP to HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name forms.portnimara.dev;
# SSL certificates - adjust paths as needed
ssl_certificate /etc/letsencrypt/live/forms.portnimara.dev/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/forms.portnimara.dev/privkey.pem;
# SSL configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# Client upload size
client_max_body_size 64M;
# Logging
access_log /var/log/nginx/forms.portnimara.dev.access.log;
error_log /var/log/nginx/forms.portnimara.dev.error.log;
# API routes - proxy to the api-nginx container
location ~ ^/(api|open|local/temp|forms/assets)/ {
proxy_pass http://127.0.0.1:7654;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_read_timeout 300s;
proxy_connect_timeout 75s;
}
# Everything else goes to the UI container
location / {
proxy_pass http://127.0.0.1:7655;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
# WebSocket support for hot reload and real-time features
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_cache_bypass $http_upgrade;
}
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
}

View File

@ -63,9 +63,12 @@ if [ "$DEV_MODE" = true ]; then
else else
echo -e "${BLUE}Production environment setup complete!${NC}" echo -e "${BLUE}Production environment setup complete!${NC}"
echo -e "${YELLOW}Please wait a moment for all services to start${NC}" echo -e "${YELLOW}Please wait a moment for all services to start${NC}"
echo -e "${GREEN}Then visit: http://localhost${NC}" echo -e "${GREEN}Services are available on:${NC}"
echo -e "${GREEN}- UI: http://localhost:7655${NC}"
echo -e "${GREEN}- API: http://localhost:7654${NC}"
echo -e "${YELLOW}Note: Configure your host nginx to proxy to these ports${NC}"
fi fi
echo -e "${BLUE}Default admin credentials:${NC}" echo -e "${BLUE}Default admin credentials:${NC}"
echo -e "${GREEN}Email: admin@opnform.com${NC}" echo -e "${GREEN}Email: admin@opnform.com${NC}"
echo -e "${GREEN}Password: password${NC}" echo -e "${GREEN}Password: password${NC}"