Skip to content
Draft
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
3 changes: 3 additions & 0 deletions resources/js/components/entries/Listing.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<Link class="title-index-field" :href="entry.edit_url" @click.stop>
<StatusIndicator v-if="!isColumnVisible('status')" :status="entry.status" />
<span v-text="entry.title" />
<StructureBreadcrumbBadge :breadcrumb="entry.breadcrumb" />
</Link>
</template>
<template #cell-status="{ row: entry }">
Expand All @@ -39,13 +40,15 @@

<script>
import { StatusIndicator, DropdownItem, Listing } from '@/components/ui';
import StructureBreadcrumbBadge from './StructureBreadcrumbBadge.vue';
import { Link } from '@inertiajs/vue3';

export default {
emits: ['reordered', 'site-changed'],

components: {
Link,
StructureBreadcrumbBadge,
StatusIndicator,
Listing,
DropdownItem,
Expand Down
34 changes: 34 additions & 0 deletions resources/js/components/entries/StructureBreadcrumbBadge.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<template>
<Badge v-if="parts.length" size="sm" color="white" pill class="ps-0.75 pe-1.75">
<span class="inline-flex max-w-56 items-center gap-0.5 overflow-hidden">
<template v-for="(part, i) in parts" :key="i">
<Icon name="chevron-left" class="size-2.5 shrink-0" />
<span class="st-text-trim-cap truncate py-1.25" v-text="part" />
</template>
</span>
</Badge>
</template>

<script>
import { Badge, Icon } from '@/components/ui';

export default {
components: {
Badge,
Icon,
},

props: {
breadcrumb: {
type: Array,
default: null,
},
},

computed: {
parts() {
return this.breadcrumb ?? [];
},
},
};
</script>
3 changes: 3 additions & 0 deletions resources/js/components/inputs/relationship/Selector.vue
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
<a class="title-index-field" :href="entry.edit_url" @click.prevent="toggleSelection(entry.id)">
<StatusIndicator v-if="!isColumnVisible('status')" :status="entry.status" />
<span v-text="entry.title" />
<StructureBreadcrumbBadge :breadcrumb="entry.breadcrumb" />
</a>
</template>
<template #cell-status="{ row: entry }">
Expand Down Expand Up @@ -145,6 +146,7 @@ import {
StatusIndicator,
Badge,
} from '@/components/ui';
import StructureBreadcrumbBadge from '../../entries/StructureBreadcrumbBadge.vue';

export default {
components: {
Expand All @@ -163,6 +165,7 @@ export default {
Icon,
StatusIndicator,
Badge,
StructureBreadcrumbBadge,
},

// todo, when opening and closing the stack, you cant save?
Expand Down
35 changes: 35 additions & 0 deletions src/Entries/Entry.php
Original file line number Diff line number Diff line change
Expand Up @@ -976,6 +976,41 @@ public function parent()
return optional($this->page())->parent();
}

public function structureBreadcrumbs(): array
{
if (! $this->hasStructure()) {
return [];
}

$titles = [];
$page = $this->parent();
$skipRoot = $this->structure()->expectsRoot();

while ($page) {
// With a collection root, every page sits under it — omit it so
// breadcrumbs stay useful (e.g. "Fruit" instead of "Home ‹ Fruit").
if ($skipRoot && $page->isRoot()) {
break;
}

if ($title = $page->title()) {
// Nearest parent first, matching CP document titles (Title ‹ Section).
$titles[] = $title;
}

$page = $page->parent();
}

return $titles;
}

public function structureBreadcrumb(string $separator = ' ‹ '): ?string
{
$titles = $this->structureBreadcrumbs();

return empty($titles) ? null : implode($separator, $titles);
}

public function page()
{
if (! $this->hasStructure()) {
Expand Down
5 changes: 4 additions & 1 deletion src/Fieldtypes/Entries.php
Original file line number Diff line number Diff line change
Expand Up @@ -563,10 +563,13 @@ public function preload()

public function getItemHint($item): ?string
{
return collect([
$hint = collect([
$item->structureBreadcrumb(),
count($this->getConfiguredCollections()) > 1 ? __($item->collection()->title()) : null,
$this->canSelectAcrossSites() && count($this->availableSites()) > 1 ? $item->site()->name() : null,
])->filter()->implode(' • ');

return $hint !== '' ? $hint : null;
}

private function addColumn(Columns $columns, string $columnKey): void
Expand Down
1 change: 1 addition & 0 deletions src/Http/Resources/CP/Entries/ListedEntry.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public function toArray($request)

$this->merge($this->values(['slug' => $entry->slug()])),

'breadcrumb' => ($breadcrumbs = $entry->structureBreadcrumbs()) ? $breadcrumbs : null,
'permalink' => $entry->absoluteUrl(),
'edit_url' => $entry->editUrl(),
'collection' => array_merge($entry->collection()->toArray(), ['dated' => $entry->collection()->dated()]),
Expand Down
75 changes: 75 additions & 0 deletions tests/Data/Entries/EntryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1185,6 +1185,81 @@ public function it_gets_the_order_from_the_collections_structure()
$this->assertEquals(3, $four->order());
}

#[Test]
public function it_gets_a_structure_breadcrumb_from_ancestors()
{
$collection = tap(Collection::make('pages')->routes('{parent_uri}/{slug}'))->save();

$home = tap((new Entry)->locale('en')->id('home')->collection($collection)->slug('home')->data(['title' => 'Home']))->save();
$fruit = tap((new Entry)->locale('en')->id('fruit')->collection($collection)->slug('fruit')->data(['title' => 'Fruit › Bowl']))->save();
$tomato = tap((new Entry)->locale('en')->id('tomato')->collection($collection)->slug('tomato')->data(['title' => 'Tomato']))->save();
$seed = tap((new Entry)->locale('en')->id('seed')->collection($collection)->slug('seed')->data(['title' => 'Seed']))->save();

$collection->structureContents([
'max_depth' => 4,
])->save();
$collection->structure()->in('en')->tree([
[
'entry' => 'home',
'children' => [
[
'entry' => 'fruit',
'children' => [
[
'entry' => 'tomato',
'children' => [
['entry' => 'seed'],
],
],
],
],
],
],
])->save();

$this->assertSame([], $home->structureBreadcrumbs());
$this->assertSame(['Home'], $fruit->structureBreadcrumbs());
$this->assertSame(['Fruit › Bowl', 'Home'], $tomato->structureBreadcrumbs());
$this->assertSame(['Tomato', 'Fruit › Bowl', 'Home'], $seed->structureBreadcrumbs());
$this->assertNull($home->structureBreadcrumb());
$this->assertEquals('Home', $fruit->structureBreadcrumb());
$this->assertEquals('Fruit › Bowl ‹ Home', $tomato->structureBreadcrumb());
$this->assertEquals('Tomato ‹ Fruit › Bowl ‹ Home', $seed->structureBreadcrumb());

// With a collection root, siblings of the root become its children.
$collection->structureContents([
'max_depth' => 4,
'root' => true,
])->save();
$collection->structure()->in('en')->tree([
['entry' => 'home'],
[
'entry' => 'fruit',
'children' => [
[
'entry' => 'tomato',
'children' => [
['entry' => 'seed'],
],
],
],
],
])->save();

$this->assertSame([], $home->fresh()->structureBreadcrumbs());
$this->assertSame([], $fruit->fresh()->structureBreadcrumbs());
$this->assertSame(['Fruit › Bowl'], $tomato->fresh()->structureBreadcrumbs());
$this->assertSame(['Tomato', 'Fruit › Bowl'], $seed->fresh()->structureBreadcrumbs());
$this->assertNull($home->fresh()->structureBreadcrumb());
$this->assertNull($fruit->fresh()->structureBreadcrumb());
$this->assertEquals('Fruit › Bowl', $tomato->fresh()->structureBreadcrumb());
$this->assertEquals('Tomato ‹ Fruit › Bowl', $seed->fresh()->structureBreadcrumb());

$flat = tap(Collection::make('flat'))->save();
$this->assertSame([], (new Entry)->collection($flat)->structureBreadcrumbs());
$this->assertNull((new Entry)->collection($flat)->structureBreadcrumb());
}

#[Test]
public function it_gets_the_order_from_the_collections_structure_when_the_tree_contains_a_null_item()
{
Expand Down
30 changes: 30 additions & 0 deletions tests/Fieldtypes/EntriesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,36 @@ public function it_doesnt_inherit_the_item_cache_from_another_fieldtype_instance
$this->assertTrue($second->getItemData(['123'])->first()['invalid']);
}

#[Test]
public function it_includes_structure_breadcrumb_before_collection_in_item_hints()
{
$pages = tap(Facades\Collection::make('pages')->routes('{parent_uri}/{slug}'))->save();
$home = EntryFactory::id('home')->collection($pages)->slug('home')->data(['title' => 'Home'])->create();
$fruit = EntryFactory::id('fruit')->collection($pages)->slug('fruit')->data(['title' => 'Fruit'])->create();
$tomato = EntryFactory::id('tomato')->collection($pages)->slug('tomato')->data(['title' => 'Tomato'])->create();

$pages->structureContents(['max_depth' => 3])->save();
$pages->structure()->in('en')->tree([
[
'entry' => 'home',
'children' => [
[
'entry' => 'fruit',
'children' => [
['entry' => 'tomato'],
],
],
],
],
])->save();

$fieldtype = $this->fieldtype(['collections' => ['pages', 'blog']]);

$this->assertEquals('Fruit ‹ Home • Pages', $fieldtype->getItemHint($tomato->fresh()));
$this->assertEquals('Pages', $fieldtype->getItemHint($home->fresh()));
$this->assertNull($this->fieldtype(['collections' => ['pages']])->getItemHint($home->fresh()));
}

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