Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[5.x] Ability to select entries from other sites #9229

Merged
merged 33 commits into from
Jul 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
ee8d5eb
Add site filter to the entries selector
aerni Dec 19, 2023
7c2222d
Only query sites the user has permission to
aerni Dec 19, 2023
a5ea34a
Hide filter if only one authorized site
aerni Dec 19, 2023
7714328
Only show configured sites in the site filter
aerni Dec 20, 2023
9b6b240
Account for multiple collections
aerni Dec 20, 2023
e7c6294
fallback to selected site
edalzell Dec 20, 2023
be7086e
handle `select` mode
edalzell Dec 20, 2023
fa3b1b1
add site when in stack mode and have multiple sites
edalzell Dec 20, 2023
73b9335
tidy
edalzell Dec 20, 2023
635a546
Merge branch '4.x' into feature/entries-fieldtype-multisite
jasonvarga Jan 26, 2024
5a45cbf
prevent deselecting
jasonvarga Jan 29, 2024
8b137df
opting into the site setting opts out of localizing during augmentation
jasonvarga Jan 30, 2024
57476b9
undo unnecessary js changes
jasonvarga Jan 30, 2024
a654e32
wrap in option
jasonvarga Jan 30, 2024
09b524b
rework condition so it prevents checking sites if we know its not an …
jasonvarga Jan 30, 2024
5dd82ea
more than one
jasonvarga Jan 30, 2024
61a9033
fluent
jasonvarga Jan 30, 2024
43428b1
do a single flatmap for both situations
jasonvarga Jan 30, 2024
39d5406
itll be here
jasonvarga Jan 30, 2024
78afe75
test
jasonvarga Jan 30, 2024
3b2e77a
allow the select ui mode to pick from other sites too
jasonvarga Jan 30, 2024
0257658
Merge branch '5.x' into feature/entries-fieldtype-multisite
jasonvarga May 29, 2024
ecc77f6
Merge branch '5.x' into feature/entries-fieldtype-multisite
jasonvarga Jul 12, 2024
0039d1c
use a more generic approach for hints, and show collections in the hint
jasonvarga Jul 12, 2024
2c2faf1
Merge branch '5.x' into feature/entries-fieldtype-multisite
jasonvarga Jul 17, 2024
cee1f2b
Merge branch '5.x' into feature/entries-fieldtype-multisite
jasonvarga Jul 17, 2024
79653c0
wip
jasonvarga Jul 17, 2024
23546e2
add option for nav to opt into picking entries across sites
jasonvarga Jul 18, 2024
5f08a74
not needed
jasonvarga Jul 18, 2024
e44bd99
Merge branch '5.x' into feature/entries-fieldtype-multisite
jasonvarga Jul 18, 2024
bedc871
revert
jasonvarga Jul 18, 2024
076a37e
revert
jasonvarga Jul 18, 2024
50596e8
alphabetize
jasonvarga Jul 18, 2024
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
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
Loading