mirror of
https://github.com/usetrmnl/byos_laravel.git
synced 2026-01-13 15:07:49 +00:00
Compare commits
5 commits
6d02415b7d
...
94d5fca879
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94d5fca879 | ||
|
|
dc676327c2 | ||
|
|
e3bb9ad4e2 | ||
|
|
e176f2828e | ||
|
|
164a990dfe |
9 changed files with 1018 additions and 479 deletions
|
|
@ -11,7 +11,6 @@ use App\Liquid\Filters\StandardFilters;
|
||||||
use App\Liquid\Filters\StringMarkup;
|
use App\Liquid\Filters\StringMarkup;
|
||||||
use App\Liquid\Filters\Uniqueness;
|
use App\Liquid\Filters\Uniqueness;
|
||||||
use App\Liquid\Tags\TemplateTag;
|
use App\Liquid\Tags\TemplateTag;
|
||||||
use App\Services\ImageGenerationService;
|
|
||||||
use App\Services\Plugin\Parsers\ResponseParserRegistry;
|
use App\Services\Plugin\Parsers\ResponseParserRegistry;
|
||||||
use App\Services\PluginImportService;
|
use App\Services\PluginImportService;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
|
|
@ -25,6 +24,7 @@ use Illuminate\Support\Facades\Http;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
use Illuminate\Support\Facades\Process;
|
use Illuminate\Support\Facades\Process;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
use InvalidArgumentException;
|
||||||
use Keepsuit\LaravelLiquid\LaravelLiquidExtension;
|
use Keepsuit\LaravelLiquid\LaravelLiquidExtension;
|
||||||
use Keepsuit\Liquid\Exceptions\LiquidException;
|
use Keepsuit\Liquid\Exceptions\LiquidException;
|
||||||
use Keepsuit\Liquid\Extensions\StandardExtension;
|
use Keepsuit\Liquid\Extensions\StandardExtension;
|
||||||
|
|
@ -455,7 +455,7 @@ class Plugin extends Model
|
||||||
public function render(string $size = 'full', bool $standalone = true, ?Device $device = null): string
|
public function render(string $size = 'full', bool $standalone = true, ?Device $device = null): string
|
||||||
{
|
{
|
||||||
if ($this->plugin_type !== 'recipe') {
|
if ($this->plugin_type !== 'recipe') {
|
||||||
throw new \InvalidArgumentException('Render method is only applicable for recipe plugins.');
|
throw new InvalidArgumentException('Render method is only applicable for recipe plugins.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->render_markup) {
|
if ($this->render_markup) {
|
||||||
|
|
@ -565,17 +565,30 @@ class Plugin extends Model
|
||||||
|
|
||||||
if ($this->render_markup_view) {
|
if ($this->render_markup_view) {
|
||||||
if ($standalone) {
|
if ($standalone) {
|
||||||
return view('trmnl-layouts.single', [
|
$renderedView = view($this->render_markup_view, [
|
||||||
|
'size' => $size,
|
||||||
|
'data' => $this->data_payload,
|
||||||
|
'config' => $this->configuration ?? [],
|
||||||
|
])->render();
|
||||||
|
|
||||||
|
if ($size === 'full') {
|
||||||
|
return view('trmnl-layouts.single', [
|
||||||
|
'colorDepth' => $device?->colorDepth(),
|
||||||
|
'deviceVariant' => $device?->deviceVariant() ?? 'og',
|
||||||
|
'noBleed' => $this->no_bleed,
|
||||||
|
'darkMode' => $this->dark_mode,
|
||||||
|
'scaleLevel' => $device?->scaleLevel(),
|
||||||
|
'slot' => $renderedView,
|
||||||
|
])->render();
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('trmnl-layouts.mashup', [
|
||||||
|
'mashupLayout' => $this->getPreviewMashupLayoutForSize($size),
|
||||||
'colorDepth' => $device?->colorDepth(),
|
'colorDepth' => $device?->colorDepth(),
|
||||||
'deviceVariant' => $device?->deviceVariant() ?? 'og',
|
'deviceVariant' => $device?->deviceVariant() ?? 'og',
|
||||||
'noBleed' => $this->no_bleed,
|
|
||||||
'darkMode' => $this->dark_mode,
|
'darkMode' => $this->dark_mode,
|
||||||
'scaleLevel' => $device?->scaleLevel(),
|
'scaleLevel' => $device?->scaleLevel(),
|
||||||
'slot' => view($this->render_markup_view, [
|
'slot' => $renderedView,
|
||||||
'size' => $size,
|
|
||||||
'data' => $this->data_payload,
|
|
||||||
'config' => $this->configuration ?? [],
|
|
||||||
])->render(),
|
|
||||||
])->render();
|
])->render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -606,4 +619,61 @@ class Plugin extends Model
|
||||||
default => '1Tx1B',
|
default => '1Tx1B',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Duplicate the plugin, copying all attributes and handling render_markup_view
|
||||||
|
*
|
||||||
|
* @param int|null $userId Optional user ID for the duplicate. If not provided, uses the original plugin's user_id.
|
||||||
|
* @return Plugin The newly created duplicate plugin
|
||||||
|
*/
|
||||||
|
public function duplicate(?int $userId = null): self
|
||||||
|
{
|
||||||
|
// Get all attributes except id and uuid
|
||||||
|
// Use toArray() to get cast values (respects JSON casts)
|
||||||
|
$attributes = $this->toArray();
|
||||||
|
unset($attributes['id'], $attributes['uuid']);
|
||||||
|
|
||||||
|
// Handle render_markup_view - copy file content to render_markup
|
||||||
|
if ($this->render_markup_view) {
|
||||||
|
try {
|
||||||
|
$basePath = resource_path('views/'.str_replace('.', '/', $this->render_markup_view));
|
||||||
|
$paths = [
|
||||||
|
$basePath.'.blade.php',
|
||||||
|
$basePath.'.liquid',
|
||||||
|
];
|
||||||
|
|
||||||
|
$fileContent = null;
|
||||||
|
$markupLanguage = null;
|
||||||
|
foreach ($paths as $path) {
|
||||||
|
if (file_exists($path)) {
|
||||||
|
$fileContent = file_get_contents($path);
|
||||||
|
// Determine markup language based on file extension
|
||||||
|
$markupLanguage = str_ends_with($path, '.liquid') ? 'liquid' : 'blade';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($fileContent !== null) {
|
||||||
|
$attributes['render_markup'] = $fileContent;
|
||||||
|
$attributes['markup_language'] = $markupLanguage;
|
||||||
|
$attributes['render_markup_view'] = null;
|
||||||
|
} else {
|
||||||
|
// File doesn't exist, remove the view reference
|
||||||
|
$attributes['render_markup_view'] = null;
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// If file reading fails, remove the view reference
|
||||||
|
$attributes['render_markup_view'] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append " (Copy)" to the name
|
||||||
|
$attributes['name'] = $this->name.' (Copy)';
|
||||||
|
|
||||||
|
// Set user_id - use provided userId or fall back to original plugin's user_id
|
||||||
|
$attributes['user_id'] = $userId ?? $this->user_id;
|
||||||
|
|
||||||
|
// Create and return the new plugin
|
||||||
|
return self::create($attributes);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,32 @@ use ZipArchive;
|
||||||
|
|
||||||
class PluginImportService
|
class PluginImportService
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* Validate YAML settings
|
||||||
|
*
|
||||||
|
* @param array $settings The parsed YAML settings
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
private function validateYAML(array $settings): void
|
||||||
|
{
|
||||||
|
if (!isset($settings['custom_fields']) || !is_array($settings['custom_fields'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($settings['custom_fields'] as $field) {
|
||||||
|
if (isset($field['field_type']) && $field['field_type'] === 'multi_string') {
|
||||||
|
|
||||||
|
if (isset($field['default']) && str_contains($field['default'], ',')) {
|
||||||
|
throw new Exception("Validation Error: The default value for multistring fields like `{$field['keyname']}` cannot contain commas.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($field['placeholder']) && str_contains($field['placeholder'], ',')) {
|
||||||
|
throw new Exception("Validation Error: The placeholder value for multistring fields like `{$field['keyname']}` cannot contain commas.");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Import a plugin from a ZIP file
|
* Import a plugin from a ZIP file
|
||||||
*
|
*
|
||||||
|
|
@ -58,6 +84,7 @@ class PluginImportService
|
||||||
// Parse settings.yml
|
// Parse settings.yml
|
||||||
$settingsYaml = File::get($filePaths['settingsYamlPath']);
|
$settingsYaml = File::get($filePaths['settingsYamlPath']);
|
||||||
$settings = Yaml::parse($settingsYaml);
|
$settings = Yaml::parse($settingsYaml);
|
||||||
|
$this->validateYAML($settings);
|
||||||
|
|
||||||
// Read full.liquid content
|
// Read full.liquid content
|
||||||
$fullLiquid = File::get($filePaths['fullLiquidPath']);
|
$fullLiquid = File::get($filePaths['fullLiquidPath']);
|
||||||
|
|
@ -144,11 +171,12 @@ class PluginImportService
|
||||||
* @param string|null $zipEntryPath Optional path to specific plugin in monorepo
|
* @param string|null $zipEntryPath Optional path to specific plugin in monorepo
|
||||||
* @param string|null $preferredRenderer Optional preferred renderer (e.g., 'trmnl-liquid')
|
* @param string|null $preferredRenderer Optional preferred renderer (e.g., 'trmnl-liquid')
|
||||||
* @param string|null $iconUrl Optional icon URL to set on the plugin
|
* @param string|null $iconUrl Optional icon URL to set on the plugin
|
||||||
|
* @param bool $allowDuplicate If true, generate a new UUID for trmnlp_id if a plugin with the same trmnlp_id already exists
|
||||||
* @return Plugin The created plugin instance
|
* @return Plugin The created plugin instance
|
||||||
*
|
*
|
||||||
* @throws Exception If the ZIP file is invalid or required files are missing
|
* @throws Exception If the ZIP file is invalid or required files are missing
|
||||||
*/
|
*/
|
||||||
public function importFromUrl(string $zipUrl, User $user, ?string $zipEntryPath = null, $preferredRenderer = null, ?string $iconUrl = null): Plugin
|
public function importFromUrl(string $zipUrl, User $user, ?string $zipEntryPath = null, $preferredRenderer = null, ?string $iconUrl = null, bool $allowDuplicate = false): Plugin
|
||||||
{
|
{
|
||||||
// Download the ZIP file
|
// Download the ZIP file
|
||||||
$response = Http::timeout(60)->get($zipUrl);
|
$response = Http::timeout(60)->get($zipUrl);
|
||||||
|
|
@ -187,6 +215,7 @@ class PluginImportService
|
||||||
// Parse settings.yml
|
// Parse settings.yml
|
||||||
$settingsYaml = File::get($filePaths['settingsYamlPath']);
|
$settingsYaml = File::get($filePaths['settingsYamlPath']);
|
||||||
$settings = Yaml::parse($settingsYaml);
|
$settings = Yaml::parse($settingsYaml);
|
||||||
|
$this->validateYAML($settings);
|
||||||
|
|
||||||
// Read full.liquid content
|
// Read full.liquid content
|
||||||
$fullLiquid = File::get($filePaths['fullLiquidPath']);
|
$fullLiquid = File::get($filePaths['fullLiquidPath']);
|
||||||
|
|
@ -217,17 +246,26 @@ class PluginImportService
|
||||||
'custom_fields' => $settings['custom_fields'],
|
'custom_fields' => $settings['custom_fields'],
|
||||||
];
|
];
|
||||||
|
|
||||||
$plugin_updated = isset($settings['id'])
|
// Determine the trmnlp_id to use
|
||||||
|
$trmnlpId = $settings['id'] ?? Uuid::v7();
|
||||||
|
|
||||||
|
// If allowDuplicate is true and a plugin with this trmnlp_id already exists, generate a new UUID
|
||||||
|
if ($allowDuplicate && isset($settings['id']) && Plugin::where('user_id', $user->id)->where('trmnlp_id', $settings['id'])->exists()) {
|
||||||
|
$trmnlpId = Uuid::v7();
|
||||||
|
}
|
||||||
|
|
||||||
|
$plugin_updated = ! $allowDuplicate && isset($settings['id'])
|
||||||
&& Plugin::where('user_id', $user->id)->where('trmnlp_id', $settings['id'])->exists();
|
&& Plugin::where('user_id', $user->id)->where('trmnlp_id', $settings['id'])->exists();
|
||||||
|
|
||||||
// Create a new plugin
|
// Create a new plugin
|
||||||
$plugin = Plugin::updateOrCreate(
|
$plugin = Plugin::updateOrCreate(
|
||||||
[
|
[
|
||||||
'user_id' => $user->id, 'trmnlp_id' => $settings['id'] ?? Uuid::v7(),
|
'user_id' => $user->id, 'trmnlp_id' => $trmnlpId,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'user_id' => $user->id,
|
'user_id' => $user->id,
|
||||||
'name' => $settings['name'] ?? 'Imported Plugin',
|
'name' => $settings['name'] ?? 'Imported Plugin',
|
||||||
'trmnlp_id' => $settings['id'] ?? Uuid::v7(),
|
'trmnlp_id' => $trmnlpId,
|
||||||
'data_stale_minutes' => $settings['refresh_interval'] ?? 15,
|
'data_stale_minutes' => $settings['refresh_interval'] ?? 15,
|
||||||
'data_strategy' => $settings['strategy'] ?? 'static',
|
'data_strategy' => $settings['strategy'] ?? 'static',
|
||||||
'polling_url' => $settings['polling_url'] ?? null,
|
'polling_url' => $settings['polling_url'] ?? null,
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,8 @@ class extends Component
|
||||||
auth()->user(),
|
auth()->user(),
|
||||||
$plugin['zip_entry_path'] ?? null,
|
$plugin['zip_entry_path'] ?? null,
|
||||||
null,
|
null,
|
||||||
$plugin['logo_url'] ?? null
|
$plugin['logo_url'] ?? null,
|
||||||
|
allowDuplicate: true
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->dispatch('plugin-installed');
|
$this->dispatch('plugin-installed');
|
||||||
|
|
|
||||||
|
|
@ -164,7 +164,8 @@ class extends Component
|
||||||
auth()->user(),
|
auth()->user(),
|
||||||
null,
|
null,
|
||||||
config('services.trmnl.liquid_enabled') ? 'trmnl-liquid' : null,
|
config('services.trmnl.liquid_enabled') ? 'trmnl-liquid' : null,
|
||||||
$recipe['icon_url'] ?? null
|
$recipe['icon_url'] ?? null,
|
||||||
|
allowDuplicate: true
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->dispatch('plugin-installed');
|
$this->dispatch('plugin-installed');
|
||||||
|
|
|
||||||
516
resources/views/livewire/plugins/config-modal.blade.php
Normal file
516
resources/views/livewire/plugins/config-modal.blade.php
Normal file
|
|
@ -0,0 +1,516 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Plugin;
|
||||||
|
use Livewire\Volt\Component;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This component contains the configuation modal
|
||||||
|
*/
|
||||||
|
new class extends Component {
|
||||||
|
public Plugin $plugin;
|
||||||
|
public array $configuration_template = [];
|
||||||
|
public array $configuration = []; // holds config data
|
||||||
|
|
||||||
|
public array $multiValues = []; // UI boxes for multi_string
|
||||||
|
public array $xhrSelectOptions = [];
|
||||||
|
public array $searchQueries = [];
|
||||||
|
|
||||||
|
// ------------------------------------This section contains one-off functions for the form------------------------------------------------
|
||||||
|
public function mount(): void
|
||||||
|
{
|
||||||
|
$this -> loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function loadData(): void
|
||||||
|
{
|
||||||
|
$this->resetErrorBag();
|
||||||
|
// Reload data
|
||||||
|
$this->plugin = $this->plugin->fresh();
|
||||||
|
|
||||||
|
$this->configuration_template = $this->plugin->configuration_template ?? [];
|
||||||
|
$this->configuration = is_array($this->plugin->configuration) ? $this->plugin->configuration : [];
|
||||||
|
|
||||||
|
// Initialize multiValues by exploding the CSV strings from the DB
|
||||||
|
foreach ($this->configuration_template['custom_fields'] ?? [] as $field) {
|
||||||
|
if (($field['field_type'] ?? null) === 'multi_string') {
|
||||||
|
$fieldKey = $field['keyname'];
|
||||||
|
$rawValue = $this->configuration[$fieldKey] ?? ($field['default'] ?? '');
|
||||||
|
|
||||||
|
$currentValue = is_array($rawValue) ? '' : (string)$rawValue;
|
||||||
|
|
||||||
|
$this->multiValues[$fieldKey] = $currentValue !== ''
|
||||||
|
? array_values(array_filter(explode(',', $currentValue)))
|
||||||
|
: [''];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggered by @close on the modal to discard any typed but unsaved changes
|
||||||
|
*/
|
||||||
|
public int $resetIndex = 0; // Add this property
|
||||||
|
public function resetForm(): void
|
||||||
|
{
|
||||||
|
$this->loadData();
|
||||||
|
$this->resetIndex++; // Increment to force DOM refresh
|
||||||
|
}
|
||||||
|
|
||||||
|
public function saveConfiguration()
|
||||||
|
{
|
||||||
|
abort_unless(auth()->user()->plugins->contains($this->plugin), 403);
|
||||||
|
|
||||||
|
// final validation layer
|
||||||
|
$this->validate([
|
||||||
|
'multiValues.*.*' => ['nullable', 'string', 'regex:/^[^,]*$/'],
|
||||||
|
], [
|
||||||
|
'multiValues.*.*.regex' => 'Items cannot contain commas.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Prepare config copy to send to db
|
||||||
|
$finalValues = $this->configuration;
|
||||||
|
foreach ($this->configuration_template['custom_fields'] ?? [] as $field) {
|
||||||
|
$fieldKey = $field['keyname'];
|
||||||
|
|
||||||
|
// Handle multi_string: Join array back to CSV string
|
||||||
|
if ($field['field_type'] === 'multi_string' && isset($this->multiValues[$fieldKey])) {
|
||||||
|
$finalValues[$fieldKey] = implode(',', array_filter(array_map('trim', $this->multiValues[$fieldKey])));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle code fields: Try to JSON decode if necessary (standard TRMNL behavior)
|
||||||
|
if ($field['field_type'] === 'code' && isset($finalValues[$fieldKey]) && is_string($finalValues[$fieldKey])) {
|
||||||
|
$decoded = json_decode($finalValues[$fieldKey], true);
|
||||||
|
if (json_last_error() === JSON_ERROR_NONE && (is_array($decoded) || is_object($decoded))) {
|
||||||
|
$finalValues[$fieldKey] = $decoded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// send to db
|
||||||
|
$this->plugin->update(['configuration' => $finalValues]);
|
||||||
|
$this->configuration = $finalValues; // update local state
|
||||||
|
$this->dispatch('config-updated'); // notifies listeners
|
||||||
|
Flux::modal('configuration-modal')->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------This section contains helper functions for interacting with the form------------------------------------------------
|
||||||
|
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][] = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Livewire magic method to validate MultiValue input boxes
|
||||||
|
// Runs on every debounce
|
||||||
|
public function updatedMultiValues($value, $key)
|
||||||
|
{
|
||||||
|
$this->validate([
|
||||||
|
'multiValues.*.*' => ['nullable', 'string', 'regex:/^[^,]*$/'],
|
||||||
|
], [
|
||||||
|
'multiValues.*.*.regex' => 'Items cannot contain commas.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function loadXhrSelectOptions(string $fieldKey, string $endpoint, ?string $query = null): void
|
||||||
|
{
|
||||||
|
abort_unless(auth()->user()->plugins->contains($this->plugin), 403);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$requestData = [];
|
||||||
|
if ($query !== null) {
|
||||||
|
$requestData = [
|
||||||
|
'function' => $fieldKey,
|
||||||
|
'query' => $query
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = $query !== null
|
||||||
|
? Http::post($endpoint, $requestData)
|
||||||
|
: Http::post($endpoint);
|
||||||
|
|
||||||
|
if ($response->successful()) {
|
||||||
|
$this->xhrSelectOptions[$fieldKey] = $response->json();
|
||||||
|
} else {
|
||||||
|
$this->xhrSelectOptions[$fieldKey] = [];
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->xhrSelectOptions[$fieldKey] = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function searchXhrSelect(string $fieldKey, string $endpoint): void
|
||||||
|
{
|
||||||
|
$query = $this->searchQueries[$fieldKey] ?? '';
|
||||||
|
if (!empty($query)) {
|
||||||
|
$this->loadXhrSelectOptions($fieldKey, $endpoint, $query);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};?>
|
||||||
|
|
||||||
|
<flux:modal name="configuration-modal" @close="resetForm" class="md:w-96">
|
||||||
|
<div wire:key="config-form-{{ $resetIndex }}" class="space-y-6">
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div>
|
||||||
|
<flux:heading size="lg">Configuration</flux:heading>
|
||||||
|
<flux:subheading>Configure your plugin settings</flux:subheading>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form wire:submit="saveConfiguration">
|
||||||
|
@if(isset($configuration_template['custom_fields']) && is_array($configuration_template['custom_fields']))
|
||||||
|
@foreach($configuration_template['custom_fields'] as $field)
|
||||||
|
@php
|
||||||
|
$fieldKey = $field['keyname'] ?? $field['key'] ?? $field['name'];
|
||||||
|
$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
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
@endphp
|
||||||
|
<div class="mb-4">
|
||||||
|
@if($field['field_type'] === 'author_bio')
|
||||||
|
@continue
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if($field['field_type'] === 'copyable_webhook_url')
|
||||||
|
@continue
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if($field['field_type'] === 'string' || $field['field_type'] === 'url')
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ $field['name'] }}</flux:label>
|
||||||
|
<flux:description>{!! $safeDescription !!}</flux:description>
|
||||||
|
<flux:input
|
||||||
|
wire:model="configuration.{{ $fieldKey }}"
|
||||||
|
value="{{ $currentValue }}"
|
||||||
|
/>
|
||||||
|
<flux:description>{!! $safeHelp !!}</flux:description>
|
||||||
|
</flux:field>
|
||||||
|
|
||||||
|
@elseif($field['field_type'] === 'text')
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ $field['name'] }}</flux:label>
|
||||||
|
<flux:description>{!! $safeDescription !!}</flux:description>
|
||||||
|
<flux:textarea
|
||||||
|
wire:model="configuration.{{ $fieldKey }}"
|
||||||
|
value="{{ $currentValue }}"
|
||||||
|
/>
|
||||||
|
<flux:description>{!! $safeHelp !!}</flux:description>
|
||||||
|
</flux:field>
|
||||||
|
|
||||||
|
@elseif($field['field_type'] === 'code')
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ $field['name'] }}</flux:label>
|
||||||
|
<flux:description>{!! $safeDescription !!}</flux:description>
|
||||||
|
<flux:textarea
|
||||||
|
rows="{{ $field['rows'] ?? 3 }}"
|
||||||
|
placeholder="{{ $field['placeholder'] ?? null }}"
|
||||||
|
wire:model="configuration.{{ $fieldKey }}"
|
||||||
|
value="{{ $currentValue }}"
|
||||||
|
class="font-mono"
|
||||||
|
/>
|
||||||
|
<flux:description>{!! $safeHelp !!}</flux:description>
|
||||||
|
</flux:field>
|
||||||
|
|
||||||
|
@elseif($field['field_type'] === 'password')
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ $field['name'] }}</flux:label>
|
||||||
|
<flux:description>{!! $safeDescription !!}</flux:description>
|
||||||
|
<flux:input
|
||||||
|
type="password"
|
||||||
|
wire:model="local_configuration.{{ $fieldKey }}"
|
||||||
|
value="{{ $currentValue }}"
|
||||||
|
viewable
|
||||||
|
/>
|
||||||
|
<flux:description>{!! $safeHelp !!}</flux:description>
|
||||||
|
</flux:field>
|
||||||
|
|
||||||
|
@elseif($field['field_type'] === 'copyable')
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ $field['name'] }}</flux:label>
|
||||||
|
<flux:description>{!! $safeDescription !!}</flux:description>
|
||||||
|
<flux:input
|
||||||
|
value="{{ $field['value'] }}"
|
||||||
|
copyable
|
||||||
|
/>
|
||||||
|
<flux:description>{!! $safeHelp !!}</flux:description>
|
||||||
|
</flux:field>
|
||||||
|
|
||||||
|
@elseif($field['field_type'] === 'time_zone')
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ $field['name'] }}</flux:label>
|
||||||
|
<flux:description>{!! $safeDescription !!}</flux:description>
|
||||||
|
<flux:select
|
||||||
|
wire:model="configuration.{{ $fieldKey }}"
|
||||||
|
value="{{ $field['value'] }}"
|
||||||
|
>
|
||||||
|
<option value="">Select timezone...</option>
|
||||||
|
@foreach(timezone_identifiers_list() as $timezone)
|
||||||
|
<option value="{{ $timezone }}" {{ $currentValue === $timezone ? 'selected' : '' }}>{{ $timezone }}</option>
|
||||||
|
@endforeach
|
||||||
|
</flux:select>
|
||||||
|
<flux:description>{!! $safeHelp !!}</flux:description>
|
||||||
|
</flux:field>
|
||||||
|
|
||||||
|
@elseif($field['field_type'] === 'number')
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ $field['name'] }}</flux:label>
|
||||||
|
<flux:description>{!! $safeDescription !!}</flux:description>
|
||||||
|
<flux:input
|
||||||
|
type="number"
|
||||||
|
wire:model="configuration.{{ $fieldKey }}"
|
||||||
|
value="{{ $currentValue }}"
|
||||||
|
/>
|
||||||
|
<flux:description>{!! $safeHelp !!}</flux:description>
|
||||||
|
</flux:field>
|
||||||
|
|
||||||
|
@elseif($field['field_type'] === 'boolean')
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ $field['name'] }}</flux:label>
|
||||||
|
<flux:description>{!! $safeDescription !!}</flux:description>
|
||||||
|
<flux:checkbox
|
||||||
|
wire:model="configuration.{{ $fieldKey }}"
|
||||||
|
:checked="$currentValue"
|
||||||
|
/>
|
||||||
|
<flux:description>{!! $safeHelp !!}</flux:description>
|
||||||
|
</flux:field>
|
||||||
|
|
||||||
|
@elseif($field['field_type'] === 'date')
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ $field['name'] }}</flux:label>
|
||||||
|
<flux:description>{!! $safeDescription !!}</flux:description>
|
||||||
|
<flux:input
|
||||||
|
type="date"
|
||||||
|
wire:model="configuration.{{ $fieldKey }}"
|
||||||
|
value="{{ $currentValue }}"
|
||||||
|
/>
|
||||||
|
<flux:description>{!! $safeHelp !!}</flux:description>
|
||||||
|
</flux:field>
|
||||||
|
|
||||||
|
@elseif($field['field_type'] === 'time')
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ $field['name'] }}</flux:label>
|
||||||
|
<flux:description>{!! $safeDescription !!}</flux:description>
|
||||||
|
<flux:input
|
||||||
|
type="time"
|
||||||
|
wire:model="configuration.{{ $fieldKey }}"
|
||||||
|
value="{{ $currentValue }}"
|
||||||
|
/>
|
||||||
|
<flux:description>{!! $safeHelp !!}</flux:description>
|
||||||
|
</flux:field>
|
||||||
|
|
||||||
|
@elseif($field['field_type'] === 'select')
|
||||||
|
@if(isset($field['multiple']) && $field['multiple'] === true)
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ $field['name'] }}</flux:label>
|
||||||
|
<flux:description>{!! $safeDescription !!}</flux:description>
|
||||||
|
<flux:checkbox.group wire:model="configuration.{{ $fieldKey }}">
|
||||||
|
@if(isset($field['options']) && is_array($field['options']))
|
||||||
|
@foreach($field['options'] as $option)
|
||||||
|
@if(is_array($option))
|
||||||
|
@foreach($option as $label => $value)
|
||||||
|
<flux:checkbox label="{{ $label }}" value="{{ $value }}"/>
|
||||||
|
@endforeach
|
||||||
|
@else
|
||||||
|
@php
|
||||||
|
$key = mb_strtolower(str_replace(' ', '_', $option));
|
||||||
|
@endphp
|
||||||
|
<flux:checkbox label="{{ $option }}" value="{{ $key }}"/>
|
||||||
|
@endif
|
||||||
|
@endforeach
|
||||||
|
@endif
|
||||||
|
</flux:checkbox.group>
|
||||||
|
<flux:description>{!! $safeHelp !!}</flux:description>
|
||||||
|
</flux:field>
|
||||||
|
@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
|
||||||
|
wire:model="configuration.{{ $fieldKey }}"
|
||||||
|
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>
|
||||||
|
<flux:description>{!! $safeHelp !!}</flux:description>
|
||||||
|
</flux:field>
|
||||||
|
|
||||||
|
@elseif($field['field_type'] === 'xhrSelectSearch')
|
||||||
|
<div class="space-y-2">
|
||||||
|
|
||||||
|
<flux:label>{{ $field['name'] }}</flux:label>
|
||||||
|
<flux:description>{!! $safeDescription !!}</flux:description>
|
||||||
|
<flux:input.group>
|
||||||
|
<flux:input
|
||||||
|
wire:model="searchQueries.{{ $fieldKey }}"
|
||||||
|
placeholder="Enter search query..."
|
||||||
|
/>
|
||||||
|
<flux:button
|
||||||
|
wire:click="searchXhrSelect('{{ $fieldKey }}', '{{ $field['endpoint'] }}')"
|
||||||
|
icon="magnifying-glass"/>
|
||||||
|
</flux:input.group>
|
||||||
|
<flux:description>{!! $safeHelp !!}</flux:description>
|
||||||
|
@if((isset($xhrSelectOptions[$fieldKey]) && is_array($xhrSelectOptions[$fieldKey]) && count($xhrSelectOptions[$fieldKey]) > 0) || !empty($currentValue))
|
||||||
|
<flux:select
|
||||||
|
wire:model="configuration.{{ $fieldKey }}"
|
||||||
|
>
|
||||||
|
<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
|
||||||
|
@if(!empty($currentValue) && (!isset($xhrSelectOptions[$fieldKey]) || empty($xhrSelectOptions[$fieldKey])))
|
||||||
|
{{-- Show current value even if no options are loaded --}}
|
||||||
|
<option value="{{ $currentValue }}" selected>{{ $currentValue }}</option>
|
||||||
|
@endif
|
||||||
|
</flux:select>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@elseif($field['field_type'] === 'multi_string')
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ $field['name'] }}</flux:label>
|
||||||
|
<flux:description>{!! $safeDescription !!}</flux:description>
|
||||||
|
|
||||||
|
<div class="space-y-2 mt-2">
|
||||||
|
@foreach($multiValues[$fieldKey] as $index => $item)
|
||||||
|
<div class="flex gap-2 items-center"
|
||||||
|
wire:key="multi-{{ $fieldKey }}-{{ $index }}">
|
||||||
|
|
||||||
|
<flux:input
|
||||||
|
wire:model.live.debounce="multiValues.{{ $fieldKey }}.{{ $index }}"
|
||||||
|
:placeholder="$field['placeholder'] ?? 'Value...'"
|
||||||
|
:invalid="$errors->has('multiValues.'.$fieldKey.'.'.$index)"
|
||||||
|
class="flex-1"
|
||||||
|
/>
|
||||||
|
|
||||||
|
@if(count($multiValues[$fieldKey]) > 1)
|
||||||
|
<flux:button
|
||||||
|
variant="ghost"
|
||||||
|
icon="trash"
|
||||||
|
size="sm"
|
||||||
|
wire:click="removeMultiItem('{{ $fieldKey }}', {{ $index }})"
|
||||||
|
/>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@error("multiValues.{$fieldKey}.{$index}")
|
||||||
|
<div class="flex items-center gap-2 mt-1 text-amber-600">
|
||||||
|
<flux:icon name="exclamation-triangle" variant="micro" />
|
||||||
|
{{-- $message comes from thrown error --}}
|
||||||
|
<span class="text-xs font-medium">{{ $message }}</span>
|
||||||
|
</div>
|
||||||
|
@enderror
|
||||||
|
@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>
|
||||||
|
@else
|
||||||
|
<flux:callout variant="warning">Field type "{{ $field['field_type'] }}" not yet supported</flux:callout>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="flex-col space-y-2 items-end w-full">
|
||||||
|
<flux:spacer/>
|
||||||
|
<flux:button
|
||||||
|
type="submit"
|
||||||
|
variant="primary"
|
||||||
|
:disabled="$errors->any()"
|
||||||
|
class="disabled:opacity-50 disabled:cursor-not-allowed disabled:grayscale"
|
||||||
|
>
|
||||||
|
Save Configuration
|
||||||
|
</flux:button>
|
||||||
|
@if($errors->any())
|
||||||
|
<div class="flex items-center gap-2 text-amber-600">
|
||||||
|
<flux:icon name="exclamation-circle" variant="micro" />
|
||||||
|
<span class="text-sm font-medium">
|
||||||
|
Fix errors before saving.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</flux:modal>
|
||||||
|
|
@ -7,6 +7,7 @@ use Livewire\Volt\Component;
|
||||||
use Illuminate\Support\Facades\Blade;
|
use Illuminate\Support\Facades\Blade;
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
use Illuminate\Support\Facades\Http;
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Livewire\Attributes\On;
|
||||||
|
|
||||||
new class extends Component {
|
new class extends Component {
|
||||||
public Plugin $plugin;
|
public Plugin $plugin;
|
||||||
|
|
@ -34,17 +35,13 @@ new class extends Component {
|
||||||
public string $mashup_layout = 'full';
|
public string $mashup_layout = 'full';
|
||||||
public array $mashup_plugins = [];
|
public array $mashup_plugins = [];
|
||||||
public array $configuration_template = [];
|
public array $configuration_template = [];
|
||||||
public array $configuration = [];
|
|
||||||
public array $xhrSelectOptions = [];
|
|
||||||
public array $searchQueries = [];
|
|
||||||
public array $multiValues = [];
|
|
||||||
|
|
||||||
public function mount(): void
|
public function mount(): void
|
||||||
{
|
{
|
||||||
abort_unless(auth()->user()->plugins->contains($this->plugin), 403);
|
abort_unless(auth()->user()->plugins->contains($this->plugin), 403);
|
||||||
$this->blade_code = $this->plugin->render_markup;
|
$this->blade_code = $this->plugin->render_markup;
|
||||||
|
// required to render some stuff
|
||||||
$this->configuration_template = $this->plugin->configuration_template ?? [];
|
$this->configuration_template = $this->plugin->configuration_template ?? [];
|
||||||
$this->configuration = is_array($this->plugin->configuration) ? $this->plugin->configuration : [];
|
|
||||||
|
|
||||||
if ($this->plugin->render_markup_view) {
|
if ($this->plugin->render_markup_view) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -75,25 +72,6 @@ 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
|
||||||
|
|
@ -287,47 +265,6 @@ new class extends Component {
|
||||||
Flux::modal('add-to-playlist')->close();
|
Flux::modal('add-to-playlist')->close();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function saveConfiguration()
|
|
||||||
{
|
|
||||||
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 = [];
|
|
||||||
if (isset($this->configuration_template['custom_fields'])) {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->plugin->update([
|
|
||||||
'configuration' => $configurationValues
|
|
||||||
]);
|
|
||||||
|
|
||||||
Flux::modal('configuration-modal')->close();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getDevicePlaylists($deviceId)
|
public function getDevicePlaylists($deviceId)
|
||||||
{
|
{
|
||||||
return \App\Models\Playlist::where('device_id', $deviceId)->get();
|
return \App\Models\Playlist::where('device_id', $deviceId)->get();
|
||||||
|
|
@ -433,6 +370,17 @@ HTML;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function duplicatePlugin(): void
|
||||||
|
{
|
||||||
|
abort_unless(auth()->user()->plugins->contains($this->plugin), 403);
|
||||||
|
|
||||||
|
// Use the model's duplicate method
|
||||||
|
$newPlugin = $this->plugin->duplicate(auth()->id());
|
||||||
|
|
||||||
|
// Redirect to the new plugin's detail page
|
||||||
|
$this->redirect(route('plugins.recipe', ['plugin' => $newPlugin]));
|
||||||
|
}
|
||||||
|
|
||||||
public function deletePlugin(): void
|
public function deletePlugin(): void
|
||||||
{
|
{
|
||||||
abort_unless(auth()->user()->plugins->contains($this->plugin), 403);
|
abort_unless(auth()->user()->plugins->contains($this->plugin), 403);
|
||||||
|
|
@ -440,58 +388,15 @@ HTML;
|
||||||
$this->redirect(route('plugins.index'));
|
$this->redirect(route('plugins.index'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function loadXhrSelectOptions(string $fieldKey, string $endpoint, ?string $query = null): void
|
#[On('config-updated')]
|
||||||
{
|
public function refreshPlugin()
|
||||||
abort_unless(auth()->user()->plugins->contains($this->plugin), 403);
|
{
|
||||||
|
// This pulls the fresh 'configuration' from the DB
|
||||||
|
// and re-triggers the @if check in the Blade template
|
||||||
|
$this->plugin = $this->plugin->fresh();
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
$requestData = [];
|
|
||||||
if ($query !== null) {
|
|
||||||
$requestData = [
|
|
||||||
'function' => $fieldKey,
|
|
||||||
'query' => $query
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$response = $query !== null
|
|
||||||
? Http::post($endpoint, $requestData)
|
|
||||||
: Http::post($endpoint);
|
|
||||||
|
|
||||||
if ($response->successful()) {
|
|
||||||
$this->xhrSelectOptions[$fieldKey] = $response->json();
|
|
||||||
} else {
|
|
||||||
$this->xhrSelectOptions[$fieldKey] = [];
|
|
||||||
}
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$this->xhrSelectOptions[$fieldKey] = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function searchXhrSelect(string $fieldKey, string $endpoint): void
|
|
||||||
{
|
|
||||||
$query = $this->searchQueries[$fieldKey] ?? '';
|
|
||||||
if (!empty($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][] = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<div class="py-12">
|
<div class="py-12">
|
||||||
|
|
@ -533,6 +438,7 @@ HTML;
|
||||||
<flux:dropdown>
|
<flux:dropdown>
|
||||||
<flux:button icon="chevron-down" variant="primary"></flux:button>
|
<flux:button icon="chevron-down" variant="primary"></flux:button>
|
||||||
<flux:menu>
|
<flux:menu>
|
||||||
|
<flux:menu.item icon="document-duplicate" wire:click="duplicatePlugin">Duplicate Plugin</flux:menu.item>
|
||||||
<flux:modal.trigger name="delete-plugin">
|
<flux:modal.trigger name="delete-plugin">
|
||||||
<flux:menu.item icon="trash" variant="danger">Delete Plugin</flux:menu.item>
|
<flux:menu.item icon="trash" variant="danger">Delete Plugin</flux:menu.item>
|
||||||
</flux:modal.trigger>
|
</flux:modal.trigger>
|
||||||
|
|
@ -683,355 +589,7 @@ HTML;
|
||||||
</div>
|
</div>
|
||||||
</flux:modal>
|
</flux:modal>
|
||||||
|
|
||||||
<flux:modal name="configuration-modal" class="md:w-96">
|
<livewire:plugins.config-modal :plugin="$plugin" />
|
||||||
<div class="space-y-6">
|
|
||||||
<div>
|
|
||||||
<flux:heading size="lg">Configuration</flux:heading>
|
|
||||||
<flux:subheading>Configure your plugin settings</flux:subheading>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form wire:submit="saveConfiguration">
|
|
||||||
@if(isset($configuration_template['custom_fields']) && is_array($configuration_template['custom_fields']))
|
|
||||||
@foreach($configuration_template['custom_fields'] as $field)
|
|
||||||
@php
|
|
||||||
$fieldKey = $field['keyname'] ?? $field['key'] ?? $field['name'];
|
|
||||||
$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
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
@endphp
|
|
||||||
<div class="mb-4">
|
|
||||||
@if($field['field_type'] === 'author_bio')
|
|
||||||
@continue
|
|
||||||
@endif
|
|
||||||
|
|
||||||
@if($field['field_type'] === 'copyable_webhook_url')
|
|
||||||
@continue
|
|
||||||
@endif
|
|
||||||
|
|
||||||
@if($field['field_type'] === 'string' || $field['field_type'] === 'url')
|
|
||||||
<flux:field>
|
|
||||||
<flux:label>{{ $field['name'] }}</flux:label>
|
|
||||||
<flux:description>{!! $safeDescription !!}</flux:description>
|
|
||||||
<flux:input
|
|
||||||
wire:model="configuration.{{ $fieldKey }}"
|
|
||||||
value="{{ $currentValue }}"
|
|
||||||
/>
|
|
||||||
<flux:description>{!! $safeHelp !!}</flux:description>
|
|
||||||
</flux:field>
|
|
||||||
|
|
||||||
@elseif($field['field_type'] === 'text')
|
|
||||||
<flux:field>
|
|
||||||
<flux:label>{{ $field['name'] }}</flux:label>
|
|
||||||
<flux:description>{!! $safeDescription !!}</flux:description>
|
|
||||||
<flux:textarea
|
|
||||||
wire:model="configuration.{{ $fieldKey }}"
|
|
||||||
value="{{ $currentValue }}"
|
|
||||||
/>
|
|
||||||
<flux:description>{!! $safeHelp !!}</flux:description>
|
|
||||||
</flux:field>
|
|
||||||
|
|
||||||
@elseif($field['field_type'] === 'code')
|
|
||||||
<flux:field>
|
|
||||||
<flux:label>{{ $field['name'] }}</flux:label>
|
|
||||||
<flux:description>{!! $safeDescription !!}</flux:description>
|
|
||||||
<flux:textarea
|
|
||||||
rows="{{ $field['rows'] ?? 3 }}"
|
|
||||||
placeholder="{{ $field['placeholder'] ?? null }}"
|
|
||||||
wire:model="configuration.{{ $fieldKey }}"
|
|
||||||
value="{{ $currentValue }}"
|
|
||||||
class="font-mono"
|
|
||||||
/>
|
|
||||||
<flux:description>{!! $safeHelp !!}</flux:description>
|
|
||||||
</flux:field>
|
|
||||||
|
|
||||||
@elseif($field['field_type'] === 'password')
|
|
||||||
<flux:field>
|
|
||||||
<flux:label>{{ $field['name'] }}</flux:label>
|
|
||||||
<flux:description>{!! $safeDescription !!}</flux:description>
|
|
||||||
<flux:input
|
|
||||||
type="password"
|
|
||||||
wire:model="configuration.{{ $fieldKey }}"
|
|
||||||
value="{{ $currentValue }}"
|
|
||||||
viewable
|
|
||||||
/>
|
|
||||||
<flux:description>{!! $safeHelp !!}</flux:description>
|
|
||||||
</flux:field>
|
|
||||||
|
|
||||||
@elseif($field['field_type'] === 'copyable')
|
|
||||||
<flux:field>
|
|
||||||
<flux:label>{{ $field['name'] }}</flux:label>
|
|
||||||
<flux:description>{!! $safeDescription !!}</flux:description>
|
|
||||||
<flux:input
|
|
||||||
value="{{ $field['value'] }}"
|
|
||||||
copyable
|
|
||||||
/>
|
|
||||||
<flux:description>{!! $safeHelp !!}</flux:description>
|
|
||||||
</flux:field>
|
|
||||||
|
|
||||||
@elseif($field['field_type'] === 'time_zone')
|
|
||||||
<flux:field>
|
|
||||||
<flux:label>{{ $field['name'] }}</flux:label>
|
|
||||||
<flux:description>{!! $safeDescription !!}</flux:description>
|
|
||||||
<flux:select
|
|
||||||
wire:model="configuration.{{ $fieldKey }}"
|
|
||||||
value="{{ $field['value'] }}"
|
|
||||||
>
|
|
||||||
<option value="">Select timezone...</option>
|
|
||||||
@foreach(timezone_identifiers_list() as $timezone)
|
|
||||||
<option value="{{ $timezone }}" {{ $currentValue === $timezone ? 'selected' : '' }}>{{ $timezone }}</option>
|
|
||||||
@endforeach
|
|
||||||
</flux:select>
|
|
||||||
<flux:description>{!! $safeHelp !!}</flux:description>
|
|
||||||
</flux:field>
|
|
||||||
|
|
||||||
@elseif($field['field_type'] === 'number')
|
|
||||||
<flux:field>
|
|
||||||
<flux:label>{{ $field['name'] }}</flux:label>
|
|
||||||
<flux:description>{!! $safeDescription !!}</flux:description>
|
|
||||||
<flux:input
|
|
||||||
type="number"
|
|
||||||
wire:model="configuration.{{ $fieldKey }}"
|
|
||||||
value="{{ $currentValue }}"
|
|
||||||
/>
|
|
||||||
<flux:description>{!! $safeHelp !!}</flux:description>
|
|
||||||
</flux:field>
|
|
||||||
|
|
||||||
@elseif($field['field_type'] === 'boolean')
|
|
||||||
<flux:field>
|
|
||||||
<flux:label>{{ $field['name'] }}</flux:label>
|
|
||||||
<flux:description>{!! $safeDescription !!}</flux:description>
|
|
||||||
<flux:checkbox
|
|
||||||
wire:model="configuration.{{ $fieldKey }}"
|
|
||||||
:checked="$currentValue"
|
|
||||||
/>
|
|
||||||
<flux:description>{!! $safeHelp !!}</flux:description>
|
|
||||||
</flux:field>
|
|
||||||
|
|
||||||
@elseif($field['field_type'] === 'date')
|
|
||||||
<flux:field>
|
|
||||||
<flux:label>{{ $field['name'] }}</flux:label>
|
|
||||||
<flux:description>{!! $safeDescription !!}</flux:description>
|
|
||||||
<flux:input
|
|
||||||
type="date"
|
|
||||||
wire:model="configuration.{{ $fieldKey }}"
|
|
||||||
value="{{ $currentValue }}"
|
|
||||||
/>
|
|
||||||
<flux:description>{!! $safeHelp !!}</flux:description>
|
|
||||||
</flux:field>
|
|
||||||
|
|
||||||
@elseif($field['field_type'] === 'time')
|
|
||||||
<flux:field>
|
|
||||||
<flux:label>{{ $field['name'] }}</flux:label>
|
|
||||||
<flux:description>{!! $safeDescription !!}</flux:description>
|
|
||||||
<flux:input
|
|
||||||
type="time"
|
|
||||||
wire:model="configuration.{{ $fieldKey }}"
|
|
||||||
value="{{ $currentValue }}"
|
|
||||||
/>
|
|
||||||
<flux:description>{!! $safeHelp !!}</flux:description>
|
|
||||||
</flux:field>
|
|
||||||
|
|
||||||
@elseif($field['field_type'] === 'select')
|
|
||||||
@if(isset($field['multiple']) && $field['multiple'] === true)
|
|
||||||
<flux:field>
|
|
||||||
<flux:label>{{ $field['name'] }}</flux:label>
|
|
||||||
<flux:description>{!! $safeDescription !!}</flux:description>
|
|
||||||
<flux:checkbox.group wire:model="configuration.{{ $fieldKey }}">
|
|
||||||
@if(isset($field['options']) && is_array($field['options']))
|
|
||||||
@foreach($field['options'] as $option)
|
|
||||||
@if(is_array($option))
|
|
||||||
@foreach($option as $label => $value)
|
|
||||||
<flux:checkbox label="{{ $label }}" value="{{ $value }}"/>
|
|
||||||
@endforeach
|
|
||||||
@else
|
|
||||||
@php
|
|
||||||
$key = mb_strtolower(str_replace(' ', '_', $option));
|
|
||||||
@endphp
|
|
||||||
<flux:checkbox label="{{ $option }}" value="{{ $key }}"/>
|
|
||||||
@endif
|
|
||||||
@endforeach
|
|
||||||
@endif
|
|
||||||
</flux:checkbox.group>
|
|
||||||
<flux:description>{!! $safeHelp !!}</flux:description>
|
|
||||||
</flux:field>
|
|
||||||
@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
|
|
||||||
wire:model="configuration.{{ $fieldKey }}"
|
|
||||||
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>
|
|
||||||
<flux:description>{!! $safeHelp !!}</flux:description>
|
|
||||||
</flux:field>
|
|
||||||
|
|
||||||
@elseif($field['field_type'] === 'xhrSelectSearch')
|
|
||||||
<div class="space-y-2">
|
|
||||||
|
|
||||||
<flux:label>{{ $field['name'] }}</flux:label>
|
|
||||||
<flux:description>{!! $safeDescription !!}</flux:description>
|
|
||||||
<flux:input.group>
|
|
||||||
<flux:input
|
|
||||||
wire:model="searchQueries.{{ $fieldKey }}"
|
|
||||||
placeholder="Enter search query..."
|
|
||||||
/>
|
|
||||||
<flux:button
|
|
||||||
wire:click="searchXhrSelect('{{ $fieldKey }}', '{{ $field['endpoint'] }}')"
|
|
||||||
icon="magnifying-glass"/>
|
|
||||||
</flux:input.group>
|
|
||||||
<flux:description>{!! $safeHelp !!}</flux:description>
|
|
||||||
@if((isset($xhrSelectOptions[$fieldKey]) && is_array($xhrSelectOptions[$fieldKey]) && count($xhrSelectOptions[$fieldKey]) > 0) || !empty($currentValue))
|
|
||||||
<flux:select
|
|
||||||
wire:model="configuration.{{ $fieldKey }}"
|
|
||||||
>
|
|
||||||
<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
|
|
||||||
@if(!empty($currentValue) && (!isset($xhrSelectOptions[$fieldKey]) || empty($xhrSelectOptions[$fieldKey])))
|
|
||||||
{{-- Show current value even if no options are loaded --}}
|
|
||||||
<option value="{{ $currentValue }}" selected>{{ $currentValue }}</option>
|
|
||||||
@endif
|
|
||||||
</flux:select>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
@elseif($field['field_type'] === 'multi_string')
|
|
||||||
<flux:field>
|
|
||||||
<flux:label>{{ $field['name'] }}</flux:label>
|
|
||||||
<flux:description>{!! $safeDescription !!}</flux:description>
|
|
||||||
|
|
||||||
<div class="space-y-2 mt-2">
|
|
||||||
@foreach($multiValues[$fieldKey] as $index => $item)
|
|
||||||
<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"
|
|
||||||
pattern="[^,]*"
|
|
||||||
title="Commas are not allowed in this field"
|
|
||||||
/>
|
|
||||||
|
|
||||||
@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
|
|
||||||
<flux:callout variant="warning">Field type "{{ $field['field_type'] }}" not yet supported</flux:callout>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
@endforeach
|
|
||||||
@endif
|
|
||||||
|
|
||||||
<div class="flex">
|
|
||||||
<flux:spacer/>
|
|
||||||
<flux:button type="submit" variant="primary">Save Configuration</flux:button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</flux:modal>
|
|
||||||
|
|
||||||
<div class="mt-5 mb-5">
|
<div class="mt-5 mb-5">
|
||||||
<h3 class="text-xl font-semibold dark:text-gray-100">Settings</h3>
|
<h3 class="text-xl font-semibold dark:text-gray-100">Settings</h3>
|
||||||
|
|
|
||||||
124
tests/Feature/Livewire/Plugins/ConfigModalTest.php
Normal file
124
tests/Feature/Livewire/Plugins/ConfigModalTest.php
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Plugin;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Livewire\Volt\Volt;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
test('config modal correctly loads multi_string defaults into UI boxes', function (): void {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$this->actingAs($user);
|
||||||
|
|
||||||
|
$plugin = Plugin::create([
|
||||||
|
'uuid' => Str::uuid(),
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'name' => 'Test Plugin',
|
||||||
|
'data_strategy' => 'static',
|
||||||
|
'configuration_template' => [
|
||||||
|
'custom_fields' => [[
|
||||||
|
'keyname' => 'tags',
|
||||||
|
'field_type' => 'multi_string',
|
||||||
|
'name' => 'Reading Days',
|
||||||
|
'default' => 'alpha,beta',
|
||||||
|
]]
|
||||||
|
],
|
||||||
|
'configuration' => ['tags' => 'alpha,beta']
|
||||||
|
]);
|
||||||
|
|
||||||
|
Volt::test('plugins.config-modal', ['plugin' => $plugin])
|
||||||
|
->assertSet('multiValues.tags', ['alpha', 'beta']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('config modal validates against commas in multi_string boxes', function (): void {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$this->actingAs($user);
|
||||||
|
|
||||||
|
$plugin = Plugin::create([
|
||||||
|
'uuid' => Str::uuid(),
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'name' => 'Test Plugin',
|
||||||
|
'data_strategy' => 'static',
|
||||||
|
'configuration_template' => [
|
||||||
|
'custom_fields' => [[
|
||||||
|
'keyname' => 'tags',
|
||||||
|
'field_type' => 'multi_string',
|
||||||
|
'name' => 'Reading Days',
|
||||||
|
]]
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
Volt::test('plugins.config-modal', ['plugin' => $plugin])
|
||||||
|
->set('multiValues.tags.0', 'no,commas,allowed')
|
||||||
|
->call('saveConfiguration')
|
||||||
|
->assertHasErrors(['multiValues.tags.0' => 'regex']);
|
||||||
|
|
||||||
|
// Assert DB remains unchanged
|
||||||
|
expect($plugin->fresh()->configuration['tags'] ?? '')->not->toBe('no,commas,allowed');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('config modal merges multi_string boxes into a single CSV string on save', function (): void {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$this->actingAs($user);
|
||||||
|
|
||||||
|
$plugin = Plugin::create([
|
||||||
|
'uuid' => Str::uuid(),
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'name' => 'Test Plugin',
|
||||||
|
'data_strategy' => 'static',
|
||||||
|
'configuration_template' => [
|
||||||
|
'custom_fields' => [[
|
||||||
|
'keyname' => 'items',
|
||||||
|
'field_type' => 'multi_string',
|
||||||
|
'name' => 'Reading Days',
|
||||||
|
]]
|
||||||
|
],
|
||||||
|
'configuration' => []
|
||||||
|
]);
|
||||||
|
|
||||||
|
Volt::test('plugins.config-modal', ['plugin' => $plugin])
|
||||||
|
->set('multiValues.items.0', 'First')
|
||||||
|
->call('addMultiItem', 'items')
|
||||||
|
->set('multiValues.items.1', 'Second')
|
||||||
|
->call('saveConfiguration')
|
||||||
|
->assertHasNoErrors();
|
||||||
|
|
||||||
|
expect($plugin->fresh()->configuration['items'])->toBe('First,Second');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('config modal resetForm clears dirty state and increments resetIndex', function (): void {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$this->actingAs($user);
|
||||||
|
|
||||||
|
$plugin = Plugin::create([
|
||||||
|
'uuid' => Str::uuid(),
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'name' => 'Test Plugin',
|
||||||
|
'data_strategy' => 'static',
|
||||||
|
'configuration' => ['simple_key' => 'original_value']
|
||||||
|
]);
|
||||||
|
|
||||||
|
Volt::test('plugins.config-modal', ['plugin' => $plugin])
|
||||||
|
->set('configuration.simple_key', 'dirty_value')
|
||||||
|
->call('resetForm')
|
||||||
|
->assertSet('configuration.simple_key', 'original_value')
|
||||||
|
->assertSet('resetIndex', 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('config modal dispatches update event for parent warning refresh', function (): void {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$this->actingAs($user);
|
||||||
|
|
||||||
|
$plugin = Plugin::create([
|
||||||
|
'uuid' => Str::uuid(),
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'name' => 'Test Plugin',
|
||||||
|
'data_strategy' => 'static'
|
||||||
|
]);
|
||||||
|
|
||||||
|
Volt::test('plugins.config-modal', ['plugin' => $plugin])
|
||||||
|
->call('saveConfiguration')
|
||||||
|
->assertDispatched('config-updated');
|
||||||
|
});
|
||||||
|
|
@ -427,6 +427,65 @@ YAML;
|
||||||
->and($displayIncidentField['default'])->toBe('true');
|
->and($displayIncidentField['default'])->toBe('true');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('throws exception when multi_string default value contains a comma', function (): void {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
// YAML with a comma in the 'default' field of a multi_string
|
||||||
|
$invalidYaml = <<<YAML
|
||||||
|
name: Test Plugin
|
||||||
|
refresh_interval: 30
|
||||||
|
strategy: static
|
||||||
|
polling_verb: get
|
||||||
|
static_data: '{"test": "data"}'
|
||||||
|
custom_fields:
|
||||||
|
- keyname: api_key
|
||||||
|
field_type: multi_string
|
||||||
|
default: default-api-key1,default-api-key2
|
||||||
|
label: API Key
|
||||||
|
YAML;
|
||||||
|
|
||||||
|
$zipContent = createMockZipFile([
|
||||||
|
'src/settings.yml' => $invalidYaml,
|
||||||
|
'src/full.liquid' => getValidFullLiquid(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$zipFile = UploadedFile::fake()->createWithContent('invalid-default.zip', $zipContent);
|
||||||
|
$pluginImportService = new PluginImportService();
|
||||||
|
|
||||||
|
expect(fn () => $pluginImportService->importFromZip($zipFile, $user))
|
||||||
|
->toThrow(Exception::class, "Validation Error: The default value for multistring fields like `api_key` cannot contain commas.");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws exception when multi_string placeholder contains a comma', function (): void {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
// YAML with a comma in the 'placeholder' field
|
||||||
|
$invalidYaml = <<<YAML
|
||||||
|
name: Test Plugin
|
||||||
|
refresh_interval: 30
|
||||||
|
strategy: static
|
||||||
|
polling_verb: get
|
||||||
|
static_data: '{"test": "data"}'
|
||||||
|
custom_fields:
|
||||||
|
- keyname: api_key
|
||||||
|
field_type: multi_string
|
||||||
|
default: default-api-key
|
||||||
|
label: API Key
|
||||||
|
placeholder: "value1, value2"
|
||||||
|
YAML;
|
||||||
|
|
||||||
|
$zipContent = createMockZipFile([
|
||||||
|
'src/settings.yml' => $invalidYaml,
|
||||||
|
'src/full.liquid' => getValidFullLiquid(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$zipFile = UploadedFile::fake()->createWithContent('invalid-placeholder.zip', $zipContent);
|
||||||
|
$pluginImportService = new PluginImportService();
|
||||||
|
|
||||||
|
expect(fn () => $pluginImportService->importFromZip($zipFile, $user))
|
||||||
|
->toThrow(Exception::class, "Validation Error: The placeholder value for multistring fields like `api_key` cannot contain commas.");
|
||||||
|
});
|
||||||
|
|
||||||
// Helper methods
|
// Helper methods
|
||||||
function createMockZipFile(array $files): string
|
function createMockZipFile(array $files): string
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -737,3 +737,175 @@ test('plugin model preserves multi_string csv format', function (): void {
|
||||||
|
|
||||||
expect($plugin->fresh()->configuration['tags'])->toBe('laravel,pest,security');
|
expect($plugin->fresh()->configuration['tags'])->toBe('laravel,pest,security');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('plugin duplicate copies all attributes except id and uuid', function (): void {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
$original = Plugin::factory()->create([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'name' => 'Original Plugin',
|
||||||
|
'data_stale_minutes' => 30,
|
||||||
|
'data_strategy' => 'polling',
|
||||||
|
'polling_url' => 'https://api.example.com/data',
|
||||||
|
'polling_verb' => 'get',
|
||||||
|
'polling_header' => 'Authorization: Bearer token123',
|
||||||
|
'polling_body' => '{"query": "test"}',
|
||||||
|
'render_markup' => '<div>Test markup</div>',
|
||||||
|
'markup_language' => 'blade',
|
||||||
|
'configuration' => ['api_key' => 'secret123'],
|
||||||
|
'configuration_template' => [
|
||||||
|
'custom_fields' => [
|
||||||
|
[
|
||||||
|
'keyname' => 'api_key',
|
||||||
|
'field_type' => 'string',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'no_bleed' => true,
|
||||||
|
'dark_mode' => true,
|
||||||
|
'data_payload' => ['test' => 'data'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$duplicate = $original->duplicate();
|
||||||
|
|
||||||
|
// Refresh to ensure casts are applied
|
||||||
|
$original->refresh();
|
||||||
|
$duplicate->refresh();
|
||||||
|
|
||||||
|
expect($duplicate->id)->not->toBe($original->id)
|
||||||
|
->and($duplicate->uuid)->not->toBe($original->uuid)
|
||||||
|
->and($duplicate->name)->toBe('Original Plugin (Copy)')
|
||||||
|
->and($duplicate->user_id)->toBe($original->user_id)
|
||||||
|
->and($duplicate->data_stale_minutes)->toBe($original->data_stale_minutes)
|
||||||
|
->and($duplicate->data_strategy)->toBe($original->data_strategy)
|
||||||
|
->and($duplicate->polling_url)->toBe($original->polling_url)
|
||||||
|
->and($duplicate->polling_verb)->toBe($original->polling_verb)
|
||||||
|
->and($duplicate->polling_header)->toBe($original->polling_header)
|
||||||
|
->and($duplicate->polling_body)->toBe($original->polling_body)
|
||||||
|
->and($duplicate->render_markup)->toBe($original->render_markup)
|
||||||
|
->and($duplicate->markup_language)->toBe($original->markup_language)
|
||||||
|
->and($duplicate->configuration)->toBe($original->configuration)
|
||||||
|
->and($duplicate->configuration_template)->toBe($original->configuration_template)
|
||||||
|
->and($duplicate->no_bleed)->toBe($original->no_bleed)
|
||||||
|
->and($duplicate->dark_mode)->toBe($original->dark_mode)
|
||||||
|
->and($duplicate->data_payload)->toBe($original->data_payload)
|
||||||
|
->and($duplicate->render_markup_view)->toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('plugin duplicate copies render_markup_view file content to render_markup', function (): void {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
// Create a test blade file
|
||||||
|
$testViewPath = resource_path('views/recipes/test-duplicate.blade.php');
|
||||||
|
$testContent = '<div class="test-view">Test Content</div>';
|
||||||
|
|
||||||
|
// Ensure directory exists
|
||||||
|
if (! is_dir(dirname($testViewPath))) {
|
||||||
|
mkdir(dirname($testViewPath), 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
file_put_contents($testViewPath, $testContent);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$original = Plugin::factory()->create([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'name' => 'View Plugin',
|
||||||
|
'render_markup' => null,
|
||||||
|
'render_markup_view' => 'recipes.test-duplicate',
|
||||||
|
'markup_language' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$duplicate = $original->duplicate();
|
||||||
|
|
||||||
|
expect($duplicate->render_markup)->toBe($testContent)
|
||||||
|
->and($duplicate->markup_language)->toBe('blade')
|
||||||
|
->and($duplicate->render_markup_view)->toBeNull()
|
||||||
|
->and($duplicate->name)->toBe('View Plugin (Copy)');
|
||||||
|
} finally {
|
||||||
|
// Clean up test file
|
||||||
|
if (file_exists($testViewPath)) {
|
||||||
|
unlink($testViewPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('plugin duplicate handles liquid file extension', function (): void {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
// Create a test liquid file
|
||||||
|
$testViewPath = resource_path('views/recipes/test-duplicate-liquid.liquid');
|
||||||
|
$testContent = '<div class="test-view">{{ data.message }}</div>';
|
||||||
|
|
||||||
|
// Ensure directory exists
|
||||||
|
if (! is_dir(dirname($testViewPath))) {
|
||||||
|
mkdir(dirname($testViewPath), 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
file_put_contents($testViewPath, $testContent);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$original = Plugin::factory()->create([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'name' => 'Liquid Plugin',
|
||||||
|
'render_markup' => null,
|
||||||
|
'render_markup_view' => 'recipes.test-duplicate-liquid',
|
||||||
|
'markup_language' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$duplicate = $original->duplicate();
|
||||||
|
|
||||||
|
expect($duplicate->render_markup)->toBe($testContent)
|
||||||
|
->and($duplicate->markup_language)->toBe('liquid')
|
||||||
|
->and($duplicate->render_markup_view)->toBeNull();
|
||||||
|
} finally {
|
||||||
|
// Clean up test file
|
||||||
|
if (file_exists($testViewPath)) {
|
||||||
|
unlink($testViewPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('plugin duplicate handles missing view file gracefully', function (): void {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
$original = Plugin::factory()->create([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'name' => 'Missing View Plugin',
|
||||||
|
'render_markup' => null,
|
||||||
|
'render_markup_view' => 'recipes.nonexistent-view',
|
||||||
|
'markup_language' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$duplicate = $original->duplicate();
|
||||||
|
|
||||||
|
expect($duplicate->render_markup_view)->toBeNull()
|
||||||
|
->and($duplicate->name)->toBe('Missing View Plugin (Copy)');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('plugin duplicate uses provided user_id', function (): void {
|
||||||
|
$user1 = User::factory()->create();
|
||||||
|
$user2 = User::factory()->create();
|
||||||
|
|
||||||
|
$original = Plugin::factory()->create([
|
||||||
|
'user_id' => $user1->id,
|
||||||
|
'name' => 'Original Plugin',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$duplicate = $original->duplicate($user2->id);
|
||||||
|
|
||||||
|
expect($duplicate->user_id)->toBe($user2->id)
|
||||||
|
->and($duplicate->user_id)->not->toBe($original->user_id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('plugin duplicate falls back to original user_id when no user_id provided', function (): void {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
$original = Plugin::factory()->create([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'name' => 'Original Plugin',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$duplicate = $original->duplicate();
|
||||||
|
|
||||||
|
expect($duplicate->user_id)->toBe($original->user_id);
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue