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
2 changes: 2 additions & 0 deletions routes/actions.php
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,8 @@
Route::post('delete-elements/delete', [DeleteElementsController::class, 'destroy']);
Route::any('delete-elements/replace-relations-modal', [DeleteElementsController::class, 'replaceRelationsModal']);
Route::post('delete-elements/replace-relations', [DeleteElementsController::class, 'replaceRelations']);
Route::any('delete-elements/replace-references-modal', [DeleteElementsController::class, 'replaceReferencesModal']);
Route::post('delete-elements/replace-references', [DeleteElementsController::class, 'replaceReferences']);

Route::post('elements/create', CreateElementController::class);
Route::any('elements/edit', EditElementController::class);
Expand Down
2 changes: 1 addition & 1 deletion src/Cms.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@

public const string VERSION = '6.0.0-alpha.5';

public const string SCHEMA_VERSION = '6.0.0.2';
public const string SCHEMA_VERSION = '6.0.0.3';

public const string MIN_VERSION_REQUIRED = '5.9.0';

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

use CraftCms\Cms\Database\Migration;
use CraftCms\Cms\Database\Table;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
public function up(): void
{
if (Schema::hasTable(Table::FIELDREFERENCES)) {
return;
}

Schema::create(Table::FIELDREFERENCES, function (Blueprint $table) {
$table->integer('id', true);
$table->integer('fieldId');
$table->uuid('fieldInstanceUid');
$table->integer('sourceId');
$table->integer('sourceSiteId')->nullable();
$table->integer('targetId');
});

Schema::createIndex(Table::FIELDREFERENCES, ['fieldId', 'fieldInstanceUid', 'sourceId', 'sourceSiteId', 'targetId'], unique: true);
Schema::createIndex(Table::FIELDREFERENCES, ['targetId']);

Schema::table(Table::FIELDREFERENCES, function (Blueprint $table) {
$table->foreign('fieldId')->references('id')->on(Table::FIELDS)->cascadeOnDelete();
$table->foreign('sourceId')->references('id')->on(Table::ELEMENTS)->cascadeOnDelete();
$table->foreign('sourceSiteId')->references('id')->on(Table::SITES)->cascadeOnDelete()->cascadeOnUpdate();
});
}

public function down(): void
{
Schema::dropIfExists(Table::FIELDREFERENCES);
}
};
15 changes: 15 additions & 0 deletions src/Database/Migrations/Install.php
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,16 @@ public function createTables(?Logger $logger = null): void
$table->char('uid', 36)->default('0');
});

$logger?->subLabel('fieldreferences');
Schema::create(Table::FIELDREFERENCES, function (Blueprint $table) {
$table->integer('id', true);
$table->integer('fieldId');
$table->uuid('fieldInstanceUid');
$table->integer('sourceId');
$table->integer('sourceSiteId')->nullable();
$table->integer('targetId');
});

$logger?->subLabel('gqltokens');
Schema::create('gqltokens', function (Blueprint $table) {
$table->integer('id', true);
Expand Down Expand Up @@ -1069,6 +1079,8 @@ public function createIndexes(): void
Schema::createIndex(Table::ENTRYTYPES, ['dateDeleted']);
Schema::createIndex(Table::FIELDLAYOUTS, ['dateDeleted']);
Schema::createIndex(Table::FIELDLAYOUTS, ['type']);
Schema::createIndex(Table::FIELDREFERENCES, ['fieldId', 'fieldInstanceUid', 'sourceId', 'sourceSiteId', 'targetId'], unique: true);
Schema::createIndex(Table::FIELDREFERENCES, ['targetId']);
Schema::createIndex(Table::FIELDS, ['handle', 'context']);
Schema::createIndex(Table::FIELDS, ['context']);
Schema::createIndex(Table::FIELDS, ['dateDeleted']);
Expand Down Expand Up @@ -1213,6 +1225,9 @@ public function addForeignKeys(): void
Schema::table(Table::ENTRIES, fn (Blueprint $table) => $table->foreign('fieldId')->references('id')->on(Table::FIELDS)->cascadeOnDelete());
Schema::table(Table::ENTRIES, fn (Blueprint $table) => $table->foreign('primaryOwnerId')->references('id')->on(Table::ELEMENTS)->cascadeOnDelete());
Schema::table(Table::ENTRYTYPES, fn (Blueprint $table) => $table->foreign('fieldLayoutId')->references('id')->on(Table::FIELDLAYOUTS)->nullOnDelete());
Schema::table(Table::FIELDREFERENCES, fn (Blueprint $table) => $table->foreign('fieldId')->references('id')->on(Table::FIELDS)->cascadeOnDelete());
Schema::table(Table::FIELDREFERENCES, fn (Blueprint $table) => $table->foreign('sourceId')->references('id')->on(Table::ELEMENTS)->cascadeOnDelete());
Schema::table(Table::FIELDREFERENCES, fn (Blueprint $table) => $table->foreign('sourceSiteId')->references('id')->on(Table::SITES)->cascadeOnDelete()->cascadeOnUpdate());
Schema::table(Table::GQLTOKENS, fn (Blueprint $table) => $table->foreign('schemaId')->references('id')->on(Table::GQLSCHEMAS)->nullOnDelete());
Schema::table(Table::RELATIONS, fn (Blueprint $table) => $table->foreign('fieldId')->references('id')->on(Table::FIELDS)->cascadeOnDelete());
Schema::table(Table::RELATIONS, fn (Blueprint $table) => $table->foreign('sourceId')->references('id')->on(Table::ELEMENTS)->cascadeOnDelete());
Expand Down
2 changes: 2 additions & 0 deletions src/Database/Table.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@

public const string FIELDLAYOUTS = 'fieldlayouts';

public const string FIELDREFERENCES = 'fieldreferences';

public const string FIELDS = 'fields';

public const string GQLSCHEMAS = 'gqlschemas';
Expand Down
2 changes: 2 additions & 0 deletions src/Element/Concerns/HasDeletionBlockers.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace CraftCms\Cms\Element\Concerns;

use CraftCms\Cms\Element\DeletionBlockers\FieldReferencesDeletionBlocker;
use CraftCms\Cms\Element\DeletionBlockers\RelationDeletionBlocker;
use CraftCms\Cms\Element\ElementCollection;
use CraftCms\Cms\Element\Events\DefineDeletionBlockers;
Expand All @@ -22,6 +23,7 @@ public static function deletionBlockers(ElementCollection $elements, bool $hardD
'defaultSort' => ['section', 'asc'],
],
]),
new FieldReferencesDeletionBlocker($elements, $hardDelete),
];

event($event = new DefineDeletionBlockers(
Expand Down
131 changes: 131 additions & 0 deletions src/Element/DeletionBlockers/FieldReferencesDeletionBlocker.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
<?php

declare(strict_types=1);

namespace CraftCms\Cms\Element\DeletionBlockers;

use CraftCms\Cms\Cp\Html\ElementIndexHtml;
use CraftCms\Cms\Element\Contracts\ElementInterface;
use CraftCms\Cms\Element\ElementCollection;
use CraftCms\Cms\Field\FieldReferences;
use CraftCms\Cms\Support\Html;

use function CraftCms\Cms\t;

class FieldReferencesDeletionBlocker extends BaseDeletionBlocker
{
private readonly int $referenceCount;

public function __construct(
ElementCollection $elements,
bool $hardDelete,
array $config = [],
) {
parent::__construct($elements, $hardDelete, $config);

$this->referenceCount = $this->fieldReferences()->referenceCountForTargets($this->elements->ids()->all());
}

public function isActive(): bool
{
return $this->referenceCount !== 0;
}

public function getSummary(): string
{
$targetElementType = $this->targetElementType();

return t('The {numTargets, plural, =1{{targetTypeSingular} is} other{{targetTypePlural} are}} referenced by fields in {numSources, number} other {numSources, plural, =1{element} other{elements}}.', [
'targetTypeSingular' => $targetElementType::lowerDisplayName(),
'targetTypePlural' => $targetElementType::pluralLowerDisplayName(),
'numSources' => $this->referenceCount,
'numTargets' => $this->elements->count(),
]);
}

public function getDetails(): ?string
{
$groups = $this->fieldReferences()->referenceIdsByTypeForTargets($this->elements->ids()->all());

if ($groups->isEmpty()) {
return null;
}

return $groups
->map(function ($sourceIds, string $sourceElementType) {
/** @var class-string<ElementInterface> $sourceElementType */
return Html::tag('h3', $sourceElementType::pluralDisplayName()).
app(ElementIndexHtml::class)->html($sourceElementType, [
'context' => 'pane',
'sources' => false,
'jsSettings' => [
'criteria' => [
'id' => $sourceIds->all(),
'drafts' => null,
'provisionalDrafts' => null,
'revisions' => false,
'status' => null,
],
],
]);
})
->join('');
}

public function getActions(): array
{
$targetElementType = $this->targetElementType();

return [
[
'icon' => 'swap',
'label' => t('Replace {numReferences, plural, =1{reference} other{references}}', [
'numReferences' => $this->referenceCount,
]),
'callback' => Html::jsWithVars(fn (
$targetElementType,
$targetIds,
$hardDelete,
) => <<<JS
new Craft.CpModal('delete-elements/replace-references-modal', {
params: {
elementType: $targetElementType,
elementIds: $targetIds,
hardDelete: $hardDelete,
},
(ev) => {
resolve(ev.response.data.message);
},
onCancel: () => {
reject();
},
})
JS, [
$targetElementType,
$this->elements->ids()->all(),
$this->hardDelete,
]),
],
[
'icon' => 'xmark',
'label' => t('Ignore {numReferences, plural, =1{reference} other{references}}', [
'numReferences' => $this->referenceCount,
]),
'callback' => 'resolve();',
],
];
}

/**
* @return class-string<ElementInterface>
*/
private function targetElementType(): string
{
return $this->elements->first()::class;
}

private function fieldReferences(): FieldReferences
{
return app(FieldReferences::class);
}
}
116 changes: 116 additions & 0 deletions src/Element/Jobs/ReplaceReferences.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<?php

declare(strict_types=1);

namespace CraftCms\Cms\Element\Jobs;

use CraftCms\Cms\Element\Contracts\ElementInterface;
use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\Field\Contracts\TracksReferencesFieldInterface;
use CraftCms\Cms\Field\FieldReferences;
use CraftCms\Cms\FieldLayout\LayoutElements\CustomField;
use CraftCms\Cms\Queue\BatchedElementJob;
use CraftCms\Cms\Support\Facades\Elements;
use CraftCms\Cms\Support\Facades\I18N;
use Illuminate\Contracts\Database\Query\Builder;
use Illuminate\Support\Collection;
use Override;
use Throwable;

class ReplaceReferences extends BatchedElementJob
{
public function __construct(
/** @var class-string<ElementInterface> */
public string $sourceElementType,

/** @var class-string<ElementInterface> */
public string $targetElementType,

public ?int $sourceSiteId,

/** @var array<int, array{fieldInstanceUid: string, sourceId: int}> */
public array $refs,

/** @var int[] */
public array $oldTargetIds,

public int $newTargetId,
) {
parent::__construct();
}

#[Override]
protected function getQuery(): Builder
{
$query = $this->sourceElementType::find()
->id(Collection::make($this->refs)->pluck('sourceId')->unique()->values()->all())
->drafts(null)
->provisionalDrafts(null)
->revisions(false)
->trashed(false)
->status(null)
->orderBy('elements.id');

if ($this->sourceSiteId !== null) {
$query->siteId($this->sourceSiteId);
} else {
$query->siteId('*')->unique();
}

return $query;
}

protected function processElement(ElementInterface $element): void
{
$fieldInstanceUids = Collection::make($this->refs)
->filter(fn (array $ref) => $ref['sourceId'] === $element->id)
->pluck('fieldInstanceUid')
->unique();

if ($fieldInstanceUids->isEmpty()) {
return;
}

$saveElement = false;
$fieldReferences = app(FieldReferences::class);

foreach ($fieldInstanceUids as $fieldInstanceUid) {
$layoutElement = $element->getFieldLayout()?->getElementByUid($fieldInstanceUid);

if (! $layoutElement instanceof CustomField) {
continue;
}

$field = $layoutElement->getField();

if (! $field instanceof TracksReferencesFieldInterface) {
continue;
}

if ($fieldReferences->replaceReferences($field, $element, $this->oldTargetIds, $this->newTargetId)) {
$saveElement = true;
}
}

if (! $saveElement) {
return;
}

$element->ruleset->useScenario(ElementRules::SCENARIO_ESSENTIALS);
$element->resaving = true;

try {
Elements::saveElement(element: $element, runValidation: false, propagate: false);
} catch (Throwable $e) {
report($e);
}
}

#[Override]
protected function defaultDescription(): string
{
return I18N::prep('Replacing {type} references', [
'type' => $this->targetElementType::lowerDisplayName(),
]);
}
}
Loading
Loading