feat(plugins): add new field types + validate & cast user data before storing settings

+ refactor form fields components
This commit is contained in:
Yassine Doghri 2024-05-12 18:38:33 +00:00
parent 82714e7155
commit 6f833fc76a
42 changed files with 857 additions and 352 deletions

View File

@ -65,12 +65,17 @@ class CategoryModel extends Model
$options = array_reduce( $options = array_reduce(
$categories, $categories,
static function (array $result, Category $category): array { static function (array $result, Category $category): array {
$result[$category->id] = ''; $label = '';
if ($category->parent instanceof Category) { if ($category->parent instanceof Category) {
$result[$category->id] = lang('Podcast.category_options.' . $category->parent->code) . ' '; $label = lang('Podcast.category_options.' . $category->parent->code) . ' ';
} }
$result[$category->id] .= lang('Podcast.category_options.' . $category->code); $label .= lang('Podcast.category_options.' . $category->code);
$result[] = [
'value' => $category->id,
'label' => $label,
];
return $result; return $result;
}, },
[], [],

View File

@ -56,7 +56,10 @@ class LanguageModel extends Model
$options = array_reduce( $options = array_reduce(
$languages, $languages,
static function (array $result, Language $language): array { static function (array $result, Language $language): array {
$result[$language->code] = $language->native_name; $result[] = [
'value' => $language->code,
'label' => $language->native_name,
];
return $result; return $result;
}, },
[], [],

View File

@ -8,7 +8,7 @@ import Dropdown from "./modules/Dropdown";
import HotKeys from "./modules/HotKeys"; import HotKeys from "./modules/HotKeys";
import "./modules/markdown-preview"; import "./modules/markdown-preview";
import "./modules/markdown-write-preview"; import "./modules/markdown-write-preview";
import MultiSelect from "./modules/MultiSelect"; import SelectMulti from "./modules/SelectMulti";
import "./modules/permalink-edit"; import "./modules/permalink-edit";
import "./modules/play-soundbite"; import "./modules/play-soundbite";
import PublishMessageWarning from "./modules/PublishMessageWarning"; import PublishMessageWarning from "./modules/PublishMessageWarning";
@ -26,7 +26,7 @@ import "./modules/xml-editor";
Dropdown(); Dropdown();
Tooltip(); Tooltip();
Select(); Select();
MultiSelect(); SelectMulti();
Slugify(); Slugify();
SidebarToggler(); SidebarToggler();
ClientTimezone(); ClientTimezone();

View File

@ -1,6 +1,6 @@
import Choices from "choices.js"; import Choices from "choices.js";
const MultiSelect = (): void => { const SelectMulti = (): void => {
// Pass single element // Pass single element
const multiSelects: NodeListOf<HTMLSelectElement> = const multiSelects: NodeListOf<HTMLSelectElement> =
document.querySelectorAll("select[multiple]"); document.querySelectorAll("select[multiple]");
@ -49,4 +49,4 @@ const MultiSelect = (): void => {
} }
}; };
export default MultiSelect; export default SelectMulti;

View File

@ -25,7 +25,10 @@
} }
.choices [hidden] { .choices [hidden] {
display: none !important; position: absolute;
opacity: 0;
z-index: -9999;
pointer-events: none;
} }
.choices[data-type*="select-one"] { .choices[data-type*="select-one"] {

View File

@ -11,13 +11,13 @@
} }
&:checked + .form-switch-slider::before { &:checked + .form-switch-slider::before {
@apply transform translate-x-8; @apply transform translate-x-6;
} }
&:checked + .form-switch-slider::after { &:checked + .form-switch-slider::after {
@apply transform translate-x-0 left-2; @apply transform translate-x-0 left-1.5;
content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23ffffff'%3E%3Cpath fill='none' d='M0 0h24v24H0z'/%3E%3Cpath d='m10 15.172 9.192-9.193 1.415 1.414L10 18l-6.364-6.364 1.414-1.414z'/%3E%3C/svg%3E%0A"); content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23ffffff'%3E%3Cpath d='m10 15.172 9.192-9.193 1.415 1.414L10 18l-6.364-6.364 1.414-1.414z'/%3E%3C/svg%3E%0A");
} }
&:checked + .form-switch-slider.form-switch-slider--small::before { &:checked + .form-switch-slider.form-switch-slider--small::before {
@ -30,7 +30,7 @@
} }
.form-switch-slider { .form-switch-slider {
@apply relative inset-0 flex-shrink-0 w-16 h-8 transition duration-200 rounded-full cursor-pointer bg-highlight border-contrast border-3; @apply relative inset-0 flex-shrink-0 h-8 transition duration-200 rounded-full cursor-pointer w-14 bg-highlight border-contrast border-3;
&.form-switch-slider--small { &.form-switch-slider--small {
@apply w-12 h-6; @apply w-12 h-6;
@ -56,10 +56,11 @@
} }
&::after { &::after {
@apply absolute w-5 h-5 transition duration-150 transform translate-x-5; @apply absolute w-4 h-4 transition duration-150 transform top-1;
content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' %3E%3Cpath fill='none' d='M0 0h24v24H0z'/%3E%3Cpath d='m12 10.586 4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z'/%3E%3C/svg%3E%0A"); --tw-translate-x: 1.125rem;
top: 3px;
content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' %3E%3Cpath d='m12 10.586 4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z'/%3E%3C/svg%3E%0A");
left: 10px; left: 10px;
} }
} }

View File

@ -22,12 +22,12 @@ class Checkbox extends FormComponent
{ {
$checkboxInput = form_checkbox( $checkboxInput = form_checkbox(
[ [
'id' => $this->value, 'id' => $this->id,
'name' => $this->name, 'name' => $this->name,
'class' => 'form-checkbox bg-elevated text-accent-base border-contrast border-3 w-6 h-6', 'class' => 'form-checkbox bg-elevated text-accent-base border-contrast border-3 w-6 h-6',
], ],
'yes', 'yes',
old($this->name) ? old($this->name) === $this->value : $this->isChecked, old($this->name) ? old($this->name) === 'yes' : $this->isChecked,
); );
$hint = $this->hint === '' ? '' : (new Hint([ $hint = $this->hint === '' ? '' : (new Hint([

View File

@ -6,13 +6,13 @@ namespace App\Views\Components\Forms;
class ColorRadioButton extends FormComponent class ColorRadioButton extends FormComponent
{ {
protected array $props = ['isChecked']; protected array $props = ['isSelected'];
protected array $casts = [ protected array $casts = [
'isChecked' => 'boolean', 'isSelected' => 'boolean',
]; ];
protected bool $isChecked = false; protected bool $isSelected = false;
public function render(): string public function render(): string
{ {
@ -29,7 +29,7 @@ class ColorRadioButton extends FormComponent
$radioInput = form_radio( $radioInput = form_radio(
$data, $data,
$this->value, $this->value,
old($this->name) ? old($this->name) === $this->value : $this->isChecked, old($this->name) ? old($this->name) === $this->value : $this->isSelected,
); );
return <<<HTML return <<<HTML

View File

@ -13,6 +13,7 @@ class DatetimePicker extends FormComponent
public function render(): string public function render(): string
{ {
$dateInput = form_input([ $dateInput = form_input([
'name' => $this->name,
'class' => 'rounded-l-lg border-0 border-rounded-r-none flex-1 focus:ring-0', 'class' => 'rounded-l-lg border-0 border-rounded-r-none flex-1 focus:ring-0',
'data-input' => '', 'data-input' => '',
], old($this->name, $this->value)); ], old($this->name, $this->value));

View File

@ -66,8 +66,8 @@ class Field extends Component
unset($this->attributes['class']); unset($this->attributes['class']);
$this->attributes['name'] = $this->name; $this->attributes['name'] = $this->name;
$this->attributes['isRequired'] = $this->isRequired ? 'true' : 'false'; $this->attributes['isRequired'] = var_export($this->isRequired, true);
$this->attributes['isReadonly'] = $this->isReadonly ? 'true' : 'false'; $this->attributes['isReadonly'] = var_export($this->isReadonly, true);
$element = __NAMESPACE__ . '\\' . $this->as; $element = __NAMESPACE__ . '\\' . $this->as;
$fieldElement = new $element($this->attributes); $fieldElement = new $element($this->attributes);
@ -75,7 +75,7 @@ class Field extends Component
<div class="{$fieldClass}"> <div class="{$fieldClass}">
{$label->render()} {$label->render()}
{$helperText} {$helperText}
<div class="w-full mt-1"> <div class="relative w-full mt-1">
{$fieldElement->render()} {$fieldElement->render()}
</div> </div>
</div> </div>

View File

@ -25,7 +25,10 @@ abstract class FormComponent extends Component
protected string $name; protected string $name;
protected string $value = ''; /**
* @var null|string|list<string>
*/
protected null|string|array $value = null;
protected bool $isRequired = false; protected bool $isRequired = false;
@ -57,9 +60,4 @@ abstract class FormComponent extends Component
$this->attributes['readonly'] = 'readonly'; $this->attributes['readonly'] = 'readonly';
} }
} }
public function setValue(string $value): void
{
$this->value = htmlspecialchars_decode($value, ENT_QUOTES);
}
} }

View File

@ -29,6 +29,6 @@ class Input extends FormComponent
$this->attributes['type'] = $this->type; $this->attributes['type'] = $this->type;
$this->attributes['value'] = $this->value; $this->attributes['value'] = $this->value;
return form_input($this->attributes, old($this->name, $this->value)); return form_input($this->attributes, old($this->name, (string) $this->value));
} }
} }

View File

@ -34,6 +34,8 @@ class Label extends Component
'slot' => $this->hint, 'slot' => $this->hint,
]))->render(); ]))->render();
$this->attributes['for'] = $this->for;
return <<<HTML return <<<HTML
<label {$this->getStringifiedAttributes()}>{$this->slot}{$optionalText}{$hint}</label> <label {$this->getStringifiedAttributes()}>{$this->slot}{$optionalText}{$hint}</label>
HTML; HTML;

