Skip to content

Commit

Permalink
[5.x] Ability to select entries from other sites (#9229)
Browse files Browse the repository at this point in the history
Co-authored-by: edalzell <[email protected]>
Co-authored-by: Jason Varga <[email protected]>
  • Loading branch information
3 people authored Jul 18, 2024
1 parent a96a6a1 commit ff74133
Show file tree
Hide file tree
Showing 13 changed files with 136 additions and 19 deletions.
4 changes: 3 additions & 1 deletion resources/js/components/navigation/View.vue
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@
:site="site"
:collections="collections"
:max-items="maxPagesSelection"
:can-select-across-sites="canSelectAcrossSites"
@selected="entriesSelected"
/>

Expand Down Expand Up @@ -203,7 +204,8 @@ export default {
site: { type: String, required: true },
sites: { type: Array, required: true },
blueprint: { type: Object, required: true },
canEdit: { type: Boolean, required: true }
canEdit: { type: Boolean, required: true },
canSelectAcrossSites: { type: Boolean, required: true }
},
data() {
Expand Down
2 changes: 2 additions & 0 deletions resources/js/components/structures/PageSelector.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export default {
props: {
site: String,
collections: Array,
canSelectAcrossSites: Boolean,
maxItems: {
type: Number,
required: false,
Expand All @@ -39,6 +40,7 @@ export default {
config: {
type: 'entries',
collections: this.collections,
select_across_sites: this.canSelectAcrossSites,
},
columns: [
{ label: __('Title'), field: 'title' },
Expand Down
1 change: 1 addition & 0 deletions resources/lang/en/fieldtypes.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
'entries.config.collections' => 'Choose which collections the user can select from.',
'entries.config.query_scopes' => 'Choose which query scopes should be applied when retrieving selectable entries.',
'entries.config.search_index' => 'An appropriate search index will be used automatically where possible, but you may define an explicit one.',
'entries.config.select_across_sites' => 'Allow selecting entries from other sites. This also disables localizing options on the front-end. Learn more in the [documentation](https://statamic.dev/fieldtypes/entries#select-across-sites).',
'entries.title' => 'Entries',
'float.title' => 'Float',
'form.config.max_items' => 'Set a maximum number of selectable forms.',
Expand Down
1 change: 1 addition & 0 deletions resources/lang/en/messages.php
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@
'navigation_configure_collections_instructions' => 'Enable linking to entries in these collections.',
'navigation_configure_handle_instructions' => 'Used to reference this navigation on the frontend. It\'s non-trivial to change later.',
'navigation_configure_intro' => 'Navigations are multi-level lists of links that can be used to build navbars, footers, sitemaps, and other forms of frontend navigation.',
'navigation_configure_select_across_sites' => 'Allow selecting entries from other sites.',
'navigation_configure_settings_intro' => 'Enable linking to collections, set a max depth, and other behaviors.',
'navigation_configure_title_instructions' => 'We recommend a name that matches where it will be used, like "Main Nav" or "Footer Nav".',
'navigation_documentation_instructions' => 'Learn more about building, configuring, and rendering Navigations.',
Expand Down
1 change: 1 addition & 0 deletions resources/views/navigation/show.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
:expects-root="{{ $str::bool($expectsRoot) }}"
:blueprint="{{ json_encode($blueprint) }}"
:can-edit="{{ Statamic\Support\Str::bool($user->can('edit', $nav)) }}"
:can-select-across-sites="{{ Statamic\Support\Str::bool($nav->canSelectAcrossSites()) }}"
>
<template #twirldown>
@can('edit', $nav)
Expand Down
76 changes: 62 additions & 14 deletions src/Fieldtypes/Entries.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Statamic\Contracts\Data\Localization;
use Statamic\Contracts\Entries\Entry as EntryContract;
use Statamic\CP\Column;
use Statamic\CP\Columns;
use Statamic\Exceptions\CollectionNotFoundException;
use Statamic\Facades\Blink;
use Statamic\Facades\Collection;
Expand Down Expand Up @@ -112,6 +113,11 @@ protected function configFieldItems(): array
->values()
->all(),
],
'select_across_sites' => [
'display' => __('Select Across Sites'),
'instructions' => __('statamic::fieldtypes.entries.config.select_across_sites'),
'type' => 'toggle',
],
],
],
];
Expand Down Expand Up @@ -210,7 +216,9 @@ protected function getIndexQuery($request)

$query = $this->toSearchQuery($query, $request);

if ($site = $request->site) {
if ($this->canSelectAcrossSites()) {
$query->whereIn('site', Site::authorized()->map->handle()->all());
} elseif ($site = $request->site) {
$query->where('site', $site);
}

Expand Down Expand Up @@ -332,13 +340,18 @@ private function queryBuilder($values)
$site = $parent->locale();
}

// If they've opted into selecting across sites, we won't automatically localize or
// filter out entries that don't exist in the current site. They would do that.
$shouldLocalize = ! $this->canSelectAcrossSites();

$ids = (new OrderedQueryBuilder(Entry::query(), $ids = Arr::wrap($values)))
->whereIn('id', $ids)
->get()
->map(function ($entry) use ($site) {
return optional($entry->in($site))->id();
})
->filter()
->when($shouldLocalize, fn ($entries) => $entries
->map(fn ($entry) => $entry->in($site))
->filter()
)
->map->id()
->all();

return (new StatusQueryBuilder(new OrderedQueryBuilder(Entry::query(), $ids)))
Expand Down Expand Up @@ -382,7 +395,10 @@ public function getSelectionFilters()

protected function getSelectionFilterContext()
{
return ['collections' => $this->getConfiguredCollections()];
return [
'collections' => $this->getConfiguredCollections(),
'showSiteFilter' => $this->canSelectAcrossSites(),
];
}

protected function getConfiguredCollections()
Expand All @@ -408,20 +424,20 @@ public function getColumns()
if (count($this->getConfiguredCollections()) === 1) {
$columns = $this->getBlueprint()->columns();

$status = Column::make('status')
->listable(true)
->visible(true)
->defaultVisibility(true)
->sortable(false);

$columns->put('status', $status);
$this->addColumn($columns, 'status');

$columns->setPreferred("collections.{$this->getConfiguredCollections()[0]}.columns");

return $columns->rejectUnlisted()->values();
}

return $this->getBlueprint()->columns()->values()->all();
$columns = $this->getBlueprint()->columns();

if ($this->canSelectAcrossSites()) {
$this->addColumn($columns, 'site');
}

return $columns->values();
}

protected function getItemsForPreProcessIndex($values): SupportCollection
Expand Down Expand Up @@ -467,6 +483,38 @@ public function getItemHint($item): ?string
{
return collect([
count($this->getConfiguredCollections()) > 1 ? __($item->collection()->title()) : null,
$this->canSelectAcrossSites() && count($this->availableSites()) > 1 ? $item->site()->name() : null,
])->filter()->implode('');
}

private function addColumn(Columns $columns, string $columnKey): void
{
$column = Column::make($columnKey)
->listable(true)
->visible(true)
->defaultVisibility(true)
->sortable(false);

$columns->put($columnKey, $column);
}

private function canSelectAcrossSites(): bool
{
return $this->config('select_across_sites', false);
}

private function availableSites()
{
if (! Site::hasMultiple()) {
return [];
}

$configuredSites = collect($this->getConfiguredCollections())->flatMap(fn ($collection) => Collection::find($collection)->sites());

return Site::authorized()
->when(isset($configuredSites), fn ($sites) => $sites->filter(fn ($site) => $configuredSites->contains($site->handle())))
->map->handle()
->values()
->all();
}
}
9 changes: 9 additions & 0 deletions src/Http/Controllers/CP/Navigation/NavigationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public function edit($nav)
'root' => $nav->expectsRoot(),
'sites' => $nav->trees()->keys()->all(),
'max_depth' => $nav->maxDepth(),
'select_across_sites' => $nav->canSelectAcrossSites(),
];

