diff --git a/resources/views/livewire/plugins/config-modal.blade.php b/resources/views/livewire/plugins/config-modal.blade.php
new file mode 100644
index 0000000..7aaacbb
--- /dev/null
+++ b/resources/views/livewire/plugins/config-modal.blade.php
@@ -0,0 +1,516 @@
+ 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);
+ }
+ }
+};?>
+
+
+
+
+
+ Configuration
+ Configure your plugin settings
+
+
+
+
+
+
diff --git a/resources/views/livewire/plugins/recipe.blade.php b/resources/views/livewire/plugins/recipe.blade.php
index bda8221..4482d5a 100644
--- a/resources/views/livewire/plugins/recipe.blade.php
+++ b/resources/views/livewire/plugins/recipe.blade.php
@@ -7,6 +7,7 @@ use Livewire\Volt\Component;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Http;
+use Livewire\Attributes\On;
new class extends Component {
public Plugin $plugin;
@@ -34,17 +35,13 @@ new class extends Component {
public string $mashup_layout = 'full';
public array $mashup_plugins = [];
public array $configuration_template = [];
- public array $configuration = [];
- public array $xhrSelectOptions = [];
- public array $searchQueries = [];
- public array $multiValues = [];
public function mount(): void
{
abort_unless(auth()->user()->plugins->contains($this->plugin), 403);
$this->blade_code = $this->plugin->render_markup;
+ // required to render some stuff
$this->configuration_template = $this->plugin->configuration_template ?? [];
- $this->configuration = is_array($this->plugin->configuration) ? $this->plugin->configuration : [];
if ($this->plugin->render_markup_view) {
try {
@@ -75,25 +72,6 @@ new class extends Component {
$this->fillformFields();
$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
@@ -287,47 +265,6 @@ new class extends Component {
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)
{
return \App\Models\Playlist::where('device_id', $deviceId)->get();
@@ -440,58 +377,15 @@ HTML;
$this->redirect(route('plugins.index'));
}
- public function loadXhrSelectOptions(string $fieldKey, string $endpoint, ?string $query = null): void
- {
- abort_unless(auth()->user()->plugins->contains($this->plugin), 403);
+ #[On('config-updated')]
+ public function refreshPlugin()
+ {
+ // 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][] = '';
- }
- }
}
-
?>
@@ -683,355 +577,7 @@ HTML;
-
-
-
- Configuration
- Configure your plugin settings
-
-
-
-
-
+
Settings
diff --git a/tests/Feature/Livewire/Plugins/ConfigModalTest.php b/tests/Feature/Livewire/Plugins/ConfigModalTest.php
new file mode 100644
index 0000000..4372991
--- /dev/null
+++ b/tests/Feature/Livewire/Plugins/ConfigModalTest.php
@@ -0,0 +1,124 @@
+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');
+});