From 923fbec18ce31fa1f0b8c90930cb861d0d3c76aa Mon Sep 17 00:00:00 2001 From: markhuot Date: Tue, 28 Nov 2023 21:31:38 -0500 Subject: [PATCH 01/10] entry queries and assets all use templates now --- src/actions/GetComponentType.php | 2 +- src/listeners/RegisterDefaultComponentTypes.php | 2 +- src/templates/asset/embed.twig | 3 +++ src/templates/components/asset.twig | 5 ++++- src/templates/components/entry.twig | 2 +- .../components/{elementquery.twig => entryquery.twig} | 6 +++--- src/templates/components/template.twig | 1 + src/templates/entry/link.twig | 4 +++- tests/RouteTest.php | 11 ++++++----- .../{ElementQueryTest.php => EntryQueryTest.php} | 4 ++-- 10 files changed, 25 insertions(+), 15 deletions(-) create mode 100644 src/templates/asset/embed.twig rename src/templates/components/{elementquery.twig => entryquery.twig} (80%) rename tests/components/{ElementQueryTest.php => EntryQueryTest.php} (72%) diff --git a/src/actions/GetComponentType.php b/src/actions/GetComponentType.php index c1c77be..d729f7d 100644 --- a/src/actions/GetComponentType.php +++ b/src/actions/GetComponentType.php @@ -53,7 +53,7 @@ public function byType(string $type): ComponentType $fqcn = $className; } - if ($fqcn) { + if (! empty($fqcn)) { return Craft::$container->get($fqcn, ['context' => $this->context]); } diff --git a/src/listeners/RegisterDefaultComponentTypes.php b/src/listeners/RegisterDefaultComponentTypes.php index 60a4ca1..ed4245f 100644 --- a/src/listeners/RegisterDefaultComponentTypes.php +++ b/src/listeners/RegisterDefaultComponentTypes.php @@ -9,8 +9,8 @@ class RegisterDefaultComponentTypes public function handle(RegisterComponentTypes $event): void { $event->registerTwigTemplate('keystone/asset', 'cp:keystone/components/asset.twig'); - $event->registerTwigTemplate('keystone/elementquery', 'cp:keystone/components/elementquery.twig'); $event->registerTwigTemplate('keystone/entry', 'cp:keystone/components/entry.twig'); + $event->registerTwigTemplate('keystone/entryquery', 'cp:keystone/components/entryquery.twig'); $event->registerTwigTemplate('keystone/fragment', 'cp:keystone/components/fragment.twig'); $event->registerTwigTemplate('keystone/heading', 'cp:keystone/components/heading.twig'); $event->registerTwigTemplate('keystone/icon', 'cp:keystone/components/icon.twig'); diff --git a/src/templates/asset/embed.twig b/src/templates/asset/embed.twig new file mode 100644 index 0000000..90700f5 --- /dev/null +++ b/src/templates/asset/embed.twig @@ -0,0 +1,3 @@ +{% if asset|default(false) %} + +{% endif %} diff --git a/src/templates/components/asset.twig b/src/templates/components/asset.twig index 28218af..d0cde03 100644 --- a/src/templates/components/asset.twig +++ b/src/templates/components/asset.twig @@ -9,6 +9,9 @@ } %} {% export summary = props.asset.one().title|default %} {% export category = "Data" %} +{% set slot = component.getType.defineSlot().defaults([{type: 'keystone/template', data: {template: 'cp:keystone/asset/embed'}}]) %} {% for asset in props.asset %} - + {% do component.mergeContext({asset: asset, transform: props.transform.value|default(null)}) %} + {{ slot }} + {% do component.mergeContext({asset: null, transform: null}) %} {% endfor %} diff --git a/src/templates/components/entry.twig b/src/templates/components/entry.twig index 58d367a..2ebec90 100644 --- a/src/templates/components/entry.twig +++ b/src/templates/components/entry.twig @@ -5,7 +5,7 @@ } %} {% export summary = props.entry.one().title|default %} {% export category = "Data" %} -{% set slot = component.getType().defineSlot() %} +{% set slot = component.getType().defineSlot().defaults([{type: 'keystone/template', data: {template: 'cp:keystone/entry/link'}}]) %} {% for entry in props.entry %} {% do component.mergeContext({entry: entry}) %} {{ slot }} diff --git a/src/templates/components/elementquery.twig b/src/templates/components/entryquery.twig similarity index 80% rename from src/templates/components/elementquery.twig rename to src/templates/components/entryquery.twig index fb42762..f4d13f6 100644 --- a/src/templates/components/elementquery.twig +++ b/src/templates/components/entryquery.twig @@ -1,12 +1,12 @@ {% export category = "Data" %} -{% export name = "Element Query" %} +{% export name = "Entry Query" %} {% export icon %}{% endexport %} {% export propTypes = { search: field('\\markhuot\\keystone\\fields\\Condition'), limit: field('\\craft\\fields\\Number'), } %} {% set defaultSlot = component.getType().defineSlot() %} -{% set elements = props.search.limit(props.limit|default(100)).all()|default([]) %} +{% set elements = props.search.all()|default([]) %} {% for element in elements %} - {{ defaultSlot.render({element: element}) }} + {{ defaultSlot.render({entry: element}) }} {% endfor %} diff --git a/src/templates/components/template.twig b/src/templates/components/template.twig index d65e068..b84c19f 100644 --- a/src/templates/components/template.twig +++ b/src/templates/components/template.twig @@ -1,3 +1,4 @@ +{% export summary = props.template %} {% if props.template %} {% set parts = props.template|split(':') %} {% if parts|length == 2 %} diff --git a/src/templates/entry/link.twig b/src/templates/entry/link.twig index 5601d97..ad52a09 100644 --- a/src/templates/entry/link.twig +++ b/src/templates/entry/link.twig @@ -1 +1,3 @@ -{{ entry.title }} +{% if entry|default(false) %} + {{ entry.title }} +{% endif %} diff --git a/tests/RouteTest.php b/tests/RouteTest.php index f7d6153..309fe1e 100644 --- a/tests/RouteTest.php +++ b/tests/RouteTest.php @@ -23,11 +23,12 @@ $this->actingAsAdmin() ->get(UrlHelper::cpUrl('keystone/components/edit', $component->getQueryCondition())) ->assertOk(); -})->with([ - 'keystone/asset', 'keystone/elementquery', 'keystone/fragment', - 'keystone/icon', 'keystone/link', 'keystone/heading', - 'keystone/section', 'keystone/text', -]); +})->with(collect(scandir(__DIR__.'/../src/templates/components')) + ->filter(fn ($file) => ! str_starts_with($file, '.')) + ->filter(fn ($file) => str_ends_with($file, '.twig')) + ->map(fn ($file) => 'keystone/'.preg_replace('/\.twig$/', '', $file)) + ->all() +); it('stores a component', function () { $component = Component::factory() diff --git a/tests/components/ElementQueryTest.php b/tests/components/EntryQueryTest.php similarity index 72% rename from tests/components/ElementQueryTest.php rename to tests/components/EntryQueryTest.php index 2a7182c..24b7f65 100644 --- a/tests/components/ElementQueryTest.php +++ b/tests/components/EntryQueryTest.php @@ -5,9 +5,9 @@ it('renders element queries', function () { $entry = Entry::factory()->title('foobarbaz')->create(); - $component = Component::factory()->type('keystone/elementquery')->create(); + $component = Component::factory()->type('keystone/entryquery')->create(); $test = Component::factory()->type('keystone/text')->path($component->id)->create(); - $test->data->merge(['text' => '{element.title}'])->save(); + $test->data->merge(['text' => '{entry.title}'])->save(); $response = $component->render(); expect($response)->toContain('foobarbaz'); }); From 203f9bca81bad12a80ab44b837b8311559ea2ea7 Mon Sep 17 00:00:00 2001 From: markhuot Date: Tue, 28 Nov 2023 22:45:31 -0500 Subject: [PATCH 02/10] disclosures persist to the database --- src/base/SlotDefinition.php | 14 +++++++++++ src/controllers/ComponentsController.php | 20 +++++++++++++++ src/db/Table.php | 2 +- src/fields/Keystone.php | 2 +- src/migrations/Install.php | 13 ++++++++++ src/models/Component.php | 31 ++++++++++++++++++++---- src/models/ComponentDisclosure.php | 27 +++++++++++++++++++++ src/resources/components/alpine.js | 6 ++++- src/templates/components/asset.twig | 4 ++- src/templates/components/entry.twig | 4 ++- src/templates/components/entryquery.twig | 2 +- src/templates/components/template.twig | 1 + src/templates/field.twig | 9 +++---- 13 files changed, 119 insertions(+), 16 deletions(-) create mode 100644 src/models/ComponentDisclosure.php diff --git a/src/base/SlotDefinition.php b/src/base/SlotDefinition.php index ade1f90..7057c72 100644 --- a/src/base/SlotDefinition.php +++ b/src/base/SlotDefinition.php @@ -8,6 +8,8 @@ class SlotDefinition { + protected bool $collapsed = false; + public function __construct( protected ?Component $component = null, protected ?string $name = null, @@ -43,6 +45,18 @@ public function defaults(array $componentConfig): self return $this; } + public function collapsed(bool $collapsed=true): self + { + $this->collapsed = $collapsed; + + return $this; + } + + public function isCollapsed(): bool + { + return $this->collapsed; + } + public function allows(string $type): bool { if (! empty($this->whitelist)) { diff --git a/src/controllers/ComponentsController.php b/src/controllers/ComponentsController.php index 6e6a3de..437929f 100644 --- a/src/controllers/ComponentsController.php +++ b/src/controllers/ComponentsController.php @@ -105,4 +105,24 @@ public function actionMove() 'fieldHtml' => $data->getTargetElement()->getFieldHtml($data->getTargetField()), ]); } + + public function actionToggleDisclosure() + { + /** @var Component $component */ + $component = $this->request->getQueryParamObjectOrFail(Component::class); + $defns = $component->getType()->getSlotDefinitions(); + $defaultState = $defns->every(fn ($d) => $d->isCollapsed()) ? 'closed' : 'open'; + $state = $component->disclosure->state ?? $defaultState; + $newState = $state === 'open' ? 'closed' : 'open'; + + if ($newState === $defaultState) { + $component->disclosure->delete(); + } + else { + $component->disclosure->state = $newState; + $component->disclosure->save(); + } + + return $this->asSuccess('Saved'); + } } diff --git a/src/db/Table.php b/src/db/Table.php index b182da6..acb1d86 100644 --- a/src/db/Table.php +++ b/src/db/Table.php @@ -5,6 +5,6 @@ class Table { const COMPONENTS = '{{%keystone_components}}'; - const COMPONENT_DATA = '{{%keystone_component_data}}'; + const COMPONENT_DISCLOSURES = '{{%keystone_component_disclosure}}'; } diff --git a/src/fields/Keystone.php b/src/fields/Keystone.php index ce9a481..599c4c2 100644 --- a/src/fields/Keystone.php +++ b/src/fields/Keystone.php @@ -42,7 +42,7 @@ protected function inputHtml(mixed $value, ElementInterface $element = null): st return Craft::$app->getView()->renderTemplate('keystone/field', [ 'element' => $element, 'field' => $this, - 'component' => $this->getFragment($element), + 'component' => $this->getFragment($element)->withDisclosures(), 'getComponentTypes' => new GetComponentType, ]); } diff --git a/src/migrations/Install.php b/src/migrations/Install.php index fb4df24..d0f4d00 100644 --- a/src/migrations/Install.php +++ b/src/migrations/Install.php @@ -32,16 +32,29 @@ public function safeUp() 'uid' => $this->uid(), ]); + $this->createTable(Table::COMPONENT_DISCLOSURES, [ + 'id' => $this->primaryKey(), + 'userId' => $this->integer(), + 'componentId' => $this->integer(), + 'state' => $this->enum('state', ['open', 'closed']), + 'dateCreated' => $this->dateTime()->notNull(), + 'dateUpdated' => $this->dateTime()->notNull(), + 'uid' => $this->uid(), + ]); + $this->createIndex(null, Table::COMPONENTS, ['id', 'elementId']); $this->addForeignKey(null, Table::COMPONENTS, ['elementId'], \craft\db\Table::ELEMENTS, ['id'], 'CASCADE', null); $this->addForeignKey(null, Table::COMPONENTS, ['fieldId'], \craft\db\Table::FIELDS, ['id'], 'CASCADE', null); $this->addForeignKey(null, Table::COMPONENTS, ['dataId'], Table::COMPONENT_DATA, ['id'], 'CASCADE', null); + $this->addForeignKey(null, Table::COMPONENT_DISCLOSURES, ['userId'], \craft\db\Table::USERS, ['id'], 'CASCADE', null); + $this->addForeignKey(null, Table::COMPONENT_DISCLOSURES, ['componentId'], Table::COMPONENTS, ['id'], 'CASCADE', null); return true; } public function safeDown() { + $this->dropTableIfExists(Table::COMPONENT_DISCLOSURES); $this->dropTableIfExists(Table::COMPONENTS); $this->dropTableIfExists(Table::COMPONENT_DATA); diff --git a/src/models/Component.php b/src/models/Component.php index 3b70766..d9fb3f7 100644 --- a/src/models/Component.php +++ b/src/models/Component.php @@ -47,6 +47,8 @@ class Component extends ActiveRecord protected ?ComponentType $_type = null; + protected bool $withDisclosures = false; + public static function factory(): \markhuot\keystone\factories\Component { return new \markhuot\keystone\factories\Component; @@ -73,6 +75,18 @@ public function getData(): ActiveQuery return $this->hasOne(ComponentData::class, ['id' => 'dataId']); } + public function getDisclosure(): ActiveQuery + { + return $this->hasOne(ComponentDisclosure::class, ['componentId' => 'id']); + } + + public function withDisclosures(bool $withDisclosures=true): self + { + $this->withDisclosures = $withDisclosures; + + return $this; + } + /** * @return array */ @@ -124,6 +138,13 @@ public function __get($name) $value = $data; } + if ($name === 'disclosure' && $value === null) { + $this->populateRelation($name, $data = new ComponentDisclosure); + $data->userId = app()->getUser()->getIdentity()->id; + $data->componentId = $this->id; + $value = $data; + } + if ($name === 'data' && $value instanceof ComponentData) { $value->setNormalizer((new NormalizeFieldDataForComponent($this))->handle(...)); } @@ -286,20 +307,20 @@ public function getSlot(string $name = null): SlotCollection if ($this->slotted === null && $this->elementId && $this->fieldId) { $components = Component::find() - ->with('data') + ->with(array_filter(['data', $this->withDisclosures ? 'disclosure' : null])) ->where(['and', ['elementId' => $this->elementId], ['fieldId' => $this->fieldId], + new OrCondition(array_filter([ + ! $this->getChildPath() ? ['path' => null] : null, + ['like', 'path', $this->getChildPath().'%', false], + ])), // this is intentionally left out. We don't want to limit our query by slot name // because children of this component may not share the same name. We need to pull // all children out of the database and then the slot name filtering happens below // before being returned. // ['slot' => $name], - new OrCondition(array_filter([ - ! $this->getChildPath() ? ['path' => null] : null, - ['like', 'path', $this->getChildPath().'%', false], - ])), ]) ->orderBy('sortOrder') ->collect(); diff --git a/src/models/ComponentDisclosure.php b/src/models/ComponentDisclosure.php new file mode 100644 index 0000000..e76aa9f --- /dev/null +++ b/src/models/ComponentDisclosure.php @@ -0,0 +1,27 @@ +{% endexport %} {% if props.template %} {% set parts = props.template|split(':') %} {% if parts|length == 2 %} diff --git a/src/templates/field.twig b/src/templates/field.twig index a504121..43c28dc 100644 --- a/src/templates/field.twig +++ b/src/templates/field.twig @@ -15,7 +15,7 @@ {% endif %}