View File

@ -28,7 +28,7 @@ class MarkdownEditor extends FormComponent
$textarea = form_textarea( $textarea = form_textarea(
$this->attributes, $this->attributes,
old($this->name, $this->value) old($this->name, (string) $this->value)
); );
$markdownIcon = icon('markdown-fill', [ $markdownIcon = icon('markdown-fill', [
'class' => 'mr-1 text-lg opacity-40', 'class' => 'mr-1 text-lg opacity-40',

View File

@ -1,43 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Views\Components\Forms;
class MultiSelect extends FormComponent
{
protected array $props = ['options', 'selected'];
protected array $casts = [
'options' => 'array',
'selected' => 'array',
];
/**
* @var array<string, string>
*/
protected array $options = [];
/**
* @var string[]
*/
protected array $selected = [];
public function render(): string
{
$this->mergeClass('w-full bg-elevated border-3 border-contrast rounded-lg');
$defaultAttributes = [
'data-class' => $this->attributes['class'],
'multiple' => 'multiple',
'data-select-text' => lang('Common.forms.multiSelect.selectText'),
'data-loading-text' => lang('Common.forms.multiSelect.loadingText'),
'data-no-results-text' => lang('Common.forms.multiSelect.noResultsText'),
'data-no-choices-text' => lang('Common.forms.multiSelect.noChoicesText'),
'data-max-item-text' => lang('Common.forms.multiSelect.maxItemText'),
];
$extra = [...$defaultAttributes, ...$this->attributes];
return form_dropdown($this->name, $this->options, $this->selected, $extra);
}
}

View File

@ -11,10 +11,10 @@ class RadioButton extends FormComponent
protected array $props = ['isChecked', 'hint']; protected array $props = ['isChecked', 'hint'];
protected array $casts = [ protected array $casts = [
'isChecked' => 'boolean', 'isSelected' => 'boolean',
]; ];
protected bool $isChecked = false; protected bool $isSelected = false;
protected string $hint = ''; protected string $hint = '';
@ -33,7 +33,7 @@ class RadioButton extends FormComponent
$radioInput = form_radio( $radioInput = form_radio(
$data, $data,
$this->value, $this->value,
old($this->name) ? old($this->name) === $this->value : $this->isChecked, old($this->name) ? old($this->name) === $this->value : $this->isSelected,
); );
$hint = $this->hint === '' ? '' : (new Hint([ $hint = $this->hint === '' ? '' : (new Hint([

View File

@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace App\Views\Components\Forms;
use App\Views\Components\Hint;
class RadioGroup extends FormComponent
{
protected array $props = ['label', 'options', 'hint', 'helper'];
protected array $casts = [
'options' => 'array',
];
protected string $label;
/**
* @var array{value:string,label:string,hint?:string}
*/
protected array $options = [];
protected string $helper = '';
protected string $hint = '';
public function render(): string
{
$this->mergeClass('flex flex-col');
$options = '';
foreach ($this->options as $option) {
$options .= (new RadioButton([
'value' => $option['value'],
'name' => $this->name,
'slot' => $option['label'],
'hint' => $option['hint'] ?? '',
'isSelected' => var_export($this->value === null ? ($option['value'] === $this->options[array_key_first($this->options)]['value']) : ($this->value === $option['value']), true),
'isRequired' => var_export($this->isRequired, true),
]))->render();
}
$helperText = '';
if ($this->helper !== '') {
$helperId = $this->name . 'Help';
$helperText = (new Helper([
'id' => $helperId,
'slot' => $this->helper,
]))->render();
$this->attributes['aria-describedby'] = $helperId;
}
$hint = $this->hint === '' ? '' : (new Hint([
'class' => 'ml-1',
'slot' => $this->hint,
]))->render();
return <<<HTML
<fieldset {$this->getStringifiedAttributes()}>
<legend class="-mb-1 text-sm font-semibold">{$this->label}{$hint}</legend>
{$helperText}
<div class="flex gap-1 mt-1">{$options}</div>
</fieldset>
HTML;
}
}

View File

@ -18,7 +18,7 @@ class Section extends Component
{ {
$subtitle = $this->subtitle === '' ? '' : '<p class="text-sm text-skin-muted">' . $this->subtitle . '</p>'; $subtitle = $this->subtitle === '' ? '' : '<p class="text-sm text-skin-muted">' . $this->subtitle . '</p>';
$this->mergeClass('w-full p-8 bg-elevated border-3 flex flex-col items-start border-subtle rounded-xl'); $this->mergeClass('w-full p-4 sm:p-6 md:p-8 bg-elevated border-3 flex flex-col items-start border-subtle rounded-xl');
return <<<HTML return <<<HTML
<fieldset {$this->getStringifiedAttributes()}> <fieldset {$this->getStringifiedAttributes()}>

View File

@ -6,7 +6,7 @@ namespace App\Views\Components\Forms;
class Select extends FormComponent class Select extends FormComponent
{ {
protected array $props = ['options', 'selected']; protected array $props = ['options', 'defaultValue'];
protected array $casts = [ protected array $casts = [
'options' => 'array', 'options' => 'array',
@ -17,7 +17,7 @@ class Select extends FormComponent
*/ */
protected array $options = []; protected array $options = [];
protected string $selected = ''; protected string $defaultValue = '';
public function render(): string public function render(): string
{ {
@ -29,8 +29,18 @@ class Select extends FormComponent
'data-no-choices-text' => lang('Common.forms.multiSelect.noChoicesText'), 'data-no-choices-text' => lang('Common.forms.multiSelect.noChoicesText'),
'data-max-item-text' => lang('Common.forms.multiSelect.maxItemText'), 'data-max-item-text' => lang('Common.forms.multiSelect.maxItemText'),
]; ];
$extra = [...$defaultAttributes, ...$this->attributes]; $this->attributes = [...$defaultAttributes, ...$this->attributes];
return form_dropdown($this->name, $this->options, old($this->name, $this->selected !== '' ? [$this->selected] : []), $extra); $options = '';
$selected = $this->value ?? $this->defaultValue;
foreach ($this->options as $option) {
$options .= '<option value="' . $option['value'] . '"' . ($option['value'] === $selected ? ' selected' : '') . '>' . $option['label'] . '</option>';
}
$this->attributes['name'] = $this->name;
return <<<HTML
<select {$this->getStringifiedAttributes()}>{$options}</select>
HTML;
} }
} }

View File

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace App\Views\Components\Forms;
class SelectMulti extends FormComponent
{
protected array $props = ['options', 'defaultValue'];
protected array $casts = [
'value' => 'array',
'options' => 'array',
'defaultValue' => 'array',
];
/**
* @var array<string, string>
*/
protected array $options = [];
/**
* @var list<string>
*/
protected array $defaultValue = [];
public function render(): string
{
$this->mergeClass('w-full bg-elevated border-3 border-contrast rounded-lg relative');
$defaultAttributes = [
'multiple' => 'multiple',
'data-select-text' => lang('Common.forms.multiSelect.selectText'),
'data-loading-text' => lang('Common.forms.multiSelect.loadingText'),
'data-no-results-text' => lang('Common.forms.multiSelect.noResultsText'),
'data-no-choices-text' => lang('Common.forms.multiSelect.noChoicesText'),
'data-max-item-text' => lang('Common.forms.multiSelect.maxItemText'),
];
$this->attributes = [...$defaultAttributes, ...$this->attributes];
$options = '';
$selected = $this->value ?? $this->defaultValue;
foreach ($this->options as $option) {
$options .= '<option value="' . $option['value'] . '"' . (in_array($option['value'], $selected, true) ? ' selected' : '') . '>' . $option['label'] . '</option>';
}
$this->attributes['name'] = $this->name . '[]';
return <<<HTML
<select {$this->getStringifiedAttributes()}>{$options}</select>
HTML;
}
}

View File

@ -14,27 +14,23 @@ class Toggler extends FormComponent
'isChecked' => 'boolean', 'isChecked' => 'boolean',
]; ];
/**
* @var 'base'|'small
*/
protected string $size = 'base';
protected string $hint = ''; protected string $hint = '';
protected bool $isChecked = false; protected bool $isChecked = false;
public function render(): string public function render(): string
{ {
$sizeClass = match ($this->size) {
'small' => 'form-switch-slider form-switch-slider--small',
default => 'form-switch-slider',
};
$this->mergeClass('relative justify-between inline-flex items-center gap-x-2'); $this->mergeClass('relative justify-between inline-flex items-center gap-x-2');
$checkbox = form_checkbox([ $checkbox = form_checkbox(
'class' => 'form-switch', [
], 'yes', old($this->name) === 'yes' ? true : $this->isChecked); 'id' => $this->id,
'name' => $this->name,
'class' => 'form-switch',
],
'yes',
old($this->name) ? old($this->name) === 'yes' : $this->isChecked
);
$hint = $this->hint === '' ? '' : (new Hint([ $hint = $this->hint === '' ? '' : (new Hint([
'class' => 'ml-1', 'class' => 'ml-1',
@ -43,9 +39,9 @@ class Toggler extends FormComponent
return <<<HTML return <<<HTML
<label {$this->getStringifiedAttributes()}> <label {$this->getStringifiedAttributes()}>
<span class="">{$this->slot}{$hint}</span> <span>{$this->slot}{$hint}</span>
{$checkbox} {$checkbox}
<span class="{$sizeClass}"></span> <span class="form-switch-slider"></span>
</label> </label>
HTML; HTML;
} }

View File

@ -10,7 +10,10 @@ use App\Models\EpisodeModel;
use App\Models\PodcastModel; use App\Models\PodcastModel;
use CodeIgniter\Exceptions\PageNotFoundException; use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse; use CodeIgniter\HTTP\RedirectResponse;
use CodeIgniter\HTTP\URI;
use CodeIgniter\I18n\Time;
use Modules\Admin\Controllers\BaseController; use Modules\Admin\Controllers\BaseController;
use Modules\Plugins\Core\Markdown;
use Modules\Plugins\Core\Plugins; use Modules\Plugins\Core\Plugins;
class PluginController extends BaseController class PluginController extends BaseController
@ -103,9 +106,41 @@ class PluginController extends BaseController
throw PageNotFoundException::forPageNotFound(); throw PageNotFoundException::forPageNotFound();
} }
// construct validation rules first
$rules = [];
foreach ($plugin->getSettingsFields('general') as $field) { foreach ($plugin->getSettingsFields('general') as $field) {
$optionValue = $this->request->getPost($field->key); $typeRules = $plugins::FIELDS_VALIDATIONS[$field->type];
$plugins->setOption($plugin, $field->key, $optionValue); if (! in_array('permit_empty', $typeRules, true) && ! $field->optional) {
$typeRules[] = 'required';
}
$rules[$field->key] = $typeRules;
}
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
$validatedData = $this->validator->getValidated();
foreach ($plugin->getSettingsFields('general') as $field) {
$value = $validatedData[$field->key] ?? null;
$fieldValue = match ($plugins::FIELDS_CASTS[$field->type] ?? 'text') {
'bool' => $value === 'yes',
'int' => (int) $value,
'uri' => new URI($value),
'datetime' => Time::createFromFormat(
'Y-m-d H:i',
$value,
$this->request->getPost('client_timezone')
)->setTimezone(app_timezone()),
'markdown' => new Markdown($value),
default => $value === '' ? null : $value,
};
$plugins->setOption($plugin, $field->key, $fieldValue);
} }
return redirect()->back() return redirect()->back()

View File

@ -16,11 +16,11 @@ use League\CommonMark\Extension\SmartPunct\SmartPunctExtension;
use League\CommonMark\MarkdownConverter; use League\CommonMark\MarkdownConverter;
use Modules\Plugins\ExternalImageProcessor; use Modules\Plugins\ExternalImageProcessor;
use Modules\Plugins\ExternalLinkProcessor; use Modules\Plugins\ExternalLinkProcessor;
use Modules\Plugins\Manifest\Field;
use Modules\Plugins\Manifest\Manifest; use Modules\Plugins\Manifest\Manifest;
use Modules\Plugins\Manifest\Person; use Modules\Plugins\Manifest\Person;
use Modules\Plugins\Manifest\Repository; use Modules\Plugins\Manifest\Repository;
use Modules\Plugins\Manifest\Settings; use Modules\Plugins\Manifest\Settings;
use Modules\Plugins\Manifest\SettingsField;
use RuntimeException; use RuntimeException;
/** /**
@ -163,7 +163,7 @@ abstract class BasePlugin implements PluginInterface
} }
/** /**
* @return SettingsField[] * @return Field[]
*/ */
final public function getSettingsFields(string $type): array final public function getSettingsFields(string $type): array
{ {

View File

@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace Modules\Plugins\Core;
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\Autolink\AutolinkExtension;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\DisallowedRawHtml\DisallowedRawHtmlExtension;
use League\CommonMark\Extension\SmartPunct\SmartPunctExtension;
use League\CommonMark\MarkdownConverter;
use Stringable;
class Markdown implements Stringable
{
public function __construct(
protected string $markdown
) {
}
public function __toString(): string
{
return $this->markdown;
}
public function renderHTML(): string
{
$config = [
'html_input' => 'escape',
'allow_unsafe_links' => false,
];
$environment = new Environment($config);
$environment->addExtension(new CommonMarkCoreExtension());
$environment->addExtension(new AutolinkExtension());
$environment->addExtension(new SmartPunctExtension());
$environment->addExtension(new DisallowedRawHtmlExtension());
$converter = new MarkdownConverter($environment);
return (string) $converter->convert($this->markdown);
}
}

View File

@ -23,6 +23,30 @@ class Plugins
*/ */
public const HOOKS = ['channelTag', 'itemTag', 'siteHead']; public const HOOKS = ['channelTag', 'itemTag', 'siteHead'];
public const FIELDS_VALIDATIONS = [
'checkbox' => ['permit_empty'],
'datetime' => ['valid_date[Y-m-d H:i]'],
'email' => ['valid_email'],
'markdown' => ['string'],
'number' => ['integer'],
'radio-group' => ['string'],
'select' => ['string'],
'select-multiple' => ['permit_empty', 'is_list'],
'text' => ['string'],
'textarea' => ['string'],
'toggler' => ['permit_empty'],
'url' => ['valid_url_strict'],
];
public const FIELDS_CASTS = [
'checkbox' => 'bool',
'datetime' => 'datetime',
'number' => 'int',
'toggler' => 'bool',
'url' => 'uri',
'markdown' => 'markdown',
];
/** /**
* @var array<BasePlugin> * @var array<BasePlugin>
*/ */

View File

@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace Modules\Plugins\Manifest;
/**
* @property 'text'|'email'|'url'|'markdown'|'number'|'switch' $type
* @property string $key
* @property string $label
* @property string $hint
* @property string $helper
* @property bool $optional
*/
class Field extends ManifestObject
{
protected const VALIDATION_RULES = [
'type' => 'permit_empty|in_list[checkbox,datetime,email,markdown,number,radio-group,select-multiple,select,text,textarea,toggler,url]',
'key' => 'required|alpha_dash',
'label' => 'required|string',
'hint' => 'permit_empty|string',
'helper' => 'permit_empty|string',
'optional' => 'permit_empty|is_boolean',
'options' => 'permit_empty|is_list',
];
protected const CASTS = [
'options' => [Option::class],
];
protected string $type = 'text';
protected string $key;
protected string $label;
protected string $hint = '';
protected string $helper = '';
protected bool $optional = false;
/**
* @var Option[]
*/
protected array $options = [];
/**
* @return array{label:string,value:string,hint:string}[]
*/
public function getOptionsArray(): array
{
$optionsArray = [];
foreach ($this->options as $option) {
$optionsArray[] = [
'label' => $option->label,
'value' => $option->value,
'hint' => (string) $option->hint,
];
}
return $optionsArray;
}
}

View File

@ -39,9 +39,6 @@ class Manifest extends ManifestObject
'repository' => 'permit_empty|is_list', 'repository' => 'permit_empty|is_list',
]; ];
/**
* @var array<string,array{string}|string>
*/
protected const CASTS = [ protected const CASTS = [
'authors' => [Person::class], 'authors' => [Person::class],
'homepage' => URI::class, 'homepage' => URI::class,

View File

@ -39,6 +39,11 @@ abstract class ManifestObject
throw new Exception('Undefined object property ' . static::class . '::' . $name); throw new Exception('Undefined object property ' . static::class . '::' . $name);
} }
public function __isset(string $property): bool
{
return property_exists($this, $property);
}
public function load(): void public function load(): void
{ {
/** @var Validation $validation */ /** @var Validation $validation */

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Modules\Plugins\Manifest;
/**
* @property string $label
* @property string $value
* @property ?string $hint
*/
class Option extends ManifestObject
{
protected const VALIDATION_RULES = [
'label' => 'required|string',
'value' => 'required|alpha_dash',
'hint' => 'permit_empty|string',
];
protected string $label;
protected string $value;
protected ?string $hint = null;
}

View File

@ -5,9 +5,9 @@ declare(strict_types=1);
namespace Modules\Plugins\Manifest; namespace Modules\Plugins\Manifest;
/** /**
* @property SettingsField[] $general * @property Field[] $general
* @property SettingsField[] $podcast * @property Field[] $podcast
* @property SettingsField[] $episode * @property Field[] $episode
*/ */
class Settings extends ManifestObject class Settings extends ManifestObject
{ {
@ -21,23 +21,23 @@ class Settings extends ManifestObject
* @var array<string,array{string}|string> * @var array<string,array{string}|string>
*/ */
protected const CASTS = [ protected const CASTS = [
'general' => [SettingsField::class], 'general' => [Field::class],
'podcast' => [SettingsField::class], 'podcast' => [Field::class],
'episode' => [SettingsField::class], 'episode' => [Field::class],
]; ];
/** /**
* @var SettingsField[] * @var Field[]
*/ */
protected array $general = []; protected array $general = [];
/** /**
* @var SettingsField[] * @var Field[]
*/ */
protected array $podcast = []; protected array $podcast = [];
/** /**
* @var SettingsField[] * @var Field[]
*/ */
protected array $episode = []; protected array $episode = [];
} }

View File

@ -1,37 +0,0 @@
<?php
declare(strict_types=1);
namespace Modules\Plugins\Manifest;
/**
* @property 'text'|'email'|'url'|'markdown'|'number'|'switch' $type
* @property string $key
* @property string $label
* @property string $hint
* @property string $helper
* @property bool $optional
*/
class SettingsField extends ManifestObject
{
protected const VALIDATION_RULES = [
'type' => 'permit_empty|in_list[text,email,url,markdown,number,switch]',
'key' => 'required|alpha_dash',
'label' => 'required|string',
'hint' => 'permit_empty|string',
'helper' => 'permit_empty|string',
'optional' => 'permit_empty|is_boolean',
];
protected string $type = 'text';
protected string $key;
protected string $label;
protected string $hint = '';
protected string $helper = '';
protected bool $optional = false;
}

View File

@ -97,20 +97,26 @@
"general": { "general": {
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/$defs/settings-field" "$ref": "#/$defs/field"
} },
"minItems": 1,
"uniqueItems": true
}, },
"podcast": { "podcast": {
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/$defs/settings-field" "$ref": "#/$defs/field"
} },
"minItems": 1,
"uniqueItems": true
}, },
"episode": { "episode": {
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/$defs/settings-field" "$ref": "#/$defs/field"
} },
"minItems": 1,
"uniqueItems": true
} }
} }
}, },
@ -158,15 +164,48 @@
} }
} }
}, },
"settings-field": { "section": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"fields": {
"type": "array",
"items": {
"$ref": "#/$defs/field"
},
"minItems": 1,
"uniqueItems": true
}
}
},
"field": {
"type": "object", "type": "object",
"properties": { "properties": {
"type": { "type": {
"enum": ["text", "email", "url", "markdown", "number", "switch"], "enum": [
"checkbox",
"datetime",
"email",
"markdown",
"number",
"radio-group",
"select-multiple",
"select",
"text",
"textarea",
"toggler",
"url"
],
"default": "text" "default": "text"
}, },
"key": { "key": {
"type": "string" "type": "string",
"pattern": "^[A-Za-z]+[\\w\\-\\:\\.]*$"
}, },
"label": { "label": {
"type": "string" "type": "string"
@ -179,10 +218,56 @@
}, },
"optional": { "optional": {
"type": "boolean" "type": "boolean"
},
"options": {
"type": "array",
"items": {
"$ref": "#/$defs/option"
},
"minItems": 1,
"uniqueItems": true
} }
}, },
"required": ["key", "label"], "required": ["key", "label"],
"additionalProperties": false,
"allOf": [
{ "$ref": "#/$defs/field-multiple-implies-options-is-required" }
]
},
"option": {
"type": "object",
"properties": {
"label": {
"type": "string"
},
"value": {
"type": "string"
},
"hint": {
"type": "string"
}
},
"required": ["label", "value"],
"additionalProperties": false "additionalProperties": false
},
"field-multiple-implies-options-is-required": {
"anyOf": [
{
"not": {
"properties": {
"type": {
"anyOf": [
{ "const": "radio-group" },
{ "const": "select" },
{ "const": "select-multiple" }
]
}
},
"required": ["type"]
}
},
{ "required": ["options"] }
]
} }
} }
} }

