Skip to content

Commit

Permalink
better draft handling, save handling, and delete support
Browse files Browse the repository at this point in the history
  • Loading branch information
markhuot committed Oct 11, 2023
1 parent a0e05c0 commit 166fb14
Show file tree
Hide file tree
Showing 9 changed files with 292 additions and 139 deletions.
20 changes: 20 additions & 0 deletions src/actions/DeleteComponent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace markhuot\keystone\actions;

use markhuot\keystone\models\Component;
use yii\db\Expression;

class DeleteComponent
{
public function handle(Component $component)
{
Component::updateAll(['sortOrder' => new Expression('sortOrder - 1')], ['and',
['elementId' => $component->elementId],
['fieldId' => $component->fieldId],
['>', 'sortOrder', $component->sortOrder]
]);

$component->delete();
}
}
201 changes: 156 additions & 45 deletions src/actions/DuplicateComponentTree.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,63 +14,174 @@ class DuplicateComponentTree
{
static array $mapping = [];

public function handle(ElementInterface $source, ElementInterface $destination, Keystone $field)
/**
* The goal of this method is to performantly scan through two sets of components and copy the
* components from the source in to the destination. We do this by looping over the source and
* destination components in a single loop. We compare the ordered IDs and either create, update
* or delete them out of the destination.
*
* There's _a lot_ of extra code here just to make sure we're not deleting components that don't
* actually need to be deleted. Instead we're just bumping their "dateUpdated" so we get accurate
* tracking of changes.
*
* source: [1,4,5,7]
* destination: [1,2,3,4,6,7,8]
* 1==1 // nothing, advance both
* 2<4 // delete 2, advance destination
* 3<4 // delete 3, advance destination
* 4==4 // advance both
* 6>5 // insert five in to destination, advance source
* 6<7 // delete six, advance source
* 7==7 // advance both
* 8==null // delete 8, advance destination
*
* destination: [1,2]
* source: [3,4]
* 1<3 // delete 1, advance source
* 2<3 // delete 2, advance source
* null!=3 // insert 3, advance source
* null!=4 // insert 4, advance source
*
*/
public function handle(ElementInterface $sourceElement, ElementInterface $destinationElement, Keystone $field)
{
$sourceQuery = Component::find()->where([
'elementId' => $sourceElement->id,
'fieldId' => $field->id,
])->orderBy(['id' => 'asc']);
$sourceBatch = $sourceQuery->each();
$sourceBatch->next();

$destinationQuery = Component::find()->where([
'elementId' => $destinationElement->id,
'fieldId' => $field->id,
])->orderBy(['id' => 'asc']);
$destinationBatch = $destinationQuery->each();
$destinationBatch->next();

while (true) {
/** @var Component $source */
$source = $sourceBatch->current();
/** @var Component $destination */
$destination = $destinationBatch->current();

// if we've continued on past the end of our lists we can stop here
if ($source === false && $destination === false) {
break;
}

else if ($source !== false && $destination === false) {
// insert source
\markhuot\craftpest\helpers\test\dump(2);

$new = new Component;
$new->id = $source->id;
$new->elementId = $destinationElement->id;
$new->fieldId = $field->id;
$new->dataId = $source->dataId;
$new->sortOrder = $source->sortOrder;
$new->path = $source->path;
$new->level = $source->level;
$new->slot = $source->slot;
$new->dateCreated = DateTimeHelper::now();
$new->dateUpdated = DateTimeHelper::now();
$new->uid = StringHelper::UUID();
$new->save();

$sourceBatch->next();
}

else if ($source === false && $destination !== false) {
// delete destination
\markhuot\craftpest\helpers\test\dump(3);
Component::deleteAll([
'id' => $destination->id,
'elementId' => $destinationElement->id,
'fieldId' => $field->id,
]);


$destinationBatch->next();
}

// if the IDs are the same we can update in place
else if ($source->id === $destination->id) {
\markhuot\craftpest\helpers\test\dump(4);
$destination->dataId = $source->dataId;
$destination->sortOrder = $source->sortOrder;
$destination->path = $source->path;
$destination->level = $source->level;
$destination->slot = $source->slot;
$destination->save();

$sourceBatch->next();
$destinationBatch->next();
}

// if the destination ID is missing from the source, delete it
else if ($source->id > $destination->id) {
\markhuot\craftpest\helpers\test\dump(5);
Component::deleteAll([
'id' => $destination->id,
'elementId' => $destinationElement->id,
'fieldId' => $field->id,
]);

$destinationBatch->next();
}

// if the source ID is missing from the destination, insert it
else if ($source->id < $destination->id) {
\markhuot\craftpest\helpers\test\dump(6);
$new = new Component;
$new->id = $source->id;
$new->elementId = $destinationElement->id;
$new->fieldId = $field->id;
$new->dataId = $source->dataId;
$new->sortOrder = $source->sortOrder;
$new->path = $source->path;
$new->level = $source->level;
$new->slot = $source->slot;
$new->dateCreated = DateTimeHelper::now();
$new->dateUpdated = DateTimeHelper::now();
$new->uid = StringHelper::UUID();
$new->save();

$sourceBatch->next();
}
}
}

/**
* @deprecated
*/
public function simpleHandle(ElementInterface $source, ElementInterface $destination, Keystone $field)
{
// Delete existing components since the duplicated components will replace them
Craft::$app->db->createCommand()->delete(Table::COMPONENTS, [
'elementId' => $destination->id,
'fieldId' => $field->id,
])->execute();

$query = Component::find()->where([
'elementId' => $source->id,
'fieldId' => $field->id,
])->orderBy(['path' => 'asc']);
])->orderBy(['path' => 'asc', 'sortOrder' => 'asc']);

foreach ($query->each() as $component) {
foreach ($query->each() as $existing) {
$duplicate = new Component;
$duplicate->id = $component->id;
$duplicate->id = $existing->id;
$duplicate->elementId = $destination->id;
$duplicate->fieldId = $field->id;
$duplicate->dataId = $component->dataId;
$duplicate->sortOrder = $component->sortOrder;
$duplicate->path = $this->remapPath($component->path);
$duplicate->level = $component->level;
$duplicate->slot = $component->slot;
$duplicate->dataId = $existing->dataId;
$duplicate->sortOrder = $existing->sortOrder;
$duplicate->path = $existing->path;
$duplicate->level = $existing->level;
$duplicate->slot = $existing->slot;
$duplicate->dateCreated = DateTimeHelper::now();
$duplicate->dateUpdated = DateTimeHelper::now();
$duplicate->uid = StringHelper::UUID();
$duplicate->save();

static::$mapping[$component->id] = $duplicate->id;
}

// I'd love to use INSERT INTO components (elementId, fieldId, ...) SELECT * FROM components WHERE elementId= and fieldId=
// but Craft breaks this because they subclass the db->createCommand() and don't allow you to pass
// a query as the second parameter. They look for $columns[dateCreated] on that second param which
// works when setting raw values but not if a query is passed because it tries to $query[dateCreated]
// and Query isn't array-accessible.
//
// $query = Component::find()->select(['fieldId', 'componentId', 'sortOrder', 'path', 'level', 'slot'])->where([
// 'elementId' => $source->id,
// 'fieldId' => $field->id,
// ]);
// $params = [
// 'elementId' => $source->id,
// 'fieldId' => $field->id,
// ];
// Craft::$app->db->createCommand(Craft::$app->db->getQueryBuilder()->insert(Table::COMPONENTS, $query, $params))->execute();
// Craft::$app->db->createCommand()->insert(Table::COMPONENTS, $query);
}

