refactor(plugins): create Field objects per field type in settings forms + handle rendering in class

update manifest.schema.json to have defaultValue type differ based on field type
This commit is contained in:
Yassine Doghri 2024-12-23 15:35:47 +00:00
parent d3a98db6d0
commit 34be5bccab
48 changed files with 3881 additions and 2559 deletions

View File

@ -19,6 +19,8 @@ class DropDeprecatedPodcastsFields extends BaseMigration
#[Override]
public function up(): void
{
// TODO: migrate data
$this->forge->dropColumn(
'podcasts',
'episode_description_footer_markdown,episode_description_footer_html,is_owner_email_removed_from_feed,medium,payment_pointer,verify_txt,custom_rss,partner_id,partner_link_url,partner_image_url'

View File

@ -55,7 +55,7 @@ use RuntimeException;
* @property string $language_code
* @property int $category_id
* @property Category|null $category
* @property string $other_categories_ids
* @property int[] $other_categories_ids
* @property Category[] $other_categories
* @property string|null $parental_advisory
* @property string|null $publisher
@ -111,7 +111,10 @@ class Podcast extends Entity
*/
protected ?array $other_categories = null;
protected string $other_categories_ids = '';
/**
* @var int[]
*/
protected array $other_categories_ids = [];
/**
* @var Episode[]|null
@ -523,10 +526,13 @@ class Podcast extends Entity
return $this->other_categories;
}
public function getOtherCategoriesIds(): string
/**
* @return int[]
*/
public function getOtherCategoriesIds(): array
{
if ($this->other_categories_ids === '') {
$this->other_categories_ids = implode(',', array_column($this->getOtherCategories(), 'id'));
if ($this->other_categories_ids === []) {
$this->other_categories_ids = array_column($this->getOtherCategories(), 'id');
}
return $this->other_categories_ids;

View File

@ -21,4 +21,9 @@ class OtherRules
{
return is_array($str);
}
public function is_string_or_list(mixed $str = null): bool
{
return is_string($str) || is_array($str);
}
}

View File

@ -10,12 +10,9 @@ class CodeEditor extends FormComponent
{
protected array $props = ['content', 'lang'];
/**
* @var array<string, string>
*/
protected array $attributes = [
'rows' => '6',
'class' => 'textarea',
'class' => 'bg-elevated w-full rounded-lg border-3 border-contrast focus:border-contrast focus-within:ring-accent transition',
];
protected string $lang = '';

View File

@ -26,9 +26,15 @@ abstract class FormComponent extends Component
protected string $name;
protected string $value = '';
/**
* @var string|string[]|null
*/
protected string|array|null $value = null;
protected string $defaultValue = '';
/**
* @var string|string[]
*/
protected string|array $defaultValue = '';
protected bool $isRequired = false;
@ -61,8 +67,16 @@ abstract class FormComponent extends Component
}
}
protected function getValue(): string
protected function getValue(): string|array
{
return old($this->name, $this->value === '' ? $this->defaultValue : $this->value);
$valueCast = $this->casts['value'] ?? '';
if ($valueCast === 'array') {
return old($this->name, in_array($this->value, [[], null], true) ? $this->defaultValue : $this->value) ?? [];
}
return old(
$this->name,
in_array($this->value, ['', null], true) ? $this->defaultValue : $this->value
) ?? '';
}
}

View File

@ -11,7 +11,9 @@ class SelectMulti extends FormComponent
protected array $props = ['options'];
protected array $casts = [
'options' => 'array',
'value' => 'array',
'defaultValue' => 'array',
'options' => 'array',
];
/**
@ -36,9 +38,9 @@ class SelectMulti extends FormComponent
$this->attributes = [...$defaultAttributes, ...$this->attributes];
$options = '';
$selected = explode(',', $this->getValue()) ?? [];
$selected = $this->getValue();
foreach ($this->options as $option) {
$options .= '<option ' . (array_key_exists('description', $option) ? 'data-label-description="' . $option['description'] . '" ' : '') . 'value="' . $option['value'] . '"' . (in_array((string) $option['value'], $selected, true) ? ' selected' : '') . '>' . $option['label'] . '</option>';
$options .= '<option ' . (array_key_exists('description', $option) ? 'data-label-description="' . $option['description'] . '" ' : '') . 'value="' . $option['value'] . '"' . (in_array($option['value'], $selected, true) ? ' selected' : '') . '>' . $option['label'] . '</option>';
}
$this->attributes['name'] = $this->name . '[]';

View File

@ -8,6 +8,10 @@ use Override;
class Textarea extends FormComponent
{
protected array $attributes = [
'rows' => '6',
];
public function setValue(string $value): void
{
$this->value = htmlspecialchars_decode($value);

View File

@ -9,7 +9,7 @@
"php": "^8.3",
"adaures/ipcat-php": "^v1.0.0",
"adaures/podcast-persons-taxonomy": "^v1.0.1",
"aws/aws-sdk-php": "^3.334.7",
"aws/aws-sdk-php": "^3.336.2",
"chrisjean/php-ico": "^1.0.4",
"cocur/slugify": "^v4.6.0",
"codeigniter4/framework": "v4.5.5",
@ -35,8 +35,8 @@
"codeigniter/phpstan-codeigniter": "v1.5.1",
"mikey179/vfsstream": "^v1.6.12",
"phpstan/extension-installer": "^1.4.3",
"phpstan/phpstan": "^2.0.3",
"phpunit/phpunit": "^11.5.1",
"phpstan/phpstan": "^2.0.4",
"phpunit/phpunit": "^11.5.2",
"rector/rector": "^2.0.3",
"symplify/coding-standard": "^12.2.3",
"symplify/easy-coding-standard": "^12.5.4"

64
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "8bdf144f383531caa188f1c211ec092a",
"content-hash": "f95311413c714e2dcef9364da4348b30",
"packages": [
{
"name": "adaures/ipcat-php",
@ -189,16 +189,16 @@
},
{
"name": "aws/aws-sdk-php",
"version": "3.334.7",
"version": "3.336.2",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
"reference": "8e0104e95a1edba209e077e6c4212b8cca04686f"
"reference": "954bfdfc048840ca34afe2a2e1cbcff6681989c4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/8e0104e95a1edba209e077e6c4212b8cca04686f",
"reference": "8e0104e95a1edba209e077e6c4212b8cca04686f",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/954bfdfc048840ca34afe2a2e1cbcff6681989c4",
"reference": "954bfdfc048840ca34afe2a2e1cbcff6681989c4",
"shasum": ""
},
"require": {
@ -275,9 +275,9 @@
"support": {
"forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
"issues": "https://github.com/aws/aws-sdk-php/issues",
"source": "https://github.com/aws/aws-sdk-php/tree/3.334.7"
"source": "https://github.com/aws/aws-sdk-php/tree/3.336.2"
},
"time": "2024-12-16T19:09:36+00:00"
"time": "2024-12-20T19:05:10+00:00"
},
{
"name": "brick/math",
@ -1354,16 +1354,16 @@
},
{
"name": "laminas/laminas-escaper",
"version": "2.14.0",
"version": "2.15.0",
"source": {
"type": "git",
"url": "https://github.com/laminas/laminas-escaper.git",
"reference": "0f7cb975f4443cf22f33408925c231225cfba8cb"
"reference": "c612b0488ae486284c39885efca494c180f16351"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laminas/laminas-escaper/zipball/0f7cb975f4443cf22f33408925c231225cfba8cb",
"reference": "0f7cb975f4443cf22f33408925c231225cfba8cb",
"url": "https://api.github.com/repos/laminas/laminas-escaper/zipball/c612b0488ae486284c39885efca494c180f16351",
"reference": "c612b0488ae486284c39885efca494c180f16351",
"shasum": ""
},
"require": {
@ -1375,12 +1375,12 @@
"zendframework/zend-escaper": "*"
},
"require-dev": {
"infection/infection": "^0.27.9",
"laminas/laminas-coding-standard": "~3.0.0",
"infection/infection": "^0.27.11",
"laminas/laminas-coding-standard": "~3.0.1",
"maglnet/composer-require-checker": "^3.8.0",
"phpunit/phpunit": "^9.6.16",
"phpunit/phpunit": "^9.6.22",
"psalm/plugin-phpunit": "^0.19.0",
"vimeo/psalm": "^5.21.1"
"vimeo/psalm": "^5.26.1"
},
"type": "library",
"autoload": {
@ -1407,7 +1407,7 @@
"type": "community_bridge"
}
],
"time": "2024-10-24T10:12:53+00:00"
"time": "2024-12-17T19:39:54+00:00"
},
{
"name": "league/commonmark",
@ -3637,11 +3637,11 @@
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.x-dev"
},
"phpstan": {
"includes": ["extension.neon"]
},
"branch-alias": {
"dev-main": "3.x-dev"
}
},
"autoload": {
@ -4316,16 +4316,16 @@
},
{
"name": "phpstan/phpstan",
"version": "2.0.3",
"version": "2.0.4",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
"reference": "46b4d3529b12178112d9008337beda0cc2a1a6b4"
"reference": "50d276fc3bf1430ec315f2f109bbde2769821524"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/46b4d3529b12178112d9008337beda0cc2a1a6b4",
"reference": "46b4d3529b12178112d9008337beda0cc2a1a6b4",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/50d276fc3bf1430ec315f2f109bbde2769821524",
"reference": "50d276fc3bf1430ec315f2f109bbde2769821524",
"shasum": ""
},
"require": {
@ -4360,7 +4360,7 @@
"type": "github"
}
],
"time": "2024-11-28T22:19:37+00:00"
"time": "2024-12-17T17:14:01+00:00"
},
{
"name": "phpunit/php-code-coverage",
@ -4654,16 +4654,16 @@
},
{
"name": "phpunit/phpunit",
"version": "11.5.1",
"version": "11.5.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "2b94d4f2450b9869fa64a46fd8a6a41997aef56a"
"reference": "153d0531b9f7e883c5053160cad6dd5ac28140b3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/2b94d4f2450b9869fa64a46fd8a6a41997aef56a",
"reference": "2b94d4f2450b9869fa64a46fd8a6a41997aef56a",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/153d0531b9f7e883c5053160cad6dd5ac28140b3",
"reference": "153d0531b9f7e883c5053160cad6dd5ac28140b3",
"shasum": ""
},
"require": {
@ -4677,13 +4677,13 @@
"phar-io/manifest": "^2.0.4",
"phar-io/version": "^3.2.1",
"php": ">=8.2",
"phpunit/php-code-coverage": "^11.0.7",
"phpunit/php-code-coverage": "^11.0.8",
"phpunit/php-file-iterator": "^5.1.0",
"phpunit/php-invoker": "^5.0.1",
"phpunit/php-text-template": "^4.0.1",
"phpunit/php-timer": "^7.0.1",
"sebastian/cli-parser": "^3.0.2",
"sebastian/code-unit": "^3.0.1",
"sebastian/code-unit": "^3.0.2",
"sebastian/comparator": "^6.2.1",
"sebastian/diff": "^6.0.2",
"sebastian/environment": "^7.2.0",
@ -4723,7 +4723,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.1"
"source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.2"
},
"funding": [
{
@ -4739,7 +4739,7 @@
"type": "tidelift"
}
],
"time": "2024-12-11T10:52:48+00:00"
"time": "2024-12-21T05:51:08+00:00"
},
{
"name": "psr/container",

View File

@ -11,18 +11,19 @@
"prepare": "astro telemetry disable"
},
"dependencies": {
"@astrojs/check": "^0.9.3",
"@astrojs/starlight": "^0.28.2",
"@astrojs/starlight-tailwind": "^2.0.3",
"@astrojs/tailwind": "^5.1.1",
"@astrojs/check": "^0.9.4",
"@astrojs/starlight": "^0.30.3",
"@astrojs/starlight-tailwind": "^3.0.0",
"@astrojs/tailwind": "^5.1.3",
"@fontsource/inter": "^5.1.0",
"@fontsource/rubik": "^5.1.0",
"astro": "^4.15.9",
"astro": "^5.1.0",
"autoprefixer": "^10.4.20",
"cssnano": "^7.0.6",
"postcss-preset-env": "^10.0.5",
"postcss-preset-env": "^10.1.2",
"sharp": "^0.33.5",
"tailwindcss": "^3.4.13",
"typescript": "^5.6.2"
"tailwindcss": "^3.4.17",
"typescript": "^5.7.2",
"zod": "3.24.1"
}
}

4622
docs/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,8 @@
import { defineCollection } from "astro:content";
import { docsLoader, i18nLoader } from "@astrojs/starlight/loaders";
import { docsSchema, i18nSchema } from "@astrojs/starlight/schema";
export const collections = {
docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
i18n: defineCollection({ loader: i18nLoader(), schema: i18nSchema() }),
};

View File

@ -1,7 +0,0 @@
import { defineCollection } from "astro:content";
import { docsSchema, i18nSchema } from "@astrojs/starlight/schema";
export const collections = {
docs: defineCollection({ schema: docsSchema() }),
i18n: defineCollection({ type: "data", schema: i18nSchema() }),
};

View File

@ -101,17 +101,18 @@ each property being a field key and the value being a `Field` object.
A field is a form element:
| Property | Type | Note |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| `type` | `checkbox` \| `datetime` \| `email` \| `group` \| `html` \| `markdown` \| `number` \| `radio-group` \| `rss` \| `select-multiple` \| `select` \| `text` \| `textarea` \| `toggler` \| `url` | Default is `text` |
| `label` (required) | `string` | Can be translated (see i18n) |
| `hint` | `string` | Can be translated (see i18n) |
| `helper` | `string` | Can be translated (see i18n) |
| `defaultValue` | `string` | You can specify multiple comma separated values for `select-multiple` |
| `optional` | `boolean` | Default is `false` |
| `options` | `Options` | Required for `radio-group`, `select-multiple`, and `select` types. |
| `multiple` | `boolean` | Default is `false` |
| `fields` | `Array<string, Field>` | Required for `group` type |
| Property | Type | Note |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| `type` | `checkbox`, `datetime`, `email`, `group`, `html`, `markdown`, `number`, `radio-group`, `rss`, `select-multiple`, `select`, `text`, `textarea`, `toggler`, `url` | Default is `text` |
| `label` (required) | `string` | Can be translated (see i18n) |
| `hint` | `string` | Can be translated (see i18n) |
| `helper` | `string` | Can be translated (see i18n) |
| `defaultValue` | `string` | You can specify multiple comma separated values for `select-multiple` |
| `validationRules` | `string` \| `array` | See [available validation rules](#available-validation-rules) |
| `optional` | `boolean` | Default is `false` |
| `options` | `Options` | Required for `radio-group`, `select-multiple`, and `select` types. |
| `multiple` | `boolean` | Default is `false` |
| `fields` | `Array<string, Field>` | Required for `group` type |
#### Options object
@ -133,3 +134,35 @@ plugin is installed.
Repository where the plugin's code lives. Helpful for people who want to
contribute.
#### Available validation rules
The following rules are a subset of
[CodeIgniter4's validation rules](https://codeigniter.com/user_guide/libraries/validation.html#available-rules).
| Rule | Parameter | Description | Example |
| --------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- |
| alpha | No | Fails if field has anything other than alphabetic characters in ASCII. | |
| alpha_dash | No | Fails if field contains anything other than alphanumeric characters, underscores or dashes in ASCII. | |
| alpha_numeric | No | Fails if field contains anything other than alphanumeric characters in ASCII. | |
| alpha_numeric_punct | No | Fails if field contains anything other than alphanumeric, space, or this limited set of punctuation characters: `~` (tilde), `!` (exclamation), `#` (number), `$` (dollar), `%` (percent), `&` (ampersand), `*` (asterisk), `-` (dash), `_` (underscore), `+` (plus), `=` (equals), `\|` (vertical bar),`:`(colon),`.` (period). | |
| alpha_numeric_space | No | Fails if field contains anything other than alphanumeric or space characters in ASCII. | |
| alpha_space | No | Fails if field contains anything other than alphabetic characters or spaces in ASCII. | |
| decimal | No | Fails if field contains anything other than a decimal number. Also accepts a `+` or `-` sign for the number. | |
| differs | Yes | Fails if field does not differ from the one in the parameter. | `differs[field_name]` |
| exact_length | Yes | Fails if field length is not exactly the parameter value. One or more comma-separated values are possible. | `exact_length[5]` or `exact_length[5,8,12]` |
| greater_than | Yes | Fails if field is less than or equal to the parameter value or not numeric. | `greater_than[8]` |
| greater_than_equal_to | Yes | Fails if field is less than the parameter value, or not numeric. | `greater_than_equal_to[5]` |
| hex | No | Fails if field contains anything other than hexadecimal characters. | |
| in_list | Yes | Fails if field is not within a predetermined list. | `in_list[red,blue,green]` |
| integer | No | Fails if field contains anything other than an integer. | |
| is_natural | No | Fails if field contains anything other than a natural number: `0`, `1`, `2`, `3`, etc. | |
| is_natural_no_zero | No | Fails if field contains anything other than a natural number, except zero: `1`, `2`, `3`, etc. | |
| less_than | Yes | Fails if field is greater than or equal to the parameter value or not numeric. | `less_than[8]` |
| less_than_equal_to | Yes | Fails if field is greater than the parameter value or not numeric. | `less_than_equal_to[8]` |
| max_length | Yes | Fails if field is longer than the parameter value. | `max_length[8]` |
| min_length | Yes | Fails if field is shorter than the parameter value. | `min_length[3]` |
| not_in_list | Yes | Fails if field is within a predetermined list. | `not_in_list[red,blue,green]` |
| regex_match | Yes | Fails if field does not match the regular expression. | `regex_match[/regex/]` |
| valid_base64 | No | Fails if field contains anything other than valid Base64 characters. | |
| valid_date | Yes | Fails if field does not contain a valid date. Any string that `strtotime()` accepts is valid if you don't specify an optional parameter that matches a date format. | `valid_date[d/m/Y]` |

View File

@ -71,12 +71,12 @@ class CreatePlugin extends BaseCommand
$pluginName = CLI::prompt(
'Plugin name (<vendor>/<name>)',
'acme/hello-world',
Manifest::VALIDATION_RULES['name']
Manifest::$validation_rules['name']
);
CLI::newLine();
$description = CLI::prompt('Description', '', Manifest::VALIDATION_RULES['description']);
$description = CLI::prompt('Description', '', Manifest::$validation_rules['description']);
CLI::newLine();
$license = CLI::prompt('License', 'UNLICENSED', Manifest::VALIDATION_RULES['license']);
$license = CLI::prompt('License', 'UNLICENSED', Manifest::$validation_rules['license']);
CLI::newLine();
$hooks = CLI::promptByMultipleKeys('Which hooks do you want to implement?', Plugins::HOOKS);

View File

@ -18,6 +18,7 @@ use Modules\Plugins\Core\Markdown;
use Modules\Plugins\Core\Plugins;
use Modules\Plugins\Core\RSS;
use Modules\Plugins\Manifest\Field;
use Modules\Plugins\Manifest\Fields\Group;
class PluginController extends BaseController
{
@ -154,7 +155,6 @@ class PluginController extends BaseController
string $podcastId = null,
string $episodeId = null
): RedirectResponse {
$plugin = $this->plugins->getPlugin($vendor, $package);
if (! $plugin instanceof BasePlugin) {
@ -182,29 +182,35 @@ class PluginController extends BaseController
}
if ($field->multiple) {
if ($field->type === 'group') {
if ($field instanceof Group) {
foreach ($field->fields as $subField) {
$typeRules = $this->plugins::FIELDS_VALIDATIONS[$subField->type];
if (! in_array('permit_empty', $typeRules, true)) {
$typeRules[] = $subField->optional ? 'permit_empty' : 'required';
}
$rules[sprintf('%s.*.%s', $field->key, $subField->key)] = $typeRules;
$rules[sprintf('%s.*.%s', $field->key, $subField->key)] = [
...$typeRules,
...$subField->validationRules,
];
}
} else {
$rules[$field->key . '.*'] = $typeRules;
$rules[$field->key . '.*'] = [...$typeRules, ...$field->validationRules];
}
} elseif ($field->type === 'group') {
} elseif ($field instanceof Group) {
foreach ($field->fields as $subField) {
$typeRules = $this->plugins::FIELDS_VALIDATIONS[$subField->type];
if (! in_array('permit_empty', $typeRules, true)) {
$typeRules[] = $subField->optional ? 'permit_empty' : 'required';
}
$rules[sprintf('%s.%s', $field->key, $subField->key)] = $typeRules;
$rules[sprintf('%s.%s', $field->key, $subField->key)] = [
...$typeRules,
...$subField->validationRules,
];
}
} else {
$rules[$field->key] = $typeRules;
$rules[$field->key] = [...$typeRules, ...$field->validationRules];
}
}
@ -288,7 +294,7 @@ class PluginController extends BaseController
continue;
}
if ($field->type === 'group') {
if ($field instanceof Group) {
foreach ($val as $subKey => $subVal) {
/** @var Field|false $subField */
$subField = array_column($field->fields, null, 'key')[$subKey] ?? false;
@ -305,7 +311,7 @@ class PluginController extends BaseController
$value[$key] = $this->castValue($val, $field->type);
}
}
} elseif ($field->type === 'group') {
} elseif ($field instanceof Group) {
foreach ($fieldValue as $subKey => $subVal) {
/** @var Field|false $subField */
$subField = array_column($field->fields, null, 'key')[$subKey] ?? false;
@ -340,10 +346,9 @@ class PluginController extends BaseController
$value,
$this->request->getPost('client_timezone')
)->setTimezone(app_timezone()),
'markdown' => new Markdown($value),
'rss' => new RSS($value),
'comma-separated-string' => implode(',', $value),
default => $value,
'markdown' => new Markdown($value),
'rss' => new RSS($value),
default => $value,
};
}
}