View File

@ -69,42 +69,49 @@
/> />
</div> </div>
<fieldset class="flex gap-1"> <x-Forms.RadioGroup
<legend><?= lang('Episode.form.type.label') ?></legend> label="<?= lang('Episode.form.type.label') ?>"
<x-Forms.RadioButton
value="full"
name="type" name="type"
hint="<?= esc(lang('Episode.form.type.full_hint')) ?>" options="<?= esc(json_encode([
isChecked="true" ><?= lang('Episode.form.type.full') ?></x-Forms.RadioButton> [
<x-Forms.RadioButton 'label' => lang('Episode.form.type.full'),
value="trailer" 'value' => 'full',
name="type" 'hint' => lang('Episode.form.type.full_hint'),
hint="<?= esc(lang('Episode.form.type.trailer_hint')) ?>" ],
isChecked="false" ><?= lang('Episode.form.type.trailer') ?></x-Forms.RadioButton> [
<x-Forms.RadioButton 'label' => lang('Episode.form.type.trailer'),
value="bonus" 'value' => 'trailer',
name="type" 'hint' => lang('Episode.form.type.trailer_hint'),
hint="<?= esc(lang('Episode.form.type.bonus_hint')) ?>" ],
isChecked="false" ><?= lang('Episode.form.type.bonus') ?></x-Forms.RadioButton> [
</fieldset> 'label' => lang('Episode.form.type.bonus'),
'value' => 'bonus',
'hint' => lang('Episode.form.type.bonus_hint'),
],
])) ?>"
isRequired="true"
/>
<fieldset class="flex gap-1"> <x-Forms.RadioGroup
<legend><?= lang('Episode.form.parental_advisory.label') ?><x-Hint class="ml-1"><?= lang('Episode.form.parental_advisory.hint') ?></x-Hint></legend> label="<?= lang('Episode.form.parental_advisory.label') ?>"
<x-Forms.RadioButton hint="<?= lang('Episode.form.parental_advisory.hint') ?>"
value="undefined"
name="parental_advisory" name="parental_advisory"
isChecked="true" ><?= lang('Episode.form.parental_advisory.undefined') ?></x-Forms.RadioButton> options="<?= esc(json_encode([
<x-Forms.RadioButton [
value="clean" 'label' => lang('Episode.form.parental_advisory.undefined'),
name="parental_advisory" 'value' => 'undefined',
isChecked="false" ><?= lang('Episode.form.parental_advisory.clean') ?></x-Forms.RadioButton> ],
<x-Forms.RadioButton [
value="explicit" 'label' => lang('Episode.form.parental_advisory.clean'),
name="parental_advisory" 'value' => 'clean',
isChecked="false" ><?= lang('Episode.form.parental_advisory.explicit') ?></x-Forms.RadioButton> ],
</fieldset> [
'label' => lang('Episode.form.parental_advisory.explicit'),
'value' => 'explicit',
],
])) ?>"
isRequired="true"
/>
</x-Forms.Section> </x-Forms.Section>

