Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
f9b1d51
Add tests
duncanmcclean Nov 11, 2025
43fe669
wip
duncanmcclean Nov 11, 2025
f25348a
Merge & extract
duncanmcclean Nov 11, 2025
f7381e0
Update property cache when saving and deleting fieldsets
duncanmcclean Nov 11, 2025
6bcada3
Add `Blueprint::all()`
duncanmcclean Nov 11, 2025
935923c
Don't need this.
duncanmcclean Nov 12, 2025
523db58
Merge branch 'master' into update-set-preview-images
duncanmcclean Nov 12, 2025
508d6c4
Write blueprints to disk and ensure global blueprints are included
duncanmcclean Nov 12, 2025
a05f422
Refactor `Blueprint::all()` to avoid calling all the facades...
duncanmcclean Nov 12, 2025
4195ff6
formatting
duncanmcclean Nov 12, 2025
5e03125
Avoid errors when array offsets don't exist
duncanmcclean Nov 12, 2025
499abb0
Add Blueprint::all() expectation where necessary
duncanmcclean Nov 12, 2025
568c0ca
Merge branch 'master' into update-set-preview-images
duncanmcclean Nov 18, 2025
664d053
Ensure parent is null when getting blueprints
duncanmcclean Nov 18, 2025
7da3fca
Preview images are saved sans folder
duncanmcclean Nov 18, 2025
d2c0eab
Add additional tests
duncanmcclean Nov 18, 2025
3aa802e
Handle assets being deleted & moved out of configured folder
duncanmcclean Nov 18, 2025
61f6339
Fix failing tests
duncanmcclean Nov 18, 2025
b0c39df
Fix more failing tests
duncanmcclean Nov 18, 2025
049e54f
Merge branch 'master' into update-set-preview-images
duncanmcclean Jan 5, 2026
d393254
add missing imports
duncanmcclean Jan 5, 2026
3d17d23
Merge branch '6.x' into update-set-preview-images
duncanmcclean Mar 11, 2026
9a49140
formatting
duncanmcclean Mar 13, 2026
c97c15e
Merge branch '6.x' into update-set-preview-images
duncanmcclean May 14, 2026
605cc6d
Fix container filter for blueprint/fieldset set preview image updates
duncanmcclean May 14, 2026
f6ae11b
Include user and user_group blueprints in Blueprint::all()
duncanmcclean May 14, 2026
8806ee3
Skip blueprint/fieldset enumeration when set preview images not confi…
duncanmcclean May 14, 2026
234e8f6
Add comment explaining setParent(null) in makeBlueprintFromFile
duncanmcclean May 14, 2026
28a1bc0
Add comment explaining findFieldsInBlueprintContents heuristic
duncanmcclean May 14, 2026
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
68 changes: 68 additions & 0 deletions src/Assets/AssetReferenceUpdater.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
namespace Statamic\Assets;

use Statamic\Data\DataReferenceUpdater;
use Statamic\Fieldtypes\Sets;
use Statamic\Support\Arr;
use Statamic\Support\Str;

class AssetReferenceUpdater extends DataReferenceUpdater
{
Expand Down Expand Up @@ -65,4 +67,70 @@ protected function recursivelyUpdateFields($fields, $dottedPrefix = null)

$this->updateNestedFieldValues($fields, $dottedPrefix);
}