View File

@ -52,14 +52,13 @@ class Plugins
];
public const FIELDS_CASTS = [
'checkbox' => 'bool',
'datetime' => 'datetime',
'markdown' => 'markdown',
'number' => 'int',
'rss' => 'rss',
'toggler' => 'bool',
'url' => 'uri',
'select-multiple' => 'comma-separated-string',
'checkbox' => 'bool',
'datetime' => 'datetime',
'markdown' => 'markdown',
'number' => 'int',
'rss' => 'rss',
'toggler' => 'bool',
'url' => 'uri',
];
/**

View File

@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Modules\Plugins\Manifest;
use Override;
use RuntimeException;
/**
* @property 'checkbox'|'datetime'|'email'|'group'|'html'|'markdown'|'number'|'radio-group'|'rss'|'select-multiple'|'select'|'text'|'textarea'|'toggler'|'url' $type
@ -14,28 +14,20 @@ use Override;
* @property string $helper
* @property string $defaultValue
* @property bool $optional
* @property Option[] $options
* @property bool $multiple
* @property Field[] $fields
* @property string[] $validationRules
*/
class Field extends ManifestObject
class Field extends ManifestObject implements FieldInterface
{
protected const VALIDATION_RULES = [
'type' => 'permit_empty|in_list[checkbox,datetime,email,group,html,markdown,number,radio-group,rss,select-multiple,select,text,textarea,toggler,url]',
'key' => 'required|alpha_dash',
'label' => 'required|string',
'hint' => 'permit_empty|string',
'helper' => 'permit_empty|string',
'defaultValue' => 'permit_empty|string',
'optional' => 'permit_empty|is_boolean',
'options' => 'permit_empty|is_list',
'multiple' => 'permit_empty|is_boolean',
'fields' => 'permit_empty|is_list',
];
protected const CASTS = [
'options' => [Option::class],
'fields' => [self::class],
public static array $validation_rules = [
'type' => 'permit_empty|in_list[checkbox,datetime,email,group,html,markdown,number,radio-group,rss,select-multiple,select,text,textarea,toggler,url]',
'key' => 'required|alpha_dash',
'label' => 'required|string',
'hint' => 'permit_empty|string',
'helper' => 'permit_empty|string',
'validationRules' => 'permit_empty|is_string_or_list',
'optional' => 'permit_empty|is_boolean',
'multiple' => 'permit_empty|is_boolean',
];
protected string $type = 'text';
@ -48,65 +40,83 @@ class Field extends ManifestObject
protected string $helper = '';
protected string $defaultValue = '';
/**
* @var string[]
*/
protected array $validationRules = [];
protected bool $optional = false;
protected bool $multiple = false;
/**
* @var Option[]
*/
protected array $options = [];
#[Override]
public function loadData(array $data): void
public function getLabel(): string
{
if (array_key_exists('options', $data)) {
$newOptions = [];
foreach ($data['options'] as $key => $option) {
$option['value'] = $key;
$newOptions[] = $option;
}
return $this->getTranslated('label');
}
$data['options'] = $newOptions;
}
public function getHint(): string
{
return $this->getTranslated('hint');
}
if (array_key_exists('fields', $data)) {
$newFields = [];
foreach ($data['fields'] as $key => $field) {
$field['key'] = $key;
$newFields[] = $field;
}
$data['fields'] = $newFields;
}
parent::loadData($data);
public function getHelper(): string
{
return $this->getTranslated('helper');
}
/**
* @return array{label:string,value:string,description:string}[]
* @param string|list<string> $values
*/
public function getOptionsArray(string $pluginKey): array
public function setValidationRules(string|array $values): void
{
$i18nKey = sprintf('%s.settings.%s.%s.options', $pluginKey, $this->type, $this->key);
$optionsArray = [];
foreach ($this->options as $option) {
$optionsArray[] = [
'value' => $option->value,
'label' => $option->getTranslated($i18nKey, 'label'),
'description' => $option->getTranslated($i18nKey, 'description'),
];
$validationRules = [];
if (is_string($values)) {
$validationRules = explode('|', $values);
}
return $optionsArray;
$allowedRules = [
'alpha',
'alpha_dash',
'alpha_numeric',
'alpha_numeric_punct',
'alpha_numeric_space',
'alpha_space',
'decimal',
'differs',
'exact_length',
'greater_than',
'greater_than_equal_to',
'hex',
'in_list',
'integer',
'is_natural',
'is_natural_no_zero',
'less_than',
'less_than_equal_to',
'max_length',
'min_length',
'not_in_list',
'regex_match',
'valid_base64',
'valid_date',
];
foreach ($validationRules as $rule) {
foreach ($allowedRules as $allowedRule) {
if (str_starts_with($rule, $allowedRule)) {
$this->validationRules[] = $rule;
}
}
}
}
public function getTranslated(string $pluginKey, string $property): string
public function render(string $name, mixed $value, string $class = ''): string
{
$key = sprintf('Plugin.%s.settings.%s.%s.%s', $pluginKey, $this->type, $this->key, $property);
throw new RuntimeException('Render function not defined in parent Field class');
}
private function getTranslated(string $property): string
{
$key = sprintf('Plugin.%s.settings.%s.%s.%s', $this->pluginKey, $this->type, $this->key, $property);
/** @var string $i18nField */
$i18nField = lang($key);

View File

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Modules\Plugins\Manifest;
interface FieldInterface
{
public function render(string $name, mixed $value, string $class = ''): string;
}

View File

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Modules\Plugins\Manifest\Fields;
use Modules\Plugins\Manifest\Field;
/**
* @property bool $defaultValue
*/
class Checkbox extends Field
{
public static array $validation_rules = [
'defaultValue' => 'permit_empty|is_boolean',
];
protected bool $defaultValue = false;
public function render(string $name, mixed $value, string $class = ''): string
{
$value = $value ? 'yes' : '';
return <<<HTML
<x-Forms.Checkbox
class="{$class}"
name="{$name}"
hint="{$this->hint}"
helper="{$this->helper}"
value="{$value}"
defaultValue="{$this->defaultValue}"
>{$this->label}</x-Forms.Checkbox>
HTML;
}
}

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Modules\Plugins\Manifest\Fields;
use Modules\Plugins\Manifest\Field;
/**
* @property string $defaultValue
*/
class Datetime extends Field
{
public static array $validation_rules = [
'defaultValue' => 'permit_empty|valid_date',
];
protected string $defaultValue = '';
public function render(string $name, mixed $value, string $class = ''): string
{
$isRequired = $this->optional ? 'false' : 'true';
return <<<HTML
<x-Forms.Field
as="DatetimePicker"
class="{$class}"
name="{$name}"
label="{$this->label}"
hint="{$this->hint}"
helper="{$this->helper}"
isRequired="{$isRequired}"
value="{$value}"
defaultValue="{$this->defaultValue}"
/>
HTML;
}
}

View File

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Modules\Plugins\Manifest\Fields;
use Modules\Plugins\Manifest\Field;
/**
* @property string $defaultValue
*/
class Email extends Field
{
public static array $validation_rules = [
'defaultValue' => 'permit_empty|valid_email',
];
protected string $defaultValue = '';
public function render(string $name, mixed $value, string $class = ''): string
{
$isRequired = $this->optional ? 'false' : 'true';
return <<<HTML
<x-Forms.Field
as="Input"
class="{$class}"
type="email"
name="{$name}"
label="{$this->label}"
hint="{$this->hint}"
helper="{$this->helper}"
isRequired="{$isRequired}"
value="{$value}"
defaultValue="{$this->defaultValue}"
/>
HTML;
}
}

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Modules\Plugins\Manifest\Fields;
use Modules\Plugins\Manifest\Field;
use Modules\Plugins\Manifest\WithFieldsTrait;
use Override;
use RuntimeException;
class Group extends Field
{
use WithFieldsTrait;
public function __construct(string $pluginKey)
{
$this->injectRules();
parent::__construct($pluginKey);
}
#[Override]
public function loadData(array $data): void
{
$data = $this->transformData($data);
parent::loadData($data);
}
public function render(string $name, mixed $value, string $class = ''): string
{
// TODO: render group, depending on multiple
throw new RuntimeException('Render function not defined in Group Field class');
}
}

View File

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Modules\Plugins\Manifest\Fields;
use Modules\Plugins\Manifest\Field;
/**
* @property string $defaultValue
*/
class Html extends Field
{
public static array $validation_rules = [
'defaultValue' => 'permit_empty|string',
];
protected string $defaultValue = '';
public function render(string $name, mixed $value, string $class = ''): string
{
$isRequired = $this->optional ? 'false' : 'true';
$value = htmlspecialchars($value ?? '');
return <<<HTML
<x-Forms.Field
as="CodeEditor"
lang="html"
class="{$class}"
name="{$name}"
label="{$this->label}"
hint="{$this->hint}"
helper="{$this->helper}"
isRequired="{$isRequired}"
value="{$value}"
defaultValue="{$this->defaultValue}"
/>
HTML;
}
}

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Modules\Plugins\Manifest\Fields;
use Modules\Plugins\Manifest\Field;
/**
* @property string $defaultValue
*/
class Markdown extends Field
{
public static array $validation_rules = [
'defaultValue' => 'permit_empty|string',
];
protected string $defaultValue = '';
public function render(string $name, mixed $value, string $class = ''): string
{
$isRequired = $this->optional ? 'false' : 'true';
return <<<HTML
<x-Forms.Field
as="MarkdownEditor"
class="{$class}"
name="{$name}"
label="{$this->label}"
hint="{$this->hint}"
helper="{$this->helper}"
isRequired="{$isRequired}"
value="{$value}"
defaultValue="{$this->defaultValue}"
/>
HTML;
}
}

View File

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Modules\Plugins\Manifest\Fields;
use Modules\Plugins\Manifest\Field;
/**
* @property int|null $defaultValue
*/
class Number extends Field
{
public static array $validation_rules = [
'defaultValue' => 'permit_empty|numeric',
];
protected ?int $defaultValue = null;
public function render(string $name, mixed $value, string $class = ''): string
{
$isRequired = $this->optional ? 'false' : 'true';
return <<<HTML
<x-Forms.Field
as="Input"
type="number"
class="{$class}"
name="{$name}"
label="{$this->label}"
hint="{$this->hint}"
helper="{$this->helper}"
isRequired="{$isRequired}"
value="{$value}"
defaultValue="{$this->defaultValue}"
/>
HTML;
}
}

View File

@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Modules\Plugins\Manifest\Fields;
use Modules\Plugins\Manifest\Field;
use Modules\Plugins\Manifest\WithOptionsTrait;
use Override;
/**
* @property string $defaultValue
*/
class RadioGroup extends Field
{
use WithOptionsTrait;
public static array $validation_rules = [
'defaultValue' => 'permit_empty|string',
];
protected string $defaultValue = '';
public function __construct(string $pluginKey)
{
$this->injectRules();
parent::__construct($pluginKey);
}
#[Override]
public function loadData(array $data): void
{
$data = $this->transformData($data);
parent::loadData($data);
}
public function render(string $name, mixed $value, string $class = ''): string
{
$isRequired = $this->optional ? 'false' : 'true';
$options = esc(json_encode($this->getOptionsArray()));
return <<<HTML
<x-Forms.RadioGroup
class="{$class}"
name="{$name}"
label="{$this->label}"
hint="{$this->hint}"
helper="{$this->helper}"
options="{$options}"
isRequired="{$isRequired}"
value="{$value}"
defaultValue="{$this->defaultValue}"
/>
HTML;
}
}

View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Modules\Plugins\Manifest\Fields;
use Modules\Plugins\Manifest\Field;
/**
* @property string $defaultValue
*/
class Rss extends Field
{
public static array $validation_rules = [
'defaultValue' => 'permit_empty|string',
];
protected string $defaultValue = '';
public function render(string $name, mixed $value, string $class = ''): string
{
$isRequired = $this->optional ? 'false' : 'true';
$value = htmlspecialchars((string) $value);
$defaultValue = esc($this->defaultValue);
return <<<HTML
<x-Forms.Field
as="CodeEditor"
lang="xml"
class="{$class}"
name="{$name}"
label="{$this->label}"
hint="{$this->hint}"
helper="{$this->helper}"
isRequired="{$isRequired}"
value="{$value}"
defaultValue="{$defaultValue}"
/>
HTML;
}
}

View File

@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace Modules\Plugins\Manifest\Fields;
use Modules\Plugins\Manifest\Field;
use Modules\Plugins\Manifest\Option;
use Modules\Plugins\Manifest\WithOptionsTrait;
use Override;
/**
* @property string $defaultValue
*/
class Select extends Field
{
use WithOptionsTrait;
public static array $validation_rules = [
'defaultValue' => 'permit_empty|string',
'options' => 'is_list',
];
protected array $casts = [
'options' => [Option::class],
];
protected string $defaultValue = '';
public function __construct(string $pluginKey)
{
$this->injectRules();
parent::__construct($pluginKey);
}
#[Override]
public function loadData(array $data): void
{
$data = $this->transformData($data);
parent::loadData($data);
}
public function render(string $name, mixed $value, string $class = ''): string
{
$isRequired = $this->optional ? 'false' : 'true';
$options = esc(json_encode($this->getOptionsArray()));
return <<<HTML
<x-Forms.Field
as="Select"
class="{$class}"
name="{$name}"
label="{$this->label}"
hint="{$this->hint}"
helper="{$this->helper}"
options="{$options}"
isRequired="{$isRequired}"
value="{$value}"
defaultValue="{$this->defaultValue}"
/>
HTML;
}
}

View File

@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace Modules\Plugins\Manifest\Fields;
use Modules\Plugins\Manifest\Field;
use Modules\Plugins\Manifest\WithOptionsTrait;
use Override;
/**
* @property list<string> $defaultValue
*/
class SelectMultiple extends Field
{
use WithOptionsTrait;
public static array $validation_rules = [
'defaultValue' => 'permit_empty|is_list',
];
/**
* @var list<string>
*/
protected array $defaultValue = [];
public function __construct(string $pluginKey)
{
$this->injectRules();
parent::__construct($pluginKey);
}
#[Override]
public function loadData(array $data): void
{
$data = $this->transformData($data);
parent::loadData($data);
}
public function render(string $name, mixed $value, string $class = ''): string
{
$isRequired = $this->optional ? 'false' : 'true';
$options = esc(json_encode($this->getOptionsArray()));
$value = esc(json_encode($value));
$defaultValue = esc(json_encode($this->defaultValue));
return <<<HTML
<x-Forms.Field
as="SelectMulti"
class="{$class}"
name="{$name}"
label="{$this->label}"
hint="{$this->hint}"
helper="{$this->helper}"
options="{$options}"
isRequired="{$isRequired}"
value="{$value}"
defaultValue="{$defaultValue}"
/>
HTML;
}
}

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Modules\Plugins\Manifest\Fields;
use Modules\Plugins\Manifest\Field;
/**
* @property string $defaultValue
*/
class Text extends Field
{
public static array $validation_rules = [
'defaultValue' => 'permit_empty|string',
];
protected string $defaultValue = '';
public function render(string $name, mixed $value, string $class = ''): string
{
$isRequired = $this->optional ? 'false' : 'true';
return <<<HTML
<x-Forms.Field
as="Input"
class="{$class}"
name="{$name}"
label="{$this->label}"
hint="{$this->hint}"
helper="{$this->helper}"
isRequired="{$isRequired}"
value="{$value}"
defaultValue="{$this->defaultValue}"
/>
HTML;
}
}

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Modules\Plugins\Manifest\Fields;
use Modules\Plugins\Manifest\Field;
/**
* @property string $defaultValue
*/
class Textarea extends Field
{
public static array $validation_rules = [
'defaultValue' => 'permit_empty|string',
];
protected string $defaultValue = '';
public function render(string $name, mixed $value, string $class = ''): string
{
$isRequired = $this->optional ? 'false' : 'true';
return <<<HTML
<x-Forms.Field
as="Textarea"
class="{$class}"
name="{$name}"
label="{$this->label}"
hint="{$this->hint}"
helper="{$this->helper}"
isRequired="{$isRequired}"
value="{$value}"
defaultValue="{$this->defaultValue}"
/>
HTML;
}
}

View File

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Modules\Plugins\Manifest\Fields;
use Modules\Plugins\Manifest\Field;
/**
* @property bool $defaultValue
*/
class Toggler extends Field
{
public static array $validation_rules = [
'defaultValue' => 'permit_empty|is_boolean',
];
protected bool $defaultValue = false;
public function render(string $name, mixed $value, string $class = ''): string
{
$value = $value ? 'yes' : '';
return <<<HTML
<x-Forms.Toggler
class="{$class}"
name="{$name}"
hint="{$this->hint}"
helper="{$this->helper}"
value="{$value}"
defaultValue="{$this->defaultValue}"
>{$this->label}</x-Forms.Toggler>
HTML;
}
}

View File

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Modules\Plugins\Manifest\Fields;
use Modules\Plugins\Manifest\Field;
/**
* @property string $defaultValue
*/
class Url extends Field
{
public static array $validation_rules = [
'defaultValue' => 'permit_empty|valid_url_strict',
];
protected string $defaultValue = '';
public function render(string $name, mixed $value, string $class = ''): string
{
$isRequired = $this->optional ? 'false' : 'true';
return <<<HTML
<x-Forms.Field
as="Input"
class="{$class}"
type="url"
placeholder="https://…"
name="{$name}"
label="{$this->label}"
hint="{$this->hint}"
helper="{$this->helper}"
isRequired="{$isRequired}"
value="{$value}"
defaultValue="{$this->defaultValue}"
/>
HTML;
}
}

View File

@ -23,10 +23,7 @@ use CodeIgniter\HTTP\URI;
*/
class Manifest extends ManifestObject
{
/**
* @var array<string,string>
*/
public const VALIDATION_RULES = [
public static array $validation_rules = [
'name' => 'required|max_length[128]|regex_match[/^[a-z0-9]([_.-]?[a-z0-9]+)*\/[a-z0-9]([_.-]?[a-z0-9]+)*$/]',
'version' => 'required|regex_match[/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/]',
'description' => 'permit_empty|max_length[256]',
@ -41,7 +38,7 @@ class Manifest extends ManifestObject
'repository' => 'permit_empty|is_list',
];
protected const CASTS = [
protected array $casts = [
'authors' => [Person::class],
'homepage' => URI::class,
'settings' => Settings::class,

View File

@ -9,12 +9,15 @@ use Exception;
abstract class ManifestObject
{
protected const VALIDATION_RULES = [];
/**
* @var array<string,string>
*/
public static array $validation_rules = [];
/**
* @var array<string,string|array{string}>
*/
protected const CASTS = [];
protected array $casts = [];
/**
* @var array<string,array<string,string>>
@ -25,11 +28,28 @@ abstract class ManifestObject
protected readonly string $pluginKey,
) {
self::$errors[$pluginKey] = [];
$class = static::class;
$validation_rules = [];
$casts = [];
while ($class = get_parent_class($class)) {
$validation_rules = [...$validation_rules, ...get_class_vars($class)['validation_rules']];
$casts = [...$casts, ...get_class_vars($class)['casts']];
}
$this::$validation_rules = [...$validation_rules, ...$this::$validation_rules];
$this->casts = [...$casts, ...$this->casts];
}
public function __get(string $name): mixed
{
if (property_exists($this, $name)) {
// if a get method exists for this property, return that
$method = 'get' . str_replace(' ', '', ucwords(str_replace(['-', '_'], ' ', $name)));
if (method_exists($this, $method)) {
return $this->{$method}();
}
return $this->{$name};
}
@ -73,7 +93,7 @@ abstract class ManifestObject
/** @var Validation $validation */
$validation = service('validation');
$validation->setRules($this::VALIDATION_RULES);
$validation->setRules($this::$validation_rules);
if (! $validation->run($data)) {
foreach ($validation->getErrors() as $key => $message) {
@ -84,23 +104,30 @@ abstract class ManifestObject
}
foreach ($validation->getValidated() as $key => $value) {
if (array_key_exists($key, $this::CASTS)) {
$cast = $this::CASTS[$key];
$method = 'set' . str_replace(' ', '', ucwords(str_replace('_', ' ', $key)));
if (is_callable([$this, $method])) {
$this->{$method}($value);
continue;
}
if (is_array($cast)) {
if (is_array($value)) {
foreach ($value as $valueKey => $valueElement) {
if (is_subclass_of($cast[0], self::class)) {
$value[$valueKey] = new $cast[0]($this->pluginKey);
$value[$valueKey]->loadData($valueElement);
} else {
$value[$valueKey] = new $cast[0]($valueElement);
}
if (array_key_exists($key, $this->casts)) {
$cast = $this->casts[$key];
if (is_array($cast) && is_array($value)) {
foreach ($value as $valueKey => $valueElement) {
if (is_subclass_of($cast[0], self::class)) {
$manifestClass = $cast[0] === Field::class ? $this->getFieldClass(
$valueElement
) : $cast[0];
$value[$valueKey] = new $manifestClass($this->pluginKey);
$value[$valueKey]->loadData($valueElement);
} else {
$value[$valueKey] = new $cast[0]($valueElement);
}
}
} elseif (is_subclass_of($cast, self::class)) {
$manifestClass = $cast === Field::class ? $this->getFieldClass($value) : $cast;
$valueElement = $value;
$value = new $cast($this->pluginKey);
$value = new $manifestClass($this->pluginKey);
$value->loadData($valueElement ?? []);
} else {
$value = new $cast($value ?? []);
@ -123,4 +150,17 @@ abstract class ManifestObject
{
self::$errors[$this->pluginKey][$errorKey] = $errorMessage;
}
/**
* @param array<mixed> $data
*/
private function getFieldClass(array $data): string
{
$fieldType = $data['type'] ?? 'text';
return rtrim(Field::class, "\Field") . '\\Fields\\' . str_replace(
' ',
'',
ucwords(str_replace('-', ' ', $fieldType))
);
}
}

View File

@ -11,7 +11,7 @@ namespace Modules\Plugins\Manifest;
*/
class Option extends ManifestObject
{
protected const VALIDATION_RULES = [
public static array $validation_rules = [
'label' => 'required|string',
'value' => 'required|alpha_numeric_punct',
'description' => 'permit_empty|string',

View File

@ -15,18 +15,18 @@ use Override;
*/
class Person extends ManifestObject
{
protected const VALIDATION_RULES = [
protected const AUTHOR_STRING_PATTERN = '/^(?<name>[^<>()]*)\s*(<(?<email>.*)>)?\s*(\((?<url>.*)\))?$/';
public static array $validation_rules = [
'name' => 'required',
'email' => 'permit_empty|valid_email',
'url' => 'permit_empty|valid_url_strict',
];
protected const AUTHOR_STRING_PATTERN = '/^(?<name>[^<>()]*)\s*(<(?<email>.*)>)?\s*(\((?<url>.*)\))?$/';
/**
* @var array<string,array{string}|string>
*/
protected const CASTS = [
protected array $casts = [
'url' => URI::class,
];

View File

@ -13,7 +13,7 @@ use CodeIgniter\HTTP\URI;
*/
class Repository extends ManifestObject
{
protected const VALIDATION_RULES = [
public static array $validation_rules = [
'type' => 'permit_empty|in_list[git]',
'url' => 'required|valid_url_strict',
'directory' => 'permit_empty',
@ -22,7 +22,7 @@ class Repository extends ManifestObject
/**
* @var array<string,array{string}|string>
*/
protected const CASTS = [
protected array $casts = [
'url' => URI::class,
];

View File

@ -13,7 +13,7 @@ use Override;
*/
class Settings extends ManifestObject
{
protected const VALIDATION_RULES = [
public static array $validation_rules = [
'general' => 'permit_empty|is_list',
'podcast' => 'permit_empty|is_list',
'episode' => 'permit_empty|is_list',
@ -22,7 +22,7 @@ class Settings extends ManifestObject
/**
* @var array<string,array{string}|string>
*/
protected const CASTS = [
protected array $casts = [
'general' => [Field::class],
'podcast' => [Field::class],
'episode' => [Field::class],

View File

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Modules\Plugins\Manifest;
/**
* @property Field[] $fields
*/
trait WithFieldsTrait
{
/**
* @var Field[]
*/
protected array $options = [];
public function injectRules(): void
{
$this::$validation_rules = [...$this::$validation_rules, ...[
'fields' => 'is_list',
]];
$this->casts = [...$this->casts, ...[
'fields' => [Field::class],
]];
}
/**
* @param array<mixed> $data
* @return array<mixed>
*/
public function transformData(array $data): array
{
if (array_key_exists('fields', $data)) {
$newFields = [];
foreach ($data['fields'] as $key => $field) {
$field['key'] = $key;
$newFields[] = $field;
}
$data['fields'] = $newFields;
}
return $data;
}
}

View File

@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace Modules\Plugins\Manifest;
/**
* @property Option[] $options
*/
trait WithOptionsTrait
{
/**
* @var Option[]
*/
protected array $options = [];
public function injectRules(): void
{
if (isset($this::$validation_rules)) {
$this::$validation_rules = [...$this::$validation_rules, ...[
'options' => 'is_list',
]];
}
if (isset($this->casts)) {
$this->casts = [...$this->casts, ...[
'options' => [Option::class],
]];
}
}
/**
* @param array<mixed> $data
* @return array<mixed>
*/
public function transformData(array $data): array
{
if (array_key_exists('options', $data)) {
$newOptions = [];
foreach ($data['options'] as $key => $option) {
$option['value'] = $key;
$newOptions[] = $option;
}
$data['options'] = $newOptions;
}
return $data;
}
/**
* @return array{label:string,value:string,description:string}[]
*/
public function getOptionsArray(): array
{
$i18nKey = sprintf('%s.settings.%s.%s.options', $this->pluginKey, $this->type, $this->key);
$optionsArray = [];
foreach ($this->options as $option) {
$optionsArray[] = [
'value' => $option->value,
'label' => $option->getTranslated($i18nKey, 'label'),
'description' => $option->getTranslated($i18nKey, 'description'),
];
}
return $optionsArray;
}
}

View File

@ -204,32 +204,30 @@
"optional": {
"type": "boolean"
},
"defaultValue": {
"type": "string"
},
"options": {
"type": "object",
"patternProperties": {
"^[A-Za-z0-9]+[\\w\\-\\:\\.]*$": { "$ref": "#/$defs/option" }
},
"additionalProperties": false
"validationRules": {
"anyOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
]
},
"multiple": {
"type": "boolean"
},
"fields": {
"type": "object",
"patternProperties": {
"^[A-Za-z]+[\\w\\-\\:\\.]*$": { "$ref": "#/$defs/field" }
},
"additionalProperties": false
}
},
"required": ["label"],
"additionalProperties": false,
"unevaluatedProperties": false,
"allOf": [
{ "$ref": "#/$defs/field-multiple-implies-options-is-required" },
{ "$ref": "#/$defs/field-group-type-implies-fields-is-required" }
{ "$ref": "#/$defs/require-fields-for-group-type" },
{ "$ref": "#/$defs/default-value-based-on-type" }
]
},
"option": {
@ -246,37 +244,160 @@
"additionalProperties": false
},
"field-multiple-implies-options-is-required": {
"anyOf": [
"if": {
"properties": {
"type": {
"enum": ["radio-group", "select", "select-multiple"]
}
}
},
"then": {
"properties": {
"options": {
"type": "object",
"patternProperties": {
"^[A-Za-z0-9]+[\\w\\-\\:\\.]*$": { "$ref": "#/$defs/option" }
},
"additionalProperties": false
}
},
"required": ["options"]
}
},
"require-fields-for-group-type": {
"if": {
"properties": {
"type": {
"const": "group"
}
}
},
"then": {
"properties": {
"fields": {
"type": "object",
"patternProperties": {
"^[A-Za-z]+[\\w\\-\\:\\.]*$": { "$ref": "#/$defs/field" }
},
"additionalProperties": false
}
},
"required": ["fields"]
}
},
"default-value-based-on-type": {
"allOf": [
{
"not": {
"if": {
"properties": {
"type": {
"anyOf": [
{ "const": "radio-group" },
{ "const": "select" },
{ "const": "select-multiple" }
"enum": [
"html",
"markdown",
"radio-group",
"rss",
"select",
"text",
"textarea"
]
}
},
"required": ["type"]
}
},
"then": {
"properties": {
"defaultValue": { "type": "string" }
}
}
},
{ "required": ["options"] }
]
},
"field-group-type-implies-fields-is-required": {
"anyOf": [
{
"not": {
"if": {
"properties": {
"type": {
"anyOf": [{ "const": "group" }]
"enum": ["checkbox", "toggler"]
}
},
"required": ["type"]
}
},
"then": {
"properties": {
"defaultValue": { "type": "boolean" }
}
}
},
{ "required": ["fields"] }
{
"if": {
"properties": {
"type": {
"const": "datetime"
}
}
},
"then": {
"properties": {
"defaultValue": { "type": "string", "format": "date-time" }
}
}
},
{
"if": {
"properties": {
"type": {
"const": "email"
}
}
},
"then": {
"properties": {
"defaultValue": { "type": "string", "format": "email" }
}
}
},
{
"if": {
"properties": {
"type": {
"const": "number"
}
}
},
"then": {
"properties": {
"defaultValue": { "type": "number" }
}
}
},
{
"if": {
"properties": {
"type": {
"const": "select-multiple"
}
}
},
"then": {
"properties": {
"defaultValue": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
},
{
"if": {
"properties": {
"type": {
"const": "url"
}
}
},
"then": {
"properties": {
"defaultValue": { "type": "string", "format": "uri" }
}
}
}
]
}
}

View File

@ -35,7 +35,7 @@
"@codemirror/lang-xml": "^6.1.0",
"@codemirror/language": "^6.10.7",
"@codemirror/state": "^6.5.0",
"@codemirror/view": "^6.35.3",
"@codemirror/view": "^6.36.1",
"@floating-ui/dom": "^1.6.12",
"@github/clipboard-copy-element": "^1.3.0",
"@github/hotkey": "^3.1.1",
@ -51,7 +51,7 @@
"leaflet.markercluster": "^1.5.3",
"lit": "^3.2.1",
"marked": "^15.0.4",
"wavesurfer.js": "^7.8.11",
"wavesurfer.js": "^7.8.12",
"xml-formatter": "^3.6.3"
},
"devDependencies": {
@ -76,7 +76,7 @@
"eslint": "^9.17.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.1",
"globals": "^15.13.0",
"globals": "^15.14.0",
"husky": "^9.1.7",
"is-ci": "^4.1.0",
"lint-staged": "^15.2.11",
@ -91,10 +91,10 @@
"stylelint": "^16.12.0",
"stylelint-config-standard": "^36.0.1",
"svgo": "^3.3.2",
"tailwindcss": "^3.4.16",
"tailwindcss": "^3.4.17",
"typescript": "~5.7.2",
"typescript-eslint": "^8.18.1",
"vite": "^6.0.3",
"vite": "^6.0.5",
"vite-plugin-pwa": "^0.21.1",
"workbox-build": "^7.3.0",
"workbox-core": "^7.3.0",

116
pnpm-lock.yaml generated
View File

@ -29,8 +29,8 @@ importers:
specifier: ^6.5.0
version: 6.5.0
"@codemirror/view":
specifier: ^6.35.3
version: 6.35.3
specifier: ^6.36.1
version: 6.36.1
"@floating-ui/dom":
specifier: ^1.6.12
version: 1.6.12
@ -77,8 +77,8 @@ importers:
specifier: ^15.0.4
version: 15.0.4
wavesurfer.js:
specifier: ^7.8.11
version: 7.8.11
specifier: ^7.8.12
version: 7.8.12
xml-formatter:
specifier: ^3.6.3
version: 3.6.3
@ -112,10 +112,10 @@ importers:
version: 13.2.3(semantic-release@24.2.0(typescript@5.7.2))
"@tailwindcss/forms":
specifier: ^0.5.9
version: 0.5.9(tailwindcss@3.4.16)
version: 0.5.9(tailwindcss@3.4.17)
"@tailwindcss/typography":
specifier: ^0.5.15
version: 0.5.15(tailwindcss@3.4.16)
version: 0.5.15(tailwindcss@3.4.17)
"@types/eslint__js":
specifier: ^8.42.3
version: 8.42.3
@ -147,8 +147,8 @@ importers:
specifier: ^5.2.1
version: 5.2.1(@types/eslint@9.6.1)(eslint-config-prettier@9.1.0(eslint@9.17.0(jiti@2.4.1)))(eslint@9.17.0(jiti@2.4.1))(prettier@3.4.2)
globals:
specifier: ^15.13.0
version: 15.13.0
specifier: ^15.14.0
version: 15.14.0
husky:
specifier: ^9.1.7
version: 9.1.7
@ -192,8 +192,8 @@ importers:
specifier: ^3.3.2
version: 3.3.2
tailwindcss:
specifier: ^3.4.16
version: 3.4.16
specifier: ^3.4.17
version: 3.4.17
typescript:
specifier: ~5.7.2
version: 5.7.2
@ -201,11 +201,11 @@ importers:
specifier: ^8.18.1
version: 8.18.1(eslint@9.17.0(jiti@2.4.1))(typescript@5.7.2)
vite:
specifier: ^6.0.3
version: 6.0.3(@types/node@22.9.0)(jiti@2.4.1)(terser@5.36.0)(yaml@2.6.1)
specifier: ^6.0.5
version: 6.0.5(@types/node@22.9.0)(jiti@2.4.1)(terser@5.36.0)(yaml@2.6.1)
vite-plugin-pwa:
specifier: ^0.21.1
version: 0.21.1(vite@6.0.3(@types/node@22.9.0)(jiti@2.4.1)(terser@5.36.0)(yaml@2.6.1))(workbox-build@7.3.0)(workbox-window@7.3.0)
version: 0.21.1(vite@6.0.5(@types/node@22.9.0)(jiti@2.4.1)(terser@5.36.0)(yaml@2.6.1))(workbox-build@7.3.0)(workbox-window@7.3.0)
workbox-build:
specifier: ^7.3.0
version: 7.3.0
@ -1083,10 +1083,10 @@ packages:
integrity: sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw==,
}
"@codemirror/view@6.35.3":
"@codemirror/view@6.36.1":
resolution:
{
integrity: sha512-ScY7L8+EGdPl4QtoBiOzE4FELp7JmNUsBvgBcCakXWM2uiv/K89VAzU3BMDscf0DsACLvTKePbd5+cFDTcei6g==,
integrity: sha512-miD1nyT4m4uopZaDdO2uXU/LLHliKNYL9kB1C1wJHrunHLm/rpkb5QVSokqgw9hFqEZakrdlb/VGWX8aYZTslQ==,
}
"@colors/colors@1.5.0":
@ -4796,10 +4796,10 @@ packages:
}
engines: { node: ">=18" }
globals@15.13.0:
globals@15.14.0:
resolution:
{
integrity: sha512-49TewVEz0UxZjr1WYYsWpPrhyC/B/pA8Bq0fUmet2n+eR7yn0IvNzNaoBwnK6mdkzcN+se7Ez9zUgULTz2QH4g==,
integrity: sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==,
}
engines: { node: ">=18" }
@ -8267,10 +8267,10 @@ packages:
}
engines: { node: ">=10.0.0" }
tailwindcss@3.4.16:
tailwindcss@3.4.17:
resolution:
{
integrity: sha512-TI4Cyx7gDiZ6r44ewaJmt0o6BrMCT5aK5e0rmJ/G9Xq3w7CX/5VXl/zIPEJZFUK5VEqwByyhqNPycPlvcK4ZNw==,
integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==,
}
engines: { node: ">=14.0.0" }
hasBin: true
@ -8699,10 +8699,10 @@ packages:
"@vite-pwa/assets-generator":
optional: true
vite@6.0.3:
vite@6.0.5:
resolution:
{
integrity: sha512-Cmuo5P0ENTN6HxLSo6IHsjCLn/81Vgrp81oaiFFMRa8gGDj5xEjIcEpf2ZymZtZR8oU0P2JX5WuUp/rlXcHkAw==,
integrity: sha512-akD5IAH/ID5imgue2DYhzsEwCi0/4VKY31uhMLEYJwPP4TiUp8pL5PIK+Wo7H8qT8JY9i+pVfPydcFPYD1EL7g==,
}
engines: { node: ^18.0.0 || ^20.0.0 || >=22.0.0 }
hasBin: true
@ -8748,10 +8748,10 @@ packages:
integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==,
}
wavesurfer.js@7.8.11:
wavesurfer.js@7.8.12:
resolution:
{
integrity: sha512-bZs7A0vtTVOhuPoDGOXVevAIm+KVYBGwddjL9AeOS7kp/oPcVH9hQWQyR2rBAAfN6s0BKI+EdPEalkNaOmkA6A==,
integrity: sha512-Ovyv3ASEXXWmQVh3clpaZufkraRSg2Uv+28Z5zBHL4nB1HgTZ64lcFMUXX7yZlV5WAIN5ST9w3naaYmOdV2+iw==,
}
wcwidth@1.0.1:
@ -9015,14 +9015,6 @@ packages:
integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==,
}
yaml@2.6.0:
resolution:
{
integrity: sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==,
}
engines: { node: ">= 14" }
hasBin: true
yaml@2.6.1:
resolution:
{
@ -9785,23 +9777,23 @@ snapshots:
"@babel/helper-string-parser": 7.25.9
"@babel/helper-validator-identifier": 7.25.9
"@codemirror/autocomplete@6.18.2(@codemirror/language@6.10.7)(@codemirror/state@6.5.0)(@codemirror/view@6.35.3)(@lezer/common@1.2.3)":
"@codemirror/autocomplete@6.18.2(@codemirror/language@6.10.7)(@codemirror/state@6.5.0)(@codemirror/view@6.36.1)(@lezer/common@1.2.3)":
dependencies:
"@codemirror/language": 6.10.7
"@codemirror/state": 6.5.0
"@codemirror/view": 6.35.3
"@codemirror/view": 6.36.1
"@lezer/common": 1.2.3
"@codemirror/commands@6.7.1":
dependencies:
"@codemirror/language": 6.10.7
"@codemirror/state": 6.5.0
"@codemirror/view": 6.35.3
"@codemirror/view": 6.36.1
"@lezer/common": 1.2.3
"@codemirror/lang-css@6.3.1(@codemirror/view@6.35.3)":
"@codemirror/lang-css@6.3.1(@codemirror/view@6.36.1)":
dependencies:
"@codemirror/autocomplete": 6.18.2(@codemirror/language@6.10.7)(@codemirror/state@6.5.0)(@codemirror/view@6.35.3)(@lezer/common@1.2.3)
"@codemirror/autocomplete": 6.18.2(@codemirror/language@6.10.7)(@codemirror/state@6.5.0)(@codemirror/view@6.36.1)(@lezer/common@1.2.3)
"@codemirror/language": 6.10.7
"@codemirror/state": 6.5.0
"@lezer/common": 1.2.3
@ -9811,39 +9803,39 @@ snapshots:
"@codemirror/lang-html@6.4.9":
dependencies:
"@codemirror/autocomplete": 6.18.2(@codemirror/language@6.10.7)(@codemirror/state@6.5.0)(@codemirror/view@6.35.3)(@lezer/common@1.2.3)
"@codemirror/lang-css": 6.3.1(@codemirror/view@6.35.3)
"@codemirror/autocomplete": 6.18.2(@codemirror/language@6.10.7)(@codemirror/state@6.5.0)(@codemirror/view@6.36.1)(@lezer/common@1.2.3)
"@codemirror/lang-css": 6.3.1(@codemirror/view@6.36.1)
"@codemirror/lang-javascript": 6.2.2
"@codemirror/language": 6.10.7
"@codemirror/state": 6.5.0
"@codemirror/view": 6.35.3
"@codemirror/view": 6.36.1
"@lezer/common": 1.2.3
"@lezer/css": 1.1.9
"@lezer/html": 1.3.10
"@codemirror/lang-javascript@6.2.2":
dependencies:
"@codemirror/autocomplete": 6.18.2(@codemirror/language@6.10.7)(@codemirror/state@6.5.0)(@codemirror/view@6.35.3)(@lezer/common@1.2.3)
"@codemirror/autocomplete": 6.18.2(@codemirror/language@6.10.7)(@codemirror/state@6.5.0)(@codemirror/view@6.36.1)(@lezer/common@1.2.3)
"@codemirror/language": 6.10.7
"@codemirror/lint": 6.8.2
"@codemirror/state": 6.5.0
"@codemirror/view": 6.35.3
"@codemirror/view": 6.36.1
"@lezer/common": 1.2.3
"@lezer/javascript": 1.4.21
"@codemirror/lang-xml@6.1.0":
dependencies:
"@codemirror/autocomplete": 6.18.2(@codemirror/language@6.10.7)(@codemirror/state@6.5.0)(@codemirror/view@6.35.3)(@lezer/common@1.2.3)
"@codemirror/autocomplete": 6.18.2(@codemirror/language@6.10.7)(@codemirror/state@6.5.0)(@codemirror/view@6.36.1)(@lezer/common@1.2.3)
"@codemirror/language": 6.10.7
"@codemirror/state": 6.5.0
"@codemirror/view": 6.35.3
"@codemirror/view": 6.36.1
"@lezer/common": 1.2.3
"@lezer/xml": 1.0.5
"@codemirror/language@6.10.7":
dependencies:
"@codemirror/state": 6.5.0
"@codemirror/view": 6.35.3
"@codemirror/view": 6.36.1
"@lezer/common": 1.2.3
"@lezer/highlight": 1.2.1
"@lezer/lr": 1.4.2
@ -9852,20 +9844,20 @@ snapshots:
"@codemirror/lint@6.8.2":
dependencies:
"@codemirror/state": 6.5.0
"@codemirror/view": 6.35.3
"@codemirror/view": 6.36.1
crelt: 1.0.6
"@codemirror/search@6.5.7":
dependencies:
"@codemirror/state": 6.5.0
"@codemirror/view": 6.35.3
"@codemirror/view": 6.36.1
crelt: 1.0.6
"@codemirror/state@6.5.0":
dependencies:
"@marijn/find-cluster-break": 1.0.2
"@codemirror/view@6.35.3":
"@codemirror/view@6.36.1":
dependencies:
"@codemirror/state": 6.5.0
style-mod: 4.1.2
@ -10857,18 +10849,18 @@ snapshots:
dependencies:
defer-to-connect: 2.0.1
"@tailwindcss/forms@0.5.9(tailwindcss@3.4.16)":
"@tailwindcss/forms@0.5.9(tailwindcss@3.4.17)":
dependencies:
mini-svg-data-uri: 1.4.4
tailwindcss: 3.4.16
tailwindcss: 3.4.17
"@tailwindcss/typography@0.5.15(tailwindcss@3.4.16)":
"@tailwindcss/typography@0.5.15(tailwindcss@3.4.17)":
dependencies:
lodash.castarray: 4.4.0
lodash.isplainobject: 4.0.6
lodash.merge: 4.6.2
postcss-selector-parser: 6.0.10
tailwindcss: 3.4.16
tailwindcss: 3.4.17
"@trysound/sax@0.2.0": {}
@ -11342,13 +11334,13 @@ snapshots:
codemirror@6.0.1(@lezer/common@1.2.3):
dependencies:
"@codemirror/autocomplete": 6.18.2(@codemirror/language@6.10.7)(@codemirror/state@6.5.0)(@codemirror/view@6.35.3)(@lezer/common@1.2.3)
"@codemirror/autocomplete": 6.18.2(@codemirror/language@6.10.7)(@codemirror/state@6.5.0)(@codemirror/view@6.36.1)(@lezer/common@1.2.3)
"@codemirror/commands": 6.7.1
"@codemirror/language": 6.10.7
"@codemirror/lint": 6.8.2
"@codemirror/search": 6.5.7
"@codemirror/state": 6.5.0
"@codemirror/view": 6.35.3
"@codemirror/view": 6.36.1
transitivePeerDependencies:
- "@lezer/common"
@ -12165,7 +12157,7 @@ snapshots:
foreground-child@3.3.0:
dependencies:
cross-spawn: 7.0.3
cross-spawn: 7.0.6
signal-exit: 4.1.0
form-data-encoder@4.0.2: {}
@ -12320,7 +12312,7 @@ snapshots:
globals@14.0.0: {}
globals@15.13.0: {}
globals@15.14.0: {}
globalthis@1.0.4:
dependencies:
@ -13421,7 +13413,7 @@ snapshots:
postcss-load-config@4.0.2(postcss@8.4.49):
dependencies:
lilconfig: 3.1.3
yaml: 2.6.0
yaml: 2.6.1
optionalDependencies:
postcss: 8.4.49
@ -14293,7 +14285,7 @@ snapshots:
string-width: 4.2.3
strip-ansi: 6.0.1
tailwindcss@3.4.16:
tailwindcss@3.4.17:
dependencies:
"@alloc/quick-lru": 5.2.0
arg: 5.0.2
@ -14537,18 +14529,18 @@ snapshots:
spdx-correct: 3.2.0
spdx-expression-parse: 3.0.1
vite-plugin-pwa@0.21.1(vite@6.0.3(@types/node@22.9.0)(jiti@2.4.1)(terser@5.36.0)(yaml@2.6.1))(workbox-build@7.3.0)(workbox-window@7.3.0):
vite-plugin-pwa@0.21.1(vite@6.0.5(@types/node@22.9.0)(jiti@2.4.1)(terser@5.36.0)(yaml@2.6.1))(workbox-build@7.3.0)(workbox-window@7.3.0):
dependencies:
debug: 4.3.7
pretty-bytes: 6.1.1
tinyglobby: 0.2.10
vite: 6.0.3(@types/node@22.9.0)(jiti@2.4.1)(terser@5.36.0)(yaml@2.6.1)
vite: 6.0.5(@types/node@22.9.0)(jiti@2.4.1)(terser@5.36.0)(yaml@2.6.1)
workbox-build: 7.3.0
workbox-window: 7.3.0
transitivePeerDependencies:
- supports-color
vite@6.0.3(@types/node@22.9.0)(jiti@2.4.1)(terser@5.36.0)(yaml@2.6.1):
vite@6.0.5(@types/node@22.9.0)(jiti@2.4.1)(terser@5.36.0)(yaml@2.6.1):
dependencies:
esbuild: 0.24.0
postcss: 8.4.49
@ -14562,7 +14554,7 @@ snapshots:
w3c-keyname@2.2.8: {}
wavesurfer.js@7.8.11: {}
wavesurfer.js@7.8.12: {}
wcwidth@1.0.1:
dependencies:
@ -14775,8 +14767,6 @@ snapshots:
yallist@3.1.1: {}
yaml@2.6.0: {}
yaml@2.6.1: {}
yargs-parser@18.1.3:

View File

@ -1,184 +0,0 @@
<?php switch ($type): case 'checkbox': ?>
<x-Forms.Checkbox
class="<?= $class ?>"
name="<?= $name ?>"
hint="<?= $hint ?>"
helper="<?= $helper ?>"
value="<?= $value ? 'yes' : '' ?>"
defaultValue="<?= $defaultValue ?>"
><?= $label ?></x-Forms.Checkbox>
<?php break;
case 'toggler': ?>
<x-Forms.Toggler
class="<?= $class ?>"
name="<?= $name ?>"
hint="<?= $hint ?>"
helper="<?= $helper ?>"
value="<?= $value ? 'yes' : '' ?>"
defaultValue="<?= $defaultValue ?>"
><?= $label ?></x-Forms.Toggler>
<?php break;
case 'radio-group': ?>
<x-Forms.RadioGroup
class="<?= $class ?>"
name="<?= $name ?>"
label="<?= $label ?>"
hint="<?= $hint ?>"
helper="<?= $helper ?>"
options="<?= $options ?>"
isRequired="<?= $optional ? 'false' : 'true' ?>"
value="<?= $value ?>"
defaultValue="<?= $defaultValue ?>"
/>
<?php break;
case 'select': ?>
<x-Forms.Field
as="Select"
class="<?= $class ?>"
name="<?= $name ?>"
label="<?= $label ?>"
hint="<?= $hint ?>"
helper="<?= $helper ?>"
options="<?= $options ?>"
isRequired="<?= $optional ? 'false' : 'true' ?>"
value="<?= $value ?>"
defaultValue="<?= $defaultValue ?>"
/>
<?php break;
case 'select-multiple': ?>
<x-Forms.Field
as="SelectMulti"
class="<?= $class ?>"
name="<?= $name ?>"
label="<?= $label ?>"
hint="<?= $hint ?>"
helper="<?= $helper ?>"
options="<?= $options ?>"
isRequired="<?= $optional ? 'false' : 'true' ?>"
value="<?= $value ?>"
defaultValue="<?= $defaultValue ?>"
/>
<?php break;
case 'email': ?>
<x-Forms.Field
as="Input"
class="<?= $class ?>"
type="email"
name="<?= $name ?>"
label="<?= $label ?>"
hint="<?= $hint ?>"
helper="<?= $helper ?>"
isRequired="<?= $optional ? 'false' : 'true' ?>"
value="<?= $value ?>"
defaultValue="<?= $defaultValue ?>"
/>
<?php break;
case 'url': ?>
<x-Forms.Field
as="Input"
class="<?= $class ?>"
type="url"
placeholder="https://…"
name="<?= $name ?>"
label="<?= $label ?>"
hint="<?= $hint ?>"
helper="<?= $helper ?>"
isRequired="<?= $optional ? 'false' : 'true' ?>"
value="<?= $value ?>"
defaultValue="<?= $defaultValue ?>"
/>
<?php break;
case 'number': ?>
<x-Forms.Field
as="Input"
class="<?= $class ?>"
type="number"
name="<?= $name ?>"
label="<?= $label ?>"
hint="<?= $hint ?>"
helper="<?= $helper ?>"
isRequired="<?= $optional ? 'false' : 'true' ?>"
value="<?= $value ?>"
defaultValue="<?= $defaultValue ?>"
/>
<?php break;
case 'textarea': ?>
<x-Forms.Field
as="Textarea"
class="<?= $class ?>"
name="<?= $name ?>"
label="<?= $label ?>"
hint="<?= $hint ?>"
helper="<?= $helper ?>"
isRequired="<?= $optional ? 'false' : 'true' ?>"
value="<?= $value ?>"
defaultValue="<?= $defaultValue ?>"
/>
<?php break;
case 'html': ?>
<x-Forms.Field
as="CodeEditor"
lang="html"
class="<?= $class ?>"
name="<?= $name ?>"
label="<?= $label ?>"
hint="<?= $hint ?>"
helper="<?= $helper ?>"
isRequired="<?= $optional ? 'false' : 'true' ?>"
value="<?= htmlspecialchars($value) ?>"
defaultValue="<?= $defaultValue ?>"
/>
<?php break;
case 'markdown': ?>
<x-Forms.Field
as="MarkdownEditor"
class="<?= $class ?>"
name="<?= $name ?>"
label="<?= $label ?>"
hint="<?= $hint ?>"
helper="<?= $helper ?>"
isRequired="<?= $optional ? 'false' : 'true' ?>"
value="<?= $value ?>"
defaultValue="<?= $defaultValue ?>"
/>
<?php break;
case 'rss': ?>
<x-Forms.Field
as="CodeEditor"
lang="xml"
class="<?= $class ?>"
name="<?= $name ?>"
label="<?= $label ?>"
hint="<?= $hint ?>"
helper="<?= $helper ?>"
isRequired="<?= $optional ? 'false' : 'true' ?>"
value="<?= htmlspecialchars($value) ?>"
defaultValue="<?= $defaultValue ?>"
/>
<?php break;
case 'datetime': ?>
<x-Forms.Field
as="DatetimePicker"
class="<?= $class ?>"
name="<?= $name ?>"
label="<?= $label ?>"
hint="<?= $hint ?>"
helper="<?= $helper ?>"
isRequired="<?= $optional ? 'false' : 'true' ?>"
value="<?= $value ?>"
defaultValue="<?= $defaultValue ?>"
/>
<?php break;
default: ?>
<x-Forms.Field
as="Input"
class="<?= $class ?>"
name="<?= $name ?>"
label="<?= $label ?>"
hint="<?= $hint ?>"
helper="<?= $helper ?>"
isRequired="<?= $optional ? 'false' : 'true' ?>"
value="<?= $value ?>"
defaultValue="<?= $defaultValue ?>"
/>
<?php endswitch; ?>

View File

@ -9,25 +9,14 @@
if ($field->type === 'group'): ?>
<div class="flex flex-col gap-4" data-field-array="<?= $field->key ?>">
<fieldset class="flex flex-col gap-6 rounded" data-field-array-container="<?= $field->key ?>">
<legend class="relative z-10 mb-4 font-bold text-heading-foreground font-display before:w-full before:absolute before:h-1/2 before:left-0 before:bottom-0 before:rounded-full before:bg-heading-background before:z-[-10] tracking-wide text-base"><?= $field->getTranslated($plugin->getKey(), 'label') ?></legend>
<legend class="relative z-10 mb-4 font-bold text-heading-foreground font-display before:w-full before:absolute before:h-1/2 before:left-0 before:bottom-0 before:rounded-full before:bg-heading-background before:z-[-10] tracking-wide text-base"><?= $field->label ?></legend>
<?php
$fieldArrayValues = get_plugin_setting($plugin->getKey(), $field->key, $context) ?? [''];
foreach ($fieldArrayValues as $index => $value): ?>
<fieldset class="relative flex flex-col border border-subtle p-4 rounded-tl-none rounded-md gap-2 bg-base" data-field-array-item="<?= $index ?>">
<legend class="absolute font-mono left-0 -top-px -ml-6 rounded-l-full rounded-r-none w-6 text-xs h-6 inline-flex items-center justify-center font-semibold border border-subtle bg-base"><span class="sr-only"><?= $field->getTranslated($plugin->getKey(), 'label') ?></span> <span data-field-array-number><?= $index + 1 ?></span></legend>
<legend class="absolute font-mono left-0 -top-px -ml-6 rounded-l-full rounded-r-none w-6 text-xs h-6 inline-flex items-center justify-center font-semibold border border-subtle bg-base"><span class="sr-only"><?= $field->label ?></span> <span data-field-array-number><?= $index + 1 ?></span></legend>
<?php foreach ($field->fields as $subfield): ?>
<?= view('plugins/_field', [
'class' => 'flex-1',
'type' => $subfield->type,
'name' => sprintf('%s[%s][%s]', $field->key, $index, $subfield->key),
'label' => $subfield->getTranslated($plugin->getKey(), 'label'),
'hint' => $subfield->getTranslated($plugin->getKey(), 'hint'),
'value' => $value[$subfield->key] ?? null,
'helper' => $subfield->getTranslated($plugin->getKey(), 'helper'),
'defaultValue' => esc($subfield->defaultValue),
'options' => esc(json_encode($subfield->getOptionsArray($plugin->getKey()))),
'optional' => $subfield->optional,
]) ?>
<?= $subfield->render(sprintf('%s[%s][%s]', $field->key, $index, $subfield->key), $value[$subfield->key] ?? null, 'flex-1'); ?>
<?php endforeach; ?>
<x-IconButton variant="danger" glyph="delete-bin-fill" data-field-array-delete="<?= $index ?>" class="absolute right-0 top-0 -mt-4 -mr-4"><?= lang('Common.forms.fieldArray.remove') ?></x-IconButton>
</fieldset>
@ -42,18 +31,7 @@
foreach ($fieldArrayValue as $index => $value): ?>
<div class="relative flex items-end" data-field-array-item="<?= $index ?>">
<span class="self-start mr-1 -ml-5 w-4 rtl text-sm before:content-['.']" data-field-array-number style="direction:rtl"><?= $index + 1 ?></span>
<?= view('plugins/_field', [
'class' => 'flex-1',
'type' => $field->type,
'name' => sprintf('%s[%s]', $field->key, $index),
'label' => $field->getTranslated($plugin->getKey(), 'label'),
'hint' => $field->getTranslated($plugin->getKey(), 'hint'),
'value' => $value,
'helper' => $field->getTranslated($plugin->getKey(), 'helper'),
'defaultValue' => esc($field->defaultValue),
'options' => esc(json_encode($field->getOptionsArray($plugin->getKey()))),
'optional' => $field->optional,
]) ?>
<?= $field->render(sprintf('%s[%s]', $field->key, $index), $value, 'flex-1'); ?>
<x-IconButton variant="danger" glyph="delete-bin-fill" data-field-array-delete="<?= $index ?>" type="button" class="mb-2 ml-2"><?= lang('Common.forms.fieldArray.remove') ?></x-IconButton>
</div>
<?php endforeach; ?>
@ -64,35 +42,13 @@
<?php elseif ($field->type === 'group'):
$value = get_plugin_setting($plugin->getKey(), $field->key, $context); ?>
<fieldset class="flex flex-col border border-subtle p-4 rounded-tl-none rounded-md gap-2 bg-base">
<legend class="relative z-10 font-bold text-heading-foreground font-display before:w-full before:absolute before:h-1/2 before:left-0 before:bottom-0 before:rounded-full before:bg-heading-background before:z-[-10] tracking-wide text-base"><?= $field->getTranslated($plugin->getKey(), 'label') ?></legend>
<legend class="relative z-10 font-bold text-heading-foreground font-display before:w-full before:absolute before:h-1/2 before:left-0 before:bottom-0 before:rounded-full before:bg-heading-background before:z-[-10] tracking-wide text-base"><?= $field->label ?></legend>
<?php foreach ($field->fields as $subfield): ?>
<?= view('plugins/_field', [
'class' => 'flex-1',
'type' => $subfield->type,
'name' => sprintf('%s[%s]', $field->key, $subfield->key),
'label' => $subfield->getTranslated($plugin->getKey(), 'label'),
'hint' => $subfield->getTranslated($plugin->getKey(), 'hint'),
'value' => $value[$subfield->key] ?? null,
'helper' => $subfield->getTranslated($plugin->getKey(), 'helper'),
'defaultValue' => esc($subfield->defaultValue),
'options' => esc(json_encode($subfield->getOptionsArray($plugin->getKey()))),
'optional' => $subfield->optional,
]) ?>
<?= $subfield->render(sprintf('%s[%s]', $field->key, $subfield->key), $value[$subfield->key] ?? null, 'flex-1'); ?>
<?php endforeach; ?>
</fieldset>
<?php else: ?>
<?= view('plugins/_field', [
'class' => '',
'type' => $field->type,
'name' => $field->key,
'label' => $field->getTranslated($plugin->getKey(), 'label'),
'hint' => $field->getTranslated($plugin->getKey(), 'hint'),
'value' => get_plugin_setting($plugin->getKey(), $field->key, $context),
'helper' => $field->getTranslated($plugin->getKey(), 'helper'),
'defaultValue' => esc($field->defaultValue),
'options' => esc(json_encode($field->getOptionsArray($plugin->getKey()))),
'optional' => $field->optional,
]) ?>
<?= $field->render($field->key, get_plugin_setting($plugin->getKey(), $field->key, $context)); ?>
<?php endif; ?>
<?php endforeach; ?>
@ -101,4 +57,5 @@
<?php endif; ?>
<x-Button class="self-end mt-4" variant="primary" type="submit"><?= lang('Common.forms.save') ?></x-Button>
</form>
</form>

View File

@ -102,7 +102,7 @@
name="other_categories"
label="<?= esc(lang('Podcast.form.other_categories')) ?>"
data-max-item-count="2"
value="<?= $podcast->other_categories_ids ?>"
value="<?= esc(json_encode($podcast->other_categories_ids)) ?>"
options="<?= esc(json_encode($categoryOptions)) ?>" />
<x-Forms.RadioGroup