protected function remapPath(?string $path)
{
if ($path === null) {
return null;
}

return collect(explode('/', $path))
->map(function ($segment) use ($path) {
if (! isset(static::$mapping[$segment])) {
throw new \RuntimeException('Could not remap ' . $path . ' because ' . $segment . ' could not be found');
}

return static::$mapping[$segment];
})
->join('/');
}
}
75 changes: 9 additions & 66 deletions src/controllers/ComponentsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ public function actionEdit()
->tabs([
['label' => 'Content', 'url' => '#tab-content'],
['label' => 'Styles', 'url' => '#tab-styles'],
['label' => 'Admin', 'url' => '#tab-admin'],
])
->action('keystone/components/update')
->contentTemplate('keystone/edit', [
Expand All @@ -65,80 +66,22 @@ public function actionEdit()

public function actionUpdate()
{
// $id = $this->request->getRequiredBodyParam('id');
// $data = $this->request->getBodyParam('fields', []);
//
// $component = Component::findOne(['id' => $id]);
// $component->data->merge($data);
// $component->data->save();
//
// $element = Craft::$app->elements->getElementById($component->elementId);
// $field = Craft::$app->fields->getFieldById($component->fieldId);

return $this->asSuccess('Component saved', [
'action' => 'edit-component',
'id' => $this->request->getRequiredBodyParam('id'),
'elementId' => $this->request->getRequiredBodyParam('elementId'),
'fieldId' => $this->request->getRequiredBodyParam('fieldId'),
'fields' => $this->request->getRequiredBodyParam('fields'),
// 'elementId' => $component->elementId,
// 'fieldId' => $component->fieldId,
// 'fieldHandle' => $field->handle,
// 'fieldHtml' => $field->getInputHtml(null, $element),
]);
}

public function actionMove()
public function actionDelete()
{
$sourceId = $this->request->getRequiredBodyParam('source');
$source = Component::findOne(['id' => $sourceId]);
$targetId = $this->request->getRequiredBodyParam('target');
$target = Component::findOne(['id' => $targetId]);
$position = $this->request->getRequiredBodyParam('position');

// remove ourselves from the list
Component::updateAll([
'sortOrder' => new Expression('sortOrder - 1')
], ['and',
['=', 'elementId', $source->elementId],
['=', 'fieldId', $source->fieldId],
['path' => $source->path],
['>', 'sortOrder', $source->sortOrder]
]);

// Refresh our target to get the updated/correct sortOrder
$target->refresh();

// make room for the insertion
if ($position === 'above') {
Component::updateAll([
'sortOrder' => new Expression('sortOrder + 1')
], ['and',
['=', 'elementId', $target->elementId],
['=', 'fieldId', $target->fieldId],
['path' => $target->path],
['>=', 'sortOrder', $target->sortOrder]
]);
}
if ($position === 'below')
{
Component::updateAll([
'sortOrder' => new Expression('sortOrder + 1')
], ['and',
['=', 'elementId', $target->elementId],
['=', 'fieldId', $target->fieldId],
['path' => $target->path],
['>', 'sortOrder', $target->sortOrder]
]);
}

$source->path = $target->path;
$source->sortOrder = $position == 'above' ? $target->sortOrder : $target->sortOrder + 1;
$source->save();

$element = Craft::$app->elements->getElementById($source->elementId);
$field = Craft::$app->fields->getFieldById($source->fieldId);

return $this->asSuccess('Component moved', [
'fieldHtml' => $field->getInputHtml(null, $element),
return $this->asSuccess('Component deleted', [
'action' => 'delete-component',
'id' => $this->request->getRequiredBodyParam('id'),
'elementId' => $this->request->getRequiredBodyParam('elementId'),
'fieldId' => $this->request->getRequiredBodyParam('fieldId'),
]);
}

Expand Down
20 changes: 17 additions & 3 deletions src/fields/Keystone.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use craft\base\Field;
use craft\web\View;
use markhuot\keystone\actions\AddComponent;
use markhuot\keystone\actions\DeleteComponent;
use markhuot\keystone\actions\DuplicateComponentTree;
use markhuot\keystone\actions\EditComponentData;
use markhuot\keystone\actions\GetComponentType;
Expand Down Expand Up @@ -66,12 +67,19 @@ public function normalizeValueFromRequest(mixed $value, ?ElementInterface $eleme
}

if ($payload['name'] === 'edit-component') {
['id' => $id, 'elementId' => $elementId, 'fields' => $fields] = $payload;
$component = Component::findOne(['id' => $id, 'elementId' => $elementId]);
['id' => $id, 'elementId' => $elementId, 'fieldId' => $fieldId, 'fields' => $fields] = $payload;
$component = Component::findOne(['id' => $id, 'elementId' => $elementId, 'fieldId' => $fieldId]);
(new EditComponentData)->handle($component, $fields);
OverrideDraftResponseWithFieldHtml::override($element, $this);
}

if ($payload['name'] === 'delete-component') {
['id' => $id, 'fieldId' => $fieldId] = $payload;
$component = Component::findOne(['id' => $id, 'elementId' => $element->id, 'fieldId' => $fieldId]);
(new DeleteComponent)->handle($component);
OverrideDraftResponseWithFieldHtml::override($element, $this);
}

return null;
}

Expand All @@ -93,6 +101,12 @@ protected function inputHtml(mixed $value, ?ElementInterface $element = null): s
*/
public function normalizeValue(mixed $value, ?ElementInterface $element = null): mixed
{
// If the value has already been normalized, return it
if ($value instanceof Component) {
return $value;
}

// Otherwise fetch the components out of the database
return $this->getFragment($element);
}

Expand All @@ -103,7 +117,7 @@ public function afterElementSave(ElementInterface $element, bool $isNew): void
{
// If we're duplicating an element to create a draft or revision, duplicate the component
// tree as well
if ($element->duplicateOf && $isNew) {
if ($element->duplicateOf && ($element->isNewForSite || $element->updatingFromDerivative)) {
(new DuplicateComponentTree)->handle($element->duplicateOf, $element, $this);
}

Expand Down
Loading

0 comments on commit 166fb14

Please sign in to comment.