View File

@ -73,40 +73,51 @@
/> />
</div> </div>
<fieldset class="flex gap-1"> <x-Forms.RadioGroup
<legend><?= lang('Episode.form.type.label') ?></legend> label="<?= lang('Episode.form.type.label') ?>"
<x-Forms.RadioButton
value="full"
name="type" name="type"
hint="<?= esc(lang('Episode.form.type.full_hint')) ?>" value="<?= $episode->type ?>"
isChecked="<?= $episode->type === 'full' ? 'true' : 'false' ?>" ><?= lang('Episode.form.type.full') ?></x-Forms.RadioButton> options="<?= esc(json_encode([
<x-Forms.RadioButton [
value="trailer" 'label' => lang('Episode.form.type.full'),
name="type" 'value' => 'full',
hint="<?= esc(lang('Episode.form.type.trailer_hint')) ?>" 'hint' => lang('Episode.form.type.full_hint'),
isChecked="<?= $episode->type === 'trailer' ? 'true' : 'false' ?>" ><?= lang('Episode.form.type.trailer') ?></x-Forms.RadioButton> ],
<x-Forms.RadioButton [
value="bonus" 'label' => lang('Episode.form.type.trailer'),
name="type" 'value' => 'trailer',
hint="<?= esc(lang('Episode.form.type.bonus_hint')) ?>" 'hint' => lang('Episode.form.type.trailer_hint'),
isChecked="<?= $episode->type === 'bonus' ? 'true' : 'false' ?>" ><?= lang('Episode.form.type.bonus') ?></x-Forms.RadioButton> ],
</fieldset> [
'label' => lang('Episode.form.type.bonus'),
'value' => 'bonus',
'hint' => lang('Episode.form.type.bonus_hint'),
],
])) ?>"
isRequired="true"
/>
<fieldset class="flex gap-1"> <x-Forms.RadioGroup
<legend><?= lang('Episode.form.parental_advisory.label') ?><x-Hint class="ml-1"><?= lang('Episode.form.parental_advisory.hint') ?></x-Hint></legend> label="<?= lang('Episode.form.parental_advisory.label') ?>"
<x-Forms.RadioButton hint="<?= lang('Episode.form.parental_advisory.hint') ?>"
value="undefined"
name="parental_advisory" name="parental_advisory"
isChecked="<?= $episode->parental_advisory === null ? 'true' : 'false' ?>" ><?= lang('Episode.form.parental_advisory.undefined') ?></x-Forms.RadioButton> value="<?= $episode->parental_advisory ?>"
<x-Forms.RadioButton options="<?= esc(json_encode([
value="clean" [
name="parental_advisory" 'label' => lang('Episode.form.parental_advisory.undefined'),
isChecked="<?= $episode->parental_advisory === 'clean' ? 'true' : 'false' ?>" ><?= lang('Episode.form.parental_advisory.clean') ?></x-Forms.RadioButton> 'value' => 'undefined',
<x-Forms.RadioButton ],
value="explicit" [
name="parental_advisory" 'label' => lang('Episode.form.parental_advisory.clean'),
isChecked="<?= $episode->parental_advisory === 'explicit' ? 'true' : 'false' ?>" ><?= lang('Episode.form.parental_advisory.explicit') ?></x-Forms.RadioButton> 'value' => 'clean',
</fieldset> ],
[
'label' => lang('Episode.form.parental_advisory.explicit'),
'value' => 'explicit',
],
])) ?>"
isRequired="true"
/>
</x-Forms.Section> </x-Forms.Section>

