byos_laravel/resources/views/livewire/plugins/config-modal.blade.php
jerremyng 164a990dfe add validation for config_modal
Commas are now not allowed in multistring inputs. config_modal was also refactored and extracted as its own file (code was getting messy)
some basic tests were also created
2026-01-06 19:38:12 +01:00

516 lines
33 KiB
PHP

<?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>