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

Nested resources #12651

Merged
merged 15 commits into from
May 6, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@
@if (filled($activeManager) && isset($managers[$activeManager]))
<div
@if (count($managers) > 1)
id="relationManager{{ ucfirst($activeManager) }}"
role="tabpanel"
tabindex="0"
@endif
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public function getWidgetData(): array
return [
'activeTab' => $this->activeTab,
'paginators' => $this->paginators,
'parentRecord' => $this->parentRecord,
'tableColumnSearches' => $this->tableColumnSearches,
'tableFilters' => $this->tableFilters,
'tableGrouping' => $this->tableGrouping,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,9 @@ public function mountCanAuthorizeResourceAccess(): void
public static function authorizeResourceAccess(): void
{
abort_unless(static::getResource()::canAccess(), 403);

if ($parentResource = static::getParentResource()) {
abort_unless($parentResource::canAccess(), 403);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<?php

namespace Filament\Resources\Pages\Concerns;

use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Livewire\Attributes\Locked;

trait InteractsWithParentRecord
{
#[Locked]
public ?Model $parentRecord = null;

public function mountInteractsWithParentRecord(): void
{
$this->mountParentRecord();
}

public function mountParentRecord(): void
{
if ($this->parentRecord) {
return;
}

$parentResourceRegistration = static::getResource()::getParentResourceRegistration();

if (! $parentResourceRegistration) {
return;
}

$this->parentRecord = $this->resolveParentRecord(request()->route()->parameters());

$this->authorizeParentRecordAccess();
}

protected function authorizeParentRecordAccess(): void
{
abort_unless(static::getParentResource()::canView($this->getParentRecord()), 403);
}

/**
* @param array<string, mixed> $parameters
*/
protected function resolveParentRecord(array $parameters): Model
{
$modifyQuery = null;

$parentResourceRegistration = static::getResource()::getParentResourceRegistration();
$parentRecord = null;
$parentResourceRegistrations = [];

while ($parentResourceRegistration) {
$parentResourceRegistrations[] = $parentResourceRegistration;

$parentResourceRegistration = $parentResourceRegistration->getParentResource()::getParentResourceRegistration();
}

if (count($parentResourceRegistrations)) {
$parentResourceRegistrations = array_reverse($parentResourceRegistrations);
$parentRecord = null;
$previousParentResourceRegistration = null;

foreach ($parentResourceRegistrations as $parentResourceRegistration) {
$previousParentRecord = $parentRecord;

$parentResource = $parentResourceRegistration->getParentResource();
$parentRecord = $parentResource::resolveRecordRouteBinding(
$parentRecordKey = $parameters[
$parentResourceRegistration->getParentRouteParameterName()
] ?? null,
$modifyQuery,
);

if ($parentRecord === null) {
throw (new ModelNotFoundException())->setModel($parentResource::getModel(), [$parentRecordKey]);
}

if ($previousParentRecord) {
$parentRecord->setRelation(
$previousParentResourceRegistration->getInverseRelationshipName(),
$previousParentRecord,
);
}

$modifyQuery = fn (Builder $query) => $parentResourceRegistration->getChildResource()::scopeEloquentQueryToParent($query, $parentRecord);

$previousParentResourceRegistration = $parentResourceRegistration;
}
}

return $parentRecord;
}

public function getParentRecord(): ?Model
{
return $this->parentRecord;
}

public function getParentRecordTitle(): string | Htmlable | null
{
$resource = static::getParentResource();

if (! $resource::hasRecordTitle()) {
return $resource::getTitleCaseModelLabel();
}

return $resource::getRecordTitle($this->getParentRecord());
}

public static function getParentResource(): ?string
{
return static::getResource()::getParentResourceRegistration()?->getParentResource();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Filament\Actions\Action;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Livewire\Attributes\Locked;
Expand All @@ -20,12 +21,25 @@ public function mountCanAuthorizeAccess(): void

protected function resolveRecord(int | string $key): Model
{
$record = static::getResource()::resolveRecordRouteBinding($key);
$this->mountParentRecord();

$parentRecord = $this->getParentRecord();
$modifyQuery = null;

if ($parentRecord) {
$modifyQuery = fn (Builder $query) => static::getResource()::scopeEloquentQueryToParent($query, $parentRecord);
}

$record = static::getResource()::resolveRecordRouteBinding($key, $modifyQuery);

if ($record === null) {
throw (new ModelNotFoundException())->setModel($this->getModel(), [$key]);
}

if ($parentRecord) {
$record->setRelation(static::getResource()::getParentResourceRegistration()->getInverseRelationshipName(), $parentRecord);
}

return $record;
}

Expand All @@ -50,22 +64,19 @@ public function getRecordTitle(): string | Htmlable
*/
public function getBreadcrumbs(): array
{
$resource = static::getResource();

$breadcrumbs = [
$resource::getUrl() => $resource::getBreadcrumb(),
];
$breadcrumbs = parent::getBreadcrumbs();

$resource = static::getResource();
$record = $this->getRecord();

if ($record->exists && $resource::hasRecordTitle()) {
if ($resource::hasPage('view') && $resource::canView($record)) {
$breadcrumbs[
$resource::getUrl('view', ['record' => $record])
$this->getResourceUrl('view')
] = $this->getRecordTitle();
} elseif ($resource::hasPage('edit') && $resource::canEdit($record)) {
$breadcrumbs[
$resource::getUrl('edit', ['record' => $record])
$this->getResourceUrl('edit')
] = $this->getRecordTitle();
} else {
$breadcrumbs[] = $this->getRecordTitle();
Expand All @@ -74,10 +85,6 @@ public function getBreadcrumbs(): array

$breadcrumbs[] = $this->getBreadcrumb();

if (filled($cluster = static::getCluster())) {
return $cluster::unshiftClusterBreadcrumbs($breadcrumbs);
}

return $breadcrumbs;
}

Expand Down
17 changes: 13 additions & 4 deletions packages/panels/src/Resources/Pages/CreateRecord.php
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,10 @@ protected function handleRecordCreation(array $data): Model
{
$record = new ($this->getModel())($data);

if ($parentRecord = $this->getParentRecord()) {
return $this->associateRecordWithParent($record, $parentRecord);
}

if (
static::getResource()::isScopedToTenant() &&
($tenant = Filament::getTenant())
Expand All @@ -191,6 +195,11 @@ protected function associateRecordWithTenant(Model $record, Model $tenant): Mode
return $relationship->save($record);
}

protected function associateRecordWithParent(Model $record, Model $parent): Model
{
return static::getResource()::getParentResourceRegistration()->getRelationship($parent)->save($record);
}

/**
* @param array<string, mixed> $data
* @return array<string, mixed>
Expand Down Expand Up @@ -238,7 +247,7 @@ protected function getCancelFormAction(): Action
{
return Action::make('cancel')
->label(__('filament-panels::resources/pages/create-record.form.actions.cancel.label'))
->alpineClickHandler('document.referrer ? window.history.back() : (window.location.href = ' . Js::from($this->previousUrl ?? static::getResource()::getUrl()) . ')')
->alpineClickHandler('document.referrer ? window.history.back() : (window.location.href = ' . Js::from($this->previousUrl ?? $this->getResourceUrl()) . ')')
->color('gray');
}

Expand Down Expand Up @@ -280,14 +289,14 @@ protected function getRedirectUrl(): string
$resource = static::getResource();

if ($resource::hasPage('view') && $resource::canView($this->getRecord())) {
return $resource::getUrl('view', ['record' => $this->getRecord(), ...$this->getRedirectUrlParameters()]);
return $this->getResourceUrl('view', $this->getRedirectUrlParameters());
}

if ($resource::hasPage('edit') && $resource::canEdit($this->getRecord())) {
return $resource::getUrl('edit', ['record' => $this->getRecord(), ...$this->getRedirectUrlParameters()]);
return $this->getResourceUrl('edit', $this->getRedirectUrlParameters());
}

return $resource::getUrl('index');
return $this->getResourceUrl();
}

/**
Expand Down
8 changes: 4 additions & 4 deletions packages/panels/src/Resources/Pages/EditRecord.php
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ protected function configureViewAction(ViewAction $action): void
->form(fn (Schema $form): Schema => static::getResource()::form($form));

if ($resource::hasPage('view')) {
$action->url(fn (): string => static::getResource()::getUrl('view', ['record' => $this->getRecord()]));
$action->url(fn (): string => $this->getResourceUrl('view'));
}
}

Expand All @@ -294,7 +294,7 @@ protected function configureForceDeleteAction(ForceDeleteAction $action): void

$action
->authorize($resource::canForceDelete($this->getRecord()))
->successRedirectUrl($resource::getUrl('index'));
->successRedirectUrl($this->getResourceUrl());
}

protected function configureReplicateAction(ReplicateAction $action): void
Expand All @@ -315,7 +315,7 @@ protected function configureDeleteAction(DeleteAction $action): void

$action
->authorize($resource::canDelete($this->getRecord()))
->successRedirectUrl($resource::getUrl('index'));
->successRedirectUrl($this->getResourceUrl());
}

public function getTitle(): string | Htmlable
Expand Down Expand Up @@ -357,7 +357,7 @@ protected function getCancelFormAction(): Action
{
return Action::make('cancel')
->label(__('filament-panels::resources/pages/edit-record.form.actions.cancel.label'))
->alpineClickHandler('document.referrer ? window.history.back() : (window.location.href = ' . Js::from($this->previousUrl ?? static::getResource()::getUrl()) . ')')
->alpineClickHandler('document.referrer ? window.history.back() : (window.location.href = ' . Js::from($this->previousUrl ?? $this->getResourceUrl()) . ')')
->color('gray');
}

Expand Down
18 changes: 13 additions & 5 deletions packages/panels/src/Resources/Pages/ListRecords.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,12 +124,14 @@ protected function configureCreateAction(CreateAction $action): void
->modelLabel($this->getModelLabel() ?? static::getResource()::getModelLabel())
->schema(fn (Schema $schema): Schema => $this->form($schema->columns(2)));

if (($action instanceof CreateAction) && static::getResource()::isScopedToTenant()) {
if ($parentRecord = $this->getParentRecord()) {
$action->relationship(fn (): Relation => $resource::getParentResourceRegistration()->getRelationship($parentRecord));
} elseif (static::getResource()::isScopedToTenant()) {
$action->relationship(($tenant = Filament::getTenant()) ? fn (): Relation => static::getResource()::getTenantRelationship($tenant) : null);
}

if ($resource::hasPage('create')) {
$action->url(fn (): string => $resource::getUrl('create'));
$action->url(fn (): string => $this->getResourceUrl('create'));
}
}

Expand Down Expand Up @@ -164,7 +166,7 @@ protected function configureEditAction(EditAction $action): void
->icon(FilamentIcon::resolve('actions::edit-action') ?? 'heroicon-m-pencil-square');

if ($resource::hasPage('edit')) {
$action->url(fn (Model $record): string => $resource::getUrl('edit', ['record' => $record]));
$action->url(fn (Model $record): string => $this->getResourceUrl('edit', ['record' => $record]));
}
}

Expand Down Expand Up @@ -199,7 +201,7 @@ protected function configureViewAction(ViewAction $action): void
->schema(fn (Schema $schema): Schema => $this->infolist($this->form($schema->columns(2))));

if ($resource::hasPage('view')) {
$action->url(fn (Model $record): string => $resource::getUrl('view', ['record' => $record]));
$action->url(fn (Model $record): string => $this->getResourceUrl('view', ['record' => $record]));
}
}

Expand Down Expand Up @@ -256,6 +258,12 @@ protected function makeTable(): Table
{
$table = $this->makeBaseTable()
->query(fn (): Builder => $this->getTableQuery())
->when(
$this->getParentRecord(),
fn (Table $table, Model $parentRecord): Table => $table->modifyQueryUsing(
fn (Builder $query) => static::getResource()::scopeEloquentQueryToParent($query, $parentRecord),
),
)
->modifyQueryUsing($this->modifyQueryWithActiveTab(...))
->when($this->getModelLabel(), fn (Table $table, string $modelLabel): Table => $table->modelLabel($modelLabel))
->when($this->getPluralModelLabel(), fn (Table $table, string $pluralModelLabel): Table => $table->pluralModelLabel($pluralModelLabel))
Expand Down Expand Up @@ -320,7 +328,7 @@ protected function makeTable(): Table
continue;
}

return $resource::getUrl($action, ['record' => $record]);
return $this->getResourceUrl($action, ['record' => $record]);
}

return null;
Expand Down
8 changes: 2 additions & 6 deletions packages/panels/src/Resources/Pages/ManageRecords.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,8 @@

class ManageRecords extends ListRecords
{
public function getBreadcrumbs(): array
public function hasResourceBreadcrumbs(): bool
{
if (filled($cluster = static::getCluster())) {
return $cluster::unshiftClusterBreadcrumbs([]);
}

return [];
return false;
}
}
Loading
Loading