View File

@ -24,7 +24,7 @@
> >
<x-Forms.Field <x-Forms.Field
as="MultiSelect" as="SelectMulti"
id="persons" id="persons"
name="persons[]" name="persons[]"
label="<?= esc(lang('Person.episode_form.persons')) ?>" label="<?= esc(lang('Person.episode_form.persons')) ?>"
@ -35,7 +35,7 @@
/> />
<x-Forms.Field <x-Forms.Field
as="MultiSelect" as="SelectMulti"
id="roles" id="roles"
name="roles[]" name="roles[]"
label="<?= esc(lang('Person.episode_form.roles')) ?>" label="<?= esc(lang('Person.episode_form.roles')) ?>"

View File

@ -42,7 +42,7 @@
<x-Forms.RadioButton <x-Forms.RadioButton
value="landscape" value="landscape"
name="format" name="format"
isChecked="true" isSelected="true"
isRequired="true" isRequired="true"
hint="<?= esc(lang('VideoClip.form.format.landscape_hint')) ?>"><?= lang('VideoClip.format.landscape') ?></x-Forms.RadioButton> hint="<?= esc(lang('VideoClip.form.format.landscape_hint')) ?>"><?= lang('VideoClip.format.landscape') ?></x-Forms.RadioButton>
<x-Forms.RadioButton <x-Forms.RadioButton
@ -65,7 +65,7 @@
value="<?= esc($themeName) ?>" value="<?= esc($themeName) ?>"
name="theme" name="theme"
isRequired="true" isRequired="true"
isChecked="<?= $themeName === 'pine' ? 'true' : 'false' ?>" isSelected="<?= $themeName === 'pine' ? 'true' : 'false' ?>"
style="--color-accent-base: <?= $colors['preview']?>; --color-background-preview: <?= $colors['preview-background'] ?>"><?= lang('Settings.theme.' . $themeName) ?></x-Forms.ColorRadioButton> style="--color-accent-base: <?= $colors['preview']?>; --color-background-preview: <?= $colors['preview-background'] ?>"><?= lang('Settings.theme.' . $themeName) ?></x-Forms.ColorRadioButton>
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>

