Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Unreleased

- Added fluent APIs for creating and modifying `CraftCms\Cms\FieldLayout\FieldLayout`, `CraftCms\Cms\FieldLayout\FieldLayoutTab`, and field layout elements.
- Changed `CraftCms\Cms\FieldLayout\LayoutElements\BaseField::label()` to accept an optional label and return the field layout element when one is passed. Overrides must accept the new optional argument.
- Renamed the protected `CraftCms\Cms\FieldLayout\LayoutElements\BaseField::instructions()`, `tip()`, and `warning()` methods to `instructionsText()`, `tipText()`, and `warningText()`.
- Updated core asset I/O to resolve Craft filesystem definitions and configured storage targets through Laravel filesystem disks.
- Updated elevated session prompts to use the modern control panel frontend while preserving the legacy JavaScript APIs.
- Login attempts are now rate limited.
Expand Down
37 changes: 16 additions & 21 deletions database/Factories/FieldLayoutFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
use CraftCms\Cms\Entry\Elements\Entry;
use CraftCms\Cms\Field\Field;
use CraftCms\Cms\Field\Models\Field as FieldModel;
use CraftCms\Cms\FieldLayout\FieldLayout as FieldLayoutConfig;
use CraftCms\Cms\FieldLayout\FieldLayoutElement;
use CraftCms\Cms\FieldLayout\FieldLayoutTab;
use CraftCms\Cms\FieldLayout\LayoutElements\CustomField;
use CraftCms\Cms\FieldLayout\Models\FieldLayout;
use CraftCms\Cms\Support\Str;
use Illuminate\Database\Eloquent\Factories\Factory;

class FieldLayoutFactory extends Factory
Expand All @@ -24,30 +26,23 @@ public function definition(): array
];
}

public function withContentTab(array $elements = [], string $name = 'Content'): self
/** @param FieldLayoutElement[] $elements */
public function withContentTab(array $elements = [], ?string $name = null): self
{
return $this->state(fn () => [
'config' => [
'tabs' => [
[
'uid' => Str::uuid()->toString(),
'name' => $name,
'elements' => $elements,
],
],
],
]);
return $this->state(function (array $attributes) use ($elements, $name) {
$layout = FieldLayoutConfig::create($attributes['type']);
$layout->tab($name ?? FieldLayoutConfig::defaultTabName(), fn (FieldLayoutTab $tab) => $tab->add(...$elements));

return ['config' => $layout->getConfig()];
});
}

public function forField(Field|FieldModel $field, bool $required = false): self
{
return $this->withContentTab([
[
'uid' => Str::uuid()->toString(),
'type' => CustomField::class,
'fieldUid' => $field->uid,
'required' => $required,
],
]);
$element = $field instanceof FieldModel
? new CustomField(config: ['fieldUid' => $field->uid])
: CustomField::for($field);

return $this->withContentTab([$element->required($required)]);
}
}
2 changes: 1 addition & 1 deletion src/Address/Addresses.php
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ public function getFieldLayout(): FieldLayout
if (! $firstTab) {
$firstTab = new FieldLayoutTab([
'layout' => $fieldLayout,
'name' => t('Content'),
'name' => FieldLayout::defaultTabName(),
]);
$fieldLayout->setTabs([$firstTab]);
}
Expand Down
2 changes: 1 addition & 1 deletion src/Cp/FieldLayoutDesigner/FieldLayoutDesigner.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public function html(FieldLayout $fieldLayout, array $config = []): string
'uid' => Str::uuid()->toString(),
'layout' => $fieldLayout,
]);
$tab->name = $config['pretendTabName'] ?? t('Content');
$tab->name = $config['pretendTabName'] ?? FieldLayout::defaultTabName();

// Any extra tabs?
if (! empty($tabs)) {
Expand Down
82 changes: 81 additions & 1 deletion src/FieldLayout/FieldLayout.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@

use function CraftCms\Cms\t;

/** @phpstan-consistent-constructor */
class FieldLayout extends Component
{
use LegacyConstants;
Expand Down Expand Up @@ -154,6 +155,85 @@ public function __construct(
}
}

public static function defaultTabName(): string
{
return t('Content');
}

/**
* @param class-string<ElementInterface> $type
*/
public static function create(string $type): static
{
return new static(['type' => $type]);
}

/**
* Creates or modifies a tab by name.
*
* Use {@see defaultTabName()} to target the tab created for mandatory fields.
*
* @param Closure(FieldLayoutTab): mixed $closure
*/
public function tab(string $name, Closure $closure): static
{
$tab = array_find($this->getTabs(), fn (FieldLayoutTab $tab) => $tab->name === $name);

if ($tab === null) {
$tab = new FieldLayoutTab([
'layout' => $this,
'name' => $name,
'elements' => [],
]);
$this->setTabs([...$this->getTabs(), $tab]);
}

$closure($tab);

return $this;
}
Comment thread
riasvdv marked this conversation as resolved.

public function getTab(string $name): FieldLayoutTab
{
return array_find($this->getTabs(), fn (FieldLayoutTab $tab) => $tab->name === $name)
?? throw new InvalidArgumentException(sprintf('Unknown tab: %s', $name));
}
Comment thread
riasvdv marked this conversation as resolved.

public function removeField(FieldInterface|string $field): static
{
if (is_string($field)) {
$field = Fields::getFieldByHandle($field)
?? throw new InvalidArgumentException(sprintf('Unknown field handle: %s', $field));
}

foreach ($this->getTabs() as $tab) {
$elements = array_filter(
$tab->getElements(),
fn (FieldLayoutElement $element) => ! $element instanceof CustomField || $element->getFieldUid() !== $field->uid,
);

if (count($elements) !== count($tab->getElements())) {
$tab->setElements(array_values($elements));
}
}

return $this;
}

public function removeTab(string $name): static
{
$tabs = array_values(array_filter(
$this->getTabs(),
fn (FieldLayoutTab $tab) => $tab->name !== $name,
));

if (count($tabs) !== count($this->getTabs())) {
$this->setTabs($tabs);
}

return $this;
}
Comment thread
riasvdv marked this conversation as resolved.

/**
* Creates a new field layout from the given config.
*/
Expand Down Expand Up @@ -817,7 +897,7 @@ public function prependElements(array $elements): void
$this->_tabs[] = $tab = new FieldLayoutTab([
'layout' => $this,
'layoutId' => $this->id,
'name' => t('Content'),
'name' => static::defaultTabName(),
'sortOrder' => 1,
'elements' => [],
]);
Expand Down
14 changes: 14 additions & 0 deletions src/FieldLayout/FieldLayoutComponent.php
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,13 @@ public function setUserCondition(mixed $userCondition): void
$this->_userCondition = $userCondition;
}

public function userCondition(mixed $userCondition): static
{
$this->setUserCondition($userCondition);

return $this;
}