/**
* Update fields in blueprints and fieldsets.
*
* @return void
*/
protected function updateBlueprintFields()
{
if (
! ($config = Sets::previewImageConfig())
|| $this->container !== $config['container']
|| ! Str::startsWith($this->originalValue, $config['folder'].'/')
) {
return;
}

$contents = $this->item->contents();

$fieldPaths = $this->findFieldsInBlueprintContents($contents, fieldtypes: ['bard', 'replicator']);

foreach ($fieldPaths as $fieldPath) {
$fieldContents = Arr::get($contents, $fieldPath);

if (! isset($fieldContents['sets'])) {
continue;
}

$fieldContents['sets'] = collect($fieldContents['sets'])
->map(function ($setGroup) {
if (! isset($setGroup['sets'])) {
return $setGroup;
}

$setGroup['sets'] = collect($setGroup['sets'])
->map(function ($set) {
if (isset($set['image'])) {
$fullPath = Sets::previewImageConfig()['folder'].'/'.$set['image'];

if ($fullPath !== $this->originalValue) {
return $set;
}

if (Str::startsWith($this->newValue, Sets::previewImageConfig()['folder'].'/')) {
$set['image'] = Str::after($this->newValue, Sets::previewImageConfig()['folder'].'/');
} else {
unset($set['image']);
}

$this->updated = true;
}

return $set;
})
->all();

return $setGroup;
})
->all();

Arr::set($contents, $fieldPath, $fieldContents);
}

if ($this->updated) {
$this->item->setContents($contents);
}
}
}
49 changes: 48 additions & 1 deletion src/Data/DataReferenceUpdater.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
namespace Statamic\Data;

use Statamic\Facades\Blink;
use Statamic\Fields\Blueprint;
use Statamic\Fields\Fields;
use Statamic\Fields\Fieldset;
use Statamic\Fieldtypes\UpdatesReferences;
use Statamic\Git\Subscriber as GitSubscriber;
use Statamic\Support\Arr;
Expand Down Expand Up @@ -66,7 +69,11 @@ public function updateReferences($originalValue, $newValue)
$this->originalValue = $originalValue;
$this->newValue = $newValue;

$this->recursivelyUpdateFields($this->getTopLevelFields());
if ($this->item instanceof Blueprint || $this->item instanceof Fieldset) {
$this->updateBlueprintFields();
} else {
$this->recursivelyUpdateFields($this->getTopLevelFields());
}

if ($this->updated) {
$this->saveItem();
Expand All @@ -75,6 +82,46 @@ public function updateReferences($originalValue, $newValue)
return (bool) $this->updated;
}

/**
* Update fields in blueprints and fieldsets.
*
* @return void
*/
abstract protected function updateBlueprintFields();

/**
* Finds fields of a given type in the contents of a blueprint.
* Returns dot-notation paths to the fields.
*
* Note: This uses a simple heuristic that matches any nested array with
* a 'type' key whose value is in the given fieldtypes list. While this
* could theoretically match non-field structures, in practice blueprint
* contents don't contain these strings in other contexts (like validation
* rules), and downstream code guards against false matches by checking
* for the presence of required keys like 'sets'.
*
* @param array $array
* @param array $fieldtypes
* @param string|null $dottedPrefix
* @param array $fieldPaths
* @return array
*/
protected function findFieldsInBlueprintContents($array, $fieldtypes, $dottedPrefix = '', &$fieldPaths = [])
{
foreach ($array as $key => $value) {
if (is_array($value)) {
$fieldPath = $dottedPrefix ? "$dottedPrefix.$key" : $key;
$this->findFieldsInBlueprintContents($value, $fieldtypes, $fieldPath, $fieldPaths);
}

if (is_string($value) && $key === 'type' && in_array($value, $fieldtypes)) {
$fieldPaths[] = $dottedPrefix;
}
}

return $fieldPaths;
}