View File

@ -1,14 +1,144 @@
<form method="POST" action="<?= $action ?>" class="flex flex-col max-w-sm gap-4" > <form method="POST" action="<?= $action ?>" class="flex flex-col max-w-xl gap-4 p-4 sm:p-6 md:p-8 bg-elevated border-3 border-subtle rounded-xl" >
<?= csrf_field() ?> <?= csrf_field() ?>
<?php $hasDatetime = false; ?>
<?php foreach ($plugin->getSettingsFields($type) as $field): ?> <?php foreach ($plugin->getSettingsFields($type) as $field): ?>
<x-Forms.Field <?php switch ($field->type): case 'checkbox': ?>
name="<?= esc($field->key) ?>" <x-Forms.Checkbox
label="<?= esc($field->label) ?>" name="<?= $field->key ?>"
hint="<?= esc($field->hint) ?>" hint="<?= esc($field->hint) ?>"
helper="<?= esc($field->helper) ?>" isChecked="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ? 'true' : 'false' ?>"
required="<?= $field->optional ? 'false' : 'true' ?>" ><?= $field->label ?></x-Forms.Checkbox>
value="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ?>" <?php break;
/> case 'toggler': ?>
<x-Forms.Toggler
name="<?= $field->key ?>"
hint="<?= esc($field->hint) ?>"
isChecked="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ? 'true' : 'false' ?>"
><?= $field->label ?></x-Forms.Toggler>
<?php break;
case 'radio-group': ?>
<x-Forms.RadioGroup
label="<?= $field->label ?>"
name="<?= $field->key ?>"
options="<?= esc(json_encode($field->getOptionsArray())) ?>"
hint="<?= esc($field->hint) ?>"
helper="<?= esc($field->helper) ?>"
isRequired="<?= $field->optional ? 'false' : 'true' ?>"
value="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ?>"
/>
<?php break;
case 'select': ?>
<x-Forms.Field
as="Select"
name="<?= $field->key ?>"
label="<?= $field->label ?>"
options="<?= esc(json_encode($field->getOptionsArray())) ?>"
hint="<?= esc($field->hint) ?>"
helper="<?= esc($field->helper) ?>"
isRequired="<?= $field->optional ? 'false' : 'true' ?>"
value="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ?>"
/>
<?php break;
case 'select-multiple': ?>
<x-Forms.Field
as="SelectMulti"
name="<?= $field->key ?>"
label="<?= $field->label ?>"
hint="<?= esc($field->hint) ?>"
helper="<?= esc($field->helper) ?>"
options="<?= esc(json_encode($field->getOptionsArray())) ?>"
isRequired="<?= $field->optional ? 'false' : 'true' ?>"
value="<?= esc(json_encode(get_plugin_option($plugin->getKey(), $field->key, $context))) ?>"
/>
<?php break;
case 'email': ?>
<x-Forms.Field
as="Input"
type="email"
name="<?= $field->key ?>"
label="<?= esc($field->label) ?>"
hint="<?= esc($field->hint) ?>"
helper="<?= esc($field->helper) ?>"
isRequired="<?= $field->optional ? 'false' : 'true' ?>"
value="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ?>"
/>
<?php break;
case 'url': ?>
<x-Forms.Field
as="Input"
type="url"
placeholder="https://…"
name="<?= $field->key ?>"
label="<?= esc($field->label) ?>"
hint="<?= esc($field->hint) ?>"
helper="<?= esc($field->helper) ?>"
isRequired="<?= $field->optional ? 'false' : 'true' ?>"
value="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ?>"
/>
<?php break;
case 'number': ?>
<x-Forms.Field
as="Input"
type="number"
name="<?= $field->key ?>"
label="<?= esc($field->label) ?>"
hint="<?= esc($field->hint) ?>"
helper="<?= esc($field->helper) ?>"
isRequired="<?= $field->optional ? 'false' : 'true' ?>"
value="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ?>"
/>
<?php break;
case 'textarea': ?>
<x-Forms.Field
as="Textarea"
name="<?= $field->key ?>"
label="<?= esc($field->label) ?>"
hint="<?= esc($field->hint) ?>"
helper="<?= esc($field->helper) ?>"
isRequired="<?= $field->optional ? 'false' : 'true' ?>"
value="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ?>"
/>
<?php break;
case 'markdown': ?>
<x-Forms.Field
as="MarkdownEditor"
name="<?= $field->key ?>"
label="<?= esc($field->label) ?>"
hint="<?= esc($field->hint) ?>"
helper="<?= esc($field->helper) ?>"
isRequired="<?= $field->optional ? 'false' : 'true' ?>"
value="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ?>"
/>
<?php break;
case 'datetime': ?>
<?php $hasDatetime = true ?>
<x-Forms.Field
as="DatetimePicker"
name="<?= $field->key ?>"
label="<?= esc($field->label) ?>"
hint="<?= esc($field->hint) ?>"
helper="<?= esc($field->helper) ?>"
isRequired="<?= $field->optional ? 'false' : 'true' ?>"
value="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ?>"
/>
<?php break;
default: ?>
<x-Forms.Field
as="Input"
name="<?= $field->key ?>"
label="<?= esc($field->label) ?>"
hint="<?= esc($field->hint) ?>"
helper="<?= esc($field->helper) ?>"
isRequired="<?= $field->optional ? 'false' : 'true' ?>"
value="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ?>"
/>
<?php endswitch; ?>
<?php endforeach; ?> <?php endforeach; ?>
<?php if ($hasDatetime): ?>
<input type="hidden" name="client_timezone" value="UTC" />
<?php endif; ?>
<x-Button class="self-end mt-4" variant="primary" type="submit"><?= lang('Common.forms.save') ?></x-Button> <x-Button class="self-end mt-4" variant="primary" type="submit"><?= lang('Common.forms.save') ?></x-Button>
</form> </form>

View File

@ -42,7 +42,7 @@
]) ?>" data-tooltip="bottom"><?= lang('Platforms.register') ?></a> ]) ?>" data-tooltip="bottom"><?= lang('Platforms.register') ?></a>
<?php endif; ?> <?php endif; ?>
</div> </div>
<fieldset> <fieldset class="flex flex-col">
<x-Forms.Field <x-Forms.Field
label="<?= esc(lang('Platforms.your_link')) ?>" label="<?= esc(lang('Platforms.your_link')) ?>"
class="w-full mt-4" class="w-full mt-4"

View File

@ -41,41 +41,46 @@
isRequired="true" isRequired="true"
disallowList="header,quote" /> disallowList="header,quote" />
<fieldset> <x-Forms.RadioGroup
<legend><?= lang('Podcast.form.type.label') ?></legend> label="<?= lang('Podcast.form.type.label') ?>"
<div class="flex gap-2"> name="type"
<x-Forms.RadioButton options="<?= esc(json_encode([
value="episodic" [
name="type" 'label' => lang('Podcast.form.type.episodic'),
hint="<?= esc(lang('Podcast.form.type.episodic_hint')) ?>" 'value' => 'episodic',
isChecked="true'" ><?= lang('Podcast.form.type.episodic') ?></x-Forms.RadioButton> 'hint' => lang('Podcast.form.type.episodic_hint'),
<x-Forms.RadioButton ],
value="serial" [
name="type" 'label' => lang('Podcast.form.type.serial'),
hint="<?= esc(lang('Podcast.form.type.serial_hint')) ?>" 'value' => 'serial',
isChecked="false" ><?= lang('Podcast.form.type.serial') ?></x-Forms.RadioButton> 'hint' => lang('Podcast.form.type.serial_hint'),
</div> ],
</fieldset> ])) ?>"
<fieldset> isRequired="true"
<legend><?= lang('Podcast.form.medium.label') ?></legend> />
<div class="flex gap-2">
<x-Forms.RadioButton <x-Forms.RadioGroup
value="podcast" label="<?= lang('Podcast.form.medium.label') ?>"
name="medium" name="medium"
hint="<?= esc(lang('Podcast.form.medium.podcast_hint')) ?>" options="<?= esc(json_encode([
isChecked="true" ><?= lang('Podcast.form.medium.podcast') ?></x-Forms.RadioButton> [
<x-Forms.RadioButton 'label' => lang('Podcast.form.medium.podcast'),
value="music" 'value' => 'podcast',
name="medium" 'hint' => lang('Podcast.form.medium.podcast_hint'),
hint="<?= esc(lang('Podcast.form.medium.music_hint')) ?>" ],
isChecked="false" ><?= lang('Podcast.form.medium.music') ?></x-Forms.RadioButton> [
<x-Forms.RadioButton 'label' => lang('Podcast.form.medium.music'),
value="audiobook" 'value' => 'music',
name="medium" 'hint' => lang('Podcast.form.medium.music_hint'),
hint="<?= esc(lang('Podcast.form.medium.audiobook_hint')) ?>" ],
isChecked="false" ><?= lang('Podcast.form.medium.audiobook') ?></x-Forms.RadioButton> [
</div> 'label' => lang('Podcast.form.medium.audiobook'),
</fieldset> 'value' => 'audiobook',
'hint' => lang('Podcast.form.medium.audiobook_hint'),
],
])) ?>"
isRequired="true"
/>
</x-Forms.Section> </x-Forms.Section>
<x-Forms.Section <x-Forms.Section
@ -86,7 +91,7 @@
as="Select" as="Select"
name="language" name="language"
label="<?= esc(lang('Podcast.form.language')) ?>" label="<?= esc(lang('Podcast.form.language')) ?>"
selected="<?= $browserLang ?>" defaultValue="<?= $browserLang ?>"
isRequired="true" isRequired="true"
options="<?= esc(json_encode($languageOptions)) ?>" /> options="<?= esc(json_encode($languageOptions)) ?>" />
@ -98,29 +103,32 @@
options="<?= esc(json_encode($categoryOptions)) ?>" /> options="<?= esc(json_encode($categoryOptions)) ?>" />
<x-Forms.Field <x-Forms.Field
as="MultiSelect" as="SelectMulti"
name="other_categories[]" name="other_categories[]"
label="<?= esc(lang('Podcast.form.other_categories')) ?>" label="<?= esc(lang('Podcast.form.other_categories')) ?>"
data-max-item-count="2" data-max-item-count="2"
options="<?= esc(json_encode($categoryOptions)) ?>" /> options="<?= esc(json_encode($categoryOptions)) ?>" />
<fieldset class="mb-4"> <x-Forms.RadioGroup
<legend><?= lang('Podcast.form.parental_advisory.label') ?><x-Hint class="ml-1"><?= lang('Podcast.form.parental_advisory.hint') ?></x-Hint></legend> label="<?= lang('Podcast.form.parental_advisory.label') ?>"
<div class="flex gap-2"> hint="<?= lang('Podcast.form.parental_advisory.hint') ?>"
<x-Forms.RadioButton name="parental_advisory"
value="undefined" options="<?= esc(json_encode([
name="parental_advisory" [
isChecked="true" ><?= lang('Podcast.form.parental_advisory.undefined') ?></x-Forms.RadioButton> 'label' => lang('Podcast.form.parental_advisory.undefined'),
<x-Forms.RadioButton 'value' => 'undefined',
value="clean" ],
name="parental_advisory" [
isChecked="false" ><?= lang('Podcast.form.parental_advisory.clean') ?></x-Forms.RadioButton> 'label' => lang('Podcast.form.parental_advisory.clean'),
<x-Forms.RadioButton 'value' => 'clean',
value="explicit" ],
name="parental_advisory" [
isChecked="false" ><?= lang('Podcast.form.parental_advisory.explicit') ?></x-Forms.RadioButton> 'label' => lang('Podcast.form.parental_advisory.explicit'),
</div> 'value' => 'explicit',
</fieldset> ],
])) ?>"
isRequired="true"
/>
</x-Forms.Section> </x-Forms.Section>
<x-Forms.Section <x-Forms.Section

View File

@ -64,41 +64,48 @@
isRequired="true" isRequired="true"
disallowList="header,quote" /> disallowList="header,quote" />
<fieldset> <x-Forms.RadioGroup
<legend><?= lang('Podcast.form.type.label') ?></legend> label="<?= lang('Podcast.form.type.label') ?>"
<div class="flex gap-2"> name="type"
<x-Forms.RadioButton value="<?= $podcast->type ?>"
value="episodic" options="<?= esc(json_encode([
name="type" [
hint="<?= esc(lang('Podcast.form.type.episodic_hint')) ?>" 'label' => lang('Podcast.form.type.episodic'),
isChecked="<?= $podcast->type === 'episodic' ? 'true' : 'false' ?>" ><?= lang('Podcast.form.type.episodic') ?></x-Forms.RadioButton> 'value' => 'episodic',
<x-Forms.RadioButton 'hint' => lang('Podcast.form.type.episodic_hint'),
value="serial" ],
name="type" [
hint="<?= esc(lang('Podcast.form.type.serial_hint')) ?>" 'label' => lang('Podcast.form.type.serial'),
isChecked="<?= $podcast->type === 'serial' ? 'true' : 'false' ?>" ><?= lang('Podcast.form.type.serial') ?></x-Forms.RadioButton> 'value' => 'serial',
</div> 'hint' => lang('Podcast.form.type.serial_hint'),
</fieldset> ],
<fieldset> ])) ?>"
<legend><?= lang('Podcast.form.medium.label') ?><x-Hint class="ml-1"><?= lang('Podcast.form.medium.hint') ?></x-Hint></legend> isRequired="true"
<div class="flex gap-2"> />
<x-Forms.RadioButton
value="podcast" <x-Forms.RadioGroup
name="medium" label="<?= lang('Podcast.form.medium.label') ?>"
hint="<?= esc(lang('Podcast.form.medium.podcast_hint')) ?>" name="medium"
isChecked="<?= $podcast->medium === 'podcast' ? 'true' : 'false' ?>" ><?= lang('Podcast.form.medium.podcast') ?></x-Forms.RadioButton> value="<?= $podcast->medium ?>"
<x-Forms.RadioButton options="<?= esc(json_encode([
value="music" [
name="medium" 'label' => lang('Podcast.form.medium.podcast'),
hint="<?= esc(lang('Podcast.form.medium.music_hint')) ?>" 'value' => 'podcast',
isChecked="<?= $podcast->medium === 'music' ? 'true' : 'false' ?>" ><?= lang('Podcast.form.medium.music') ?></x-Forms.RadioButton> 'hint' => lang('Podcast.form.medium.podcast_hint'),
<x-Forms.RadioButton ],
value="audiobook" [
name="medium" 'label' => lang('Podcast.form.medium.music'),
hint="<?= esc(lang('Podcast.form.medium.audiobook_hint')) ?>" 'value' => 'music',
isChecked="<?= $podcast->medium === 'audiobook' ? 'true' : 'false' ?>" ><?= lang('Podcast.form.medium.audiobook') ?></x-Forms.RadioButton> 'hint' => lang('Podcast.form.medium.music_hint'),
</div> ],
</fieldset> [
'label' => lang('Podcast.form.medium.audiobook'),
'value' => 'audiobook',
'hint' => lang('Podcast.form.medium.audiobook_hint'),
],
])) ?>"
isRequired="true"
/>
</x-Forms.Section> </x-Forms.Section>
<x-Forms.Section <x-Forms.Section
@ -109,7 +116,7 @@
as="Select" as="Select"
name="language" name="language"
label="<?= esc(lang('Podcast.form.language')) ?>" label="<?= esc(lang('Podcast.form.language')) ?>"
selected="<?= $podcast->language_code ?>" value="<?= $podcast->language_code ?>"
options="<?= esc(json_encode($languageOptions)) ?>" options="<?= esc(json_encode($languageOptions)) ?>"
isRequired="true" /> isRequired="true" />
@ -117,35 +124,39 @@
as="Select" as="Select"
name="category" name="category"
label="<?= esc(lang('Podcast.form.category')) ?>" label="<?= esc(lang('Podcast.form.category')) ?>"
selected="<?= $podcast->category_id ?>" value="<?= $podcast->category_id ?>"
options="<?= esc(json_encode($categoryOptions)) ?>" options="<?= esc(json_encode($categoryOptions)) ?>"
isRequired="true" /> isRequired="true" />
<x-Forms.Field <x-Forms.Field
as="MultiSelect" as="SelectMulti"
name="other_categories[]" name="other_categories[]"
label="<?= esc(lang('Podcast.form.other_categories')) ?>" label="<?= esc(lang('Podcast.form.other_categories')) ?>"
data-max-item-count="2" data-max-item-count="2"
selected="<?= esc(json_encode($podcast->other_categories_ids)) ?>" value="<?= esc(json_encode($podcast->other_categories_ids)) ?>"
options="<?= esc(json_encode($categoryOptions)) ?>" /> options="<?= esc(json_encode($categoryOptions)) ?>" />
<fieldset class="mb-4"> <x-Forms.RadioGroup
<legend><?= lang('Podcast.form.parental_advisory.label') ?><x-Hint class="ml-1"><?= lang('Podcast.form.parental_advisory.hint') ?></x-Hint></legend> label="<?= lang('Podcast.form.parental_advisory.label') ?>"
<div class="flex gap-2"> hint="<?= lang('Podcast.form.parental_advisory.hint') ?>"
<x-Forms.RadioButton name="parental_advisory"
value="undefined" value="<?= $podcast->parental_advisory ?>"
name="parental_advisory" options="<?= esc(json_encode([
isChecked="<?= $podcast->parental_advisory === null ? 'true' : 'false' ?>" ><?= lang('Podcast.form.parental_advisory.undefined') ?></x-Forms.RadioButton> [
<x-Forms.RadioButton 'label' => lang('Podcast.form.parental_advisory.undefined'),
value="clean" 'value' => 'undefined',
name="parental_advisory" ],
isChecked="<?= $podcast->parental_advisory === 'clean' ? 'true' : 'false' ?>" ><?= lang('Podcast.form.parental_advisory.clean') ?></x-Forms.RadioButton> [
<x-Forms.RadioButton 'label' => lang('Podcast.form.parental_advisory.clean'),
value="explicit" 'value' => 'clean',
name="parental_advisory" ],
isChecked="<?= $podcast->parental_advisory === 'explicit' ? 'true' : 'false' ?>" ><?= lang('Podcast.form.parental_advisory.explicit') ?></x-Forms.RadioButton> [
</div> 'label' => lang('Podcast.form.parental_advisory.explicit'),
</fieldset> 'value' => 'explicit',
],
])) ?>"
isRequired="true"
/>
</x-Forms.Section> </x-Forms.Section>
<x-Forms.Section <x-Forms.Section

View File

@ -24,7 +24,7 @@
> >
<x-Forms.Field <x-Forms.Field
as="MultiSelect" as="SelectMulti"
id="persons" id="persons"
name="persons[]" name="persons[]"
label="<?= esc(lang('Person.podcast_form.persons')) ?>" label="<?= esc(lang('Person.podcast_form.persons')) ?>"
@ -34,7 +34,7 @@
isRequired="true" /> isRequired="true" />
<x-Forms.Field <x-Forms.Field
as="MultiSelect" as="SelectMulti"
id="roles" id="roles"
name="roles[]" name="roles[]"
label="<?= esc(lang('Person.podcast_form.roles')) ?>" label="<?= esc(lang('Person.podcast_form.roles')) ?>"

View File

@ -26,7 +26,7 @@
class="theme-<?= $themeName ?> mx-auto" class="theme-<?= $themeName ?> mx-auto"
value="<?= esc($themeName) ?>" value="<?= esc($themeName) ?>"
name="theme" name="theme"
isChecked="<?= $themeName === service('settings') isSelected="<?= $themeName === service('settings')
->get('App.theme') ? 'true' : 'false' ?>" ><?= lang('Settings.theme.' . $themeName) ?></x-Forms.ColorRadioButton> ->get('App.theme') ? 'true' : 'false' ?>" ><?= lang('Settings.theme.' . $themeName) ?></x-Forms.ColorRadioButton>
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>