diff --git a/docs/guide/sharp-breadcrumb.md b/docs/guide/sharp-breadcrumb.md
index 0ec393b56..6e1207338 100644
--- a/docs/guide/sharp-breadcrumb.md
+++ b/docs/guide/sharp-breadcrumb.md
@@ -2,22 +2,6 @@
Under the hood Sharp manages a breadcrumb to keep track of stacked pages.
-## Display the breadcrumb
-
-You can activate the breadcrumb display in sharp's configuration:
-
-```php
-class SharpServiceProvider extends SharpAppServiceProvider
-{
- protected function configureSharp(SharpConfigBuilder $config): void
- {
- $config
- ->displayBreadcrumb()
- // [...]
- }
-}
-```
-
## Configure entity label
In Entity classes, you can define how an entity should be labeled in the breadcrumb with the `label` attribute:
@@ -74,6 +58,72 @@ class PostShow extends \Code16\Sharp\Show\SharpShow
In the Form, the breadcrumb label is only used in one particular case: when coming from an embedded Entity List inside a Show Page. In this case, the Show Page and the Form entity are different, and the breadcrumb helps to keep track of the current edited entity.
:::
+## Configure custom labels cache
+
+Breadcrumb labels are cached for 30 minutes to reduce DB queries between each navigation. If you don't want to cache them, which means all `SharpShow` in breadcrumb are loaded on every navigation, you can update the config in the SharpServiceProvider:
+
+```php
+class SharpServiceProvider extends SharpAppServiceProvider
+{
+ protected function configureSharp(SharpConfigBuilder $config): void
+ {
+ $config
+ ->configureBreadcrumbLabelsCache(false)
+ // ...
+ }
+}
+```
+
+Alternatively, you can change the cache duration (default is 30 minutes):
+
+```php
+class SharpServiceProvider extends SharpAppServiceProvider
+{
+ protected function configureSharp(SharpConfigBuilder $config): void
+ {
+ $config
+ ->configureBreadcrumbLabelsCache(duration: 10)
+ // ...
+ }
+}
+```
+
+### Lazy loading
+
+In some cases, having the labels replaced by the default Entity label is acceptable and you want to have less DB queries, you can activate the lazy loading:
+
+```php
+class SharpServiceProvider extends SharpAppServiceProvider
+{
+ protected function configureSharp(SharpConfigBuilder $config): void
+ {
+ $config
+ ->enableBreadcrumbLabelsLazyLoading()
+ }
+}
+```
+::: warning
+Be aware that the user may see the breadcrumb with default entity labels (e.g. "Posts > Post > Category > Edit") when :
+- a nested page is accessed directly (e.g. direct link)
+- cached labels are expired
+:::
+
+## Hide the breadcrumb
+
+If you don't want any breadcrumb, you can hide it in sharp's configuration:
+
+```php
+class SharpServiceProvider extends SharpAppServiceProvider
+{
+ protected function configureSharp(SharpConfigBuilder $config): void
+ {
+ $config
+ ->displayBreadcrumb(false)
+ // [...]
+ }
+}
+```
+
## Interact with Sharp's Breadcrumb
-Refer to [the Context documentation](context.md) to find out how to interact with Sharp's breadcrumb.
\ No newline at end of file
+Refer to [the Context documentation](context.md) to find out how to interact with Sharp's breadcrumb.
diff --git a/resources/js/Pages/Dashboard/Dashboard.vue b/resources/js/Pages/Dashboard/Dashboard.vue
index c1f934ca8..ff7eec47b 100644
--- a/resources/js/Pages/Dashboard/Dashboard.vue
+++ b/resources/js/Pages/Dashboard/Dashboard.vue
@@ -56,7 +56,7 @@
-
+
diff --git a/resources/js/Pages/Form/Form.vue b/resources/js/Pages/Form/Form.vue
index f2d8250ca..9c1da75cc 100644
--- a/resources/js/Pages/Form/Form.vue
+++ b/resources/js/Pages/Form/Form.vue
@@ -77,7 +77,7 @@
-
+
diff --git a/resources/js/Pages/Show/Show.vue b/resources/js/Pages/Show/Show.vue
index cca1e86bd..2256dd48b 100644
--- a/resources/js/Pages/Show/Show.vue
+++ b/resources/js/Pages/Show/Show.vue
@@ -128,7 +128,7 @@
-
+
diff --git a/src/Config/SharpConfigBuilder.php b/src/Config/SharpConfigBuilder.php
index 65baaa6e6..eec017334 100644
--- a/src/Config/SharpConfigBuilder.php
+++ b/src/Config/SharpConfigBuilder.php
@@ -33,7 +33,14 @@ class SharpConfigBuilder
'name' => 'Sharp',
'custom_url_segment' => 'sharp',
'display_sharp_version_in_title' => true,
- 'display_breadcrumb' => true,
+ 'breadcrumb' => [
+ 'display' => true,
+ 'labels' => [
+ 'lazy_loading' => false,
+ 'cache' => true,
+ 'cache_duration' => 30,
+ ],
+ ],
'entities' => [],
'entity_resolver' => null,
'global_filters' => [],
@@ -132,7 +139,22 @@ public function displaySharpVersionInTitle(bool $displaySharpVersionInTitle = tr
public function displayBreadcrumb(bool $displayBreadcrumb = true): self
{
- $this->config['display_breadcrumb'] = $displayBreadcrumb;
+ $this->config['breadcrumb']['display'] = $displayBreadcrumb;
+
+ return $this;
+ }
+
+ public function enableBreadcrumbLabelsLazyLoading(bool $lazyLoading = true): self
+ {
+ $this->config['breadcrumb']['labels']['lazy_loading'] = $lazyLoading;
+
+ return $this;
+ }
+
+ public function configureBreadcrumbLabelsCache(bool $cache = true, int $duration = 30): self
+ {
+ $this->config['breadcrumb']['labels']['cache'] = $cache;
+ $this->config['breadcrumb']['labels']['cache_duration'] = $duration;
return $this;
}
diff --git a/src/Data/BreadcrumbItemData.php b/src/Data/BreadcrumbItemData.php
index 8c5c20658..784a276be 100644
--- a/src/Data/BreadcrumbItemData.php
+++ b/src/Data/BreadcrumbItemData.php
@@ -8,7 +8,6 @@
final class BreadcrumbItemData extends Data
{
public function __construct(
- public string $type,
public string $label,
public ?string $documentTitleLabel,
public string $entityKey,
diff --git a/src/Http/Context/SharpBreadcrumb.php b/src/Http/Context/SharpBreadcrumb.php
index 2019da910..e5ee1c245 100644
--- a/src/Http/Context/SharpBreadcrumb.php
+++ b/src/Http/Context/SharpBreadcrumb.php
@@ -2,6 +2,7 @@
namespace Code16\Sharp\Http\Context;
+use Closure;
use Code16\Sharp\Http\Context\Util\BreadcrumbItem;
use Code16\Sharp\Utils\Entities\SharpEntityManager;
use Code16\Sharp\Utils\Entities\ValueObjects\EntityKey;
@@ -27,22 +28,17 @@ public function setCurrentInstanceLabel(?string $label): self
public function allSegments(): array
{
- $url = sharp()->config()->get('custom_url_segment')
- .'/'.sharp()->context()->globalFilterUrlSegmentValue();
+ $segments = $this->breadcrumbItems();
+ $url = $this->baseUrlPrefix();
+ $lastIndex = $segments->count() - 1;
- return $this
- ->breadcrumbItems()
- ->map(function ($item, $index) use (&$url) {
- $url = sprintf('%s/%s/%s',
- $url,
- $item->type,
- isset($item->instance) ? "{$item->key}/{$item->instance}" : $item->key,
- );
- $isLeaf = $index === count($this->breadcrumbItems()) - 1;
+ return $segments
+ ->map(function (BreadcrumbItem $item, int $index) use (&$url, $lastIndex) {
+ $url = $this->appendSegmentToUrl($url, $item);
+ $isLeaf = $index === $lastIndex;
return [
- 'type' => $this->getFrontTypeNameFor($item->type),
- 'label' => sharp()->config()->get('display_breadcrumb')
+ 'label' => sharp()->config()->get('breadcrumb.display')
? $this->getBreadcrumbLabelFor($item, $isLeaf)
: '',
'documentTitleLabel' => $this->getDocumentTitleLabelFor($item, $isLeaf),
@@ -77,9 +73,8 @@ public function getCurrentSegmentUrl(): string
{
return url(
sprintf(
- '%s/%s/%s',
- sharp()->config()->get('custom_url_segment'),
- sharp()->context()->globalFilterUrlSegmentValue(),
+ '%s/%s',
+ $this->baseUrlPrefix(),
$this->getCurrentPath()
)
);
@@ -87,22 +82,16 @@ public function getCurrentSegmentUrl(): string
public function getCurrentPath(): ?string
{
- return $this->breadcrumbItems()
- ->map(fn (BreadcrumbItem $item) => $item->toUri())
- ->implode('/');
+ return $this->breadcrumbPathFor($this->breadcrumbItems());
}
public function getPreviousSegmentUrl(): string
{
return url(
sprintf(
- '%s/%s/%s',
- sharp()->config()->get('custom_url_segment'),
- sharp()->context()->globalFilterUrlSegmentValue(),
- $this->breadcrumbItems()
- ->slice(0, -1)
- ->map(fn (BreadcrumbItem $item) => $item->toUri())
- ->implode('/')
+ '%s/%s',
+ $this->baseUrlPrefix(),
+ $this->breadcrumbPathFor($this->breadcrumbItems()->slice(0, -1))
)
);
}
@@ -139,56 +128,15 @@ private function findPreviousSegment(string $type, ?string $entityKeyOrClassName
->first();
}
- private function getFrontTypeNameFor(string $type): string
- {
- return match ($type) {
- 's-list' => 'list',
- 's-form' => 'form',
- 's-show' => 'show',
- 's-dashboard' => 'dashboard',
- default => '',
- };
- }
-
private function getBreadcrumbLabelFor(BreadcrumbItem $item, bool $isLeaf): string
{
- switch ($item->type) {
- case 's-list':
- return app(SharpMenuManager::class)
- ->getEntityMenuItem($item->key)
- ?->getLabel() ?: trans('sharp::breadcrumb.entityList');
- case 's-dashboard':
- return app(SharpMenuManager::class)
- ->getEntityMenuItem($item->key)
- ?->getLabel() ?: trans('sharp::breadcrumb.dashboard');
- case 's-show':
- return trans('sharp::breadcrumb.show', [
- 'entity' => $this->getEntityLabelForInstance($item, $isLeaf),
- ]);
- case 's-form':
- // A Form is always a leaf
- $previousItem = $this->breadcrumbItems()[$item->depth - 1];
-
- if (
- ($previousItem->isShow() && ! $this->isSameEntityKeys($previousItem->key, $item->key, true))
- || ($item->isForm() && $this->currentInstanceLabel)
- ) {
- // The form entityKey is different from the previous entityKey in the breadcrumb: we are in a EEL case.
- return isset($item->instance)
- ? trans('sharp::breadcrumb.form.edit_entity', [
- 'entity' => $this->getEntityLabelForInstance($item, true),
- ])
- : trans('sharp::breadcrumb.form.create_entity', [
- 'entity' => $this->getEntityLabelForInstance($item, true),
- ]);
- }
-
- return isset($item->instance) || ($previousItem->type === 's-show' && ! isset($previousItem->instance))
- ? trans('sharp::breadcrumb.form.edit')
- : trans('sharp::breadcrumb.form.create');
- }
-
- return $item->key;
+ return match ($item->type) {
+ 's-list' => $this->labelForList($item),
+ 's-dashboard' => $this->labelForDashboard($item),
+ 's-show' => $this->labelForShow($item, $isLeaf),
+ 's-form' => $this->labelForForm($item),
+ default => $item->key,
+ };
}
/**
@@ -201,51 +149,114 @@ private function getDocumentTitleLabelFor(BreadcrumbItem $item, bool $isLeaf): ?
return null;
}
- $previousItem = $this->breadcrumbItems()[$item->depth - 1] ?? null;
+ $previousItem = $this->previousItemFor($item);
return match ($item->type) {
- 's-show' => trans('sharp::breadcrumb.show', [
- 'entity' => $this->getEntityLabelForInstance($item, $isLeaf),
- ]),
- 's-form' => isset($item->instance) || ($previousItem->type === 's-show' && ! isset($previousItem->instance))
+ 's-show' => $this->labelForShow($item, $isLeaf),
+ 's-form' => $this->shouldUseEditLabelForForm($item, $previousItem)
? trans('sharp::breadcrumb.form.edit_entity', [
- 'entity' => $this->getEntityLabelForInstance($item, $isLeaf),
+ 'entity' => $this->resolveEntityLabelForItem($item, true),
])
: trans('sharp::breadcrumb.form.create_entity', [
- 'entity' => $this->getEntityLabelForInstance($item, $isLeaf),
+ 'entity' => $this->resolveEntityLabelForItem($item, true),
]),
default => null
};
}
+ private function labelForList(BreadcrumbItem $item): string
+ {
+ return app(SharpMenuManager::class)
+ ->getEntityMenuItem($item->key)
+ ?->getLabel() ?: trans('sharp::breadcrumb.entityList');
+ }
+
+ private function labelForDashboard(BreadcrumbItem $item): string
+ {
+ return app(SharpMenuManager::class)
+ ->getEntityMenuItem($item->key)
+ ?->getLabel() ?: trans('sharp::breadcrumb.dashboard');
+ }
+
+ private function labelForShow(BreadcrumbItem $item, bool $isLeaf): string
+ {
+ return trans('sharp::breadcrumb.show', [
+ 'entity' => $this->resolveEntityLabelForItem($item, $isLeaf),
+ ]);
+ }
+
+ private function labelForForm(BreadcrumbItem $item): string
+ {
+ // A Form is always a leaf
+ $previousItem = $this->previousItemFor($item);
+
+ if ($this->shouldUseEntityLabelForForm($item, $previousItem)) {
+ // The form entityKey is different from the previous entityKey in the breadcrumb: we are in a EEL case.
+ return isset($item->instance)
+ ? trans('sharp::breadcrumb.form.edit_entity', [
+ 'entity' => $this->resolveEntityLabelForItem($item, true),
+ ])
+ : trans('sharp::breadcrumb.form.create_entity', [
+ 'entity' => $this->resolveEntityLabelForItem($item, true),
+ ]);
+ }
+
+ return $this->shouldUseEditLabelForForm($item, $previousItem)
+ ? trans('sharp::breadcrumb.form.edit')
+ : trans('sharp::breadcrumb.form.create');
+ }
+
+ private function shouldUseEntityLabelForForm(BreadcrumbItem $item, ?BreadcrumbItem $previousItem): bool
+ {
+ return ($previousItem->isShow() && ! $this->isSameEntityKeys($previousItem->key, $item->key, true))
+ || ($item->isForm() && $this->currentInstanceLabel);
+ }
+
+ private function shouldUseEditLabelForForm(BreadcrumbItem $item, ?BreadcrumbItem $previousItem): bool
+ {
+ return isset($item->instance)
+ || ($previousItem->type === 's-show' && ! isset($previousItem->instance));
+ }
+
public function getParentShowCachedBreadcrumbLabel(): ?string
{
$item = $this->breadcrumbItems()->last();
- return Cache::get("sharp.breadcrumb.{$item->key}.s-show.{$item->instance}");
+ if (! $item || ! $item->isForm()) {
+ return null;
+ }
+
+ return $this->resolveParentShowLabel($item);
}
/**
* Only for Shows and Forms.
*/
- private function getEntityLabelForInstance(BreadcrumbItem $item, bool $isLeaf): string
+ private function resolveEntityLabelForItem(BreadcrumbItem $item, bool $isLeaf): string
{
- $cacheKey = "sharp.breadcrumb.{$item->key}.{$item->type}.{$item->instance}";
-
if ($isLeaf && $this->currentInstanceLabel) {
- Cache::put($cacheKey, $this->currentInstanceLabel, now()->addMinutes(30));
+ $this->storeCachedLabel($item, $this->currentInstanceLabel);
return $this->currentInstanceLabel;
}
- if ($item->isForm() && ($cached = $this->getParentShowCachedBreadcrumbLabel())) {
- return $cached;
+ if ($item->isForm()) {
+ if ($label = $this->resolveParentShowLabel($item)) {
+ return $label;
+ }
}
if (! $isLeaf) {
- // The breadcrumb custom label may have been cached on the way up
- if ($value = Cache::get($cacheKey)) {
- return $value;
+ if ($label = $this->getCachedLabel($item)) {
+ return $label;
+ }
+ }
+
+ if ($item->isShow() && ! $isLeaf && ! sharp()->config()->get('breadcrumb.labels.lazy_loading')) {
+ if ($label = $this->loadShowBreadcrumbLabel($item)) {
+ $this->storeCachedLabel($item, $label);
+
+ return $label;
}
}
@@ -254,6 +265,155 @@ private function getEntityLabelForInstance(BreadcrumbItem $item, bool $isLeaf):
->getLabelOrFail((new EntityKey($item->key))->multiformKey());
}
+ private function resolveParentShowLabel(BreadcrumbItem $formItem): ?string
+ {
+ if (! $formItem->isForm()) {
+ return null;
+ }
+
+ if ($formItem->instanceId() === null && ! $formItem->isSingleForm()) {
+ return null;
+ }
+
+ if ($label = $this->getCachedLabel($formItem, 's-show')) {
+ return $label;
+ }
+
+ if (sharp()->config()->get('breadcrumb.labels.lazy_loading')) {
+ return null;
+ }
+
+ $items = $this->breadcrumbItems()->values();
+ $formIndex = $items->search(fn (BreadcrumbItem $item) => $item->is($formItem));
+
+ if ($formIndex === false) {
+ return null;
+ }
+
+ $parentShowItem = $items
+ ->slice(0, $formIndex)
+ ->reverse()
+ ->first(fn (BreadcrumbItem $item) => $item->isShow()
+ && $item->key === $formItem->key
+ && $item->instanceId() === $formItem->instanceId()
+ );
+
+ if (! $parentShowItem || ! $this->canLoadShowLabel($parentShowItem)) {
+ return null;
+ }
+
+ $label = $this->loadShowBreadcrumbLabel($parentShowItem);
+
+ if (! $label) {
+ return null;
+ }
+
+ $this->storeCachedLabel($parentShowItem, $label);
+
+ return $label;
+ }
+
+ private function loadShowBreadcrumbLabel(BreadcrumbItem $showItem): ?string
+ {
+ if (! $this->canLoadShowLabel($showItem)) {
+ return null;
+ }
+
+ $show = app(SharpEntityManager::class)
+ ->entityFor($showItem->key)
+ ->getShowOrFail();
+
+ $show->buildShowConfig();
+
+ if (! $show->getBreadcrumbAttribute()) {
+ return null;
+ }
+
+ $data = [];
+
+ $this->forceRequestSegments(
+ $this->getFakeRequestSegmentsFor($showItem),
+ function () use ($show, $showItem, &$data) {
+ $data = $show->instance($showItem->instanceId());
+ }
+ );
+
+ return $show->getBreadcrumbCustomLabel($data);
+ }
+
+ private function canLoadShowLabel(BreadcrumbItem $showItem): bool
+ {
+ if (! $showItem->isShow()) {
+ return false;
+ }
+
+ if ($showItem->instanceId() !== null) {
+ return true;
+ }
+
+ return $showItem->isSingleShow();
+ }
+
+ private function breadcrumbCacheKey(BreadcrumbItem $item, ?string $type = null): string
+ {
+ $type = $type ?: $item->type;
+
+ return "sharp.breadcrumb.{$item->key}.{$type}.{$item->instance}";
+ }
+
+ private function getCachedLabel(BreadcrumbItem $item, ?string $type = null): ?string
+ {
+ if (! sharp()->config()->get('breadcrumb.labels.cache')) {
+ return null;
+ }
+
+ return Cache::get($this->breadcrumbCacheKey($item, $type));
+ }
+
+ private function storeCachedLabel(BreadcrumbItem $item, string $label, ?string $type = null): void
+ {
+ if (! sharp()->config()->get('breadcrumb.labels.cache')) {
+ return;
+ }
+
+ Cache::put(
+ $this->breadcrumbCacheKey($item, $type),
+ $label,
+ now()->addMinutes((int) sharp()->config()->get('breadcrumb.labels.cache_duration')),
+ );
+ }
+
+ private function baseUrlPrefix(): string
+ {
+ return sprintf(
+ '%s/%s',
+ sharp()->config()->get('custom_url_segment'),
+ sharp()->context()->globalFilterUrlSegmentValue(),
+ );
+ }
+
+ private function appendSegmentToUrl(string $url, BreadcrumbItem $item): string
+ {
+ return sprintf(
+ '%s/%s/%s',
+ $url,
+ $item->type,
+ isset($item->instance) ? "{$item->key}/{$item->instance}" : $item->key,
+ );
+ }
+
+ private function breadcrumbPathFor(Collection $items): string
+ {
+ return $items
+ ->map(fn (BreadcrumbItem $item) => $item->toUri())
+ ->implode('/');
+ }
+
+ private function previousItemFor(BreadcrumbItem $item): ?BreadcrumbItem
+ {
+ return $this->breadcrumbItems()[$item->depth - 1] ?? null;
+ }
+
private function isSameEntityKeys(string $key1, string $key2, bool $compareBaseEntities): bool
{
if ($compareBaseEntities) {
@@ -271,7 +431,11 @@ private function buildBreadcrumbFromRequest(): void
$depth = 0;
if (count($segments) !== 0) {
- $this->breadcrumbItems()->add(
+ if (! in_array($segments[0], ['s-list', 's-show', 's-dashboard'])) {
+ return;
+ }
+
+ $this->breadcrumbItems->add(
(new BreadcrumbItem($segments[0], $segments[1]))->setDepth($depth++),
);
@@ -293,7 +457,7 @@ private function buildBreadcrumbFromRequest(): void
$segments = $segments->slice($instance !== null ? 2 : 1)->values();
- $this->breadcrumbItems()->add(
+ $this->breadcrumbItems->add(
(new BreadcrumbItem($type, $key))
->setDepth($depth++)
->setInstance($instance),
@@ -302,6 +466,14 @@ private function buildBreadcrumbFromRequest(): void
}
}
+ private function getFakeRequestSegmentsFor(BreadcrumbItem $item): Collection
+ {
+ return $this->breadcrumbItems()
+ ->slice(0, $item->depth + 1)
+ ->values()
+ ->flatMap(fn (BreadcrumbItem $item) => explode('/', $item->toUri()));
+ }
+
protected function getSegmentsFromRequest(): Collection
{
if ($this->forcedSegments) {
@@ -324,9 +496,16 @@ protected function getSegmentsFromRequest(): Collection
return collect(request()->segments())->skip(2)->values();
}
- public function forceRequestSegments(array|Collection $segments): void
+ public function forceRequestSegments(array|Collection $segments, ?Closure $callback = null): void
{
$this->breadcrumbItems = null;
$this->forcedSegments = collect($segments)->values();
+
+ if ($callback) {
+ $callback();
+
+ $this->breadcrumbItems = null;
+ $this->forcedSegments = null;
+ }
}
}
diff --git a/src/Http/Context/Util/BreadcrumbItem.php b/src/Http/Context/Util/BreadcrumbItem.php
index d3bc74c73..ae4e16cf9 100644
--- a/src/Http/Context/Util/BreadcrumbItem.php
+++ b/src/Http/Context/Util/BreadcrumbItem.php
@@ -4,6 +4,7 @@
use Code16\Sharp\Utils\Entities\SharpEntityManager;
use Code16\Sharp\Utils\Entities\ValueObjects\EntityKey;
+use InvalidArgumentException;
class BreadcrumbItem
{
@@ -16,6 +17,10 @@ public function __construct(string $type, string $key)
{
$this->type = $type;
$this->key = $key;
+
+ if (! in_array($type, ['s-list', 's-show', 's-form', 's-dashboard'])) {
+ throw new InvalidArgumentException("Invalid type [$type] for BreadcrumbItem");
+ }
}
public function setDepth(int $depth): self
diff --git a/src/Http/Middleware/HandleInertiaRequests.php b/src/Http/Middleware/HandleInertiaRequests.php
index 4ad29b6ca..62067be65 100644
--- a/src/Http/Middleware/HandleInertiaRequests.php
+++ b/src/Http/Middleware/HandleInertiaRequests.php
@@ -91,7 +91,7 @@ public function share(Request $request)
'sharp.auth.suggest_remember_me' => sharp()->config()->get('auth.suggest_remember_me'),
'sharp.custom_url_segment' => sharp()->config()->get('custom_url_segment'),
'sharp.display_sharp_version_in_title' => sharp()->config()->get('display_sharp_version_in_title'),
- 'sharp.display_breadcrumb' => sharp()->config()->get('display_breadcrumb'),
+ 'sharp.breadcrumb.display' => sharp()->config()->get('breadcrumb.display'),
'sharp.name' => sharp()->config()->get('name'),
'sharp.theme.logo_height' => sharp()->config()->get('theme.logo_height'),
],
diff --git a/src/Utils/Traits/HandleCustomBreadcrumb.php b/src/Utils/Traits/HandleCustomBreadcrumb.php
index f082201e2..53fa2309c 100644
--- a/src/Utils/Traits/HandleCustomBreadcrumb.php
+++ b/src/Utils/Traits/HandleCustomBreadcrumb.php
@@ -22,6 +22,11 @@ public function configureBreadcrumbCustomLabelAttribute(
return $this;
}
+ public function getBreadcrumbAttribute(): ?string
+ {
+ return $this->breadcrumbAttribute;
+ }
+
public function getBreadcrumbCustomLabel(array $data): ?string
{
if (! $this->breadcrumbAttribute) {
diff --git a/tests/Http/BreadcrumbTest.php b/tests/Http/BreadcrumbTest.php
index e4437f0d0..9ad4c9137 100644
--- a/tests/Http/BreadcrumbTest.php
+++ b/tests/Http/BreadcrumbTest.php
@@ -8,6 +8,7 @@
use Code16\Sharp\Tests\Fixtures\Sharp\PersonShow;
use Code16\Sharp\Utils\Entities\SharpEntityManager;
use Code16\Sharp\Utils\Fields\FieldsContainer;
+use Illuminate\Support\Facades\Cache;
use Inertia\Testing\AssertableInertia as Assert;
beforeEach(function () {
@@ -107,6 +108,152 @@
);
});
+it('load parent show labels when not cached', function () {
+ Cache::flush();
+
+ $show = new class() extends PersonShow
+ {
+ public static array $requestedIds = [];
+
+ public function buildShowConfig(): void
+ {
+ $this->configureBreadcrumbCustomLabelAttribute('name');
+ }
+
+ public function find($id): array
+ {
+ $id = (int) $id;
+ self::$requestedIds[] = $id;
+
+ return $this->transform([
+ 'id' => $id,
+ 'name' => $id === 1 ? 'Marie Curie' : 'Albert Einstein',
+ ]);
+ }
+ };
+
+ fakeShowFor('person', $show);
+
+ $this
+ ->get(
+ route('code16.sharp.show.show', [
+ 'parentUri' => 's-list/person/s-show/person/1',
+ 'person',
+ 2,
+ ])
+ )
+ ->assertOk()
+ ->assertInertia(fn (Assert $page) => $page
+ ->where('breadcrumb.items.1.label', 'Marie Curie')
+ ->where('breadcrumb.items.2.label', 'Albert Einstein')
+ );
+
+ expect($show::$requestedIds)->toContain(1);
+});
+
+it('set correct segment for loaded parent shows', function () {
+ Cache::flush();
+
+ $show = new class() extends PersonShow
+ {
+ public static array $breadcrumbPaths = [];
+
+ public function buildShowConfig(): void
+ {
+ $this->configureBreadcrumbCustomLabelAttribute('name');
+ }
+
+ public function find($id): array
+ {
+ $id = (int) $id;
+ self::$breadcrumbPaths[$id] = sharp()->context()->breadcrumb()->getCurrentPath();
+
+ return $this->transform([
+ 'id' => $id,
+ 'name' => $id === 1 ? 'Marie Curie' : 'Albert Einstein',
+ ]);
+ }
+ };
+
+ fakeShowFor('person', $show);
+
+ $this
+ ->get(
+ route('code16.sharp.show.show', [
+ 'parentUri' => 's-list/person/s-show/person/1',
+ 'person',
+ 2,
+ ])
+ )
+ ->assertOk()
+ ->assertInertia(fn (Assert $page) => $page
+ ->where('breadcrumb.items.1.entityKey', 'person')
+ ->where('breadcrumb.items.1.url', route('code16.sharp.show.show', [
+ 'parentUri' => 's-list/person',
+ 'person',
+ 1,
+ ]))
+ ->where('breadcrumb.items.2.entityKey', 'person')
+ ->where('breadcrumb.items.2.url', route('code16.sharp.show.show', [
+ 'parentUri' => 's-list/person/s-show/person/1',
+ 'person',
+ 2,
+ ]))
+ );
+
+ expect($show::$breadcrumbPaths)->toEqual([
+ 1 => 's-list/person/s-show/person/1',
+ 2 => 's-list/person/s-show/person/1/s-show/person/2',
+ ]);
+});
+
+it('does not load parent show labels when lazy loading', function () {
+ Cache::flush();
+ sharp()->config()->enableBreadcrumbLabelsLazyLoading();
+
+ app(SharpEntityManager::class)
+ ->entityFor('person')
+ ->setLabel('Scientist');
+
+ $show = new class() extends PersonShow
+ {
+ public static array $requestedIds = [];
+
+ public function buildShowConfig(): void
+ {
+ $this->configureBreadcrumbCustomLabelAttribute('name');
+ }
+
+ public function find($id): array
+ {
+ $id = (int) $id;
+ self::$requestedIds[] = $id;
+
+ return $this->transform([
+ 'id' => $id,
+ 'name' => $id === 1 ? 'Marie Curie' : 'Albert Einstein',
+ ]);
+ }
+ };
+
+ fakeShowFor('person', $show);
+
+ $this
+ ->get(
+ route('code16.sharp.show.show', [
+ 'parentUri' => 's-list/person/s-show/person/1',
+ 'person',
+ 2,
+ ])
+ )
+ ->assertOk()
+ ->assertInertia(fn (Assert $page) => $page
+ ->where('breadcrumb.items.1.label', 'Scientist')
+ );
+
+ expect($show::$requestedIds)->not->toContain(1);
+});
+
it('uses custom labels on show leaf if configured', function () {
fakeShowFor('person', new class() extends PersonShow
{
@@ -254,6 +401,44 @@ public function find($id): array
);
});
+it('uses custom labels limit on form leaf if configured', function () {
+ fakeFormFor('person', new class() extends PersonForm
+ {
+ public function buildFormFields(FieldsContainer $formFields): void
+ {
+ $formFields->addField(SharpFormEditorField::make('name'));
+ }
+
+ public function buildFormConfig(): void
+ {
+ $this->configureBreadcrumbCustomLabelAttribute('name', limit: 20);
+ }
+
+ public function find($id): array
+ {
+ return $this->transform([
+ 'id' => 1,
+ 'name' => 'A very long name that should be limited',
+ ]);
+ }
+ });
+
+ $this
+ ->get(
+ route('code16.sharp.form.edit', [
+ 'parentUri' => 's-list/person',
+ 'person',
+ 1,
+ ])
+ )
+ ->assertOk()
+ ->assertInertia(fn (Assert $page) => $page
+ ->where('breadcrumb.items.0.label', 'List')
+ // Data is not formatted for breadcrumb:
+ ->where('breadcrumb.items.1.label', 'Edit “A very long name tha...”')
+ );
+});
+
it('uses localized custom labels on form leaf if configured', function () {
fakeFormFor('person', new class() extends PersonForm
{
diff --git a/tests/Http/Form/FormControllerTest.php b/tests/Http/Form/FormControllerTest.php
index de49f84af..603297d00 100644
--- a/tests/Http/Form/FormControllerTest.php
+++ b/tests/Http/Form/FormControllerTest.php
@@ -21,6 +21,7 @@
use Code16\Sharp\Utils\Entities\SharpEntityManager;
use Code16\Sharp\Utils\Fields\FieldsContainer;
use Code16\Sharp\Utils\PageAlerts\PageAlert;
+use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Exceptions;
use Inertia\Testing\AssertableInertia as Assert;
@@ -544,6 +545,42 @@ public function find($id): array
);
});
+it('formats form title based on queried parent show breadcrumb', function () {
+ Cache::flush();
+
+ $show = new class() extends PersonShow
+ {
+ public static array $requestedIds = [];
+
+ public function buildShowConfig(): void
+ {
+ $this->configureBreadcrumbCustomLabelAttribute('name');
+ }
+
+ public function find($id): array
+ {
+ $id = (int) $id;
+ self::$requestedIds[] = $id;
+
+ return $this->transform([
+ 'id' => $id,
+ 'name' => 'Marie Curie',
+ ]);
+ }
+ };
+
+ fakeShowFor('person', $show);
+ fakeFormFor('person', new PersonForm());
+
+ $this->get('/sharp/root/s-list/person/s-show/person/1/s-form/person/1')
+ ->assertOk()
+ ->assertInertia(fn (Assert $page) => $page
+ ->where('form.title', 'Edit “Marie Curie”')
+ );
+
+ expect($show::$requestedIds)->toContain(1);
+});
+
it('formats form title based on configured breadcrumb attribute', function () {
fakeShowFor('person', new class() extends PersonShow
{
diff --git a/tests/Unit/Config/SharpConfigBuilderTest.php b/tests/Unit/Config/SharpConfigBuilderTest.php
index 37265138c..ca92c3cd1 100644
--- a/tests/Unit/Config/SharpConfigBuilderTest.php
+++ b/tests/Unit/Config/SharpConfigBuilderTest.php
@@ -38,3 +38,19 @@ class WithEntityKeyEntity extends SharpEntity
->toHaveKey('person')
->toHaveKey('single-person');
});
+
+it('allows to configure breadcrumb options', function () {
+ expect(sharp()->config()->get('breadcrumb.labels.lazy_loading'))->toBeFalse()
+ ->and(sharp()->config()->get('breadcrumb.labels.cache'))->toBeTrue()
+ ->and(sharp()->config()->get('breadcrumb.labels.cache_duration'))->toBe(30);
+
+ sharp()->config()->enableBreadcrumbLabelsLazyLoading();
+ sharp()->config()->configureBreadcrumbLabelsCache(false);
+
+ expect(sharp()->config()->get('breadcrumb.labels.lazy_loading'))->toBeTrue()
+ ->and(sharp()->config()->get('breadcrumb.labels.cache'))->toBeFalse();
+
+ sharp()->config()->configureBreadcrumbLabelsCache(duration: 40);
+
+ expect(sharp()->config()->get('breadcrumb.labels.cache_duration'))->toBe(40);
+});