/**
* Get top level fields off item blueprint.
*
Expand Down
24 changes: 23 additions & 1 deletion src/Fields/BlueprintRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Exception;
use Illuminate\Support\Collection;
use Statamic\Exceptions\BlueprintNotFoundException;
use Statamic\Facades;
use Statamic\Facades\Blink;
use Statamic\Facades\File;
use Statamic\Facades\Path;
Expand All @@ -23,6 +24,24 @@ class BlueprintRepository
protected $fallbacks = [];
protected $additionalNamespaces = [];

public function all()
{
$namespaces = [
...Facades\Collection::all()->map(fn ($collection) => "collections/{$collection->handle()}")->all(),
...Facades\Taxonomy::all()->map(fn ($taxonomy) => "taxonomies/{$taxonomy->handle()}")->all(),
'navigation', 'assets', 'globals', 'forms',
...$this->getAdditionalNamespaces()->keys()->all(),
];

$rootLevelBlueprints = collect(['user', 'user_group'])
->map(fn ($handle) => $this->find($handle))
->filter();

return $rootLevelBlueprints->merge(
collect($namespaces)->flatMap(fn ($namespace) => $this->in($namespace)->values())
);
}

public function setDirectories(string|array $directories)
{
if (is_string($directories)) {
Expand Down Expand Up @@ -334,7 +353,10 @@ protected function makeBlueprintFromFile($path, $namespace = null)
->setInitialPath($path)
->setNamespace($namespace ?? null)
->setContents($contents);
});
// Clear any parent that may have been set on the blueprint during a previous
// find() call. The blink cache returns the same instance, so without this,
// iterating blueprints via all() could produce instances with stale parents.
})->setParent(null);
}

protected function getNamespaceAndHandle($blueprint)
Expand Down
4 changes: 4 additions & 0 deletions src/Fields/FieldsetRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,8 @@ public function save(Fieldset $fieldset)
"{$directory}/{$handle}.yaml",
YAML::dump($fieldset->contents())
);

$this->fieldsets[$fieldset->handle()] = $fieldset;
}

public function delete(Fieldset $fieldset)
Expand All @@ -181,6 +183,8 @@ public function delete(Fieldset $fieldset)
}

File::delete($fieldset->path());

unset($this->fieldsets[$fieldset->handle()]);
}

public function reset(Fieldset $fieldset)
Expand Down
29 changes: 29 additions & 0 deletions src/Listeners/UpdateAssetReferences.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
use Statamic\Events\AssetReplaced;
use Statamic\Events\AssetSaved;
use Statamic\Events\Subscriber;
use Statamic\Facades\Blueprint;
use Statamic\Facades\Fieldset;
use Statamic\Fieldtypes\Sets;

class UpdateAssetReferences extends Subscriber implements ShouldQueue
{
Expand Down Expand Up @@ -112,6 +115,32 @@ protected function replaceReferences($asset, $originalPath, $newPath)
}
});

// Only enumerate blueprints/fieldsets when set preview images are configured
// and the asset is in the configured container to avoid unnecessary disk I/O.
if (($config = Sets::previewImageConfig()) && $container === $config['container']) {
Blueprint::all()
->each(function ($blueprint) use ($container, $originalPath, $newPath, &$hasUpdatedItems) {
$updated = AssetReferenceUpdater::item($blueprint)
->filterByContainer($container)
->updateReferences($originalPath, $newPath);

if ($updated) {
$hasUpdatedItems = true;
}
});

Fieldset::all()
->each(function ($fieldset) use ($container, $originalPath, $newPath, &$hasUpdatedItems) {
$updated = AssetReferenceUpdater::item($fieldset)
->filterByContainer($container)
->updateReferences($originalPath, $newPath);

if ($updated) {
$hasUpdatedItems = true;
}
});
}

if ($hasUpdatedItems) {
AssetReferencesUpdated::dispatch($asset);
}
Expand Down
10 changes: 10 additions & 0 deletions src/Taxonomies/TermReferenceUpdater.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,14 @@ protected function recursivelyUpdateFields($fields, $dottedPrefix = null)

$this->updateNestedFieldValues($fields, $dottedPrefix);
}

/**
* Update fields in blueprints and fieldsets.
*
* @return void
*/
protected function updateBlueprintFields()
{
//
}
}
1 change: 1 addition & 0 deletions tests/Assets/AssetFolderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1174,6 +1174,7 @@ private function containerWithDisk()
Storage::fake('local');

$container = Facades\AssetContainer::make('test')->disk('local');
Facades\AssetContainer::shouldReceive('find')->with('assets')->andReturn(null);
Facades\AssetContainer::shouldReceive('findByHandle')->with('test')->andReturn($container);
Facades\AssetContainer::shouldReceive('save')->with($container);

Expand Down
3 changes: 3 additions & 0 deletions tests/Assets/AssetTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1199,6 +1199,7 @@ public function it_can_be_moved_to_another_folder_with_a_new_filename()
$disk->put('old/asset.txt', 'The asset contents');
$container = Facades\AssetContainer::make('test')->disk('local');
Facades\AssetContainer::shouldReceive('save')->with($container);
Facades\AssetContainer::shouldReceive('find')->with('assets')->andReturnNull();
Facades\AssetContainer::shouldReceive('findByHandle')->with('test')->andReturn($container);
$asset = $container->makeAsset('old/asset.txt')->data(['foo' => 'bar']);
$asset->save();
Expand Down Expand Up @@ -1239,6 +1240,7 @@ public function it_lowercases_when_moving_to_another_folder_with_a_new_filename(
$disk->put('old/asset.txt', 'The asset contents');
$container = Facades\AssetContainer::make('test')->disk('local');
Facades\AssetContainer::shouldReceive('save')->with($container);
Facades\AssetContainer::shouldReceive('find')->with('assets')->andReturnNull();
Facades\AssetContainer::shouldReceive('findByHandle')->with('test')->andReturn($container);
$asset = $container->makeAsset('old/asset.txt');
$asset->save();
Expand All @@ -1262,6 +1264,7 @@ public function it_doesnt_lowercase_moved_files_when_configured()
$disk->put('old/asset.txt', 'The asset contents');
$container = Facades\AssetContainer::make('test')->disk('local');
Facades\AssetContainer::shouldReceive('save')->with($container);
Facades\AssetContainer::shouldReceive('find')->with('assets')->andReturnNull();
Facades\AssetContainer::shouldReceive('findByHandle')->with('test')->andReturn($container);
$asset = $container->makeAsset('old/asset.txt');
$asset->save();
Expand Down
55 changes: 55 additions & 0 deletions tests/Fields/BlueprintRepositoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@
use Statamic\Fields\Blueprint;
use Statamic\Fields\BlueprintRepository;
use Statamic\Support\FileCollection;
use Tests\PreventSavingStacheItemsToDisk;
use Tests\TestCase;

class BlueprintRepositoryTest extends TestCase
{
use PreventSavingStacheItemsToDisk;

private $repo;

public function setUp(): void
Expand All @@ -26,6 +29,58 @@ public function setUp(): void
Facades\Blueprint::swap($this->repo);
}

#[Test]
public function it_gets_all_blueprints()
{
$this->repo->setDirectories($this->fakeStacheDirectory.'/dev-null/blueprints');

$collection = tap(Facades\Collection::make('test'))->save();
$collection->entryBlueprint()->save();

$taxonomy = tap(Facades\Taxonomy::make('test'))->save();
$taxonomy->termBlueprint()->save();

$nav = tap(Facades\Nav::make('test'))->save();
$nav->blueprint()->save();

$assetContainer = tap(Facades\AssetContainer::make('test'))->save();
$assetContainer->blueprint()->save();

Facades\GlobalSet::make('test')->save();
$this->repo->make('test')->setNamespace('globals')->save();

$form = tap(Facades\Form::make('test'))->save();
$form->blueprint()->save();

$all = $this->repo->all();

$this->assertEveryItemIsInstanceOf(Blueprint::class, $all);
$this->assertEquals([
'collections.test.test',
'taxonomies.test.test',
'navigation.test',
'assets.test',
'globals.test',
'forms.test',
], $all->map->fullyQualifiedHandle()->all());
}

#[Test]
public function it_includes_user_and_user_group_blueprints_in_all()
{
$this->repo->setDirectories($this->fakeStacheDirectory.'/dev-null/blueprints');

// Create user and user_group blueprints
$this->repo->make('user')->save();
$this->repo->make('user_group')->save();

$all = $this->repo->all();

$this->assertEveryItemIsInstanceOf(Blueprint::class, $all);
$this->assertContains('user', $all->map->fullyQualifiedHandle()->all());
$this->assertContains('user_group', $all->map->fullyQualifiedHandle()->all());
}

#[Test]
public function it_gets_a_blueprint()
{
Expand Down
1 change: 1 addition & 0 deletions tests/Git/GitEventTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,7 @@ public function it_batches_asset_references_changes_into_one_commit()
],
]);

BlueprintRepository::shouldReceive('all')->andReturn(collect([$blueprint]));
BlueprintRepository::shouldReceive('in')->with('collections/pages')->andReturn(collect([$blueprint]));

foreach (range(1, 3) as $i) {
Expand Down
Loading
Loading