mirror of
https://github.com/usetrmnl/byos_laravel.git
synced 2026-01-13 15:07:49 +00:00
Compare commits
4 commits
838b4fd33b
...
a3f792944c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3f792944c | ||
|
|
3e670d37c0 | ||
|
|
46e792bc6d | ||
|
|
9019561bb3 |
9 changed files with 521 additions and 173 deletions
|
|
@ -9,7 +9,8 @@ RUN apk add --no-cache composer
|
||||||
# Add Chromium and Image Magick for puppeteer.
|
# Add Chromium and Image Magick for puppeteer.
|
||||||
RUN apk add --no-cache \
|
RUN apk add --no-cache \
|
||||||
imagemagick-dev \
|
imagemagick-dev \
|
||||||
chromium
|
chromium \
|
||||||
|
libzip-dev
|
||||||
|
|
||||||
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
|
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
|
||||||
ENV PUPPETEER_DOCKER=1
|
ENV PUPPETEER_DOCKER=1
|
||||||
|
|
@ -19,7 +20,7 @@ RUN chmod 777 /usr/src/php/ext/imagick
|
||||||
RUN curl -fsSL https://github.com/Imagick/imagick/archive/refs/tags/3.8.0.tar.gz | tar xvz -C "/usr/src/php/ext/imagick" --strip 1
|
RUN curl -fsSL https://github.com/Imagick/imagick/archive/refs/tags/3.8.0.tar.gz | tar xvz -C "/usr/src/php/ext/imagick" --strip 1
|
||||||
|
|
||||||
# Install PHP extensions
|
# Install PHP extensions
|
||||||
RUN docker-php-ext-install imagick
|
RUN docker-php-ext-install imagick zip
|
||||||
|
|
||||||
# Composer uses its php binary, but we want it to use the container's one
|
# Composer uses its php binary, but we want it to use the container's one
|
||||||
RUN rm -f /usr/bin/php84
|
RUN rm -f /usr/bin/php84
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,8 @@ RUN apk add --no-cache \
|
||||||
nodejs \
|
nodejs \
|
||||||
npm \
|
npm \
|
||||||
imagemagick-dev \
|
imagemagick-dev \
|
||||||
chromium
|
chromium \
|
||||||
|
libzip-dev
|
||||||
|
|
||||||
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
|
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
|
||||||
ENV PUPPETEER_DOCKER=1
|
ENV PUPPETEER_DOCKER=1
|
||||||
|
|
@ -24,7 +25,7 @@ RUN chmod 777 /usr/src/php/ext/imagick
|
||||||
RUN curl -fsSL https://github.com/Imagick/imagick/archive/refs/tags/3.8.0.tar.gz | tar xvz -C "/usr/src/php/ext/imagick" --strip 1
|
RUN curl -fsSL https://github.com/Imagick/imagick/archive/refs/tags/3.8.0.tar.gz | tar xvz -C "/usr/src/php/ext/imagick" --strip 1
|
||||||
|
|
||||||
# Install PHP extensions
|
# Install PHP extensions
|
||||||
RUN docker-php-ext-install imagick
|
RUN docker-php-ext-install imagick zip
|
||||||
|
|
||||||
RUN rm -f /usr/bin/php84
|
RUN rm -f /usr/bin/php84
|
||||||
RUN ln -s /usr/local/bin/php /usr/bin/php84
|
RUN ln -s /usr/local/bin/php /usr/bin/php84
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,11 @@ class Plugin extends Model
|
||||||
$model->current_image = null;
|
$model->current_image = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Sanitize configuration template on save
|
||||||
|
static::saving(function ($model): void {
|
||||||
|
$model->sanitizeTemplate();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public function user()
|
public function user()
|
||||||
|
|
@ -69,6 +74,25 @@ class Plugin extends Model
|
||||||
return $this->belongsTo(User::class);
|
return $this->belongsTo(User::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sanitize configuration template descriptions and help texts (since they allow HTML rendering)
|
||||||
|
protected function sanitizeTemplate(): void
|
||||||
|
{
|
||||||
|
$template = $this->configuration_template;
|
||||||
|
|
||||||
|
if (isset($template['custom_fields']) && is_array($template['custom_fields'])) {
|
||||||
|
foreach ($template['custom_fields'] as &$field) {
|
||||||
|
if (isset($field['description'])) {
|
||||||
|
$field['description'] = \Stevebauman\Purify\Facades\Purify::clean($field['description']);
|
||||||
|
}
|
||||||
|
if (isset($field['help_text'])) {
|
||||||
|
$field['help_text'] = \Stevebauman\Purify\Facades\Purify::clean($field['help_text']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->configuration_template = $template;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function hasMissingRequiredConfigurationFields(): bool
|
public function hasMissingRequiredConfigurationFields(): bool
|
||||||
{
|
{
|
||||||
if (! isset($this->configuration_template['custom_fields']) || empty($this->configuration_template['custom_fields'])) {
|
if (! isset($this->configuration_template['custom_fields']) || empty($this->configuration_template['custom_fields'])) {
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@
|
||||||
"livewire/volt": "^1.7",
|
"livewire/volt": "^1.7",
|
||||||
"om/icalparser": "^3.2",
|
"om/icalparser": "^3.2",
|
||||||
"spatie/browsershot": "^5.0",
|
"spatie/browsershot": "^5.0",
|
||||||
|
"stevebauman/purify": "^6.3",
|
||||||
"symfony/yaml": "^7.3",
|
"symfony/yaml": "^7.3",
|
||||||
"wnx/sidecar-browsershot": "^2.6"
|
"wnx/sidecar-browsershot": "^2.6"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
129
composer.lock
generated
129
composer.lock
generated
|
|
@ -4,7 +4,7 @@
|
||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "4d958d48655a5ad9e3de6b4a9fb52b0a",
|
"content-hash": "25c2a1a4a2f2594adefe25ddb6a072fb",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "aws/aws-crt-php",
|
"name": "aws/aws-crt-php",
|
||||||
|
|
@ -814,6 +814,67 @@
|
||||||
],
|
],
|
||||||
"time": "2025-03-06T22:45:56+00:00"
|
"time": "2025-03-06T22:45:56+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "ezyang/htmlpurifier",
|
||||||
|
"version": "v4.19.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/ezyang/htmlpurifier.git",
|
||||||
|
"reference": "b287d2a16aceffbf6e0295559b39662612b77fcf"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/b287d2a16aceffbf6e0295559b39662612b77fcf",
|
||||||
|
"reference": "b287d2a16aceffbf6e0295559b39662612b77fcf",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"cerdic/css-tidy": "^1.7 || ^2.0",
|
||||||
|
"simpletest/simpletest": "dev-master"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"cerdic/css-tidy": "If you want to use the filter 'Filter.ExtractStyleBlocks'.",
|
||||||
|
"ext-bcmath": "Used for unit conversion and imagecrash protection",
|
||||||
|
"ext-iconv": "Converts text to and from non-UTF-8 encodings",
|
||||||
|
"ext-tidy": "Used for pretty-printing HTML"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"library/HTMLPurifier.composer.php"
|
||||||
|
],
|
||||||
|
"psr-0": {
|
||||||
|
"HTMLPurifier": "library/"
|
||||||
|
},
|
||||||
|
"exclude-from-classmap": [
|
||||||
|
"/library/HTMLPurifier/Language/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"LGPL-2.1-or-later"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Edward Z. Yang",
|
||||||
|
"email": "admin@htmlpurifier.org",
|
||||||
|
"homepage": "http://ezyang.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Standards compliant HTML filter written in PHP",
|
||||||
|
"homepage": "http://htmlpurifier.org/",
|
||||||
|
"keywords": [
|
||||||
|
"html"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/ezyang/htmlpurifier/issues",
|
||||||
|
"source": "https://github.com/ezyang/htmlpurifier/tree/v4.19.0"
|
||||||
|
},
|
||||||
|
"time": "2025-10-17T16:34:55+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "firebase/php-jwt",
|
"name": "firebase/php-jwt",
|
||||||
"version": "v6.11.1",
|
"version": "v6.11.1",
|
||||||
|
|
@ -4947,6 +5008,72 @@
|
||||||
],
|
],
|
||||||
"time": "2025-01-13T13:04:43+00:00"
|
"time": "2025-01-13T13:04:43+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "stevebauman/purify",
|
||||||
|
"version": "v6.3.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/stevebauman/purify.git",
|
||||||
|
"reference": "3acb5e77904f420ce8aad8fa1c7f394e82daa500"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/stevebauman/purify/zipball/3acb5e77904f420ce8aad8fa1c7f394e82daa500",
|
||||||
|
"reference": "3acb5e77904f420ce8aad8fa1c7f394e82daa500",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ezyang/htmlpurifier": "^4.17",
|
||||||
|
"illuminate/contracts": "^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
|
||||||
|
"illuminate/support": "^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
|
||||||
|
"php": ">=7.4"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"orchestra/testbench": "^5.0|^6.0|^7.0|^8.0|^9.0|^10.0",
|
||||||
|
"phpunit/phpunit": "^8.0|^9.0|^10.0|^11.5.3"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"aliases": {
|
||||||
|
"Purify": "Stevebauman\\Purify\\Facades\\Purify"
|
||||||
|
},
|
||||||
|
"providers": [
|
||||||
|
"Stevebauman\\Purify\\PurifyServiceProvider"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Stevebauman\\Purify\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Steve Bauman",
|
||||||
|
"email": "steven_bauman@outlook.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "An HTML Purifier / Sanitizer for Laravel",
|
||||||
|
"keywords": [
|
||||||
|
"Purifier",
|
||||||
|
"clean",
|
||||||
|
"cleaner",
|
||||||
|
"html",
|
||||||
|
"laravel",
|
||||||
|
"purification",
|
||||||
|
"purify"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/stevebauman/purify/issues",
|
||||||
|
"source": "https://github.com/stevebauman/purify/tree/v6.3.1"
|
||||||
|
},
|
||||||
|
"time": "2025-05-21T16:53:09+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/clock",
|
"name": "symfony/clock",
|
||||||
"version": "v8.0.0",
|
"version": "v8.0.0",
|
||||||
|
|
|
||||||
13
package-lock.json
generated
13
package-lock.json
generated
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "laravel-trmnl-server",
|
"name": "laravel",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
|
|
@ -156,7 +156,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz",
|
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz",
|
||||||
"integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==",
|
"integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/state": "^6.0.0",
|
"@codemirror/state": "^6.0.0",
|
||||||
"@codemirror/view": "^6.23.0",
|
"@codemirror/view": "^6.23.0",
|
||||||
|
|
@ -193,7 +192,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
|
||||||
"integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
|
"integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@marijn/find-cluster-break": "^1.0.0"
|
"@marijn/find-cluster-break": "^1.0.0"
|
||||||
}
|
}
|
||||||
|
|
@ -215,7 +213,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.8.tgz",
|
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.8.tgz",
|
||||||
"integrity": "sha512-XcE9fcnkHCbWkjeKyi0lllwXmBLtyYb5dt89dJyx23I9+LSh5vZDIuk7OLG4VM1lgrXZQcY6cxyZyk5WVPRv/A==",
|
"integrity": "sha512-XcE9fcnkHCbWkjeKyi0lllwXmBLtyYb5dt89dJyx23I9+LSh5vZDIuk7OLG4VM1lgrXZQcY6cxyZyk5WVPRv/A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/state": "^6.5.0",
|
"@codemirror/state": "^6.5.0",
|
||||||
"crelt": "^1.0.6",
|
"crelt": "^1.0.6",
|
||||||
|
|
@ -718,7 +715,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
|
||||||
"integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
|
"integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lezer/common": "^1.3.0"
|
"@lezer/common": "^1.3.0"
|
||||||
}
|
}
|
||||||
|
|
@ -1614,7 +1610,6 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
|
|
@ -1898,8 +1893,7 @@
|
||||||
"version": "0.0.1521046",
|
"version": "0.0.1521046",
|
||||||
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1521046.tgz",
|
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1521046.tgz",
|
||||||
"integrity": "sha512-vhE6eymDQSKWUXwwA37NtTTVEzjtGVfDr3pRbsWEQ5onH/Snp2c+2xZHWJJawG/0hCCJLRGt4xVtEVUVILol4w==",
|
"integrity": "sha512-vhE6eymDQSKWUXwwA37NtTTVEzjtGVfDr3pRbsWEQ5onH/Snp2c+2xZHWJJawG/0hCCJLRGt4xVtEVUVILol4w==",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/dunder-proto": {
|
"node_modules/dunder-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
|
|
@ -2951,7 +2945,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
|
|
@ -2978,7 +2971,6 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
|
|
@ -3429,7 +3421,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.6.tgz",
|
||||||
"integrity": "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==",
|
"integrity": "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,10 @@
|
||||||
@apply !mb-0 !leading-tight;
|
@apply !mb-0 !leading-tight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-flux-description] a {
|
||||||
|
@apply text-accent underline hover:opacity-80;
|
||||||
|
}
|
||||||
|
|
||||||
input:focus[data-flux-control],
|
input:focus[data-flux-control],
|
||||||
textarea:focus[data-flux-control],
|
textarea:focus[data-flux-control],
|
||||||
select:focus[data-flux-control] {
|
select:focus[data-flux-control] {
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ new class extends Component {
|
||||||
public array $configuration = [];
|
public array $configuration = [];
|
||||||
public array $xhrSelectOptions = [];
|
public array $xhrSelectOptions = [];
|
||||||
public array $searchQueries = [];
|
public array $searchQueries = [];
|
||||||
|
public array $multiValues = [];
|
||||||
|
|
||||||
public function mount(): void
|
public function mount(): void
|
||||||
{
|
{
|
||||||
|
|
@ -74,6 +75,25 @@ new class extends Component {
|
||||||
|
|
||||||
$this->fillformFields();
|
$this->fillformFields();
|
||||||
$this->data_payload_updated_at = $this->plugin->data_payload_updated_at;
|
$this->data_payload_updated_at = $this->plugin->data_payload_updated_at;
|
||||||
|
|
||||||
|
foreach ($this->configuration_template['custom_fields'] ?? [] as $field) {
|
||||||
|
if (($field['field_type'] ?? null) !== 'multi_string') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fieldKey = $field['keyname'] ?? $field['key'] ?? $field['name'];
|
||||||
|
|
||||||
|
// Get the existing value from the plugin's configuration
|
||||||
|
$rawValue = $this->configuration[$fieldKey] ?? ($field['default'] ?? '');
|
||||||
|
|
||||||
|
$currentValue = is_array($rawValue) ? '' : (string)$rawValue;
|
||||||
|
|
||||||
|
// Split CSV into array for UI boxes
|
||||||
|
$this->multiValues[$fieldKey] = $currentValue !== ''
|
||||||
|
? array_values(array_filter(explode(',', $currentValue)))
|
||||||
|
: [''];
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function fillFormFields(): void
|
public function fillFormFields(): void
|
||||||
|
|
@ -129,6 +149,19 @@ new class extends Component {
|
||||||
$validated = $this->validate();
|
$validated = $this->validate();
|
||||||
$validated['data_payload'] = json_decode(Arr::get($validated,'data_payload'), true);
|
$validated['data_payload'] = json_decode(Arr::get($validated,'data_payload'), true);
|
||||||
$this->plugin->update($validated);
|
$this->plugin->update($validated);
|
||||||
|
|
||||||
|
foreach ($this->configuration_template as $fieldKey => $field) {
|
||||||
|
if (($field['field_type'] ?? null) !== 'multi_string') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($this->multiValues[$fieldKey])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated[$fieldKey] = implode(',', array_filter(array_map('trim', $this->multiValues[$fieldKey])));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function validatePollingUrl(): void
|
protected function validatePollingUrl(): void
|
||||||
|
|
@ -258,13 +291,21 @@ new class extends Component {
|
||||||
{
|
{
|
||||||
abort_unless(auth()->user()->plugins->contains($this->plugin), 403);
|
abort_unless(auth()->user()->plugins->contains($this->plugin), 403);
|
||||||
|
|
||||||
|
foreach ($this->configuration_template['custom_fields'] ?? [] as $field) {
|
||||||
|
$fieldKey = $field['keyname'] ?? $field['key'] ?? $field['name'];
|
||||||
|
|
||||||
|
if ($field['field_type'] === 'multi_string' && isset($this->multiValues[$fieldKey])) {
|
||||||
|
// Join the boxes into a CSV string, trimming whitespace and filtering empties
|
||||||
|
$this->configuration[$fieldKey] = implode(',', array_filter(array_map('trim', $this->multiValues[$fieldKey])));
|
||||||
|
}
|
||||||
|
}
|
||||||
$configurationValues = [];
|
$configurationValues = [];
|
||||||
if (isset($this->configuration_template['custom_fields'])) {
|
if (isset($this->configuration_template['custom_fields'])) {
|
||||||
foreach ($this->configuration_template['custom_fields'] as $field) {
|
foreach ($this->configuration_template['custom_fields'] as $field) {
|
||||||
$fieldKey = $field['keyname'];
|
$fieldKey = $field['keyname'];
|
||||||
if (isset($this->configuration[$fieldKey])) {
|
if (isset($this->configuration[$fieldKey])) {
|
||||||
$value = $this->configuration[$fieldKey];
|
$value = $this->configuration[$fieldKey];
|
||||||
|
|
||||||
// For code fields, if the value is a JSON string and the original was an array, decode it
|
// For code fields, if the value is a JSON string and the original was an array, decode it
|
||||||
if ($field['field_type'] === 'code' && is_string($value)) {
|
if ($field['field_type'] === 'code' && is_string($value)) {
|
||||||
$decoded = json_decode($value, true);
|
$decoded = json_decode($value, true);
|
||||||
|
|
@ -274,7 +315,7 @@ new class extends Component {
|
||||||
$value = $decoded;
|
$value = $decoded;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$configurationValues[$fieldKey] = $value;
|
$configurationValues[$fieldKey] = $value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -433,6 +474,22 @@ HTML;
|
||||||
$this->loadXhrSelectOptions($fieldKey, $endpoint, $query);
|
$this->loadXhrSelectOptions($fieldKey, $endpoint, $query);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function addMultiItem(string $fieldKey): void
|
||||||
|
{
|
||||||
|
$this->multiValues[$fieldKey][] = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeMultiItem(string $fieldKey, int $index): void
|
||||||
|
{
|
||||||
|
unset($this->multiValues[$fieldKey][$index]);
|
||||||
|
|
||||||
|
$this->multiValues[$fieldKey] = array_values($this->multiValues[$fieldKey]);
|
||||||
|
|
||||||
|
if (empty($this->multiValues[$fieldKey])) {
|
||||||
|
$this->multiValues[$fieldKey][] = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
?>
|
?>
|
||||||
|
|
@ -639,7 +696,11 @@ HTML;
|
||||||
@php
|
@php
|
||||||
$fieldKey = $field['keyname'] ?? $field['key'] ?? $field['name'];
|
$fieldKey = $field['keyname'] ?? $field['key'] ?? $field['name'];
|
||||||
$rawValue = $configuration[$fieldKey] ?? ($field['default'] ?? '');
|
$rawValue = $configuration[$fieldKey] ?? ($field['default'] ?? '');
|
||||||
|
|
||||||
|
# These are sanitized at Model/Plugin level, safe to render HTML
|
||||||
|
$safeDescription = $field['description'] ?? '';
|
||||||
|
$safeHelp = $field['help_text'] ?? '';
|
||||||
|
|
||||||
// For code fields, if the value is an array, JSON encode it
|
// For code fields, if the value is an array, JSON encode it
|
||||||
if ($field['field_type'] === 'code' && is_array($rawValue)) {
|
if ($field['field_type'] === 'code' && is_array($rawValue)) {
|
||||||
$currentValue = json_encode($rawValue, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
$currentValue = json_encode($rawValue, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||||
|
|
@ -657,176 +718,211 @@ HTML;
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
@if($field['field_type'] === 'string' || $field['field_type'] === 'url')
|
@if($field['field_type'] === 'string' || $field['field_type'] === 'url')
|
||||||
<flux:input
|
<flux:field>
|
||||||
label="{{ $field['name'] }}"
|
<flux:label>{{ $field['name'] }}</flux:label>
|
||||||
description="{{ $field['description'] ?? '' }}"
|
<flux:description>{!! $safeDescription !!}</flux:description>
|
||||||
descriptionTrailing="{{ $field['help_text'] ?? '' }}"
|
<flux:input
|
||||||
wire:model="configuration.{{ $fieldKey }}"
|
wire:model="configuration.{{ $fieldKey }}"
|
||||||
value="{{ $currentValue }}"
|
value="{{ $currentValue }}"
|
||||||
/>
|
/>
|
||||||
|
<flux:description>{!! $safeHelp !!}</flux:description>
|
||||||
|
</flux:field>
|
||||||
|
|
||||||
@elseif($field['field_type'] === 'text')
|
@elseif($field['field_type'] === 'text')
|
||||||
<flux:textarea
|
<flux:field>
|
||||||
label="{{ $field['name'] }}"
|
<flux:label>{{ $field['name'] }}</flux:label>
|
||||||
description="{{ $field['description'] ?? '' }}"
|
<flux:description>{!! $safeDescription !!}</flux:description>
|
||||||
descriptionTrailing="{{ $field['help_text'] ?? '' }}"
|
<flux:textarea
|
||||||
wire:model="configuration.{{ $fieldKey }}"
|
wire:model="configuration.{{ $fieldKey }}"
|
||||||
value="{{ $currentValue }}"
|
value="{{ $currentValue }}"
|
||||||
/>
|
/>
|
||||||
|
<flux:description>{!! $safeHelp !!}</flux:description>
|
||||||
|
</flux:field>
|
||||||
|
|
||||||
@elseif($field['field_type'] === 'code')
|
@elseif($field['field_type'] === 'code')
|
||||||
<flux:textarea
|
<flux:field>
|
||||||
label="{{ $field['name'] }}"
|
<flux:label>{{ $field['name'] }}</flux:label>
|
||||||
description="{{ $field['description'] ?? '' }}"
|
<flux:description>{!! $safeDescription !!}</flux:description>
|
||||||
descriptionTrailing="{{ $field['help_text'] ?? '' }}"
|
<flux:textarea
|
||||||
rows="{{ $field['rows'] ?? 3 }}"
|
rows="{{ $field['rows'] ?? 3 }}"
|
||||||
placeholder="{{ $field['placeholder'] ?? null }}"
|
placeholder="{{ $field['placeholder'] ?? null }}"
|
||||||
wire:model="configuration.{{ $fieldKey }}"
|
wire:model="configuration.{{ $fieldKey }}"
|
||||||
value="{{ $currentValue }}"
|
value="{{ $currentValue }}"
|
||||||
class="font-mono"
|
class="font-mono"
|
||||||
/>
|
/>
|
||||||
|
<flux:description>{!! $safeHelp !!}</flux:description>
|
||||||
|
</flux:field>
|
||||||
|
|
||||||
@elseif($field['field_type'] === 'password')
|
@elseif($field['field_type'] === 'password')
|
||||||
<flux:input
|
<flux:field>
|
||||||
type="password"
|
<flux:label>{{ $field['name'] }}</flux:label>
|
||||||
label="{{ $field['name'] }}"
|
<flux:description>{!! $safeDescription !!}</flux:description>
|
||||||
description="{{ $field['description'] ?? '' }}"
|
<flux:input
|
||||||
descriptionTrailing="{{ $field['help_text'] ?? '' }}"
|
type="password"
|
||||||
wire:model="configuration.{{ $fieldKey }}"
|
wire:model="configuration.{{ $fieldKey }}"
|
||||||
value="{{ $currentValue }}"
|
value="{{ $currentValue }}"
|
||||||
viewable
|
viewable
|
||||||
/>
|
/>
|
||||||
|
<flux:description>{!! $safeHelp !!}</flux:description>
|
||||||
|
</flux:field>
|
||||||
|
|
||||||
@elseif($field['field_type'] === 'copyable')
|
@elseif($field['field_type'] === 'copyable')
|
||||||
<flux:input
|
<flux:field>
|
||||||
label="{{ $field['name'] }}"
|
<flux:label>{{ $field['name'] }}</flux:label>
|
||||||
description="{{ $field['description'] ?? '' }}"
|
<flux:description>{!! $safeDescription !!}</flux:description>
|
||||||
descriptionTrailing="{{ $field['help_text'] ?? '' }}"
|
<flux:input
|
||||||
value="{{ $field['value'] }}"
|
value="{{ $field['value'] }}"
|
||||||
copyable
|
copyable
|
||||||
/>
|
/>
|
||||||
|
<flux:description>{!! $safeHelp !!}</flux:description>
|
||||||
|
</flux:field>
|
||||||
|
|
||||||
@elseif($field['field_type'] === 'time_zone')
|
@elseif($field['field_type'] === 'time_zone')
|
||||||
<flux:select
|
<flux:field>
|
||||||
label="{{ $field['name'] }}"
|
<flux:label>{{ $field['name'] }}</flux:label>
|
||||||
wire:model="configuration.{{ $fieldKey }}"
|
<flux:description>{!! $safeDescription !!}</flux:description>
|
||||||
description="{{ $field['description'] ?? '' }}"
|
<flux:select
|
||||||
descriptionTrailing="{{ $field['help_text'] ?? '' }}"
|
wire:model="configuration.{{ $fieldKey }}"
|
||||||
>
|
value="{{ $field['value'] }}"
|
||||||
<option value="">Select timezone...</option>
|
>
|
||||||
@foreach(timezone_identifiers_list() as $timezone)
|
<option value="">Select timezone...</option>
|
||||||
<option value="{{ $timezone }}" {{ $currentValue === $timezone ? 'selected' : '' }}>{{ $timezone }}</option>
|
@foreach(timezone_identifiers_list() as $timezone)
|
||||||
@endforeach
|
<option value="{{ $timezone }}" {{ $currentValue === $timezone ? 'selected' : '' }}>{{ $timezone }}</option>
|
||||||
</flux:select>
|
@endforeach
|
||||||
|
</flux:select>
|
||||||
|
<flux:description>{!! $safeHelp !!}</flux:description>
|
||||||
|
</flux:field>
|
||||||
|
|
||||||
@elseif($field['field_type'] === 'number')
|
@elseif($field['field_type'] === 'number')
|
||||||
<flux:input
|
<flux:field>
|
||||||
type="number"
|
<flux:label>{{ $field['name'] }}</flux:label>
|
||||||
label="{{ $field['name'] }}"
|
<flux:description>{!! $safeDescription !!}</flux:description>
|
||||||
description="{{ $field['description'] ?? $field['name'] }}"
|
<flux:input
|
||||||
descriptionTrailing="{{ $field['help_text'] ?? '' }}"
|
type="number"
|
||||||
wire:model="configuration.{{ $fieldKey }}"
|
wire:model="configuration.{{ $fieldKey }}"
|
||||||
value="{{ $currentValue }}"
|
value="{{ $currentValue }}"
|
||||||
/>
|
/>
|
||||||
|
<flux:description>{!! $safeHelp !!}</flux:description>
|
||||||
|
</flux:field>
|
||||||
|
|
||||||
@elseif($field['field_type'] === 'boolean')
|
@elseif($field['field_type'] === 'boolean')
|
||||||
<flux:checkbox
|
<flux:field>
|
||||||
label="{{ $field['name'] }}"
|
<flux:label>{{ $field['name'] }}</flux:label>
|
||||||
description="{{ $field['description'] ?? $field['name'] }}"
|
<flux:description>{!! $safeDescription !!}</flux:description>
|
||||||
descriptionTrailing="{{ $field['help_text'] ?? '' }}"
|
<flux:checkbox
|
||||||
wire:model="configuration.{{ $fieldKey }}"
|
wire:model="configuration.{{ $fieldKey }}"
|
||||||
:checked="$currentValue"
|
:checked="$currentValue"
|
||||||
/>
|
/>
|
||||||
|
<flux:description>{!! $safeHelp !!}</flux:description>
|
||||||
|
</flux:field>
|
||||||
|
|
||||||
@elseif($field['field_type'] === 'date')
|
@elseif($field['field_type'] === 'date')
|
||||||
<flux:input
|
<flux:field>
|
||||||
type="date"
|
<flux:label>{{ $field['name'] }}</flux:label>
|
||||||
label="{{ $field['name'] }}"
|
<flux:description>{!! $safeDescription !!}</flux:description>
|
||||||
description="{{ $field['description'] ?? $field['name'] }}"
|
<flux:input
|
||||||
descriptionTrailing="{{ $field['help_text'] ?? '' }}"
|
type="date"
|
||||||
wire:model="configuration.{{ $fieldKey }}"
|
wire:model="configuration.{{ $fieldKey }}"
|
||||||
value="{{ $currentValue }}"
|
value="{{ $currentValue }}"
|
||||||
/>
|
/>
|
||||||
|
<flux:description>{!! $safeHelp !!}</flux:description>
|
||||||
|
</flux:field>
|
||||||
|
|
||||||
@elseif($field['field_type'] === 'time')
|
@elseif($field['field_type'] === 'time')
|
||||||
<flux:input
|
<flux:field>
|
||||||
type="time"
|
<flux:label>{{ $field['name'] }}</flux:label>
|
||||||
label="{{ $field['name'] }}"
|
<flux:description>{!! $safeDescription !!}</flux:description>
|
||||||
description="{{ $field['description'] ?? $field['name'] }}"
|
<flux:input
|
||||||
descriptionTrailing="{{ $field['help_text'] ?? '' }}"
|
type="time"
|
||||||
wire:model="configuration.{{ $fieldKey }}"
|
wire:model="configuration.{{ $fieldKey }}"
|
||||||
value="{{ $currentValue }}"
|
value="{{ $currentValue }}"
|
||||||
/>
|
/>
|
||||||
|
<flux:description>{!! $safeHelp !!}</flux:description>
|
||||||
|
</flux:field>
|
||||||
|
|
||||||
@elseif($field['field_type'] === 'select')
|
@elseif($field['field_type'] === 'select')
|
||||||
@if(isset($field['multiple']) && $field['multiple'] === true)
|
@if(isset($field['multiple']) && $field['multiple'] === true)
|
||||||
<flux:checkbox.group
|
<flux:field>
|
||||||
label="{{ $field['name'] }}"
|
<flux:label>{{ $field['name'] }}</flux:label>
|
||||||
wire:model="configuration.{{ $fieldKey }}"
|
<flux:description>{!! $safeDescription !!}</flux:description>
|
||||||
description="{{ $field['description'] ?? '' }}"
|
<flux:checkbox.group wire:model="configuration.{{ $fieldKey }}">
|
||||||
descriptionTrailing="{{ $field['help_text'] ?? '' }}"
|
@if(isset($field['options']) && is_array($field['options']))
|
||||||
>
|
@foreach($field['options'] as $option)
|
||||||
@if(isset($field['options']) && is_array($field['options']))
|
@if(is_array($option))
|
||||||
@foreach($field['options'] as $option)
|
@foreach($option as $label => $value)
|
||||||
@if(is_array($option))
|
<flux:checkbox label="{{ $label }}" value="{{ $value }}"/>
|
||||||
@foreach($option as $label => $value)
|
@endforeach
|
||||||
<flux:checkbox label="{{ $label }}" value="{{ $value }}"/>
|
@else
|
||||||
@endforeach
|
@php
|
||||||
@else
|
$key = mb_strtolower(str_replace(' ', '_', $option));
|
||||||
@php
|
@endphp
|
||||||
$key = mb_strtolower(str_replace(' ', '_', $option));
|
<flux:checkbox label="{{ $option }}" value="{{ $key }}"/>
|
||||||
@endphp
|
@endif
|
||||||
<flux:checkbox label="{{ $option }}" value="{{ $key }}"/>
|
@endforeach
|
||||||
@endif
|
@endif
|
||||||
@endforeach
|
</flux:checkbox.group>
|
||||||
@endif
|
<flux:description>{!! $safeHelp !!}</flux:description>
|
||||||
</flux:checkbox.group>
|
</flux:field>
|
||||||
@else
|
@else
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ $field['name'] }}</flux:label>
|
||||||
|
<flux:description>{!! $safeDescription !!}</flux:description>
|
||||||
|
<flux:select wire:model="configuration.{{ $fieldKey }}">
|
||||||
|
<option value="">Select {{ $field['name'] }}...</option>
|
||||||
|
@if(isset($field['options']) && is_array($field['options']))
|
||||||
|
@foreach($field['options'] as $option)
|
||||||
|
@if(is_array($option))
|
||||||
|
@foreach($option as $label => $value)
|
||||||
|
<option value="{{ $value }}" {{ $currentValue === $value ? 'selected' : '' }}>{{ $label }}</option>
|
||||||
|
@endforeach
|
||||||
|
@else
|
||||||
|
@php
|
||||||
|
$key = mb_strtolower(str_replace(' ', '_', $option));
|
||||||
|
@endphp
|
||||||
|
<option value="{{ $key }}" {{ $currentValue === $key ? 'selected' : '' }}>{{ $option }}</option>
|
||||||
|
@endif
|
||||||
|
@endforeach
|
||||||
|
@endif
|
||||||
|
</flux:select>
|
||||||
|
<flux:description>{!! $safeHelp !!}</flux:description>
|
||||||
|
</flux:field>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@elseif($field['field_type'] === 'xhrSelect')
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ $field['name'] }}</flux:label>
|
||||||
|
<flux:description>{!! $safeDescription !!}</flux:description>
|
||||||
<flux:select
|
<flux:select
|
||||||
label="{{ $field['name'] }}"
|
|
||||||
wire:model="configuration.{{ $fieldKey }}"
|
wire:model="configuration.{{ $fieldKey }}"
|
||||||
description="{{ $field['description'] ?? '' }}"
|
wire:init="loadXhrSelectOptions('{{ $fieldKey }}', '{{ $field['endpoint'] }}')"
|
||||||
descriptionTrailing="{{ $field['help_text'] ?? '' }}"
|
|
||||||
>
|
>
|
||||||
<option value="">Select {{ $field['name'] }}...</option>
|
<option value="">Select {{ $field['name'] }}...</option>
|
||||||
@if(isset($field['options']) && is_array($field['options']))
|
@if(isset($xhrSelectOptions[$fieldKey]) && is_array($xhrSelectOptions[$fieldKey]))
|
||||||
@foreach($field['options'] as $option)
|
@foreach($xhrSelectOptions[$fieldKey] as $option)
|
||||||
@if(is_array($option))
|
@if(is_array($option))
|
||||||
@foreach($option as $label => $value)
|
@if(isset($option['id']) && isset($option['name']))
|
||||||
<option value="{{ $value }}" {{ $currentValue === $value ? 'selected' : '' }}>{{ $label }}</option>
|
{{-- xhrSelectSearch format: { 'id' => 'db-456', 'name' => 'Team Goals' } --}}
|
||||||
@endforeach
|
<option value="{{ $option['id'] }}" {{ $currentValue === (string)$option['id'] ? 'selected' : '' }}>{{ $option['name'] }}</option>
|
||||||
|
@else
|
||||||
|
{{-- xhrSelect format: { 'Braves' => 123 } --}}
|
||||||
|
@foreach($option as $label => $value)
|
||||||
|
<option value="{{ $value }}" {{ $currentValue === (string)$value ? 'selected' : '' }}>{{ $label }}</option>
|
||||||
|
@endforeach
|
||||||
|
@endif
|
||||||
@else
|
@else
|
||||||
@php
|
<option value="{{ $option }}" {{ $currentValue === (string)$option ? 'selected' : '' }}>{{ $option }}</option>
|
||||||
$key = mb_strtolower(str_replace(' ', '_', $option));
|
|
||||||
@endphp
|
|
||||||
<option value="{{ $key }}" {{ $currentValue === $key ? 'selected' : '' }}>{{ $option }}</option>
|
|
||||||
@endif
|
@endif
|
||||||
@endforeach
|
@endforeach
|
||||||
@endif
|
@endif
|
||||||
</flux:select>
|
</flux:select>
|
||||||
@endif
|
<flux:description>{!! $safeHelp !!}</flux:description>
|
||||||
@elseif($field['field_type'] === 'xhrSelect')
|
</flux:field>
|
||||||
<flux:select
|
|
||||||
label="{{ $field['name'] }}"
|
|
||||||
wire:model="configuration.{{ $fieldKey }}"
|
|
||||||
description="{{ $field['description'] ?? '' }}"
|
|
||||||
descriptionTrailing="{{ $field['help_text'] ?? '' }}"
|
|
||||||
wire:init="loadXhrSelectOptions('{{ $fieldKey }}', '{{ $field['endpoint'] }}')"
|
|
||||||
>
|
|
||||||
<option value="">Select {{ $field['name'] }}...</option>
|
|
||||||
@if(isset($xhrSelectOptions[$fieldKey]) && is_array($xhrSelectOptions[$fieldKey]))
|
|
||||||
@foreach($xhrSelectOptions[$fieldKey] as $option)
|
|
||||||
@if(is_array($option))
|
|
||||||
@if(isset($option['id']) && isset($option['name']))
|
|
||||||
{{-- xhrSelectSearch format: { 'id' => 'db-456', 'name' => 'Team Goals' } --}}
|
|
||||||
<option value="{{ $option['id'] }}" {{ $currentValue === (string)$option['id'] ? 'selected' : '' }}>{{ $option['name'] }}</option>
|
|
||||||
@else
|
|
||||||
{{-- xhrSelect format: { 'Braves' => 123 } --}}
|
|
||||||
@foreach($option as $label => $value)
|
|
||||||
<option value="{{ $value }}" {{ $currentValue === (string)$value ? 'selected' : '' }}>{{ $label }}</option>
|
|
||||||
@endforeach
|
|
||||||
@endif
|
|
||||||
@else
|
|
||||||
<option value="{{ $option }}" {{ $currentValue === (string)$option ? 'selected' : '' }}>{{ $option }}</option>
|
|
||||||
@endif
|
|
||||||
@endforeach
|
|
||||||
@endif
|
|
||||||
</flux:select>
|
|
||||||
@elseif($field['field_type'] === 'xhrSelectSearch')
|
@elseif($field['field_type'] === 'xhrSelectSearch')
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
|
|
||||||
<flux:label>{{ $field['name'] }}</flux:label>
|
<flux:label>{{ $field['name'] }}</flux:label>
|
||||||
<flux:description>{{ $field['description'] ?? '' }}</flux:description>
|
<flux:description>{!! $safeDescription !!}</flux:description>
|
||||||
<flux:input.group>
|
<flux:input.group>
|
||||||
<flux:input
|
<flux:input
|
||||||
wire:model="searchQueries.{{ $fieldKey }}"
|
wire:model="searchQueries.{{ $fieldKey }}"
|
||||||
|
|
@ -836,7 +932,7 @@ HTML;
|
||||||
wire:click="searchXhrSelect('{{ $fieldKey }}', '{{ $field['endpoint'] }}')"
|
wire:click="searchXhrSelect('{{ $fieldKey }}', '{{ $field['endpoint'] }}')"
|
||||||
icon="magnifying-glass"/>
|
icon="magnifying-glass"/>
|
||||||
</flux:input.group>
|
</flux:input.group>
|
||||||
<flux:description>{{ $field['help_text'] ?? '' }}</flux:description>
|
<flux:description>{!! $safeHelp !!}</flux:description>
|
||||||
@if((isset($xhrSelectOptions[$fieldKey]) && is_array($xhrSelectOptions[$fieldKey]) && count($xhrSelectOptions[$fieldKey]) > 0) || !empty($currentValue))
|
@if((isset($xhrSelectOptions[$fieldKey]) && is_array($xhrSelectOptions[$fieldKey]) && count($xhrSelectOptions[$fieldKey]) > 0) || !empty($currentValue))
|
||||||
<flux:select
|
<flux:select
|
||||||
wire:model="configuration.{{ $fieldKey }}"
|
wire:model="configuration.{{ $fieldKey }}"
|
||||||
|
|
@ -866,15 +962,60 @@ HTML;
|
||||||
</flux:select>
|
</flux:select>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
@elseif($field['field_type'] === 'multi_string')
|
@elseif($field['field_type'] === 'multi_string')
|
||||||
<flux:input
|
<flux:field>
|
||||||
label="{{ $field['name'] }}"
|
<flux:label>{{ $field['name'] }}</flux:label>
|
||||||
description="{{ $field['description'] ?? '' }}"
|
<flux:description>{!! $safeDescription !!}</flux:description>
|
||||||
descriptionTrailing="{{ $field['help_text'] ?? 'Enter multiple values separated by commas' }}"
|
|
||||||
wire:model="configuration.{{ $fieldKey }}"
|
<div class="space-y-2 mt-2">
|
||||||
value="{{ $currentValue }}"
|
@foreach($multiValues[$fieldKey] as $index => $item)
|
||||||
placeholder="{{ $field['placeholder'] ?? 'value1,value2' }}"
|
<div class="flex gap-2 items-center"
|
||||||
/>
|
wire:key="multi-{{ $fieldKey }}-{{ $index }}">
|
||||||
|
|
||||||
|
<flux:input
|
||||||
|
wire:model.defer="multiValues.{{ $fieldKey }}.{{ $index }}"
|
||||||
|
:placeholder="$field['placeholder'] ?? 'Value...'"
|
||||||
|
class="flex-1"
|
||||||
|
/>
|
||||||
|
|
||||||
|
@if(count($multiValues[$fieldKey]) > 1)
|
||||||
|
<flux:button
|
||||||
|
variant="ghost"
|
||||||
|
icon="trash"
|
||||||
|
size="sm"
|
||||||
|
wire:click="removeMultiItem('{{ $fieldKey }}', {{ $index }})"
|
||||||
|
/>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
|
||||||
|
<flux:button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
icon="plus"
|
||||||
|
wire:click="addMultiItem('{{ $fieldKey }}')"
|
||||||
|
>
|
||||||
|
Add Item
|
||||||
|
</flux:button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<flux:description>{!! $safeHelp !!}</flux:description>
|
||||||
|
</flux:field>
|
||||||
|
{{-- @elseif($field['field_type'] === 'multi_string')
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ $field['name'] }}</flux:label>
|
||||||
|
<flux:description>{!! $safeDescription !!}</flux:description>
|
||||||
|
|
||||||
|
|
||||||
|
{{-- <flux:input
|
||||||
|
wire:model="configuration.{{ $fieldKey }}"
|
||||||
|
value="{{ $currentValue }}"
|
||||||
|
placeholder="{{ $field['placeholder'] ?? 'value1,value2' }}"
|
||||||
|
/> --}}
|
||||||
|
|
||||||
|
{{-- <flux:description>{!! $safeHelp !!}</flux:description>
|
||||||
|
</flux:field> --}}
|
||||||
@else
|
@else
|
||||||
<flux:callout variant="warning">Field type "{{ $field['field_type'] }}" not yet supported</flux:callout>
|
<flux:callout variant="warning">Field type "{{ $field['field_type'] }}" not yet supported</flux:callout>
|
||||||
@endif
|
@endif
|
||||||
|
|
|
||||||
|
|
@ -679,3 +679,61 @@ test('plugin render includes utc_offset and time_zone_iana in trmnl.user context
|
||||||
->toContain('America/Chicago')
|
->toContain('America/Chicago')
|
||||||
->and($rendered)->toMatch('/\|-?\d+/'); // Should contain a pipe followed by a number (offset in seconds)
|
->and($rendered)->toMatch('/\|-?\d+/'); // Should contain a pipe followed by a number (offset in seconds)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin security: XSS Payload Dataset
|
||||||
|
* [Input, Expected Result, Forbidden String]
|
||||||
|
*/
|
||||||
|
dataset('xss_vectors', [
|
||||||
|
'standard_script' => ['Safe <script>alert(1)</script>', 'Safe ', '<script>'],
|
||||||
|
'attribute_event_handlers' => ['<a onmouseover="alert(1)">Link</a>', '<a>Link</a>', 'onmouseover'],
|
||||||
|
'javascript_protocol' => ['<a href="javascript:alert(1)">Click</a>', '<a>Click</a>', 'javascript:'],
|
||||||
|
'iframe_injection' => ['Watch <iframe src="https://x.com"></iframe>', 'Watch ', '<iframe>'],
|
||||||
|
'img_onerror_fallback' => ['Photo <img src=x onerror=alert(1)>', 'Photo <img src="x" alt="x">', 'onerror'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
test('plugin model sanitizes template fields on save', function (string $input, string $expected, string $forbidden): void {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
// We test the Model logic directly. This triggers the static::saving hook.
|
||||||
|
$plugin = Plugin::create([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'name' => 'Security Test',
|
||||||
|
'data_stale_minutes' => 15,
|
||||||
|
'data_strategy' => 'static',
|
||||||
|
'polling_verb' => 'get',
|
||||||
|
'configuration_template' => [
|
||||||
|
'custom_fields' => [
|
||||||
|
[
|
||||||
|
'keyname' => 'test_field',
|
||||||
|
'description' => $input,
|
||||||
|
'help_text' => $input,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$field = $plugin->fresh()->configuration_template['custom_fields'][0];
|
||||||
|
|
||||||
|
// Assert the saved data is clean
|
||||||
|
expect($field['description'])->toBe($expected)
|
||||||
|
->and($field['help_text'])->toBe($expected)
|
||||||
|
->and($field['description'])->not->toContain($forbidden);
|
||||||
|
})->with('xss_vectors');
|
||||||
|
|
||||||
|
test('plugin model preserves multi_string csv format', function (): void {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
$plugin = Plugin::create([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'name' => 'Multi-string Test',
|
||||||
|
'data_stale_minutes' => 15,
|
||||||
|
'data_strategy' => 'static',
|
||||||
|
'polling_verb' => 'get',
|
||||||
|
'configuration' => [
|
||||||
|
'tags' => 'laravel,pest,security'
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect($plugin->fresh()->configuration['tags'])->toBe('laravel,pest,security');
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue