diff --git a/app/Models/CategoryModel.php b/app/Models/CategoryModel.php index 8847d2c1..fdcf85e7 100644 --- a/app/Models/CategoryModel.php +++ b/app/Models/CategoryModel.php @@ -65,12 +65,17 @@ class CategoryModel extends Model $options = array_reduce( $categories, static function (array $result, Category $category): array { - $result[$category->id] = ''; + $label = ''; 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; }, [], diff --git a/app/Models/LanguageModel.php b/app/Models/LanguageModel.php index bec367f1..bf4fd0e1 100644 --- a/app/Models/LanguageModel.php +++ b/app/Models/LanguageModel.php @@ -56,7 +56,10 @@ class LanguageModel extends Model $options = array_reduce( $languages, static function (array $result, Language $language): array { - $result[$language->code] = $language->native_name; + $result[] = [ + 'value' => $language->code, + 'label' => $language->native_name, + ]; return $result; }, [], diff --git a/app/Resources/js/admin.ts b/app/Resources/js/admin.ts index c4fe7dea..40d0f695 100644 --- a/app/Resources/js/admin.ts +++ b/app/Resources/js/admin.ts @@ -8,7 +8,7 @@ import Dropdown from "./modules/Dropdown"; import HotKeys from "./modules/HotKeys"; import "./modules/markdown-preview"; import "./modules/markdown-write-preview"; -import MultiSelect from "./modules/MultiSelect"; +import SelectMulti from "./modules/SelectMulti"; import "./modules/permalink-edit"; import "./modules/play-soundbite"; import PublishMessageWarning from "./modules/PublishMessageWarning"; @@ -26,7 +26,7 @@ import "./modules/xml-editor"; Dropdown(); Tooltip(); Select(); -MultiSelect(); +SelectMulti(); Slugify(); SidebarToggler(); ClientTimezone(); diff --git a/app/Resources/js/modules/MultiSelect.ts b/app/Resources/js/modules/SelectMulti.ts similarity index 96% rename from app/Resources/js/modules/MultiSelect.ts rename to app/Resources/js/modules/SelectMulti.ts index 94a87f16..a1581a9b 100644 --- a/app/Resources/js/modules/MultiSelect.ts +++ b/app/Resources/js/modules/SelectMulti.ts @@ -1,6 +1,6 @@ import Choices from "choices.js"; -const MultiSelect = (): void => { +const SelectMulti = (): void => { // Pass single element const multiSelects: NodeListOf = document.querySelectorAll("select[multiple]"); @@ -49,4 +49,4 @@ const MultiSelect = (): void => { } }; -export default MultiSelect; +export default SelectMulti; diff --git a/app/Resources/styles/choices.css b/app/Resources/styles/choices.css index f9f59a34..86bda4f8 100644 --- a/app/Resources/styles/choices.css +++ b/app/Resources/styles/choices.css @@ -25,7 +25,10 @@ } .choices [hidden] { - display: none !important; + position: absolute; + opacity: 0; + z-index: -9999; + pointer-events: none; } .choices[data-type*="select-one"] { diff --git a/app/Resources/styles/switch.css b/app/Resources/styles/switch.css index fb80c4dc..3065dc40 100644 --- a/app/Resources/styles/switch.css +++ b/app/Resources/styles/switch.css @@ -11,13 +11,13 @@ } &:checked + .form-switch-slider::before { - @apply transform translate-x-8; + @apply transform translate-x-6; } &: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 { @@ -30,7 +30,7 @@ } .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 { @apply w-12 h-6; @@ -56,10 +56,11 @@ } &::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"); - top: 3px; + --tw-translate-x: 1.125rem; + + 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; } } diff --git a/app/Views/Components/Forms/Checkbox.php b/app/Views/Components/Forms/Checkbox.php index 9ffafc8b..517ff38a 100644 --- a/app/Views/Components/Forms/Checkbox.php +++ b/app/Views/Components/Forms/Checkbox.php @@ -22,12 +22,12 @@ class Checkbox extends FormComponent { $checkboxInput = form_checkbox( [ - 'id' => $this->value, + 'id' => $this->id, 'name' => $this->name, 'class' => 'form-checkbox bg-elevated text-accent-base border-contrast border-3 w-6 h-6', ], '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([ diff --git a/app/Views/Components/Forms/ColorRadioButton.php b/app/Views/Components/Forms/ColorRadioButton.php index b562d71e..9e2ebcd4 100644 --- a/app/Views/Components/Forms/ColorRadioButton.php +++ b/app/Views/Components/Forms/ColorRadioButton.php @@ -6,13 +6,13 @@ namespace App\Views\Components\Forms; class ColorRadioButton extends FormComponent { - protected array $props = ['isChecked']; + protected array $props = ['isSelected']; protected array $casts = [ - 'isChecked' => 'boolean', + 'isSelected' => 'boolean', ]; - protected bool $isChecked = false; + protected bool $isSelected = false; public function render(): string { @@ -29,7 +29,7 @@ class ColorRadioButton extends FormComponent $radioInput = form_radio( $data, $this->value, - old($this->name) ? old($this->name) === $this->value : $this->isChecked, + old($this->name) ? old($this->name) === $this->value : $this->isSelected, ); return << $this->name, 'class' => 'rounded-l-lg border-0 border-rounded-r-none flex-1 focus:ring-0', 'data-input' => '', ], old($this->name, $this->value)); diff --git a/app/Views/Components/Forms/Field.php b/app/Views/Components/Forms/Field.php index 784b3a91..3aaa64ee 100644 --- a/app/Views/Components/Forms/Field.php +++ b/app/Views/Components/Forms/Field.php @@ -66,8 +66,8 @@ class Field extends Component unset($this->attributes['class']); $this->attributes['name'] = $this->name; - $this->attributes['isRequired'] = $this->isRequired ? 'true' : 'false'; - $this->attributes['isReadonly'] = $this->isReadonly ? 'true' : 'false'; + $this->attributes['isRequired'] = var_export($this->isRequired, true); + $this->attributes['isReadonly'] = var_export($this->isReadonly, true); $element = __NAMESPACE__ . '\\' . $this->as; $fieldElement = new $element($this->attributes); @@ -75,7 +75,7 @@ class Field extends Component
{$label->render()} {$helperText} -
+
{$fieldElement->render()}
diff --git a/app/Views/Components/Forms/FormComponent.php b/app/Views/Components/Forms/FormComponent.php index 2217b2af..b34d2bd4 100644 --- a/app/Views/Components/Forms/FormComponent.php +++ b/app/Views/Components/Forms/FormComponent.php @@ -25,7 +25,10 @@ abstract class FormComponent extends Component protected string $name; - protected string $value = ''; + /** + * @var null|string|list + */ + protected null|string|array $value = null; protected bool $isRequired = false; @@ -57,9 +60,4 @@ abstract class FormComponent extends Component $this->attributes['readonly'] = 'readonly'; } } - - public function setValue(string $value): void - { - $this->value = htmlspecialchars_decode($value, ENT_QUOTES); - } } diff --git a/app/Views/Components/Forms/Input.php b/app/Views/Components/Forms/Input.php index 5ca7dfb9..6c4017c2 100644 --- a/app/Views/Components/Forms/Input.php +++ b/app/Views/Components/Forms/Input.php @@ -29,6 +29,6 @@ class Input extends FormComponent $this->attributes['type'] = $this->type; $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)); } } diff --git a/app/Views/Components/Forms/Label.php b/app/Views/Components/Forms/Label.php index 808458ff..bdd97ae5 100644 --- a/app/Views/Components/Forms/Label.php +++ b/app/Views/Components/Forms/Label.php @@ -34,6 +34,8 @@ class Label extends Component 'slot' => $this->hint, ]))->render(); + $this->attributes['for'] = $this->for; + return <<getStringifiedAttributes()}>{$this->slot}{$optionalText}{$hint} HTML; diff --git a/app/Views/Components/Forms/MarkdownEditor.php b/app/Views/Components/Forms/MarkdownEditor.php index b4c253ba..8952b53f 100644 --- a/app/Views/Components/Forms/MarkdownEditor.php +++ b/app/Views/Components/Forms/MarkdownEditor.php @@ -28,7 +28,7 @@ class MarkdownEditor extends FormComponent $textarea = form_textarea( $this->attributes, - old($this->name, $this->value) + old($this->name, (string) $this->value) ); $markdownIcon = icon('markdown-fill', [ 'class' => 'mr-1 text-lg opacity-40', diff --git a/app/Views/Components/Forms/MultiSelect.php b/app/Views/Components/Forms/MultiSelect.php deleted file mode 100644 index 327c0bff..00000000 --- a/app/Views/Components/Forms/MultiSelect.php +++ /dev/null @@ -1,43 +0,0 @@ - 'array', - 'selected' => 'array', - ]; - - /** - * @var array - */ - 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); - } -} diff --git a/app/Views/Components/Forms/RadioButton.php b/app/Views/Components/Forms/RadioButton.php index c36931db..3da37392 100644 --- a/app/Views/Components/Forms/RadioButton.php +++ b/app/Views/Components/Forms/RadioButton.php @@ -11,10 +11,10 @@ class RadioButton extends FormComponent protected array $props = ['isChecked', 'hint']; protected array $casts = [ - 'isChecked' => 'boolean', + 'isSelected' => 'boolean', ]; - protected bool $isChecked = false; + protected bool $isSelected = false; protected string $hint = ''; @@ -33,7 +33,7 @@ class RadioButton extends FormComponent $radioInput = form_radio( $data, $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([ diff --git a/app/Views/Components/Forms/RadioGroup.php b/app/Views/Components/Forms/RadioGroup.php new file mode 100644 index 00000000..907002e3 --- /dev/null +++ b/app/Views/Components/Forms/RadioGroup.php @@ -0,0 +1,67 @@ + '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 <<getStringifiedAttributes()}> + {$this->label}{$hint} + {$helperText} +
{$options}
+ + HTML; + } +} diff --git a/app/Views/Components/Forms/Section.php b/app/Views/Components/Forms/Section.php index 0b8bd2ab..4055985c 100644 --- a/app/Views/Components/Forms/Section.php +++ b/app/Views/Components/Forms/Section.php @@ -18,7 +18,7 @@ class Section extends Component { $subtitle = $this->subtitle === '' ? '' : '

' . $this->subtitle . '

'; - $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 <<getStringifiedAttributes()}> diff --git a/app/Views/Components/Forms/Select.php b/app/Views/Components/Forms/Select.php index e1af514e..a364a231 100644 --- a/app/Views/Components/Forms/Select.php +++ b/app/Views/Components/Forms/Select.php @@ -6,7 +6,7 @@ namespace App\Views\Components\Forms; class Select extends FormComponent { - protected array $props = ['options', 'selected']; + protected array $props = ['options', 'defaultValue']; protected array $casts = [ 'options' => 'array', @@ -17,7 +17,7 @@ class Select extends FormComponent */ protected array $options = []; - protected string $selected = ''; + protected string $defaultValue = ''; public function render(): string { @@ -29,8 +29,18 @@ class Select extends FormComponent 'data-no-choices-text' => lang('Common.forms.multiSelect.noChoicesText'), '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 .= ''; + } + + $this->attributes['name'] = $this->name; + + return <<getStringifiedAttributes()}>{$options} + HTML; } } diff --git a/app/Views/Components/Forms/SelectMulti.php b/app/Views/Components/Forms/SelectMulti.php new file mode 100644 index 00000000..4251724d --- /dev/null +++ b/app/Views/Components/Forms/SelectMulti.php @@ -0,0 +1,53 @@ + 'array', + 'options' => 'array', + 'defaultValue' => 'array', + ]; + + /** + * @var array + */ + protected array $options = []; + + /** + * @var list + */ + 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 .= ''; + } + + $this->attributes['name'] = $this->name . '[]'; + + return <<getStringifiedAttributes()}>{$options} + HTML; + } +} diff --git a/app/Views/Components/Forms/Toggler.php b/app/Views/Components/Forms/Toggler.php index 07e8df7e..342891b7 100644 --- a/app/Views/Components/Forms/Toggler.php +++ b/app/Views/Components/Forms/Toggler.php @@ -14,27 +14,23 @@ class Toggler extends FormComponent 'isChecked' => 'boolean', ]; - /** - * @var 'base'|'small - */ - protected string $size = 'base'; - protected string $hint = ''; protected bool $isChecked = false; 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'); - $checkbox = form_checkbox([ - 'class' => 'form-switch', - ], 'yes', old($this->name) === 'yes' ? true : $this->isChecked); + $checkbox = form_checkbox( + [ + 'id' => $this->id, + 'name' => $this->name, + 'class' => 'form-switch', + ], + 'yes', + old($this->name) ? old($this->name) === 'yes' : $this->isChecked + ); $hint = $this->hint === '' ? '' : (new Hint([ 'class' => 'ml-1', @@ -43,9 +39,9 @@ class Toggler extends FormComponent return <<getStringifiedAttributes()}> - {$this->slot}{$hint} + {$this->slot}{$hint} {$checkbox} - + HTML; } diff --git a/modules/Plugins/Controllers/PluginController.php b/modules/Plugins/Controllers/PluginController.php index 6dc6ed11..d5a10683 100644 --- a/modules/Plugins/Controllers/PluginController.php +++ b/modules/Plugins/Controllers/PluginController.php @@ -10,7 +10,10 @@ use App\Models\EpisodeModel; use App\Models\PodcastModel; use CodeIgniter\Exceptions\PageNotFoundException; use CodeIgniter\HTTP\RedirectResponse; +use CodeIgniter\HTTP\URI; +use CodeIgniter\I18n\Time; use Modules\Admin\Controllers\BaseController; +use Modules\Plugins\Core\Markdown; use Modules\Plugins\Core\Plugins; class PluginController extends BaseController @@ -103,9 +106,41 @@ class PluginController extends BaseController throw PageNotFoundException::forPageNotFound(); } + // construct validation rules first + $rules = []; foreach ($plugin->getSettingsFields('general') as $field) { - $optionValue = $this->request->getPost($field->key); - $plugins->setOption($plugin, $field->key, $optionValue); + $typeRules = $plugins::FIELDS_VALIDATIONS[$field->type]; + 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() diff --git a/modules/Plugins/Core/BasePlugin.php b/modules/Plugins/Core/BasePlugin.php index 16524e34..b9fe8674 100644 --- a/modules/Plugins/Core/BasePlugin.php +++ b/modules/Plugins/Core/BasePlugin.php @@ -16,11 +16,11 @@ use League\CommonMark\Extension\SmartPunct\SmartPunctExtension; use League\CommonMark\MarkdownConverter; use Modules\Plugins\ExternalImageProcessor; use Modules\Plugins\ExternalLinkProcessor; +use Modules\Plugins\Manifest\Field; use Modules\Plugins\Manifest\Manifest; use Modules\Plugins\Manifest\Person; use Modules\Plugins\Manifest\Repository; use Modules\Plugins\Manifest\Settings; -use Modules\Plugins\Manifest\SettingsField; use RuntimeException; /** @@ -163,7 +163,7 @@ abstract class BasePlugin implements PluginInterface } /** - * @return SettingsField[] + * @return Field[] */ final public function getSettingsFields(string $type): array { diff --git a/modules/Plugins/Core/Markdown.php b/modules/Plugins/Core/Markdown.php new file mode 100644 index 00000000..0dd36b23 --- /dev/null +++ b/modules/Plugins/Core/Markdown.php @@ -0,0 +1,44 @@ +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); + } +} diff --git a/modules/Plugins/Core/Plugins.php b/modules/Plugins/Core/Plugins.php index a4a623ef..8919957f 100644 --- a/modules/Plugins/Core/Plugins.php +++ b/modules/Plugins/Core/Plugins.php @@ -23,6 +23,30 @@ class Plugins */ 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 */ diff --git a/modules/Plugins/Manifest/Field.php b/modules/Plugins/Manifest/Field.php new file mode 100644 index 00000000..ceeef387 --- /dev/null +++ b/modules/Plugins/Manifest/Field.php @@ -0,0 +1,64 @@ + '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; + } +} diff --git a/modules/Plugins/Manifest/Manifest.php b/modules/Plugins/Manifest/Manifest.php index 11275bdd..07323135 100644 --- a/modules/Plugins/Manifest/Manifest.php +++ b/modules/Plugins/Manifest/Manifest.php @@ -39,9 +39,6 @@ class Manifest extends ManifestObject 'repository' => 'permit_empty|is_list', ]; - /** - * @var array - */ protected const CASTS = [ 'authors' => [Person::class], 'homepage' => URI::class, diff --git a/modules/Plugins/Manifest/ManifestObject.php b/modules/Plugins/Manifest/ManifestObject.php index 0569fb3d..5be54363 100644 --- a/modules/Plugins/Manifest/ManifestObject.php +++ b/modules/Plugins/Manifest/ManifestObject.php @@ -39,6 +39,11 @@ abstract class ManifestObject throw new Exception('Undefined object property ' . static::class . '::' . $name); } + public function __isset(string $property): bool + { + return property_exists($this, $property); + } + public function load(): void { /** @var Validation $validation */ diff --git a/modules/Plugins/Manifest/Option.php b/modules/Plugins/Manifest/Option.php new file mode 100644 index 00000000..ff14f918 --- /dev/null +++ b/modules/Plugins/Manifest/Option.php @@ -0,0 +1,25 @@ + 'required|string', + 'value' => 'required|alpha_dash', + 'hint' => 'permit_empty|string', + ]; + + protected string $label; + + protected string $value; + + protected ?string $hint = null; +} diff --git a/modules/Plugins/Manifest/Settings.php b/modules/Plugins/Manifest/Settings.php index d627144b..464f99de 100644 --- a/modules/Plugins/Manifest/Settings.php +++ b/modules/Plugins/Manifest/Settings.php @@ -5,9 +5,9 @@ declare(strict_types=1); namespace Modules\Plugins\Manifest; /** - * @property SettingsField[] $general - * @property SettingsField[] $podcast - * @property SettingsField[] $episode + * @property Field[] $general + * @property Field[] $podcast + * @property Field[] $episode */ class Settings extends ManifestObject { @@ -21,23 +21,23 @@ class Settings extends ManifestObject * @var array */ protected const CASTS = [ - 'general' => [SettingsField::class], - 'podcast' => [SettingsField::class], - 'episode' => [SettingsField::class], + 'general' => [Field::class], + 'podcast' => [Field::class], + 'episode' => [Field::class], ]; /** - * @var SettingsField[] + * @var Field[] */ protected array $general = []; /** - * @var SettingsField[] + * @var Field[] */ protected array $podcast = []; /** - * @var SettingsField[] + * @var Field[] */ protected array $episode = []; } diff --git a/modules/Plugins/Manifest/SettingsField.php b/modules/Plugins/Manifest/SettingsField.php deleted file mode 100644 index f303bcdd..00000000 --- a/modules/Plugins/Manifest/SettingsField.php +++ /dev/null @@ -1,37 +0,0 @@ - '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; -} diff --git a/modules/Plugins/Manifest/schema.json b/modules/Plugins/Manifest/schema.json index 80283589..5d2b33e7 100644 --- a/modules/Plugins/Manifest/schema.json +++ b/modules/Plugins/Manifest/schema.json @@ -97,20 +97,26 @@ "general": { "type": "array", "items": { - "$ref": "#/$defs/settings-field" - } + "$ref": "#/$defs/field" + }, + "minItems": 1, + "uniqueItems": true }, "podcast": { "type": "array", "items": { - "$ref": "#/$defs/settings-field" - } + "$ref": "#/$defs/field" + }, + "minItems": 1, + "uniqueItems": true }, "episode": { "type": "array", "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", "properties": { "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" }, "key": { - "type": "string" + "type": "string", + "pattern": "^[A-Za-z]+[\\w\\-\\:\\.]*$" }, "label": { "type": "string" @@ -179,10 +218,56 @@ }, "optional": { "type": "boolean" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/$defs/option" + }, + "minItems": 1, + "uniqueItems": true } }, "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 + }, + "field-multiple-implies-options-is-required": { + "anyOf": [ + { + "not": { + "properties": { + "type": { + "anyOf": [ + { "const": "radio-group" }, + { "const": "select" }, + { "const": "select-multiple" } + ] + } + }, + "required": ["type"] + } + }, + { "required": ["options"] } + ] } } } diff --git a/themes/cp_admin/episode/create.php b/themes/cp_admin/episode/create.php index 791c6b34..65fdbf07 100644 --- a/themes/cp_admin/episode/create.php +++ b/themes/cp_admin/episode/create.php @@ -69,42 +69,49 @@ />
-
- - - - -
+ options=" lang('Episode.form.type.full'), + 'value' => 'full', + 'hint' => lang('Episode.form.type.full_hint'), + ], + [ + 'label' => lang('Episode.form.type.trailer'), + 'value' => 'trailer', + 'hint' => lang('Episode.form.type.trailer_hint'), + ], + [ + 'label' => lang('Episode.form.type.bonus'), + 'value' => 'bonus', + 'hint' => lang('Episode.form.type.bonus_hint'), + ], + ])) ?>" + isRequired="true" +/> -
- - - - -
- - + options=" lang('Episode.form.parental_advisory.undefined'), + 'value' => 'undefined', + ], + [ + 'label' => lang('Episode.form.parental_advisory.clean'), + 'value' => 'clean', + ], + [ + 'label' => lang('Episode.form.parental_advisory.explicit'), + 'value' => 'explicit', + ], + ])) ?>" + isRequired="true" +/> diff --git a/themes/cp_admin/episode/edit.php b/themes/cp_admin/episode/edit.php index 71115a13..2b35d43a 100644 --- a/themes/cp_admin/episode/edit.php +++ b/themes/cp_admin/episode/edit.php @@ -73,40 +73,51 @@ /> -
- - - - -
+ value="type ?>" + options=" lang('Episode.form.type.full'), + 'value' => 'full', + 'hint' => lang('Episode.form.type.full_hint'), + ], + [ + 'label' => lang('Episode.form.type.trailer'), + 'value' => 'trailer', + 'hint' => lang('Episode.form.type.trailer_hint'), + ], + [ + 'label' => lang('Episode.form.type.bonus'), + 'value' => 'bonus', + 'hint' => lang('Episode.form.type.bonus_hint'), + ], + ])) ?>" + isRequired="true" +/> -
- - - - -
+ value="parental_advisory ?>" + options=" lang('Episode.form.parental_advisory.undefined'), + 'value' => 'undefined', + ], + [ + 'label' => lang('Episode.form.parental_advisory.clean'), + 'value' => 'clean', + ], + [ + 'label' => lang('Episode.form.parental_advisory.explicit'), + 'value' => 'explicit', + ], + ])) ?>" + isRequired="true" +/> diff --git a/themes/cp_admin/episode/persons.php b/themes/cp_admin/episode/persons.php index 3f79a0ba..814f3754 100644 --- a/themes/cp_admin/episode/persons.php +++ b/themes/cp_admin/episode/persons.php @@ -24,7 +24,7 @@ > diff --git a/themes/cp_admin/plugins/_settings.php b/themes/cp_admin/plugins/_settings.php index a798efe2..a8157911 100644 --- a/themes/cp_admin/plugins/_settings.php +++ b/themes/cp_admin/plugins/_settings.php @@ -1,14 +1,144 @@ -
+ + getSettingsFields($type) as $field): ?> - + type): case 'checkbox': ?> + label ?> + + label ?> + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/themes/cp_admin/podcast/_platform.php b/themes/cp_admin/podcast/_platform.php index b83ddf0c..9afb4e97 100644 --- a/themes/cp_admin/podcast/_platform.php +++ b/themes/cp_admin/podcast/_platform.php @@ -42,7 +42,7 @@ ]) ?>" data-tooltip="bottom"> -
+
-
- -
- - -
-
-
- -
- - - -
-
+ + + @@ -98,29 +103,32 @@ options="" /> -
- -
- - - -
-
+
-
- -
- - -
-
-
- -
- - - -
-
+ + +
@@ -117,35 +124,39 @@ as="Select" name="category" label="" - selected="category_id ?>" + value="category_id ?>" options="" isRequired="true" /> -
- -
- - - -
-
+
get('App.theme') ? 'true' : 'false' ?>" >