Compare commits
10 Commits
a030a84652
...
3a8e601a37
| Author | SHA1 | Date |
|---|---|---|
|
|
3a8e601a37 | |
|
|
a11fb01bef | |
|
|
a140f789c2 | |
|
|
9a42aacc3a | |
|
|
b47a528075 | |
|
|
cac88e7a3c | |
|
|
360b116062 | |
|
|
72a87f1de8 | |
|
|
61fc30b95c | |
|
|
f3a02df80e |
|
|
@ -60,6 +60,7 @@ jobs:
|
|||
build-args: |
|
||||
APP_ENV=${{ env.VERSION == 'dev' && 'local' || 'production' }}
|
||||
COMPOSER_FLAGS=${{ env.VERSION == 'dev' && '--optimize-autoloader --no-interaction' || '--no-dev --optimize-autoloader --no-interaction' }}
|
||||
APP_VERSION=${{ env.VERSION }}
|
||||
tags: ${{ env.API_TAGS }}
|
||||
cache-from: type=registry,ref=${{secrets.DOCKER_API_REPO}}:dev
|
||||
cache-to: type=inline
|
||||
|
|
@ -71,6 +72,8 @@ jobs:
|
|||
file: docker/Dockerfile.client
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
build-args: |
|
||||
APP_VERSION=${{ env.VERSION }}
|
||||
tags: ${{ env.UI_TAGS }}
|
||||
cache-from: type=registry,ref=${{secrets.DOCKER_UI_REPO}}:dev
|
||||
cache-to: type=inline
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
|
|
@ -14,6 +14,7 @@ class FeatureFlagsController extends Controller
|
|||
'self_hosted' => config('app.self_hosted', true),
|
||||
'custom_domains' => config('custom-domains.enabled', false),
|
||||
'ai_features' => !empty(config('services.openai.api_key')),
|
||||
'version' => $this->getAppVersion(),
|
||||
|
||||
'billing' => [
|
||||
'enabled' => !empty(config('cashier.key')) && !empty(config('cashier.secret')),
|
||||
|
|
@ -44,4 +45,17 @@ class FeatureFlagsController extends Controller
|
|||
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -179,9 +179,9 @@ class StoreFormSubmissionJob implements ShouldQueue
|
|||
}
|
||||
} else {
|
||||
// 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';
|
||||
} 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';
|
||||
} else {
|
||||
$finalData[$field['id']] = $answerValue;
|
||||
|
|
|
|||
|
|
@ -47,6 +47,14 @@ class IntegrationLogicRule implements DataAwareRule, ValidationRule
|
|||
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'])) {
|
||||
$this->isConditionCorrect = false;
|
||||
$this->conditionErrors[] = 'missing condition value';
|
||||
|
|
@ -54,8 +62,6 @@ class IntegrationLogicRule implements DataAwareRule, ValidationRule
|
|||
return;
|
||||
}
|
||||
|
||||
$typeField = $condition['value']['property_meta']['type'];
|
||||
$operator = $condition['value']['operator'];
|
||||
$value = $condition['value']['value'];
|
||||
|
||||
if (!isset(FormPropertyLogicRule::getConditionMapping()[$typeField])) {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,17 @@ return [
|
|||
|
||||
'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
|
||||
|
|
|
|||
|
|
@ -45,3 +45,58 @@ it('can CRUD form integration', function () {
|
|||
'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.'
|
||||
]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
it('can update form with existing record', function () {
|
||||
$user = $this->actingAsProUser();
|
||||
|
|
@ -39,3 +39,56 @@ it('can update form with existing record', function () {
|
|||
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);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,12 +8,14 @@
|
|||
v-if="isScanning"
|
||||
class="relative w-full"
|
||||
>
|
||||
<ClientOnly>
|
||||
<CameraUpload
|
||||
:is-barcode-mode="true"
|
||||
:decoders="decoders"
|
||||
@stop-webcam="stopScanning"
|
||||
@barcode-detected="handleBarcodeDetected"
|
||||
/>
|
||||
</ClientOnly>
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -10,12 +10,14 @@
|
|||
theme.fileInput.minHeight
|
||||
]"
|
||||
>
|
||||
<camera-upload
|
||||
<ClientOnly>
|
||||
<CameraUpload
|
||||
v-if="cameraUpload"
|
||||
:theme="theme"
|
||||
@upload-image="cameraFileUpload"
|
||||
@stop-webcam="isInWebcam=false"
|
||||
/>
|
||||
</ClientOnly>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
<template>
|
||||
<input-wrapper v-bind="inputWrapperProps">
|
||||
<InputWrapper v-bind="inputWrapperProps">
|
||||
<template #label>
|
||||
<slot name="label" />
|
||||
</template>
|
||||
|
||||
<span class="inline-block w-full rounded-md shadow-sm">
|
||||
<span class="inline-block w-full rounded-md shadow-xs">
|
||||
<button
|
||||
type="button"
|
||||
aria-haspopup="listbox"
|
||||
|
|
@ -24,52 +24,37 @@
|
|||
>
|
||||
<div
|
||||
v-if="currentUrl == null"
|
||||
class="text-gray-600 dark:text-gray-400"
|
||||
class="text-gray-600 dark:text-gray-400 flex justify-center"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5 inline"
|
||||
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>
|
||||
Upload image
|
||||
<Icon
|
||||
name="heroicons:cloud-arrow-up"
|
||||
class="h-5 w-5"
|
||||
/>
|
||||
<span class="ml-2">
|
||||
Upload
|
||||
</span>
|
||||
|
||||
</div>
|
||||
<div
|
||||
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">
|
||||
<img
|
||||
:src="currentUrl"
|
||||
class="h-6 rounded shadow-md"
|
||||
:src="tmpFile ?? currentUrl"
|
||||
class="h-5 rounded shadow-md border"
|
||||
>
|
||||
</div>
|
||||
<a
|
||||
href="#"
|
||||
class="hover:text-nt-blue flex"
|
||||
class="text-gray-500 hover:text-red-500 flex items-center"
|
||||
@click.prevent="clearUrl"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
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>
|
||||
<Icon
|
||||
name="heroicons:trash"
|
||||
class="h-5 w-5"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</button>
|
||||
</span>
|
||||
|
|
@ -105,7 +90,7 @@
|
|||
v-if="loading"
|
||||
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">
|
||||
Uploading your file...
|
||||
</p>
|
||||
|
|
@ -127,20 +112,10 @@
|
|||
accept="image/png, image/gif, image/jpeg, image/bmp, image/svg+xml"
|
||||
@change="manualFileUpload"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="mx-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>
|
||||
<Icon
|
||||
name="heroicons:cloud-arrow-up"
|
||||
class="x-auto h-24 w-24 text-gray-200"
|
||||
/>
|
||||
<p class="mt-5 text-sm text-gray-600">
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -161,7 +136,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</modal>
|
||||
</input-wrapper>
|
||||
</InputWrapper>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
:class="[theme.fileInput.borderRadius]"
|
||||
>
|
||||
<video
|
||||
id="webcam"
|
||||
ref="webcamRef"
|
||||
autoplay
|
||||
playsinline
|
||||
muted
|
||||
|
|
@ -12,17 +12,17 @@
|
|||
{ hidden: !isCapturing },
|
||||
theme.fileInput.minHeight,
|
||||
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
|
||||
/>
|
||||
<canvas
|
||||
id="canvas"
|
||||
ref="canvasRef"
|
||||
:class="[
|
||||
{ hidden: !capturedImage },
|
||||
theme.fileInput.borderRadius,
|
||||
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"
|
||||
class="absolute inset-0 pointer-events-none"
|
||||
>
|
||||
<!-- Semi-transparent overlay -->
|
||||
<div class="absolute inset-0 bg-black/30" />
|
||||
|
||||
<!-- Scanning area (transparent window) -->
|
||||
<div
|
||||
class="absolute inset-0 flex items-center justify-center"
|
||||
style="padding-bottom: 60px;"
|
||||
class="absolute inset-0 flex items-strech justify-center px-8 py-12"
|
||||
>
|
||||
<div class="relative w-4/5 h-3/5">
|
||||
<!-- Transparent window -->
|
||||
<div class="absolute inset-0 bg-transparent border-0" />
|
||||
|
||||
<div class="flex-grow w-full relative">
|
||||
<!-- 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 right-0 w-8 h-8 border-t-2 border-r-2 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 right-0 w-8 h-8 border-b-2 border-r-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 rounded-tr-md 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 rounded-br-md border-white" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -152,7 +145,7 @@
|
|||
<script>
|
||||
import Webcam from "webcam-easy"
|
||||
import CachedDefaultTheme from "~/lib/forms/themes/CachedDefaultTheme.js"
|
||||
import Quagga from 'quagga'
|
||||
import { BrowserMultiFormatReader, DecodeHintType, BarcodeFormat } from '@zxing/library'
|
||||
|
||||
export default {
|
||||
name: "CameraUpload",
|
||||
|
|
@ -181,7 +174,7 @@ export default {
|
|||
isCapturing: false,
|
||||
capturedImage: null,
|
||||
cameraPermissionStatus: "loading",
|
||||
quaggaInitialized: false,
|
||||
zxingReader: null,
|
||||
currentFacingMode: 'user',
|
||||
mediaStream: null
|
||||
}),
|
||||
|
|
@ -197,9 +190,10 @@ export default {
|
|||
}
|
||||
},
|
||||
mounted() {
|
||||
const webcamElement = document.getElementById("webcam")
|
||||
const canvasElement = document.getElementById("canvas")
|
||||
this.webcam = new Webcam(webcamElement, "user", canvasElement)
|
||||
// For regular camera mode, we still need the webcam.js setup
|
||||
if (!this.isBarcodeMode) {
|
||||
this.webcam = new Webcam(this.$refs.webcamRef, "user", this.$refs.canvasRef)
|
||||
}
|
||||
this.openCameraUpload()
|
||||
},
|
||||
|
||||
|
|
@ -209,29 +203,27 @@ export default {
|
|||
|
||||
methods: {
|
||||
async cleanupCurrentStream() {
|
||||
if (this.quaggaInitialized) {
|
||||
Quagga.stop()
|
||||
this.quaggaInitialized = false
|
||||
}
|
||||
|
||||
if (this.mediaStream) {
|
||||
this.mediaStream.getTracks().forEach(track => track.stop())
|
||||
this.mediaStream = null
|
||||
if (this.zxingReader) {
|
||||
this.zxingReader.reset()
|
||||
this.zxingReader = null
|
||||
}
|
||||
|
||||
if (this.webcam) {
|
||||
this.webcam.stop()
|
||||
this.webcam = null
|
||||
}
|
||||
|
||||
const webcamElement = document.getElementById("webcam")
|
||||
if (webcamElement && webcamElement.srcObject) {
|
||||
const tracks = webcamElement.srcObject.getTracks()
|
||||
// Clean up video element if needed
|
||||
if (this.$refs.webcamRef && this.$refs.webcamRef.srcObject) {
|
||||
const tracks = this.$refs.webcamRef.srcObject.getTracks()
|
||||
tracks.forEach(track => track.stop())
|
||||
webcamElement.srcObject = null
|
||||
this.$refs.webcamRef.srcObject = null
|
||||
}
|
||||
},
|
||||
|
||||
async switchCamera() {
|
||||
if (!this.isMobileDevice) return
|
||||
|
||||
try {
|
||||
// Stop current camera and clean up resources
|
||||
this.cleanupCurrentStream()
|
||||
|
|
@ -240,7 +232,13 @@ export default {
|
|||
this.currentFacingMode = this.currentFacingMode === 'user' ? 'environment' : 'user'
|
||||
|
||||
// Restart camera
|
||||
await this.openCameraUpload()
|
||||
if (this.isBarcodeMode) {
|
||||
setTimeout(() => {
|
||||
this.initZxingDirect()
|
||||
}, 500)
|
||||
} else {
|
||||
await this.openCameraUpload()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error switching camera:', error)
|
||||
this.cameraPermissionStatus = "unknown"
|
||||
|
|
@ -252,15 +250,18 @@ export default {
|
|||
this.capturedImage = null
|
||||
|
||||
try {
|
||||
const webcamElement = document.getElementById("webcam")
|
||||
const canvasElement = document.getElementById("canvas")
|
||||
if (this.isBarcodeMode) {
|
||||
// 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
|
||||
let facingMode = this.currentFacingMode
|
||||
if (this.isBarcodeMode && this.currentFacingMode === 'user') {
|
||||
// Force environment mode for barcode scanning
|
||||
facingMode = 'environment'
|
||||
}
|
||||
|
||||
// Create constraints based on device capabilities
|
||||
const constraints = {
|
||||
|
|
@ -293,25 +294,26 @@ export default {
|
|||
}
|
||||
|
||||
this.mediaStream = stream // Store the stream reference
|
||||
webcamElement.srcObject = stream
|
||||
this.$refs.webcamRef.srcObject = stream
|
||||
|
||||
this.webcam = new Webcam(
|
||||
webcamElement,
|
||||
this.$refs.webcamRef,
|
||||
facingMode,
|
||||
canvasElement
|
||||
this.$refs.canvasRef
|
||||
)
|
||||
|
||||
await new Promise((resolve) => {
|
||||
webcamElement.onloadedmetadata = () => {
|
||||
webcamElement.play()
|
||||
resolve()
|
||||
this.$refs.webcamRef.onloadedmetadata = () => {
|
||||
this.$refs.webcamRef.play().then(() => {
|
||||
resolve()
|
||||
}).catch(err => {
|
||||
console.error('Error playing video:', err)
|
||||
resolve() // Continue anyway
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
this.cameraPermissionStatus = "allowed"
|
||||
if (this.isBarcodeMode) {
|
||||
this.initQuagga()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Camera error:', err)
|
||||
if (err.name === 'NotAllowedError' || err.toString().includes('Permission denied')) {
|
||||
|
|
@ -321,47 +323,80 @@ export default {
|
|||
}
|
||||
}
|
||||
},
|
||||
initQuagga() {
|
||||
if (!this.quaggaInitialized) {
|
||||
Quagga.init({
|
||||
inputStream: {
|
||||
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()
|
||||
}
|
||||
})
|
||||
})
|
||||
initZxingDirect() {
|
||||
if (this.zxingReader) {
|
||||
this.zxingReader.reset()
|
||||
this.zxingReader = null
|
||||
}
|
||||
|
||||
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() {
|
||||
this.isCapturing = false
|
||||
|
|
@ -370,6 +405,10 @@ export default {
|
|||
this.$emit("stopWebcam")
|
||||
},
|
||||
processCapturedImage() {
|
||||
if (!this.webcam) {
|
||||
return
|
||||
}
|
||||
|
||||
this.capturedImage = this.webcam.snap()
|
||||
this.isCapturing = false
|
||||
this.webcam.stop()
|
||||
|
|
|
|||
|
|
@ -246,6 +246,8 @@ const isAutoSubmit = ref(import.meta.client && window.location.href.includes('au
|
|||
|
||||
// Create a reactive reference directly from the prop
|
||||
const darkModeRef = toRef(props, 'darkMode')
|
||||
// Create a reactive reference for the mode prop
|
||||
const modeRef = toRef(props, 'mode')
|
||||
|
||||
// Add back the local theme computation
|
||||
const theme = computed(() => {
|
||||
|
|
@ -258,7 +260,8 @@ const theme = computed(() => {
|
|||
let formManager = null
|
||||
if (props.form) {
|
||||
formManager = useFormManager(props.form, props.mode, {
|
||||
darkMode: darkModeRef
|
||||
darkMode: darkModeRef,
|
||||
mode: modeRef
|
||||
})
|
||||
formManager.initialize({
|
||||
submissionId: submissionId.value,
|
||||
|
|
|
|||
|
|
@ -17,70 +17,84 @@
|
|||
:form="form"
|
||||
label="Form Theme"
|
||||
/>
|
||||
|
||||
<color-input
|
||||
name="color"
|
||||
:form="form"
|
||||
label="Accent Color"
|
||||
class="my-4"
|
||||
>
|
||||
<template #help>
|
||||
<InputHelp>
|
||||
<span class="text-gray-500">
|
||||
Color (for buttons & inputs border) - <a
|
||||
class="text-blue-500"
|
||||
href="#"
|
||||
@click.prevent="form.color = DEFAULT_COLOR"
|
||||
>Reset</a>
|
||||
</span>
|
||||
</InputHelp>
|
||||
<template #label>
|
||||
<InputLabel>Accent Color - <a
|
||||
href="#" class="text-blue-500"
|
||||
@click.prevent="form.color = DEFAULT_COLOR"
|
||||
>Reset</a></InputLabel>
|
||||
</template>
|
||||
</color-input>
|
||||
<select-input
|
||||
name="dark_mode"
|
||||
:options="[
|
||||
{ name: 'Auto', value: 'auto' },
|
||||
{ name: 'Light Mode', value: 'light' },
|
||||
{ name: 'Dark Mode', value: 'dark' },
|
||||
]"
|
||||
|
||||
<OptionSelectorInput
|
||||
v-model="form.dark_mode"
|
||||
:form="form"
|
||||
name="dark_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
|
||||
icon="octicon:typography-16"
|
||||
title="Typography"
|
||||
title="Text & Language"
|
||||
/>
|
||||
<template v-if="useFeatureFlag('services.google.fonts')">
|
||||
<label class="text-gray-700 font-medium text-sm">Font Style</label>
|
||||
<v-button
|
||||
color="white"
|
||||
class="w-full mb-4"
|
||||
size="small"
|
||||
@click="showGoogleFontPicker = true"
|
||||
>
|
||||
<span :style="{ 'font-family': (form.font_family?form.font_family+' !important':null) }">
|
||||
{{ form.font_family || 'Default' }}
|
||||
</span>
|
||||
</v-button>
|
||||
<GoogleFontPicker
|
||||
:show="showGoogleFontPicker"
|
||||
:font="form.font_family || null"
|
||||
@close="showGoogleFontPicker=false"
|
||||
@apply="onApplyFont"
|
||||
/>
|
||||
</template>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="flex-grow my-1" v-if="useFeatureFlag('services.google.fonts')">
|
||||
<label class="text-gray-700 font-semibold text-sm mb-1 block">Font Family</label>
|
||||
<v-button
|
||||
color="white"
|
||||
class="w-full py-1.5"
|
||||
size="small"
|
||||
@click="showGoogleFontPicker = true"
|
||||
>
|
||||
<span :style="{ 'font-family': (form.font_family ? form.font_family + ' !important' : null) }">
|
||||
{{ form.font_family || 'Default' }}
|
||||
</span>
|
||||
</v-button>
|
||||
<GoogleFontPicker
|
||||
:show="showGoogleFontPicker"
|
||||
:font="form.font_family || null"
|
||||
@close="showGoogleFontPicker = false"
|
||||
@apply="onApplyFont"
|
||||
/>
|
||||
</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
|
||||
name="uppercase_labels"
|
||||
:form="form"
|
||||
label="Uppercase Input Labels"
|
||||
/>
|
||||
|
||||
<select-input
|
||||
name="language"
|
||||
class="mt-4"
|
||||
searchable
|
||||
:options="availableLocales"
|
||||
:form="form"
|
||||
label="Form Language"
|
||||
label="Uppercase Input Labels"
|
||||
/>
|
||||
|
||||
<EditorSectionHeader
|
||||
|
|
@ -88,74 +102,87 @@
|
|||
title="Layout & Sizing"
|
||||
/>
|
||||
<div class="flex space-x-4 justify-stretch">
|
||||
<select-input
|
||||
name="size"
|
||||
class="flex-grow"
|
||||
:options="[
|
||||
{ name: 'Small', value: 'sm' },
|
||||
{ name: 'Medium', value: 'md' },
|
||||
{ name: 'Large', value: 'lg' },
|
||||
]"
|
||||
:form="form"
|
||||
label="Input Size"
|
||||
/>
|
||||
<div class="flex-grow">
|
||||
<OptionSelectorInput
|
||||
seamless
|
||||
label="Input Size"
|
||||
v-model="form.size"
|
||||
:form="form"
|
||||
name="size"
|
||||
:options="[
|
||||
{ name: 'sm', label:'S'},
|
||||
{ name: 'md', label:'M' },
|
||||
{ name: 'lg', label:'L' },
|
||||
]"
|
||||
:multiple="false"
|
||||
:columns="3"
|
||||
class="mb-4"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<select-input
|
||||
name="border_radius"
|
||||
class="flex-grow"
|
||||
:options="[
|
||||
{ name: 'None', value: 'none' },
|
||||
{ name: 'Small', value: 'small' },
|
||||
{ name: 'Full', value: 'full' },
|
||||
]"
|
||||
:form="form"
|
||||
label="Input Roundness"
|
||||
/>
|
||||
<div class="flex-grow">
|
||||
<OptionSelectorInput
|
||||
label="Input Roundness"
|
||||
v-model="form.border_radius"
|
||||
seamless
|
||||
:form="form"
|
||||
name="border_radius"
|
||||
:options="[
|
||||
{ name: 'none', icon: 'i-tabler-border-corner-square' },
|
||||
{ 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>
|
||||
<select-input
|
||||
name="width"
|
||||
:options="[
|
||||
{ name: 'Centered', value: 'centered' },
|
||||
{ name: 'Full Width', value: 'full' },
|
||||
]"
|
||||
:form="form"
|
||||
label="Form Width"
|
||||
help="Useful when embedding your form"
|
||||
/>
|
||||
|
||||
<ToggleSwitchInput
|
||||
name="layout_rtl"
|
||||
<OptionSelectorInput
|
||||
v-model="form.width"
|
||||
label="Form Width"
|
||||
:form="form"
|
||||
class="mt-4"
|
||||
label="Right-to-Left Layout"
|
||||
help="Adjusts layout for RTL languages"
|
||||
name="width"
|
||||
seamless
|
||||
:options="[
|
||||
{ name: 'centered', label: 'Centered' },
|
||||
{ name: 'full', label: 'Full Width' },
|
||||
]"
|
||||
:multiple="false"
|
||||
:columns="2"
|
||||
class="mb-4 w-2/3"
|
||||
/>
|
||||
|
||||
<EditorSectionHeader
|
||||
icon="heroicons:tag-16-solid"
|
||||
title="Branding & Media"
|
||||
/>
|
||||
<image-input
|
||||
name="logo_picture"
|
||||
:form="form"
|
||||
label="Logo"
|
||||
help="Not visible when form is embedded"
|
||||
:required="false"
|
||||
/>
|
||||
<image-input
|
||||
name="cover_picture"
|
||||
:form="form"
|
||||
label="Cover image"
|
||||
help="Not visible when form is embedded"
|
||||
/>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<image-input
|
||||
name="logo_picture"
|
||||
:form="form"
|
||||
label="Logo"
|
||||
:required="false"
|
||||
/>
|
||||
|
||||
<image-input
|
||||
name="cover_picture"
|
||||
:form="form"
|
||||
label="Cover (~1500px)"
|
||||
:required="false"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<toggle-switch-input
|
||||
name="no_branding"
|
||||
:form="form"
|
||||
class="mt-4"
|
||||
@update:model-value="onChangeNoBranding"
|
||||
>
|
||||
<template #label>
|
||||
<span class="text-sm">
|
||||
Remove OpnForm Branding
|
||||
Hide OpnForm Branding
|
||||
</span>
|
||||
<pro-tag
|
||||
upgrade-modal-title="Upgrade today to remove OpnForm branding"
|
||||
|
|
@ -182,7 +209,7 @@
|
|||
name="transparent_background"
|
||||
:form="form"
|
||||
label="Transparent Background"
|
||||
help="Only applies when form is embedded"
|
||||
help="When form is embedded"
|
||||
/>
|
||||
<toggle-switch-input
|
||||
name="confetti_on_submission"
|
||||
|
|
|
|||
|
|
@ -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="{ '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-yellow-500 rounded-full w-2.5 h-2.5" />
|
||||
<div class="bg-green-500 rounded-full w-2.5 h-2.5" />
|
||||
|
|
|
|||
|
|
@ -641,12 +641,13 @@ export default {
|
|||
],
|
||||
allCountries: countryCodes,
|
||||
barcodeDecodersOptions: [
|
||||
{ name: 'QR Code', value: 'qr_reader' },
|
||||
{ name: 'EAN-13 (European Article Number)', value: 'ean_reader' },
|
||||
{ name: 'EAN-8 (European Article Number)', value: 'ean_8_reader' },
|
||||
{ name: 'UPC-A (Universal Product Code)', value: 'upc_reader' },
|
||||
{ name: 'UPC-E (Universal Product Code)', value: 'upc_e_reader' },
|
||||
{ name: 'Code 128', value: 'code_128_reader' },
|
||||
{ name: 'Code 39', value: 'code_39_reader' },
|
||||
{ name: 'Code 39', value: 'code_39_reader' }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,87 +1,61 @@
|
|||
<template>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<button
|
||||
v-for="option in availableOptions"
|
||||
:key="option.name"
|
||||
class="flex flex-col items-center justify-center p-1.5 border rounded-lg transition-colors text-gray-500"
|
||||
:class="[
|
||||
option.class ? (typeof option.class === 'function' ? option.class(isSelected(option.name)) : option.class) : {},
|
||||
{
|
||||
'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>
|
||||
<OptionSelectorInput
|
||||
:options="availableOptions"
|
||||
v-model="selectedOption"
|
||||
:multiple="false"
|
||||
:disabled="false"
|
||||
:columns="3"
|
||||
name="field_state"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
field: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
canBeDisabled: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
canBeRequired: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
canBeHidden: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
field: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
canBeDisabled: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
canBeRequired: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
canBeHidden: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
const emit = defineEmits(['update:field'])
|
||||
|
||||
defineEmits(['update:field'])
|
||||
|
||||
const options = ref([
|
||||
const options = [
|
||||
{
|
||||
name: 'required',
|
||||
label: 'Required',
|
||||
icon: 'i-ph-asterisk-bold',
|
||||
selectedIcon: 'i-ph-asterisk-bold',
|
||||
icon: 'ph:asterisk-bold',
|
||||
selectedIcon: 'ph:asterisk-bold',
|
||||
iconClass: (isActive) => isActive ? 'text-red-500' : '',
|
||||
},
|
||||
{
|
||||
name: 'hidden',
|
||||
label: 'Hidden',
|
||||
icon: 'i-heroicons-eye',
|
||||
selectedIcon: 'i-heroicons-eye-slash-solid',
|
||||
icon: 'heroicons:eye',
|
||||
selectedIcon: 'heroicons:eye-slash-solid',
|
||||
},
|
||||
{
|
||||
name: 'disabled',
|
||||
label: 'Disabled',
|
||||
icon: 'i-heroicons-lock-open',
|
||||
selectedIcon: 'i-heroicons-lock-closed-solid',
|
||||
icon: 'heroicons:lock-open',
|
||||
selectedIcon: 'heroicons:lock-closed-solid',
|
||||
}
|
||||
])
|
||||
]
|
||||
|
||||
const availableOptions = computed(() => {
|
||||
return options.value.filter(option => {
|
||||
return options.filter(option => {
|
||||
if (option.name === 'disabled') return props.canBeDisabled
|
||||
if (option.name === 'required') return props.canBeRequired
|
||||
if (option.name === 'hidden') return props.canBeHidden
|
||||
|
|
@ -89,28 +63,34 @@ const availableOptions = computed(() => {
|
|||
})
|
||||
})
|
||||
|
||||
const isSelected = (optionName) => {
|
||||
return props.field[optionName]
|
||||
}
|
||||
|
||||
const toggleOption = (optionName) => {
|
||||
const newValue = !props.field[optionName]
|
||||
|
||||
if (optionName === 'required' && newValue) {
|
||||
props.field.hidden = false
|
||||
} else if (optionName === 'hidden' && newValue) {
|
||||
const selectedOption = computed({
|
||||
get() {
|
||||
// Only one can be true at a time, priority: required > hidden > disabled
|
||||
if (props.field.required) return 'required'
|
||||
if (props.field.hidden) return 'hidden'
|
||||
if (props.field.disabled) return 'disabled'
|
||||
return null
|
||||
},
|
||||
set(optionName) {
|
||||
// Reset all
|
||||
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.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>
|
||||
|
|
@ -4,6 +4,9 @@
|
|||
<div class="flex mt-2 items-center">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 text-center w-full">
|
||||
© Copyright {{ currYear }}. All Rights Reserved
|
||||
<span v-if="version">
|
||||
<br>Version {{ version }}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex justify-center mt-5 md:mt-0">
|
||||
|
|
@ -82,22 +85,14 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed } from "vue"
|
||||
<script setup>
|
||||
import opnformConfig from "~/opnform.config.js"
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const authStore = useAuthStore()
|
||||
return {
|
||||
user: computed(() => authStore.user),
|
||||
appStore: useAppStore(),
|
||||
opnformConfig,
|
||||
}
|
||||
},
|
||||
const authStore = useAuthStore()
|
||||
|
||||
data: () => ({
|
||||
currYear: new Date().getFullYear(),
|
||||
}),
|
||||
}
|
||||
const user = computed(() => authStore.user)
|
||||
const currYear = ref(new Date().getFullYear())
|
||||
|
||||
// Use the reactive version for proper template reactivity
|
||||
const version = computed(() => useFeatureFlag('version'))
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -207,9 +207,22 @@
|
|||
"text_class": "text-pink-900",
|
||||
"is_input": true,
|
||||
"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": {
|
||||
"name": "payment",
|
||||
"title": "Payment",
|
||||
|
|
|
|||
|
|
@ -19,11 +19,17 @@ import { cloneDeep } from 'lodash'
|
|||
* Initializes and coordinates various form composables (Structure, Init, Validation, etc.)
|
||||
* 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 ---
|
||||
const config = ref(initialFormConfig) // Use ref for potentially replaceable config
|
||||
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
|
||||
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
|
||||
if (paymentBlock) {
|
||||
// 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)
|
||||
const paymentResult = await payment.processPayment(paymentBlock, isPaymentRequired)
|
||||
|
|
@ -179,7 +185,7 @@ export function useFormManager(initialFormConfig, mode = FormMode.LIVE, options
|
|||
if (paymentBlock) {
|
||||
|
||||
// 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)
|
||||
|
||||
|
|
@ -316,6 +322,8 @@ export function useFormManager(initialFormConfig, mode = FormMode.LIVE, options
|
|||
// UI-related properties
|
||||
darkMode, // Dark mode setting
|
||||
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)
|
||||
structure,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,5 @@
|
|||
export default defineNuxtRouteMiddleware(async () => {
|
||||
const authStore = useAuthStore()
|
||||
const featureFlagsStore = useFeatureFlagsStore()
|
||||
|
||||
// Ensure feature flags are loaded
|
||||
if (!featureFlagsStore.isLoaded) {
|
||||
await featureFlagsStore.fetchFlags()
|
||||
}
|
||||
|
||||
if (useFeatureFlag('self_hosted')) {
|
||||
if (authStore.check && authStore.user?.email === 'admin@opnform.com') {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export default defineNuxtConfig({
|
|||
],
|
||||
|
||||
build: {
|
||||
transpile: ["vue-notion", "query-builder-vue-3", "vue-signature-pad"],
|
||||
transpile: ["vue-notion", "query-builder-vue-3", "vue-signature-pad", "@zxing/library"],
|
||||
},
|
||||
|
||||
i18n: {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
"@vueuse/integrations": "^11.2.0",
|
||||
"@vueuse/motion": "^2.2.6",
|
||||
"@vueuse/nuxt": "^11.2.0",
|
||||
"@zxing/library": "^0.21.3",
|
||||
"amplitude-js": "^8.21.9",
|
||||
"chart.js": "^4.4.5",
|
||||
"clone-deep": "^4.0.1",
|
||||
|
|
@ -36,7 +37,6 @@
|
|||
"pinia": "^3.0.2",
|
||||
"prismjs": "^1.29.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"quagga": "^0.12.1",
|
||||
"query-builder-vue-3": "^1.0.1",
|
||||
"quill": "^2.0.2",
|
||||
"tailwind-merge": "^2.5.4",
|
||||
|
|
@ -57,6 +57,7 @@
|
|||
"@iconify-json/clarity": "^1.2.1",
|
||||
"@iconify-json/ic": "^1.2.1",
|
||||
"@iconify-json/octicon": "^1.2.1",
|
||||
"@iconify-json/tabler": "^1.2.1",
|
||||
"@nuxt/eslint-config": "^1.3.0",
|
||||
"@nuxt/icon": "^1.12.0",
|
||||
"@nuxtjs/i18n": "^9.0.0",
|
||||
|
|
@ -1588,6 +1589,15 @@
|
|||
"@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": {
|
||||
"version": "1.0.546",
|
||||
"resolved": "https://registry.npmjs.org/@iconify/collections/-/collections-1.0.546.tgz",
|
||||
|
|
@ -9132,6 +9142,26 @@
|
|||
"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": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz",
|
||||
|
|
@ -9214,6 +9244,7 @@
|
|||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
|
|
@ -9428,24 +9459,6 @@
|
|||
"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": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-1.4.3.tgz",
|
||||
|
|
@ -9496,12 +9509,6 @@
|
|||
"integrity": "sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==",
|
||||
"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": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
|
||||
|
|
@ -9548,21 +9555,6 @@
|
|||
"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": {
|
||||
"version": "1.6.7",
|
||||
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz",
|
||||
|
|
@ -9602,15 +9594,6 @@
|
|||
],
|
||||
"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": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
|
|
@ -10021,12 +10004,6 @@
|
|||
],
|
||||
"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": {
|
||||
"version": "5.4.1",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
|
||||
|
|
@ -10287,18 +10264,6 @@
|
|||
"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": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
|
||||
|
|
@ -10780,33 +10745,6 @@
|
|||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"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": {
|
||||
"version": "2.30.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
|
||||
|
|
@ -10981,15 +10919,6 @@
|
|||
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
|
||||
"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": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
|
||||
|
|
@ -11406,16 +11335,6 @@
|
|||
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
|
||||
"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": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
|
|
@ -12313,12 +12232,6 @@
|
|||
"integrity": "sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg==",
|
||||
"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": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/externality/-/externality-1.0.2.tgz",
|
||||
|
|
@ -12372,19 +12285,11 @@
|
|||
"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": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-diff": {
|
||||
|
|
@ -12431,6 +12336,7 @@
|
|||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
||||
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-levenshtein": {
|
||||
|
|
@ -12642,50 +12548,6 @@
|
|||
"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": {
|
||||
"version": "4.0.10",
|
||||
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
||||
|
|
@ -12865,55 +12727,6 @@
|
|||
"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": {
|
||||
"version": "3.1.2",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz",
|
||||
|
|
@ -13003,24 +12807,6 @@
|
|||
"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": {
|
||||
"version": "9.3.5",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-9.3.5.tgz",
|
||||
|
|
@ -13199,29 +12985,6 @@
|
|||
"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": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
|
|
@ -13373,21 +13136,6 @@
|
|||
"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": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
||||
|
|
@ -13597,12 +13345,6 @@
|
|||
"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": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
|
|
@ -13640,12 +13382,6 @@
|
|||
"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": {
|
||||
"version": "5.0.0",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz",
|
||||
|
|
@ -13964,12 +13694,6 @@
|
|||
"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": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
|
||||
|
|
@ -13994,12 +13718,6 @@
|
|||
"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": {
|
||||
"version": "0.10.1",
|
||||
"resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.10.1.tgz",
|
||||
|
|
@ -14025,12 +13743,6 @@
|
|||
"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": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.1.0.tgz",
|
||||
|
|
@ -14060,16 +13772,11 @@
|
|||
"devOptional": true,
|
||||
"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": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-stable-stringify-without-jsonify": {
|
||||
|
|
@ -14079,12 +13786,6 @@
|
|||
"devOptional": true,
|
||||
"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": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||
|
|
@ -14146,21 +13847,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": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/junk/-/junk-4.0.1.tgz",
|
||||
|
|
@ -15246,32 +14932,6 @@
|
|||
"devOptional": true,
|
||||
"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": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
|
||||
|
|
@ -15883,14 +15543,6 @@
|
|||
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
|
||||
"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": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||
|
|
@ -16865,15 +16517,6 @@
|
|||
"integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
|
||||
"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": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
|
|
@ -16927,12 +16570,6 @@
|
|||
"integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
|
||||
"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": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/on-change/-/on-change-5.0.1.tgz",
|
||||
|
|
@ -17231,15 +16868,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": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/parse-gitignore/-/parse-gitignore-2.0.0.tgz",
|
||||
|
|
@ -17418,12 +17046,6 @@
|
|||
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
|
||||
"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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
|
||||
|
|
@ -18531,18 +18153,6 @@
|
|||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"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": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
|
||||
|
|
@ -18557,6 +18167,7 @@
|
|||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
|
|
@ -18733,24 +18344,6 @@
|
|||
"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": {
|
||||
"version": "0.2.10",
|
||||
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.10.tgz",
|
||||
|
|
@ -19188,78 +18781,6 @@
|
|||
"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": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
|
|
@ -19589,6 +19110,7 @@
|
|||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sass": {
|
||||
|
|
@ -20029,31 +19551,6 @@
|
|||
"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": {
|
||||
"version": "0.0.5",
|
||||
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz",
|
||||
|
|
@ -20818,12 +20315,6 @@
|
|||
"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": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
||||
|
|
@ -20924,19 +20415,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": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
|
|
@ -20965,6 +20443,14 @@
|
|||
"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": {
|
||||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
|
||||
|
|
@ -21007,24 +20493,6 @@
|
|||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
||||
"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": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
|
|
@ -21200,12 +20668,6 @@
|
|||
"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": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
||||
|
|
@ -21573,6 +21035,7 @@
|
|||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
|
||||
"devOptional": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"punycode": "^2.1.0"
|
||||
|
|
@ -21650,20 +21113,6 @@
|
|||
"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": {
|
||||
"version": "6.3.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
"@iconify-json/clarity": "^1.2.1",
|
||||
"@iconify-json/ic": "^1.2.1",
|
||||
"@iconify-json/octicon": "^1.2.1",
|
||||
"@iconify-json/tabler": "^1.2.1",
|
||||
"@nuxt/eslint-config": "^1.3.0",
|
||||
"@nuxt/icon": "^1.12.0",
|
||||
"@nuxtjs/i18n": "^9.0.0",
|
||||
|
|
@ -49,6 +50,7 @@
|
|||
"@vueuse/integrations": "^11.2.0",
|
||||
"@vueuse/motion": "^2.2.6",
|
||||
"@vueuse/nuxt": "^11.2.0",
|
||||
"@zxing/library": "^0.21.3",
|
||||
"amplitude-js": "^8.21.9",
|
||||
"chart.js": "^4.4.5",
|
||||
"clone-deep": "^4.0.1",
|
||||
|
|
@ -64,7 +66,6 @@
|
|||
"pinia": "^3.0.2",
|
||||
"prismjs": "^1.29.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"quagga": "^0.12.1",
|
||||
"query-builder-vue-3": "^1.0.1",
|
||||
"quill": "^2.0.2",
|
||||
"tailwind-merge": "^2.5.4",
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
})
|
||||
|
|
@ -1,9 +1,18 @@
|
|||
import { useFeatureFlagsStore } from '~/stores/featureFlags'
|
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
const featureFlagsStore = useFeatureFlagsStore()
|
||||
export default defineNuxtPlugin(async (nuxtApp) => {
|
||||
// Get the pinia instance for SSR compatibility
|
||||
const { $pinia } = nuxtApp
|
||||
|
||||
nuxtApp.provide('featureFlag', (key, defaultValue = false) => {
|
||||
return featureFlagsStore.getFlag(key, defaultValue)
|
||||
})
|
||||
try {
|
||||
// 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)
|
||||
}
|
||||
})
|
||||
|
|
@ -25,7 +25,8 @@ function mergeOptions(options) {
|
|||
}
|
||||
}
|
||||
},
|
||||
debounceWait: 300
|
||||
debounceWait: 300,
|
||||
ignoreKeys: [] // Keys to ignore in history tracking
|
||||
}
|
||||
|
||||
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.
|
||||
* @param {PiniaPluginContext} context - The context provided by Pinia.
|
||||
|
|
@ -46,7 +66,7 @@ const PiniaHistory = (context) => {
|
|||
return
|
||||
}
|
||||
const mergedOptions = mergeOptions(history)
|
||||
const {max, persistent, persistentStrategy} = mergedOptions
|
||||
const {max, persistent, persistentStrategy, ignoreKeys} = mergedOptions
|
||||
|
||||
const $history = reactive({
|
||||
max,
|
||||
|
|
@ -54,19 +74,23 @@ const PiniaHistory = (context) => {
|
|||
persistentStrategy,
|
||||
done: [],
|
||||
undone: [],
|
||||
current: JSON.stringify(store.$state),
|
||||
current: JSON.stringify(filterState(store.$state, ignoreKeys)),
|
||||
trigger: true,
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
if ($history.done.length >= max) $history.done.shift() // Remove oldest state if needed
|
||||
|
||||
$history.done.push($history.current)
|
||||
$history.undone = [] // Clear redo history on new action
|
||||
$history.current = JSON.stringify(state)
|
||||
$history.current = JSON.stringify(filteredState)
|
||||
|
||||
if (persistent) {
|
||||
persistentStrategy.set(store, 'undo', $history.done)
|
||||
|
|
@ -90,7 +114,9 @@ const PiniaHistory = (context) => {
|
|||
|
||||
$history.undone.push($history.current) // Save current state for redo
|
||||
$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(() => {
|
||||
$history.current = state
|
||||
$history.trigger = true
|
||||
|
|
@ -114,7 +140,9 @@ const PiniaHistory = (context) => {
|
|||
|
||||
$history.done.push($history.current) // Save current state for undo
|
||||
$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(() => {
|
||||
$history.current = state
|
||||
$history.trigger = true
|
||||
|
|
|
|||
|
|
@ -175,35 +175,42 @@ export const useWorkingFormStore = defineStore("working_form", {
|
|||
}
|
||||
if (!this.content) return
|
||||
|
||||
const block = blocksTypes[type]
|
||||
if (block?.self_hosted !== undefined && !block.self_hosted && useFeatureFlag('self_hosted')) {
|
||||
const originalBlockDefinition = blocksTypes[type]
|
||||
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.')
|
||||
return
|
||||
}
|
||||
|
||||
if (block?.auth_required && !useAuthStore().check) {
|
||||
if (originalBlockDefinition?.auth_required && !useAuthStore().check) {
|
||||
useAlert().error('Please login first to add this block')
|
||||
return
|
||||
}
|
||||
|
||||
if (block?.max_count !== undefined) {
|
||||
if (originalBlockDefinition?.max_count !== undefined) {
|
||||
const currentCount = this.content.properties.filter(prop => prop && prop.type === type).length
|
||||
if (currentCount >= block.max_count) {
|
||||
useAlert().error(`Only ${block.max_count} '${block.title}' block(s) allowed per form.`)
|
||||
if (currentCount >= originalBlockDefinition.max_count) {
|
||||
useAlert().error(`Only ${originalBlockDefinition.max_count} '${originalBlockDefinition.title}' block(s) allowed per form.`)
|
||||
return
|
||||
}
|
||||
openSettings = true
|
||||
}
|
||||
|
||||
this.blockForm.type = type
|
||||
this.blockForm.name = blocksTypes[type]?.default_block_name || 'New Block'
|
||||
this.blockForm.type = effectiveType
|
||||
this.blockForm.name = effectiveBlockDefinition?.default_block_name || 'New Block'
|
||||
const newBlock = this.prefillDefault({ ...this.blockForm.data() })
|
||||
newBlock.id = generateUUID()
|
||||
newBlock.hidden = false
|
||||
newBlock.help_position = "below_input"
|
||||
|
||||
if (blocksTypes[type]?.default_values) {
|
||||
Object.assign(newBlock, blocksTypes[type].default_values)
|
||||
// If the type was changed due to actual_input, apply original type's change settings
|
||||
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)
|
||||
|
|
@ -253,5 +260,7 @@ export const useWorkingFormStore = defineStore("working_form", {
|
|||
this.setProperties(newFields)
|
||||
}
|
||||
},
|
||||
history: {}
|
||||
history: {
|
||||
ignoreKeys: ['structureService', 'blockForm']
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ module.exports = {
|
|||
border: 'rgba(15, 15, 15, 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: {
|
||||
height: "height",
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
---
|
||||
services:
|
||||
api: &api-environment
|
||||
api:
|
||||
image: jhumanj/opnform-api:latest
|
||||
container_name: opnform-api
|
||||
volumes: &api-environment-volumes
|
||||
volumes:
|
||||
- opnform_storage:/usr/share/nginx/html/storage:rw
|
||||
environment: &api-env
|
||||
environment:
|
||||
APP_ENV: production
|
||||
# Database settings
|
||||
DB_HOST: db
|
||||
|
|
@ -25,7 +24,7 @@ services:
|
|||
db:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy # Depend on redis being healthy too
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "php /usr/share/nginx/html/artisan about || exit 1"]
|
||||
interval: 30s
|
||||
|
|
@ -33,13 +32,49 @@ services:
|
|||
retries: 3
|
||||
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-environment
|
||||
image: jhumanj/opnform-api:latest
|
||||
container_name: opnform-api-worker
|
||||
command: ["php", "artisan", "queue:work"]
|
||||
volumes:
|
||||
- opnform_storage:/usr/share/nginx/html/storage:rw
|
||||
environment:
|
||||
<<: *api-env
|
||||
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:
|
||||
test: ["CMD-SHELL", "pgrep -f 'php artisan queue:work' > /dev/null || exit 1"]
|
||||
interval: 60s
|
||||
|
|
@ -48,22 +83,44 @@ services:
|
|||
start_period: 30s
|
||||
|
||||
api-scheduler:
|
||||
<<: *api-environment
|
||||
image: jhumanj/opnform-api:latest
|
||||
container_name: opnform-api-scheduler
|
||||
command: ["php", "artisan", "schedule:work"]
|
||||
volumes:
|
||||
- opnform_storage:/usr/share/nginx/html/storage:rw
|
||||
environment:
|
||||
<<: *api-env
|
||||
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:
|
||||
test: ["CMD-SHELL", "php /usr/share/nginx/html/artisan app:scheduler-status --mode=check --max-minutes=3 || exit 1"]
|
||||
interval: 60s
|
||||
timeout: 30s
|
||||
retries: 3
|
||||
start_period: 70s # Allow time for first scheduled run and cache write
|
||||
start_period: 70s
|
||||
|
||||
ui:
|
||||
image: jhumanj/opnform-client:latest
|
||||
container_name: opnform-client
|
||||
ports:
|
||||
- "127.0.0.1:7655:3000" # UI on port 7655
|
||||
env_file:
|
||||
- ./client/.env
|
||||
healthcheck:
|
||||
|
|
@ -95,27 +152,6 @@ services:
|
|||
volumes:
|
||||
- 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:
|
||||
postgres-data:
|
||||
opnform_storage:
|
||||
|
|
@ -27,6 +27,9 @@ RUN composer install --optimize-autoloader --no-interaction \
|
|||
# Final stage - smaller runtime image
|
||||
FROM php:8.3-fpm-alpine
|
||||
|
||||
# Accept version build argument
|
||||
ARG APP_VERSION=unknown
|
||||
|
||||
# Install runtime dependencies
|
||||
RUN apk add --no-cache \
|
||||
libzip \
|
||||
|
|
@ -75,6 +78,9 @@ RUN mkdir -p storage/framework/sessions \
|
|||
# Copy the entire application from the builder stage
|
||||
COPY --from=builder /app/ ./
|
||||
|
||||
# Set version as environment variable (more reliable than file approach)
|
||||
ENV APP_VERSION_DOCKER=$APP_VERSION
|
||||
|
||||
# Setup entrypoint
|
||||
COPY docker/php-fpm-entrypoint /usr/local/bin/opnform-entrypoint
|
||||
RUN chmod a+x /usr/local/bin/*
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -63,7 +63,10 @@ if [ "$DEV_MODE" = true ]; then
|
|||
else
|
||||
echo -e "${BLUE}Production environment setup complete!${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
|
||||
|
||||
echo -e "${BLUE}Default admin credentials:${NC}"
|
||||
|
|
|
|||
Loading…
Reference in New Issue