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

Do not pass anchor as a parameter to the route. #28

Merged
merged 13 commits into from
Dec 27, 2024
Merged
30 changes: 30 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,36 @@ If you want to add a "tel:" option in the link picker, you need to add a route l
LinkCollection::addTelephoneLink();
```

## Adding the 'anchor' link

If you want to add an "anchor" option in the link picker, you need to add a route like this, in your `AppServiceProvider`:

```php
LinkCollection::addAnchorLink();
```

Or change some of the default values:
```php
LinkCollection::addAnchorLink(
routeName: 'anchor',
group: 'General',
label: 'Anchor link',
description: 'Link to achor on current page',
);
```

This will fetch the [Architect](https://github.com/codedor/filament-architect) fields from the current page and use them as options for the anchor link. If you want to modify this behavior, you can add a `anchorList` method to your model:

```php
public function anchorList(): array
{
return [
'first-section' => 'First section',
'second-section' => 'Second section',
];
}
```

## The Link object

You can pass a callback to the `linkPicker()` function, this callback has one parameter called `$link`, this is a `Codedor\LinkPicker\Link` object. With this object you can configure the link to your needs.
Expand Down
8 changes: 8 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,11 @@ parameters:
message: "#^Access to an undefined property Livewire\\\\Component\\:\\:\\$mountedFormComponentActionsData\\.$#"
count: 1
path: src/Filament/LinkPickerInput.php
-
message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:getTranslation\\(\\)\\.$#"
count: 1
path: src/Filament/LinkPickerInput.php
-
message: "#^Call to an undefined method Illuminate\\\\Support\\\\HigherOrderCollectionProxy\\<string, Codedor\\\\FilamentArchitect\\\\Engines\\\\Architect, Illuminate\\\\Support\\\\Collection\\<\\(int\\|string\\)\\, Codedor\\\\FilamentArchitect\\\\Engines\\\\Architect\\>\\>\\:\\:anchorList\\(\\)\\.$#"
count: 1
path: src/LinkCollection.php
85 changes: 63 additions & 22 deletions src/Filament/LinkPickerInput.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
use Filament\Forms\Get;
use Filament\Forms\Set;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Routing\Route;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Optional;
use Illuminate\Support\Reflector;
use Illuminate\Support\Str;
use ReflectionParameter;
Expand Down Expand Up @@ -49,18 +51,19 @@ protected function setUp(): void

$actionNestingIndex = array_key_last($livewire->mountedFormComponentActions);

$schema->each(function (Field $field) use (&$state, $statePath, $get, $actionNestingIndex, $livewire) {
$fieldStatePath = $field->statePath;
$schema
->each(function (Field $field) use (&$state, $statePath, $get, $actionNestingIndex, $livewire) {
$fieldStatePath = $field->statePath;

data_fill(
$state,
$fieldStatePath,
data_get(
$livewire->mountedFormComponentActionsData[$actionNestingIndex] ?? [],
"{$statePath}.{$fieldStatePath}"
) ?? $get("{$statePath}.{$fieldStatePath}") ?? null
);
});
data_fill(
$state,
$fieldStatePath,
data_get(
$livewire->mountedFormComponentActionsData[$actionNestingIndex] ?? [],
"{$statePath}.{$fieldStatePath}"
) ?? $get("{$statePath}.{$fieldStatePath}") ?? null
);
});

return $state;
})
Expand Down Expand Up @@ -177,18 +180,11 @@ private function getFormSchemaForRoute(?string $selectedRoute): Collection

$schema = $link->getSchema();

$routeParameters = $this->routeParameters($link->getRoute());

// If the schema is empty, we'll check if there are any parameters
if ($schema->isEmpty()) {
$route = $link->getRoute();

$schema = collect($route->signatureParameters())
->filter(function (ReflectionParameter $parameter) {
$className = Reflector::getParameterClassName($parameter);

return $parameter->getType()
&& class_exists($className)
&& is_subclass_of($className, Model::class);
})
$schema = $routeParameters
->map(function (ReflectionParameter $parameter) {
$model = Reflector::getParameterClassName($parameter);

Expand All @@ -198,19 +194,64 @@ private function getFormSchemaForRoute(?string $selectedRoute): Collection
->searchable()
->options($model::query()
->when(method_exists($model, 'linkPickerParameterQuery'), fn ($query) => $model::linkPickerParameterQuery($query))
->withoutGlobalScopes()
->pluck(
$model::$linkPickerTitleField ?? 'id',
(new $model)->getKeyName(),
)
);
)
->live();
});
}

if ($anchorData = $link->getWithAnchors()) {
if (! data_get($anchorData, 'parameter')) {
$anchorData['parameter'] = $routeParameters->first()->name;
}

if (! data_get($anchorData, 'model')) {
$anchorData['model'] = Reflector::getParameterClassName($routeParameters->first());
}

$schema->add(
Select::make('parameters.anchor')
->hidden(fn (Get $get) => ! $get("parameters.{$anchorData['parameter']}"))
->options(function (Get $get) use ($anchorData) {
/**
* @var Model $record
*/
$record = $anchorData['model']::find($get("parameters.{$anchorData['parameter']}"));

if (method_exists($record, 'isTranslatableAttribute') && $record->isTranslatableAttribute($anchorData['field'])) {
return optional($record->getTranslation($anchorData['field'], referer_locale()))->anchorList();
}

return $record->{$anchorData['field']}->anchorList();
})
);
}

return $schema
->prepend($routeField)
->add(
Checkbox::make('newTab')
->label(__('filament-link-picker::input.new tab label'))
);
}

protected function routeParameters(Optional|Route $route): Collection
{
if ($route instanceof Optional) {
return collect();
}

return collect($route->signatureParameters())
->filter(function (ReflectionParameter $parameter) {
$className = Reflector::getParameterClassName($parameter);

return $parameter->getType()
&& class_exists($className)
&& is_subclass_of($className, Model::class);
});
}
}
21 changes: 21 additions & 0 deletions src/Link.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ class Link

protected array $parameters = [];

protected ?array $withAnchors = null;

public function __construct(
protected string $routeName,
protected ?string $label = null,
Expand Down Expand Up @@ -87,6 +89,22 @@ public function getSchema(): Collection
));
}

public function withAnchors(string $field = 'body', ?string $model = null, ?string $parameter = null)
{
$this->withAnchors = [
'field' => $field,
'model' => $model,
'parameter' => $parameter,
];

return $this;
}

public function getWithAnchors(): ?array
{
return $this->withAnchors;
}

public function group(string $group): self
{
$this->group = $group;
Expand Down Expand Up @@ -184,6 +202,9 @@ public function resolveParameters(array $parameters)
{
$route = $this->getRoute();

if (isset($parameters['anchor'])) {
unset($parameters['anchor']);
}
$route->parameters = $parameters;
$bindings = $route->bindingFields();

Expand Down
80 changes: 80 additions & 0 deletions src/LinkCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,17 @@

namespace Codedor\LinkPicker;

use Codedor\FilamentArchitect\Engines\Architect;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Resources\Pages\Page;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Str;
use Throwable;

/**
* @template TKey of array-key
Expand Down Expand Up @@ -102,6 +109,79 @@ public function addTelephoneLink(
);
}

public function addAnchorLink(
string $routeName = 'anchor',
string $group = 'General',
string $label = 'Anchor link',
string $description = 'Link to achor on current page',
): self {
return $this->addLink(
Link::make($routeName, $label)
->group($group)
->description($description)
->schema(function () {
return Select::make('anchor')
->label('Anchor')
->options(function (?Model $record) {
$request = Request::create(request()->header('referer'));

$locale = $request->query('locale');

if (filled($locale)) {
$locale = Str::of($locale)
->before('-tab')
->after('-')
->toString();
}

if (! $record) {
try {
$route = Route::getRoutes()->match($request);

/** @var Page $component */
$component = Str::replace('@__invoke', '', $route->action['uses']);

$resource = $component::getResource();
$model = $resource::getModel();

$record = $model::find($route->parameter('record'));
} catch (Throwable $e) {
return [];
}
}

if (! $record) {
return [];
}

if (method_exists($record, 'anchorList')) {
return $record->anchorList();
}

if (class_exists(Architect::class)) {
if (method_exists($record, 'setLocale')) {
$record->setLocale($locale);
}

return collect($record?->getFillable())
->map(fn ($field) => $record->getAttributeValue($field))
->filter(fn ($value) => $value instanceof Architect)
->map->anchorList()
->flatMap(fn ($values) => $values);
}

return [];
})
->required();
})
->buildUsing(function (Link $link) {
$anchor = $link->getParameter('anchor');

return "#{$anchor}";
})
);
}

/**
* Returns a flattened collection of all routes.
*
Expand Down
11 changes: 11 additions & 0 deletions src/helpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,14 @@ function ($matches) {
);
}
}

if (! function_exists('referer_locale')) {
function referer_locale(): ?string
{
$referer = request()->headers->get('referer');

preg_match('/locale=-(.*)-tab/', $referer, $matches);

return $matches[1] ?? null;
}
}