public function getElementCondition(): ?ElementConditionInterface
{
if (isset($this->_elementCondition) && ! $this->_elementCondition instanceof ElementConditionInterface) {
Expand Down Expand Up @@ -155,6 +162,13 @@ public function setElementCondition(mixed $elementCondition): void
$this->_elementCondition = $elementCondition;
}

public function elementCondition(mixed $elementCondition): static
{
$this->setElementCondition($elementCondition);

return $this;
}

/**
* Normalizes a condition.
*
Expand Down
7 changes: 7 additions & 0 deletions src/FieldLayout/FieldLayoutElement.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ public function hasCustomWidth(): bool
return false;
}

public function width(int $width): static
{
$this->width = $width;

return $this;
}

/**
* Returns the selector HTML that should be displayed within field layout designers.
*/
Expand Down
116 changes: 116 additions & 0 deletions src/FieldLayout/FieldLayoutTab.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,21 @@

namespace CraftCms\Cms\FieldLayout;

use Closure;
use CraftCms\Cms\Cp\FormFields;
use CraftCms\Cms\Cp\Icons;
use CraftCms\Cms\Element\Contracts\ElementInterface;
use CraftCms\Cms\Field\Contracts\FieldInterface;
use CraftCms\Cms\Field\Exceptions\FieldNotFoundException;
use CraftCms\Cms\Field\Fields;
use CraftCms\Cms\FieldLayout\LayoutElements\BaseField;
use CraftCms\Cms\FieldLayout\LayoutElements\CustomField;
use CraftCms\Cms\FieldLayout\LayoutElements\Heading;
use CraftCms\Cms\FieldLayout\LayoutElements\HorizontalRule;
use CraftCms\Cms\FieldLayout\LayoutElements\LineBreak;
use CraftCms\Cms\FieldLayout\LayoutElements\Markdown;
use CraftCms\Cms\FieldLayout\LayoutElements\Template;
use CraftCms\Cms\FieldLayout\LayoutElements\Tip;
use CraftCms\Cms\Plugin\Plugins;
use CraftCms\Cms\Support\Arr;
use CraftCms\Cms\Support\Html;
Expand Down Expand Up @@ -287,6 +295,114 @@ public function setElements(array $elements): void
}
}

/**
* Adds elements to the tab.
*
* Multi-instance custom fields need a unique handle override when included more than once.
*/
public function add(FieldLayoutElement ...$elements): static
{
$fieldUids = array_fill_keys(array_map(
fn (CustomField $element) => $element->getFieldUid(),
$this->getLayout()->getElementsByType(CustomField::class),
), true);

foreach ($elements as $element) {
if ($element instanceof CustomField) {
$fieldUid = $element->getFieldUid();

if (isset($fieldUids[$fieldUid]) && ! $element->isMultiInstance()) {
throw new InvalidArgumentException(sprintf(
'The field "%s" is already included in this field layout.',
$element->attribute(),
));
}

$fieldUids[$fieldUid] = true;
}
}

foreach ($elements as $element) {
$element->dateAdded ??= now();
}

$this->setElements([...$this->getElements(), ...$elements]);

return $this;
}

/** @param (Closure(CustomField): mixed)|null $configure */
public function field(FieldInterface|string $field, ?Closure $configure = null): static
{
$element = CustomField::for($field);
$configure?->__invoke($element);

return $this->add($element);
}

/** @param (Closure(Heading): mixed)|null $configure */
public function heading(string $heading, ?Closure $configure = null): static
{
$element = new Heading($heading);
$configure?->__invoke($element);

return $this->add($element);
}

/** @param (Closure(Tip): mixed)|null $configure */
public function tip(string $tip, ?Closure $configure = null): static
{
$element = new Tip($tip);
$configure?->__invoke($element);

return $this->add($element);
}

/** @param (Closure(Tip): mixed)|null $configure */
public function warning(string $warning, ?Closure $configure = null): static
{
$element = new Tip($warning)->warning();
$configure?->__invoke($element);

return $this->add($element);
}

/** @param (Closure(Markdown): mixed)|null $configure */
public function markdown(string $content, ?Closure $configure = null): static
{
$element = new Markdown($content);
$configure?->__invoke($element);

return $this->add($element);
}

/** @param (Closure(Template): mixed)|null $configure */
public function template(string $template, ?Closure $configure = null): static
{
$element = new Template($template);
$configure?->__invoke($element);

return $this->add($element);
}

/** @param (Closure(HorizontalRule): mixed)|null $configure */
public function horizontalRule(?Closure $configure = null): static
{
$element = new HorizontalRule;
$configure?->__invoke($element);

return $this->add($element);
}

/** @param (Closure(LineBreak): mixed)|null $configure */
public function lineBreak(?Closure $configure = null): static
{
$element = new LineBreak;
$configure?->__invoke($element);

return $this->add($element);
}

public function getHtmlId(): string
{
$asciiName = isset($this->name) ? Str::kebab(Str::ascii($this->name)) : '';
Expand Down
Loading
Loading