$fields = ($blueprint = $this->editFormBlueprint($nav))
Expand Down Expand Up @@ -132,6 +133,8 @@ public function update(Request $request, $nav)
foreach (array_diff($existingSites, $sites) as $site) {
$nav->in($site)->delete();
}

$nav->canSelectAcrossSites($values['select_across_sites']);
}

$nav->save();
Expand Down Expand Up @@ -231,6 +234,12 @@ public function editFormBlueprint($nav)
'mode' => 'select',
'required' => true,
];

$contents['options']['fields']['select_across_sites'] = [
'display' => __('Select Across Sites'),
'instructions' => __('statamic::messages.navigation_configure_select_across_sites'),
'type' => 'toggle',
];
}

return Blueprint::makeFromTabs($contents);
Expand Down
33 changes: 29 additions & 4 deletions src/Query/Scopes/Filters/Site.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

namespace Statamic\Query\Scopes\Filters;

use Illuminate\Support\Arr;
use Statamic\Facades;
use Statamic\Facades\Collection;
use Statamic\Query\Scopes\Filter;

class Site extends Filter
Expand Down Expand Up @@ -46,13 +48,36 @@ public function badge($values)

public function visibleTo($key)
{
return $key === 'entries' && Facades\Site::hasMultiple();
if ($key === 'entries' && $this->availableSites()->count() > 1) {
return true;
}

return $key === 'entries-fieldtype' && $this->context['showSiteFilter'] && $this->availableSites()->count() > 1;
}

protected function options()
{
return Facades\Site::authorized()->mapWithKeys(function ($site) {
return [$site->handle() => __($site->name())];
});
return $this->availableSites()
->mapWithKeys(fn ($site) => [$site->handle() => __($site->name())]);
}

protected function availableSites()
{
if (! Facades\Site::hasMultiple()) {
return collect();
}

// Get the configured sites of multiple collections when in the entries fieldtype.
$collections = Arr::get($this->context, 'collections');

// Get the configured sites of a single collection when on the entries index view.
if ($collection = Arr::get($this->context, 'collection')) {
$collections = [$collection];
}

$configuredSites = collect($collections)->flatMap(fn ($collection) => Collection::find($collection)->sites());

return Facades\Site::authorized()
->when(isset($configuredSites), fn ($sites) => $sites->filter(fn ($site) => $configuredSites->contains($site->handle())));
}
}
1 change: 1 addition & 0 deletions src/Stache/Stores/NavigationStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public function makeItemFromFile($path, $contents)
->maxDepth($data['max_depth'] ?? null)
->collections($data['collections'] ?? null)
->expectsRoot($data['root'] ?? false)
->canSelectAcrossSites($data['select_across_sites'] ?? false)
->initialPath($path);
}

Expand Down
9 changes: 9 additions & 0 deletions src/Structures/Nav.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class Nav extends Structure implements Contract
use ExistsAsFile;

protected $collections;
protected $canSelectAcrossSites = false;
private $blueprintCache;

public function save()
Expand Down Expand Up @@ -59,6 +60,7 @@ public function fileData()
return [
'title' => $this->title,
'collections' => $this->collections,
'select_across_sites' => $this->canSelectAcrossSites ? true : null,
'max_depth' => $this->maxDepth,
'root' => $this->expectsRoot ?: null,
];
Expand Down Expand Up @@ -139,4 +141,11 @@ public function blueprint()

return $blueprint;
}

public function canSelectAcrossSites($canSelect = null)
{
return $this
->fluentlyGetOrSet('canSelectAcrossSites')
->args(func_get_args());
}
}
1 change: 1 addition & 0 deletions tests/Feature/Navigation/MocksStructures.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ private function createNav($handle)
$s->shouldReceive('collections')->andReturn(collect());
$s->shouldReceive('expectsRoot')->andReturnFalse();
$s->shouldReceive('maxDepth')->andReturnNull();
$s->shouldReceive('canSelectAcrossSites')->andReturnFalse();
$s->shouldReceive('sites')->andReturn(collect(['en']));
});
}
Expand Down
1 change: 1 addition & 0 deletions tests/Feature/Navigation/UpdateNavigationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ private function validParams($overrides = [])
'collections' => ['pages'],
'root' => true,
'max_depth' => 2,
'select_across_sites' => false,
], $overrides);
}

Expand Down
16 changes: 16 additions & 0 deletions tests/Fieldtypes/EntriesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,22 @@ public function it_localizes_the_shallow_augmented_item_to_the_current_sites_loc
$this->assertNull($augmented); // 456 isnt localized
}

/** @test */
public function it_doesnt_localize_when_select_across_sites_setting_is_enabled()
{
$parent = EntryFactory::id('parent')->collection('blog')->slug('theparent')->locale('fr')->create();

EntryFactory::id('123-fr')->origin('123')->locale('fr')->collection('blog')->slug('one-fr')->data(['title' => 'Le One'])->date('2021-01-02')->create();
EntryFactory::id('789-fr')->origin('789')->locale('fr')->collection('blog')->slug('three-fr')->data(['title' => 'Le Three'])->date('2021-01-02')->published(false)->create();
EntryFactory::id('910-fr')->origin('910')->locale('fr')->collection('blog')->slug('four-fr')->data(['title' => 'Le Four'])->date('2021-01-02')->create();

$augmented = $this->fieldtype(['select_across_sites' => true], $parent)->augment(['123', 'invalid', 456, 789, 910, 'draft', 'scheduled', 'expired']);

$this->assertInstanceOf(Builder::class, $augmented);
$this->assertEveryItemIsInstanceOf(Entry::class, $augmented->get());
$this->assertEquals(['one', 'two', 'three', 'four'], $augmented->get()->map->slug()->all());
}

public function fieldtype($config = [], $parent = null)
{
$field = new Field('test', array_merge([
Expand Down

0 comments on commit ff74133

Please sign in to comment.