Compare commits

...

4 commits

Author SHA1 Message Date
Benjamin Nussbaum
a86315c5c7 fix: init exception
Some checks failed
tests / ci (push) Has been cancelled
2026-01-10 22:10:37 +01:00
Benjamin Nussbaum
887c4d130b chore: gitignore 2026-01-10 19:55:35 +01:00
Benjamin Nussbaum
74e9e1eba3 chore: update dependencies 2026-01-10 19:54:26 +01:00
jerremyng
53d4a8399f feat(#152): preview polling url
add error handling for preview

fix idx bug and add tests

fix light mode styling and remove transitions

add global styling class
2026-01-10 17:44:51 +01:00
11 changed files with 274 additions and 187 deletions

2
.gitignore vendored
View file

@ -32,3 +32,5 @@ yarn-error.log
/.ai
.DS_Store
/boost.json
/.gemini
/GEMINI.md

View file

@ -153,105 +153,67 @@ class Plugin extends Model
public function updateDataPayload(): void
{
if ($this->data_strategy === 'polling' && $this->polling_url) {
$headers = ['User-Agent' => 'usetrmnl/byos_laravel', 'Accept' => 'application/json'];
if ($this->polling_header) {
// Resolve Liquid variables in the polling header
$resolvedHeader = $this->resolveLiquidVariables($this->polling_header);
$headerLines = explode("\n", mb_trim($resolvedHeader));
foreach ($headerLines as $line) {
$parts = explode(':', $line, 2);
if (count($parts) === 2) {
$headers[mb_trim($parts[0])] = mb_trim($parts[1]);
}
}
}
// Resolve Liquid variables in the entire polling_url field first, then split by newline
$resolvedPollingUrls = $this->resolveLiquidVariables($this->polling_url);
$urls = array_filter(
array_map('trim', explode("\n", $resolvedPollingUrls)),
fn ($url): bool => ! empty($url)
);
// If only one URL, use the original logic without nesting
if (count($urls) === 1) {
$url = reset($urls);
$httpRequest = Http::withHeaders($headers);
if ($this->polling_verb === 'post' && $this->polling_body) {
// Resolve Liquid variables in the polling body
$resolvedBody = $this->resolveLiquidVariables($this->polling_body);
$httpRequest = $httpRequest->withBody($resolvedBody);
}
// URL is already resolved, use it directly
$resolvedUrl = $url;
try {
// Make the request based on the verb
$httpResponse = $this->polling_verb === 'post' ? $httpRequest->post($resolvedUrl) : $httpRequest->get($resolvedUrl);
$response = $this->parseResponse($httpResponse);
$this->update([
'data_payload' => $response,
'data_payload_updated_at' => now(),
]);
} catch (Exception $e) {
Log::warning("Failed to fetch data from URL {$resolvedUrl}: ".$e->getMessage());
$this->update([
'data_payload' => ['error' => 'Failed to fetch data'],
'data_payload_updated_at' => now(),
]);
}
return;
}
// Multiple URLs - use nested response logic
$combinedResponse = [];
foreach ($urls as $index => $url) {
$httpRequest = Http::withHeaders($headers);
if ($this->polling_verb === 'post' && $this->polling_body) {
// Resolve Liquid variables in the polling body
$resolvedBody = $this->resolveLiquidVariables($this->polling_body);
$httpRequest = $httpRequest->withBody($resolvedBody);
}
// URL is already resolved, use it directly
$resolvedUrl = $url;
try {
// Make the request based on the verb
$httpResponse = $this->polling_verb === 'post' ? $httpRequest->post($resolvedUrl) : $httpRequest->get($resolvedUrl);
$response = $this->parseResponse($httpResponse);
// Check if response is an array at root level
if (array_keys($response) === range(0, count($response) - 1)) {
// Response is a sequential array, nest under .data
$combinedResponse["IDX_{$index}"] = ['data' => $response];
} else {
// Response is an object or associative array, keep as is
$combinedResponse["IDX_{$index}"] = $response;
}
} catch (Exception $e) {
// Log error and continue with other URLs
Log::warning("Failed to fetch data from URL {$resolvedUrl}: ".$e->getMessage());
$combinedResponse["IDX_{$index}"] = ['error' => 'Failed to fetch data'];
}
}
$this->update([
'data_payload' => $combinedResponse,
'data_payload_updated_at' => now(),
]);
if ($this->data_strategy !== 'polling' || !$this->polling_url) {
return;
}
$headers = ['User-Agent' => 'usetrmnl/byos_laravel', 'Accept' => 'application/json'];
// resolve headers
if ($this->polling_header) {
$resolvedHeader = $this->resolveLiquidVariables($this->polling_header);
$headerLines = explode("\n", mb_trim($resolvedHeader));
foreach ($headerLines as $line) {
$parts = explode(':', $line, 2);
if (count($parts) === 2) {
$headers[mb_trim($parts[0])] = mb_trim($parts[1]);
}
}
}
// resolve and clean URLs
$resolvedPollingUrls = $this->resolveLiquidVariables($this->polling_url);
$urls = array_values(array_filter( // array_values ensures 0, 1, 2...
array_map('trim', explode("\n", $resolvedPollingUrls)),
fn ($url): bool => filled($url)
));
$combinedResponse = [];
// Loop through all URLs (Handles 1 or many)
foreach ($urls as $index => $url) {
$httpRequest = Http::withHeaders($headers);
if ($this->polling_verb === 'post' && $this->polling_body) {
$resolvedBody = $this->resolveLiquidVariables($this->polling_body);
$httpRequest = $httpRequest->withBody($resolvedBody);
}
try {
$httpResponse = ($this->polling_verb === 'post')
? $httpRequest->post($url)
: $httpRequest->get($url);
$response = $this->parseResponse($httpResponse);
// Nest if it's a sequential array
if (array_keys($response) === range(0, count($response) - 1)) {
$combinedResponse["IDX_{$index}"] = ['data' => $response];
} else {
$combinedResponse["IDX_{$index}"] = $response;
}
} catch (Exception $e) {
Log::warning("Failed to fetch data from URL {$url}: ".$e->getMessage());
$combinedResponse["IDX_{$index}"] = ['error' => 'Failed to fetch data'];
}
}
// unwrap IDX_0 if only one URL
$finalPayload = (count($urls) === 1) ? reset($combinedResponse) : $combinedResponse;
$this->update([
'data_payload' => $finalPayload,
'data_payload_updated_at' => now(),
]);
}
private function parseResponse(Response $httpResponse): array

146
composer.lock generated
View file

@ -62,16 +62,16 @@
},
{
"name": "aws/aws-sdk-php",
"version": "3.369.6",
"version": "3.369.10",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
"reference": "b1e1846a4b6593b6916764d86fc0890a31727370"
"reference": "e179090bf2d658be7be37afc146111966ba6f41b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/b1e1846a4b6593b6916764d86fc0890a31727370",
"reference": "b1e1846a4b6593b6916764d86fc0890a31727370",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/e179090bf2d658be7be37afc146111966ba6f41b",
"reference": "e179090bf2d658be7be37afc146111966ba6f41b",
"shasum": ""
},
"require": {
@ -153,9 +153,9 @@
"support": {
"forum": "https://github.com/aws/aws-sdk-php/discussions",
"issues": "https://github.com/aws/aws-sdk-php/issues",
"source": "https://github.com/aws/aws-sdk-php/tree/3.369.6"
"source": "https://github.com/aws/aws-sdk-php/tree/3.369.10"
},
"time": "2026-01-02T19:09:23+00:00"
"time": "2026-01-09T19:08:12+00:00"
},
{
"name": "bnussbau/laravel-trmnl-blade",
@ -877,16 +877,16 @@
},
{
"name": "firebase/php-jwt",
"version": "v6.11.1",
"version": "v7.0.2",
"source": {
"type": "git",
"url": "https://github.com/firebase/php-jwt.git",
"reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66"
"reference": "5645b43af647b6947daac1d0f659dd1fbe8d3b65"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/firebase/php-jwt/zipball/d1e91ecf8c598d073d0995afa8cd5c75c6e19e66",
"reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66",
"url": "https://api.github.com/repos/firebase/php-jwt/zipball/5645b43af647b6947daac1d0f659dd1fbe8d3b65",
"reference": "5645b43af647b6947daac1d0f659dd1fbe8d3b65",
"shasum": ""
},
"require": {
@ -934,9 +934,9 @@
],
"support": {
"issues": "https://github.com/firebase/php-jwt/issues",
"source": "https://github.com/firebase/php-jwt/tree/v6.11.1"
"source": "https://github.com/firebase/php-jwt/tree/v7.0.2"
},
"time": "2025-04-09T20:32:01+00:00"
"time": "2025-12-16T22:17:28+00:00"
},
{
"name": "fruitcake/php-cors",
@ -1678,16 +1678,16 @@
},
{
"name": "laravel/framework",
"version": "v12.44.0",
"version": "v12.46.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
"reference": "592bbf1c036042958332eb98e3e8131b29102f33"
"reference": "9dcff48d25a632c1fadb713024c952fec489c4ae"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/592bbf1c036042958332eb98e3e8131b29102f33",
"reference": "592bbf1c036042958332eb98e3e8131b29102f33",
"url": "https://api.github.com/repos/laravel/framework/zipball/9dcff48d25a632c1fadb713024c952fec489c4ae",
"reference": "9dcff48d25a632c1fadb713024c952fec489c4ae",
"shasum": ""
},
"require": {
@ -1896,7 +1896,7 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"time": "2025-12-23T15:29:43+00:00"
"time": "2026-01-07T23:26:53+00:00"
},
{
"name": "laravel/prompts",
@ -1959,16 +1959,16 @@
},
{
"name": "laravel/sanctum",
"version": "v4.2.1",
"version": "v4.2.2",
"source": {
"type": "git",
"url": "https://github.com/laravel/sanctum.git",
"reference": "f5fb373be39a246c74a060f2cf2ae2c2145b3664"
"reference": "fd447754d2d3f56950d53b930128af2e3b617de9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/sanctum/zipball/f5fb373be39a246c74a060f2cf2ae2c2145b3664",
"reference": "f5fb373be39a246c74a060f2cf2ae2c2145b3664",
"url": "https://api.github.com/repos/laravel/sanctum/zipball/fd447754d2d3f56950d53b930128af2e3b617de9",
"reference": "fd447754d2d3f56950d53b930128af2e3b617de9",
"shasum": ""
},
"require": {
@ -2018,7 +2018,7 @@
"issues": "https://github.com/laravel/sanctum/issues",
"source": "https://github.com/laravel/sanctum"
},
"time": "2025-11-21T13:59:03+00:00"
"time": "2026-01-06T23:11:51+00:00"
},
{
"name": "laravel/serializable-closure",
@ -2083,21 +2083,21 @@
},
{
"name": "laravel/socialite",
"version": "v5.24.0",
"version": "v5.24.1",
"source": {
"type": "git",
"url": "https://github.com/laravel/socialite.git",
"reference": "1d19358c28e8951dde6e36603b89d8f09e6cfbfd"
"reference": "25e28c14d55404886777af1d77cf030e0f633142"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/socialite/zipball/1d19358c28e8951dde6e36603b89d8f09e6cfbfd",
"reference": "1d19358c28e8951dde6e36603b89d8f09e6cfbfd",
"url": "https://api.github.com/repos/laravel/socialite/zipball/25e28c14d55404886777af1d77cf030e0f633142",
"reference": "25e28c14d55404886777af1d77cf030e0f633142",
"shasum": ""
},
"require": {
"ext-json": "*",
"firebase/php-jwt": "^6.4",
"firebase/php-jwt": "^6.4|^7.0",
"guzzlehttp/guzzle": "^6.0|^7.0",
"illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
"illuminate/http": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
@ -2151,20 +2151,20 @@
"issues": "https://github.com/laravel/socialite/issues",
"source": "https://github.com/laravel/socialite"
},
"time": "2025-12-09T15:37:06+00:00"
"time": "2026-01-01T02:57:21+00:00"
},
{
"name": "laravel/tinker",
"version": "v2.10.2",
"version": "v2.11.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/tinker.git",
"reference": "3bcb5f62d6f837e0f093a601e26badafb127bd4c"
"reference": "3d34b97c9a1747a81a3fde90482c092bd8b66468"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/tinker/zipball/3bcb5f62d6f837e0f093a601e26badafb127bd4c",
"reference": "3bcb5f62d6f837e0f093a601e26badafb127bd4c",
"url": "https://api.github.com/repos/laravel/tinker/zipball/3d34b97c9a1747a81a3fde90482c092bd8b66468",
"reference": "3d34b97c9a1747a81a3fde90482c092bd8b66468",
"shasum": ""
},
"require": {
@ -2173,7 +2173,7 @@
"illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
"php": "^7.2.5|^8.0",
"psy/psysh": "^0.11.1|^0.12.0",
"symfony/var-dumper": "^4.3.4|^5.0|^6.0|^7.0"
"symfony/var-dumper": "^4.3.4|^5.0|^6.0|^7.0|^8.0"
},
"require-dev": {
"mockery/mockery": "~1.3.3|^1.4.2",
@ -2215,9 +2215,9 @@
],
"support": {
"issues": "https://github.com/laravel/tinker/issues",
"source": "https://github.com/laravel/tinker/tree/v2.10.2"
"source": "https://github.com/laravel/tinker/tree/v2.11.0"
},
"time": "2025-11-20T16:29:12+00:00"
"time": "2025-12-19T19:16:45+00:00"
},
{
"name": "league/commonmark",
@ -8096,16 +8096,16 @@
"packages-dev": [
{
"name": "brianium/paratest",
"version": "v7.16.0",
"version": "v7.16.1",
"source": {
"type": "git",
"url": "https://github.com/paratestphp/paratest.git",
"reference": "a10878ed0fe0bbc2f57c980f7a08065338b970b6"
"reference": "f0fdfd8e654e0d38bc2ba756a6cabe7be287390b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paratestphp/paratest/zipball/a10878ed0fe0bbc2f57c980f7a08065338b970b6",
"reference": "a10878ed0fe0bbc2f57c980f7a08065338b970b6",
"url": "https://api.github.com/repos/paratestphp/paratest/zipball/f0fdfd8e654e0d38bc2ba756a6cabe7be287390b",
"reference": "f0fdfd8e654e0d38bc2ba756a6cabe7be287390b",
"shasum": ""
},
"require": {
@ -8116,10 +8116,10 @@
"fidry/cpu-core-counter": "^1.3.0",
"jean85/pretty-package-versions": "^2.1.1",
"php": "~8.3.0 || ~8.4.0 || ~8.5.0",
"phpunit/php-code-coverage": "^12.5.1",
"phpunit/php-code-coverage": "^12.5.2",
"phpunit/php-file-iterator": "^6",
"phpunit/php-timer": "^8",
"phpunit/phpunit": "^12.5.2",
"phpunit/phpunit": "^12.5.4",
"sebastian/environment": "^8.0.3",
"symfony/console": "^7.3.4 || ^8.0.0",
"symfony/process": "^7.3.4 || ^8.0.0"
@ -8131,7 +8131,7 @@
"ext-posix": "*",
"phpstan/phpstan": "^2.1.33",
"phpstan/phpstan-deprecation-rules": "^2.0.3",
"phpstan/phpstan-phpunit": "^2.0.10",
"phpstan/phpstan-phpunit": "^2.0.11",
"phpstan/phpstan-strict-rules": "^2.0.7",
"symfony/filesystem": "^7.3.2 || ^8.0.0"
},
@ -8173,7 +8173,7 @@
],
"support": {
"issues": "https://github.com/paratestphp/paratest/issues",
"source": "https://github.com/paratestphp/paratest/tree/v7.16.0"
"source": "https://github.com/paratestphp/paratest/tree/v7.16.1"
},
"funding": [
{
@ -8185,7 +8185,7 @@
"type": "paypal"
}
],
"time": "2025-12-09T20:03:26+00:00"
"time": "2026-01-08T07:23:06+00:00"
},
{
"name": "doctrine/deprecations",
@ -8674,16 +8674,16 @@
},
{
"name": "laravel/boost",
"version": "v1.8.7",
"version": "v1.8.9",
"source": {
"type": "git",
"url": "https://github.com/laravel/boost.git",
"reference": "7a5709a8134ed59d3e7f34fccbd74689830e296c"
"reference": "1f2c2d41b5216618170fb6730ec13bf894c5bffd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/boost/zipball/7a5709a8134ed59d3e7f34fccbd74689830e296c",
"reference": "7a5709a8134ed59d3e7f34fccbd74689830e296c",
"url": "https://api.github.com/repos/laravel/boost/zipball/1f2c2d41b5216618170fb6730ec13bf894c5bffd",
"reference": "1f2c2d41b5216618170fb6730ec13bf894c5bffd",
"shasum": ""
},
"require": {
@ -8736,20 +8736,20 @@
"issues": "https://github.com/laravel/boost/issues",
"source": "https://github.com/laravel/boost"
},
"time": "2025-12-19T15:04:12+00:00"
"time": "2026-01-07T18:43:11+00:00"
},
{
"name": "laravel/mcp",
"version": "v0.5.1",
"version": "v0.5.2",
"source": {
"type": "git",
"url": "https://github.com/laravel/mcp.git",
"reference": "10dedea054fa4eeaa9ef2ccbfdad6c3e1dbd17a4"
"reference": "b9bdd8d6f8b547c8733fe6826b1819341597ba3c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/mcp/zipball/10dedea054fa4eeaa9ef2ccbfdad6c3e1dbd17a4",
"reference": "10dedea054fa4eeaa9ef2ccbfdad6c3e1dbd17a4",
"url": "https://api.github.com/repos/laravel/mcp/zipball/b9bdd8d6f8b547c8733fe6826b1819341597ba3c",
"reference": "b9bdd8d6f8b547c8733fe6826b1819341597ba3c",
"shasum": ""
},
"require": {
@ -8809,7 +8809,7 @@
"issues": "https://github.com/laravel/mcp/issues",
"source": "https://github.com/laravel/mcp"
},
"time": "2025-12-17T06:14:23+00:00"
"time": "2025-12-19T19:32:34+00:00"
},
{
"name": "laravel/pail",
@ -8892,16 +8892,16 @@
},
{
"name": "laravel/pint",
"version": "v1.26.0",
"version": "v1.27.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/pint.git",
"reference": "69dcca060ecb15e4b564af63d1f642c81a241d6f"
"reference": "c67b4195b75491e4dfc6b00b1c78b68d86f54c90"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/pint/zipball/69dcca060ecb15e4b564af63d1f642c81a241d6f",
"reference": "69dcca060ecb15e4b564af63d1f642c81a241d6f",
"url": "https://api.github.com/repos/laravel/pint/zipball/c67b4195b75491e4dfc6b00b1c78b68d86f54c90",
"reference": "c67b4195b75491e4dfc6b00b1c78b68d86f54c90",
"shasum": ""
},
"require": {
@ -8912,9 +8912,9 @@
"php": "^8.2.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.90.0",
"illuminate/view": "^12.40.1",
"larastan/larastan": "^3.8.0",
"friendsofphp/php-cs-fixer": "^3.92.4",
"illuminate/view": "^12.44.0",
"larastan/larastan": "^3.8.1",
"laravel-zero/framework": "^12.0.4",
"mockery/mockery": "^1.6.12",
"nunomaduro/termwind": "^2.3.3",
@ -8955,7 +8955,7 @@
"issues": "https://github.com/laravel/pint/issues",
"source": "https://github.com/laravel/pint"
},
"time": "2025-11-25T21:15:52+00:00"
"time": "2026-01-05T16:49:17+00:00"
},
{
"name": "laravel/roster",
@ -9020,16 +9020,16 @@
},
{
"name": "laravel/sail",
"version": "v1.51.0",
"version": "v1.52.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/sail.git",
"reference": "1c74357df034e869250b4365dd445c9f6ba5d068"
"reference": "64ac7d8abb2dbcf2b76e61289451bae79066b0b3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/sail/zipball/1c74357df034e869250b4365dd445c9f6ba5d068",
"reference": "1c74357df034e869250b4365dd445c9f6ba5d068",
"url": "https://api.github.com/repos/laravel/sail/zipball/64ac7d8abb2dbcf2b76e61289451bae79066b0b3",
"reference": "64ac7d8abb2dbcf2b76e61289451bae79066b0b3",
"shasum": ""
},
"require": {
@ -9079,7 +9079,7 @@
"issues": "https://github.com/laravel/sail/issues",
"source": "https://github.com/laravel/sail"
},
"time": "2025-12-09T13:33:49+00:00"
"time": "2026-01-01T02:46:03+00:00"
},
{
"name": "mockery/mockery",
@ -11806,16 +11806,16 @@
},
{
"name": "webmozart/assert",
"version": "2.0.0",
"version": "2.1.1",
"source": {
"type": "git",
"url": "https://github.com/webmozarts/assert.git",
"reference": "1b34b004e35a164bc5bb6ebd33c844b2d8069a54"
"reference": "bdbabc199a7ba9965484e4725d66170e5711323b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/webmozarts/assert/zipball/1b34b004e35a164bc5bb6ebd33c844b2d8069a54",
"reference": "1b34b004e35a164bc5bb6ebd33c844b2d8069a54",
"url": "https://api.github.com/repos/webmozarts/assert/zipball/bdbabc199a7ba9965484e4725d66170e5711323b",
"reference": "bdbabc199a7ba9965484e4725d66170e5711323b",
"shasum": ""
},
"require": {
@ -11862,9 +11862,9 @@
],
"support": {
"issues": "https://github.com/webmozarts/assert/issues",
"source": "https://github.com/webmozarts/assert/tree/2.0.0"
"source": "https://github.com/webmozarts/assert/tree/2.1.1"
},
"time": "2025-12-16T21:36:00+00:00"
"time": "2026-01-08T11:28:40+00:00"
}
],
"aliases": [],

View file

@ -72,3 +72,39 @@ select:focus[data-flux-control] {
/* \[:where(&)\]:size-4 {
@apply size-4;
} */
@layer components {
/* standard container for app */
.styled-container,
.tab-button {
@apply rounded-xl border bg-white dark:bg-stone-950 dark:border-zinc-700 text-stone-800 shadow-xs;
}
.tab-button {
@apply flex items-center gap-2 px-4 py-2 text-sm font-medium;
@apply rounded-b-none shadow-none bg-inherit;
/* This makes the button sit slightly over the box border */
margin-bottom: -1px;
position: relative;
z-index: 1;
}
.tab-button.is-active {
@apply text-zinc-700 dark:text-zinc-300;
@apply border-b-white dark:border-b-zinc-800;
/* Z-index 10 ensures the bottom border of the tab hides the top border of the box */
z-index: 10;
}
.tab-button:not(.is-active) {
@apply text-zinc-500 border-transparent;
}
.tab-button:not(.is-active):hover {
@apply text-zinc-700 dark:text-zinc-300;
@apply border-zinc-300 dark:border-zinc-700;
cursor: pointer;
}
}

View file

@ -15,7 +15,7 @@
</a>
<div class="flex flex-col gap-6">
<div class="rounded-xl border bg-white dark:bg-stone-950 dark:border-stone-800 text-stone-800 shadow-xs">
<div class="styled-container">
<div class="px-10 py-8">{{ $slot }}</div>
</div>
</div>

View file

@ -16,7 +16,7 @@ new class extends Component {
@if($devices->isEmpty())
<div class="flex flex-col gap-6">
<div
class="rounded-xl border bg-white dark:bg-stone-950 dark:border-stone-800 text-stone-800 shadow-xs">
class="styled-container">
<div class="px-10 py-8">
<h1 class="text-xl font-medium dark:text-zinc-200">Add your first device</h1>
<flux:button href="{{ route('devices') }}" class="mt-4" icon="plus-circle" variant="primary"
@ -30,7 +30,7 @@ new class extends Component {
@foreach($devices as $device)
<div class="flex flex-col gap-6">
<div
class="rounded-xl border bg-white dark:bg-stone-950 dark:border-stone-800 text-stone-800 shadow-xs">
class="styled-container">
<div class="px-10 py-8">
@php
$current_image_uuid =$device->current_screen_image;

View file

@ -309,7 +309,7 @@ new class extends Component {
<div class="bg-muted flex flex-col items-center justify-center gap-6 p-6 md:p-10">
<div class="flex flex-col gap-6">
<div
class="rounded-xl border bg-white dark:bg-stone-950 dark:border-stone-800 text-stone-800 shadow-xs">
class="styled-container">
<div class="px-10 py-8">
@php
$current_image_uuid =$device->current_screen_image;

View file

@ -332,7 +332,7 @@ new class extends Component {
@endforeach
@if($devices->isEmpty() || $devices->every(fn($device) => $device->playlists->isEmpty()))
<div class="rounded-xl border bg-white dark:bg-stone-950 dark:border-stone-800 text-stone-800 shadow-xs">
<div class="styled-container">
<div class="px-10 py-8">
<h1 class="text-xl font-medium dark:text-zinc-200">No playlists found</h1>
<p class="text-sm dark:text-zinc-400 mt-2">Add playlists to your devices to see them here.</p>

View file

@ -395,7 +395,7 @@ new class extends Component {
wire:key="plugin-{{ $plugin['id'] ?? $plugin['name'] ?? $index }}"
x-data="{ pluginName: {{ json_encode(strtolower($plugin['name'] ?? '')) }} }"
x-show="searchTerm.length <= 1 || pluginName.includes(searchTerm.toLowerCase())"
class="rounded-xl border bg-white dark:bg-stone-950 dark:border-stone-800 text-stone-800 shadow-xs">
class="styled-container">
<a href="{{ ($plugin['detail_view_route']) ? route($plugin['detail_view_route']) : route('plugins.recipe', ['plugin' => $plugin['id']]) }}"
class="block h-full">
<div class="flex items-center space-x-4 px-10 py-8 h-full">

View file

@ -10,6 +10,7 @@ use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Http;
use Livewire\Attributes\On;
use Livewire\Attributes\Computed;
new class extends Component {
public Plugin $plugin;
@ -295,8 +296,6 @@ new class extends Component {
return $this->configuration[$key] ?? $default;
}
public function renderExample(string $example)
{
switch ($example) {
@ -431,6 +430,21 @@ HTML;
$this->plugin = $this->plugin->fresh();
}
// Laravel Livewire computed property: access with $this->parsed_urls
#[Computed]
private function parsedUrls()
{
if (!isset($this->polling_url)) {
return null;
}
try {
return $this->plugin->resolveLiquidVariables($this->polling_url);
} catch (\Exception $e) {
return 'PARSE_ERROR: ' . $e->getMessage();
}
}
}
?>
@ -733,15 +747,62 @@ HTML;
</div>
@if($data_strategy === 'polling')
<div class="mb-4">
<flux:textarea label="Polling URL" description="You can use configuration variables with Liquid syntax. Supports multiple requests via line break separation" wire:model="polling_url" id="polling_url"
<flux:label>Polling URL</flux:label>
<div x-data="{ subTab: 'settings' }" class="mt-2 mb-4">
<div class="flex">
<button
@click="subTab = 'settings'"
class="tab-button"
:class="subTab === 'settings' ? 'is-active' : ''"
>
<flux:icon.cog-6-tooth class="size-4"/>
Settings
</button>
<button
@click="subTab = 'preview'"
class="tab-button"
:class="subTab === 'preview' ? 'is-active' : ''"
>
<flux:icon.eye class="size-4" />
Preview URL
</button>
</div>
<div class="flex-col p-4 bg-transparent rounded-tl-none styled-container">
<div x-show="subTab === 'settings'">
<flux:field>
<flux:description>Enter the URL(s) to poll for data:</flux:description>
<flux:textarea
wire:model.live="polling_url"
placeholder="https://example.com/api"
class="block w-full" type="text" name="polling_url" autofocus>
</flux:input>
<flux:button icon="cloud-arrow-down" wire:click="updateData" class="block mt-2 w-full">
rows="5"
/>
<flux:description>
{!! 'Hint: Supports multiple requests via line break separation. You can also use configuration variables with <a href="https://help.usetrmnl.com/en/articles/12689499-dynamic-polling-urls">Liquid syntax</a>. ' !!}
</flux:description>
</flux:field>
</div>
<div x-show="subTab === 'preview'" x-cloak>
<flux:field>
<flux:description>Preview computed URLs here (readonly):</flux:description>
<flux:textarea
readonly
placeholder="Nothing to show..."
rows="5"
>
{{ $this->parsed_urls }}
</flux:textarea>
</flux:field>
</div>
<flux:button variant="primary" icon="cloud-arrow-down" wire:click="updateData" class="w-full mt-4">
Fetch data now
</flux:button>
</div>
</div>
<div class="mb-4">
<flux:radio.group wire:model.live="polling_verb" label="Polling Verb" variant="segmented">
@ -905,9 +966,6 @@ HTML;
</div>
</flux:field>
</div>
@else
<div class="flex items-center gap-6 mb-4 mt-4">

View file

@ -99,6 +99,35 @@ test('updateDataPayload handles multiple URLs with IDX_ prefixes', function ():
expect($plugin->data_payload['IDX_2'])->toBe(['headline' => 'test']);
});
test('updateDataPayload skips empty lines in polling_url and maintains sequential IDX keys', function (): void {
$plugin = Plugin::factory()->create([
'data_strategy' => 'polling',
// empty lines and extra spaces between the URL to generate empty entries
'polling_url' => "https://api1.example.com/data\n \n\nhttps://api2.example.com/weather\n ",
'polling_verb' => 'get',
]);
// Mock only the valid URLs
Http::fake([
'https://api1.example.com/data' => Http::response(['item' => 'first'], 200),
'https://api2.example.com/weather' => Http::response(['item' => 'second'], 200),
]);
$plugin->updateDataPayload();
// payload should only have 2 items, and they should be indexed 0 and 1
expect($plugin->data_payload)->toHaveCount(2);
expect($plugin->data_payload)->toHaveKey('IDX_0');
expect($plugin->data_payload)->toHaveKey('IDX_1');
// data is correct
expect($plugin->data_payload['IDX_0'])->toBe(['item' => 'first']);
expect($plugin->data_payload['IDX_1'])->toBe(['item' => 'second']);
// no empty index exists
expect($plugin->data_payload)->not->toHaveKey('IDX_2');
});
test('updateDataPayload handles single URL without nesting', function (): void {
$plugin = Plugin::factory()->create([
'data_strategy' => 'polling',