mirror of
https://github.com/usetrmnl/byos_laravel.git
synced 2026-01-13 15:07:49 +00:00
Compare commits
1 commit
b10bbca774
...
8dfe19e2f7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8dfe19e2f7 |
12 changed files with 316 additions and 593 deletions
|
|
@ -121,10 +121,6 @@ class GenerateDefaultImagesCommand extends Command
|
|||
|
||||
$browserStage = new BrowserStage($browsershotInstance);
|
||||
$browserStage->html($html);
|
||||
|
||||
// Set timezone from app config (no user context in this command)
|
||||
$browserStage->timezone(config('app.timezone'));
|
||||
|
||||
$browserStage
|
||||
->width($deviceModel->width)
|
||||
->height($deviceModel->height);
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ use App\Liquid\Filters\StringMarkup;
|
|||
use App\Liquid\Filters\Uniqueness;
|
||||
use App\Liquid\Tags\TemplateTag;
|
||||
use App\Services\PluginImportService;
|
||||
use Carbon\Carbon;
|
||||
use Exception;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
|
@ -455,12 +454,6 @@ class Plugin extends Model
|
|||
$renderedContent = '';
|
||||
|
||||
if ($this->markup_language === 'liquid') {
|
||||
// Get timezone from user or fall back to app timezone
|
||||
$timezone = $this->user->timezone ?? config('app.timezone');
|
||||
|
||||
// Calculate UTC offset in seconds
|
||||
$utcOffset = (string) Carbon::now($timezone)->getOffset();
|
||||
|
||||
// Build render context
|
||||
$context = [
|
||||
'size' => $size,
|
||||
|
|
@ -472,10 +465,10 @@ class Plugin extends Model
|
|||
'timestamp_utc' => now()->utc()->timestamp,
|
||||
],
|
||||
'user' => [
|
||||
'utc_offset' => $utcOffset,
|
||||
'utc_offset' => '0',
|
||||
'name' => $this->user->name ?? 'Unknown User',
|
||||
'locale' => 'en',
|
||||
'time_zone_iana' => $timezone,
|
||||
'time_zone_iana' => config('app.timezone'),
|
||||
],
|
||||
'plugin_settings' => [
|
||||
'instance_name' => $this->name,
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ class User extends Authenticatable // implements MustVerifyEmail
|
|||
'assign_new_devices',
|
||||
'assign_new_device_id',
|
||||
'oidc_sub',
|
||||
'timezone',
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ class ImageGenerationService
|
|||
{
|
||||
public static function generateImage(string $markup, $deviceId): string
|
||||
{
|
||||
$device = Device::with(['deviceModel', 'palette', 'deviceModel.palette', 'user'])->find($deviceId);
|
||||
$device = Device::with(['deviceModel', 'palette', 'deviceModel.palette'])->find($deviceId);
|
||||
$uuid = Uuid::uuid4()->toString();
|
||||
|
||||
try {
|
||||
|
|
@ -44,10 +44,6 @@ class ImageGenerationService
|
|||
$browserStage = new BrowserStage($browsershotInstance);
|
||||
$browserStage->html($markup);
|
||||
|
||||
// Set timezone from user or fall back to app timezone
|
||||
$timezone = $device->user->timezone ?? config('app.timezone');
|
||||
$browserStage->timezone($timezone);
|
||||
|
||||
if (config('app.puppeteer_window_size_strategy') === 'v2') {
|
||||
$browserStage
|
||||
->width($imageSettings['width'])
|
||||
|
|
@ -356,7 +352,7 @@ class ImageGenerationService
|
|||
|
||||
try {
|
||||
// Load device with relationships
|
||||
$device->load(['palette', 'deviceModel.palette', 'user']);
|
||||
$device->load(['palette', 'deviceModel.palette']);
|
||||
|
||||
// Get image generation settings from DeviceModel if available, otherwise use device settings
|
||||
$imageSettings = self::getImageSettings($device);
|
||||
|
|
@ -376,10 +372,6 @@ class ImageGenerationService
|
|||
$browserStage = new BrowserStage($browsershotInstance);
|
||||
$browserStage->html($html);
|
||||
|
||||
// Set timezone from user or fall back to app timezone
|
||||
$timezone = $device->user->timezone ?? config('app.timezone');
|
||||
$browserStage->timezone($timezone);
|
||||
|
||||
if (config('app.puppeteer_window_size_strategy') === 'v2') {
|
||||
$browserStage
|
||||
->width($imageSettings['width'])
|
||||
|
|
|
|||
|
|
@ -80,9 +80,6 @@ class PluginImportService
|
|||
$settings['custom_fields'] = [];
|
||||
}
|
||||
|
||||
// Normalize options in custom_fields (convert non-named values to named values)
|
||||
$settings['custom_fields'] = $this->normalizeCustomFieldsOptions($settings['custom_fields']);
|
||||
|
||||
// Create configuration template with the custom fields
|
||||
$configurationTemplate = [
|
||||
'custom_fields' => $settings['custom_fields'],
|
||||
|
|
@ -209,9 +206,6 @@ class PluginImportService
|
|||
$settings['custom_fields'] = [];
|
||||
}
|
||||
|
||||
// Normalize options in custom_fields (convert non-named values to named values)
|
||||
$settings['custom_fields'] = $this->normalizeCustomFieldsOptions($settings['custom_fields']);
|
||||
|
||||
// Create configuration template with the custom fields
|
||||
$configurationTemplate = [
|
||||
'custom_fields' => $settings['custom_fields'],
|
||||
|
|
@ -349,13 +343,10 @@ class PluginImportService
|
|||
} elseif ($filename === 'shared.liquid') {
|
||||
$sharedLiquidPath = $filepath;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if shared.liquid exists in the same directory as full.liquid
|
||||
if ($settingsYamlPath && $fullLiquidPath && ! $sharedLiquidPath) {
|
||||
$fullLiquidDir = dirname((string) $fullLiquidPath);
|
||||
if (File::exists($fullLiquidDir.'/shared.liquid')) {
|
||||
$sharedLiquidPath = $fullLiquidDir.'/shared.liquid';
|
||||
// If we found both required files, break the loop
|
||||
if ($settingsYamlPath && $fullLiquidPath) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -394,49 +385,6 @@ class PluginImportService
|
|||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize options in custom_fields by converting non-named values to named values
|
||||
* This ensures that options like ["true", "false"] become [["true" => "true"], ["false" => "false"]]
|
||||
*
|
||||
* @param array $customFields The custom_fields array from settings
|
||||
* @return array The normalized custom_fields array
|
||||
*/
|
||||
private function normalizeCustomFieldsOptions(array $customFields): array
|
||||
{
|
||||
foreach ($customFields as &$field) {
|
||||
// Only process select fields with options
|
||||
if (isset($field['field_type']) && $field['field_type'] === 'select' && isset($field['options']) && is_array($field['options'])) {
|
||||
$normalizedOptions = [];
|
||||
foreach ($field['options'] as $option) {
|
||||
// If option is already a named value (array with key-value pair), keep it as is
|
||||
if (is_array($option)) {
|
||||
$normalizedOptions[] = $option;
|
||||
} else {
|
||||
// Convert non-named value to named value
|
||||
// Convert boolean to string, use lowercase for label
|
||||
$value = is_bool($option) ? ($option ? 'true' : 'false') : (string) $option;
|
||||
$normalizedOptions[] = [$value => $value];
|
||||
}
|
||||
}
|
||||
$field['options'] = $normalizedOptions;
|
||||
|
||||
// Normalize default value to match normalized option values
|
||||
if (isset($field['default'])) {
|
||||
$default = $field['default'];
|
||||
// If default is boolean, convert to string to match normalized options
|
||||
if (is_bool($default)) {
|
||||
$field['default'] = $default ? 'true' : 'false';
|
||||
} else {
|
||||
// Convert to string to ensure consistency
|
||||
$field['default'] = (string) $default;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $customFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that template and context are within command-line argument limits
|
||||
*
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
"ext-simplexml": "*",
|
||||
"ext-zip": "*",
|
||||
"bnussbau/laravel-trmnl-blade": "2.0.*",
|
||||
"bnussbau/trmnl-pipeline-php": "^0.6.0",
|
||||
"bnussbau/trmnl-pipeline-php": "^0.5.0",
|
||||
"keepsuit/laravel-liquid": "^0.5.2",
|
||||
"laravel/framework": "^12.1",
|
||||
"laravel/sanctum": "^4.0",
|
||||
|
|
|
|||
623
composer.lock
generated
623
composer.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,28 +0,0 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->string('timezone')->nullable()->after('oidc_sub');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn('timezone');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -263,19 +263,7 @@ new class extends Component {
|
|||
foreach ($this->configuration_template['custom_fields'] as $field) {
|
||||
$fieldKey = $field['keyname'];
|
||||
if (isset($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
|
||||
if ($field['field_type'] === 'code' && is_string($value)) {
|
||||
$decoded = json_decode($value, true);
|
||||
// If it's valid JSON and decodes to an array/object, use the decoded value
|
||||
// Otherwise, keep the string as-is
|
||||
if (json_last_error() === JSON_ERROR_NONE && (is_array($decoded) || is_object($decoded))) {
|
||||
$value = $decoded;
|
||||
}
|
||||
}
|
||||
|
||||
$configurationValues[$fieldKey] = $value;
|
||||
$configurationValues[$fieldKey] = $this->configuration[$fieldKey];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -638,14 +626,7 @@ HTML;
|
|||
@foreach($configuration_template['custom_fields'] as $field)
|
||||
@php
|
||||
$fieldKey = $field['keyname'] ?? $field['key'] ?? $field['name'];
|
||||
$rawValue = $configuration[$fieldKey] ?? ($field['default'] ?? '');
|
||||
|
||||
// For code fields, if the value is an array, JSON encode it
|
||||
if ($field['field_type'] === 'code' && is_array($rawValue)) {
|
||||
$currentValue = json_encode($rawValue, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
} else {
|
||||
$currentValue = is_array($rawValue) ? '' : (string) $rawValue;
|
||||
}
|
||||
$currentValue = $configuration[$fieldKey] ?? '';
|
||||
@endphp
|
||||
<div class="mb-4">
|
||||
@if($field['field_type'] === 'author_bio')
|
||||
|
|
|
|||
|
|
@ -11,12 +11,9 @@ use Livewire\Volt\Component;
|
|||
new class extends Component {
|
||||
public ?int $assign_new_device_id = null;
|
||||
|
||||
public ?string $timezone = null;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->assign_new_device_id = Auth::user()->assign_new_device_id;
|
||||
$this->timezone = Auth::user()->timezone ?? config('app.timezone');
|
||||
}
|
||||
|
||||
public function updatePreferences(): void
|
||||
|
|
@ -29,11 +26,6 @@ new class extends Component {
|
|||
->whereNull('mirror_device_id');
|
||||
}),
|
||||
],
|
||||
'timezone' => [
|
||||
'nullable',
|
||||
'string',
|
||||
Rule::in(timezone_identifiers_list()),
|
||||
],
|
||||
]);
|
||||
|
||||
Auth::user()->update($validated);
|
||||
|
|
@ -47,14 +39,6 @@ new class extends Component {
|
|||
|
||||
<x-settings.layout heading="Preferences" subheading="Update your preferences">
|
||||
<form wire:submit="updatePreferences" class="my-6 w-full space-y-6">
|
||||
|
||||
<flux:select wire:model="timezone" label="Timezone">
|
||||
<flux:select.option value="" disabled>Select timezone...</flux:select.option>
|
||||
@foreach(timezone_identifiers_list() as $tz)
|
||||
<flux:select.option value="{{ $tz }}">{{ $tz }}</flux:select.option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
|
||||
<flux:select wire:model="assign_new_device_id" label="Auto-Joined Devices should mirror">
|
||||
<flux:select.option value="">None</flux:select.option>
|
||||
@foreach(auth()->user()->devices->where('mirror_device_id', null) as $device)
|
||||
|
|
|
|||
|
|
@ -388,45 +388,6 @@ it('does not set icon_url when importing from URL without iconUrl parameter', fu
|
|||
->and($plugin->icon_url)->toBeNull();
|
||||
});
|
||||
|
||||
it('normalizes non-named select options to named values', function (): void {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$settingsYaml = <<<'YAML'
|
||||
name: Test Plugin
|
||||
refresh_interval: 30
|
||||
strategy: static
|
||||
polling_verb: get
|
||||
static_data: '{}'
|
||||
custom_fields:
|
||||
- keyname: display_incident
|
||||
field_type: select
|
||||
options:
|
||||
- true
|
||||
- false
|
||||
default: true
|
||||
YAML;
|
||||
|
||||
$zipContent = createMockZipFile([
|
||||
'src/settings.yml' => $settingsYaml,
|
||||
'src/full.liquid' => getValidFullLiquid(),
|
||||
]);
|
||||
|
||||
$zipFile = UploadedFile::fake()->createWithContent('test-plugin.zip', $zipContent);
|
||||
|
||||
$pluginImportService = new PluginImportService();
|
||||
$plugin = $pluginImportService->importFromZip($zipFile, $user);
|
||||
|
||||
$customFields = $plugin->configuration_template['custom_fields'];
|
||||
$displayIncidentField = collect($customFields)->firstWhere('keyname', 'display_incident');
|
||||
|
||||
expect($displayIncidentField)->not->toBeNull()
|
||||
->and($displayIncidentField['options'])->toBe([
|
||||
['true' => 'true'],
|
||||
['false' => 'false'],
|
||||
])
|
||||
->and($displayIncidentField['default'])->toBe('true');
|
||||
});
|
||||
|
||||
// Helper methods
|
||||
function createMockZipFile(array $files): string
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Plugin;
|
||||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
uses(Illuminate\Foundation\Testing\RefreshDatabase::class);
|
||||
|
|
@ -589,93 +587,3 @@ LIQUID
|
|||
return str_contains($command, 'trmnl-liquid-cli');
|
||||
});
|
||||
});
|
||||
|
||||
test('plugin render uses user timezone when set', function (): void {
|
||||
$user = User::factory()->create([
|
||||
'timezone' => 'America/New_York',
|
||||
]);
|
||||
|
||||
$plugin = Plugin::factory()->create([
|
||||
'user_id' => $user->id,
|
||||
'markup_language' => 'liquid',
|
||||
'render_markup' => '{{ trmnl.user.time_zone_iana }}',
|
||||
]);
|
||||
|
||||
$rendered = $plugin->render();
|
||||
|
||||
expect($rendered)->toContain('America/New_York');
|
||||
});
|
||||
|
||||
test('plugin render falls back to app timezone when user timezone is not set', function (): void {
|
||||
$user = User::factory()->create([
|
||||
'timezone' => null,
|
||||
]);
|
||||
|
||||
config(['app.timezone' => 'Europe/London']);
|
||||
|
||||
$plugin = Plugin::factory()->create([
|
||||
'user_id' => $user->id,
|
||||
'markup_language' => 'liquid',
|
||||
'render_markup' => '{{ trmnl.user.time_zone_iana }}',
|
||||
]);
|
||||
|
||||
$rendered = $plugin->render();
|
||||
|
||||
expect($rendered)->toContain('Europe/London');
|
||||
});
|
||||
|
||||
test('plugin render calculates correct UTC offset from user timezone', function (): void {
|
||||
$user = User::factory()->create([
|
||||
'timezone' => 'America/New_York', // UTC-5 (EST) or UTC-4 (EDT)
|
||||
]);
|
||||
|
||||
$plugin = Plugin::factory()->create([
|
||||
'user_id' => $user->id,
|
||||
'markup_language' => 'liquid',
|
||||
'render_markup' => '{{ trmnl.user.utc_offset }}',
|
||||
]);
|
||||
|
||||
$rendered = $plugin->render();
|
||||
|
||||
// America/New_York offset should be -18000 (EST) or -14400 (EDT) in seconds
|
||||
$expectedOffset = (string) Carbon::now('America/New_York')->getOffset();
|
||||
expect($rendered)->toContain($expectedOffset);
|
||||
});
|
||||
|
||||
test('plugin render calculates correct UTC offset from app timezone when user timezone is null', function (): void {
|
||||
$user = User::factory()->create([
|
||||
'timezone' => null,
|
||||
]);
|
||||
|
||||
config(['app.timezone' => 'Europe/London']);
|
||||
|
||||
$plugin = Plugin::factory()->create([
|
||||
'user_id' => $user->id,
|
||||
'markup_language' => 'liquid',
|
||||
'render_markup' => '{{ trmnl.user.utc_offset }}',
|
||||
]);
|
||||
|
||||
$rendered = $plugin->render();
|
||||
|
||||
// Europe/London offset should be 0 (GMT) or 3600 (BST) in seconds
|
||||
$expectedOffset = (string) Carbon::now('Europe/London')->getOffset();
|
||||
expect($rendered)->toContain($expectedOffset);
|
||||
});
|
||||
|
||||
test('plugin render includes utc_offset and time_zone_iana in trmnl.user context', function (): void {
|
||||
$user = User::factory()->create([
|
||||
'timezone' => 'America/Chicago', // UTC-6 (CST) or UTC-5 (CDT)
|
||||
]);
|
||||
|
||||
$plugin = Plugin::factory()->create([
|
||||
'user_id' => $user->id,
|
||||
'markup_language' => 'liquid',
|
||||
'render_markup' => '{{ trmnl.user.time_zone_iana }}|{{ trmnl.user.utc_offset }}',
|
||||
]);
|
||||
|
||||
$rendered = $plugin->render();
|
||||
|
||||
expect($rendered)
|
||||
->toContain('America/Chicago')
|
||||
->and($rendered)->toMatch('/\|-?\d+/'); // Should contain a pipe followed by a number (offset in seconds)
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue