mokhosh / filament-kanban Goto Github PK
View Code? Open in Web Editor NEWAdd kanban boards to your Filament pages
Home Page: https://filamentphp.com/plugins/mokhosh-kanban
License: MIT License
Add kanban boards to your Filament pages
Home Page: https://filamentphp.com/plugins/mokhosh-kanban
License: MIT License
I'm looking to be able to dynamically show a Kanban board per resource record. Take for instance the following example:
A Campaign
model has a belongsToMany
relationship to an Account
model.
A Campaign
also has a hasMany
relationship to a CampaignStage
model.
The flow from a user would be that they create a campaign and attach accounts to the campaign. An account could potentially be added to many other campaigns. The user would also add a list of stages to each campaign and therefore each campaign may have a different set of stages (statuses).
On the API side, I would like to add a page to the CampaignResource::class
and add the page to the getPages()
method which would allow the page to receive the record via the InteractsWithRecords
trait. When a user clicks the table record on the list page, they would be linked to the kanban board with the record passed in the route.
Also, since the campaigns and accounts are a many-to-many relationship, the status column cannot exist on the records (in this case the accounts table) and must instead be on the pivot table.
I tried my best to work around the package as it currently stands.
One suggestion you had made in #10 is to use a query string. The problem here is that Livewire can only get the request on mount, and not subsequent requests. Therefore the query string is null on subsequent requests.
This is my code:
Page
class CampaignKanbanBoard extends KanbanBoard
{
protected static string $model = Account::class;
protected static ?string $slug = '/campaigns/board';
protected static string $recordTitleAttribute = 'name';
protected Campaign $campaign;
public function boot(): void
{
$this->campaign = Campaign::findOrFail(request()->query('id'));
}
public function getHeading(): string|Htmlable
{
return $this->campaign->name;
}
public function onStatusChanged(int $recordId, string $status, array $fromOrderedIds, array $toOrderedIds): void
{
ray($recordId, $status, $fromOrderedIds, $toOrderedIds);
}
public function onSortChanged(int $recordId, string $status, array $orderedIds): void
{
ray($recordId, $status, $orderedIds);
}
protected function records(): Collection
{
return $this->campaign->accounts()->with('campaigns.stages')->get();
}
protected function statuses(): Collection
{
$new = ['id' => 0, 'title' => 'New'];
return $this->campaign->stages->map(function ($stage) {
return ['id' => $stage->id, 'title' => $stage->name];
})->prepend($new);
}
protected function filterRecordsByStatus(Collection $records, array $status): array
{
return $records->where('pivot.campaign_stage_id', '=', $status['id'])->all();
}
}
Models
class Campaign extends Model
{
public function stages(): HasMany
{
return $this->hasMany(CampaignStage::class);
}
public function accounts(): BelongsToMany
{
return $this->belongsToMany(Account::class, 'crm.account_campaign')->withPivot('campaign_stage_id')->withTimestamps();
}
}
class Account extends Model
{
public function campaigns(): BelongsToMany
{
return $this->belongsToMany(Campaign::class, 'crm.account_campaign')->withPivot('campaign_stage_id')->withTimestamps();
}
}
class CampaignStage extends Model implements Sortable
{
use SortableTrait;
public function campaign(): BelongsTo
{
return $this->belongsTo(Campaign::class);
}
}
Migrations
public function up(): void
{
Schema::create('campaigns', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->timestamps();
});
Schema::create('campaign_stages', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->smallInteger('sort_order');
$table->foreignIdFor(Campaign::class)->constrained();
$table->timestamps();
});
Schema::create('account_campaign', function (Blueprint $table) {
$table->id();
$table->smallInteger('sort_order');
$table->foreignIdFor(CampaignStage::class)->constrained();
$table->foreignIdFor(Account::class)->constrained()->cascadeOnDelete();
$table->foreignIdFor(Campaign::class)->constrained()->cascadeOnDelete();
$table->timestamps();
});
}
Would be awesome to be able to add actions(e.g. tableactions) to the View
This way would be maybe doable to add comment functionality through a package like this
https://github.com/parallax/filament-comments
I tried to make it working, but am failing to implement it in a clean way..
2.6.1
8.3
10.0
No response
No response
No response
When i drag to other status or edit modal, the view break, but the functionality okay
already npm run dev
Did i miss something?
<?php
namespace App\Filament\Pages;
use Mokhosh\FilamentKanban\Pages\KanbanBoard;
use App\Models\Lead;
use App\Models\Funnel;
use Illuminate\Support\Collection;
use Filament\Forms;
class DentalKanbanBoard extends KanbanBoard
{
protected static string $model = Lead::class;
// protected static string $statusEnum = ModelStatus::class;
protected static string $recordTitleAttribute = 'sales_id';
protected static string $recordStatusAttribute = 'funnel_id';
protected function statuses(): Collection
{
$funnels = Funnel::where('division_id', 1)->get();
return $funnels->map(function ($funnel) {
return [
'id' => $funnel->id,
'title' => $funnel->title,
];
});
}
protected function getEditModalFormSchema(?int $recordId): array
{
return [
Forms\Components\TextInput::make('sales_id')->required(),
];
}
}
^2.7
8.2
10
macOS
No response
No response
I have Tickets in My Project which has status I would Like to use public static ?string $slug = 'kanban/{project}';
So that I can identify the for which Project the user is seeing the Kanban I get the all the tickets for it?
I have These Models:
Status: HasMany tickets | BelongsTo projects
Ticket: BelongsTo status | BelongsTo projects
Project: HasMany tickets
I'm also trying to achieve this by following your video:
Kanban Page:
<?php
namespace App\Filament\Pages;
use App\Models\Ticket;
use App\Models\TicketStatus;
use Illuminate\Support\Collection;
use Mokhosh\FilamentKanban\Pages\KanbanBoard;
class Kanban extends KanbanBoard
{
protected static string $recordTitleAttribute = 'name';
protected static string $recordStatusAttribute = 'status_id';
protected function records(): Collection
{
return Ticket::where('project_id', 1)->get();
}
protected function statuses(): Collection
{
return TicketStatus::all()->pluck('name', 'id');
}
}
Error:
Mokhosh\FilamentKanban\Pages\KanbanBoard::filterRecordsByStatus(): Argument #2 ($status) must be of type array, string given, called in /home/abbasmashaddy72/Documents/Sites/Dynamic/manage-project/vendor/mokhosh/filament-kanban/src/Pages/KanbanBoard.php on line 54
3.0
PHP 8.2
v10.10
I would like to add this type of Form, using Schema How can achieve it?
Sometime having issue Unable to if it is done: as you see in the first Part of Video
None
2.2
8.2
10.10
No response
No response
No response
I can't get it working right.
Installed package.
Run: php artisan filament-kanban:install
Doesn't seem like it published any files.
Followed the example.
I see there is an app/Providers/Filament/AdminPanelProvider for example.
copied in my own project. Doesn't seems to do anything
Registered the Provider in: /bootstrap/providers.php
Seems to work.
Go to /admin/login no css/js loaded.
I think the package does not work well with the new structure of Laravel
Fresh install laravel 11
2.7.0
8.3.2
11.1.1
Windows
No response
No response
On the smallest breakpoint, you cannot see any boards past the first whether you scroll vertically or horizonally.
Here is a gif:
Try scrolling to other boards when on the smallest breakpoint.
2
8.2
10
macOS
Chrome
No response
Hi,
Thx for the awesome plugin, the title say everything, when i have more than two status, the last (if there three) or the two last (if there four or more) don't allow to move what inside or move from other status to those who are blocked.
the only thing i change is the design of the kaban-record.blade.php:
<div
id="{{ $record['id'] }}" class="kanban"
wire:click="recordClicked('{{ $record['id'] }}', {{ @json_encode($record) }})"
{{--class="record transition bg-white dark:bg-gray-700 rounded-lg px-4 py-2 cursor-grab font-medium text-gray-600 dark:text-gray-200"--}}
@if($record['just_updated'])
x-data
x-init="
$el.classList.add('animate-pulse-twice', 'bg-primary-100', 'dark:bg-primary-800')
$el.classList.remove('bg-white', 'dark:bg-gray-700')
setTimeout(() => {
$el.classList.remove('bg-primary-100', 'dark:bg-primary-800')
$el.classList.add('bg-white', 'dark:bg-gray-700')
}, 3000)
"
@endif
>
<x-filament::section>
<table class="bg-re">
<tr>
<td rowspan="2">
<x-filament::dropdown>
<x-slot name="trigger">
<x-filament::icon-button
icon="heroicon-o-phone"
/>
</x-slot>
<x-filament::dropdown.list>
@foreach($record["phones"] as $tel)
<x-filament::dropdown.list.item
href="tel:{{ $tel->phone_number}}"
tag="a">
{{ $tel->phone_number}}
<br>
<span class="text-xs text-gray-900 italic">
{{ $tel->phone_type}}
</span>
</x-filament::dropdown.list.item>
@endforeach
</x-filament::dropdown.list>
</x-filament::dropdown>
<x-filament::dropdown>
<x-slot name="trigger">
<x-filament::icon-button
icon="fluentui-mail-add-20-o"
/>
</x-slot>
<x-filament::dropdown.list>
@foreach($record["emails"] as $mail)
<x-filament::dropdown.list.item
href="mailto:{{ $mail->email}}"
tag="a">
{{ $mail->email}}
</x-filament::dropdown.list.item>
@endforeach
</x-filament::dropdown.list>
</x-filament::dropdown>
<x-filament::dropdown>
<x-slot name="trigger">
<x-filament::icon-button
icon="mdi-google-maps"
/>
</x-slot>
<x-filament::dropdown.list>
@foreach($record["adresses"] as $adresse)
@if(!empty($adresse->adresse_ligne_2) && !is_null($adresse->adresse_ligne_2))
@php($adresseToLink = $adresse->adresse_ligne_1."+".$adresse->adresse_ligne_2."+".$adresse->adresse_cp."+".$adresse->adresse_ville."+".$adresse->adresse_pays)
@else
@php($adresseToLink = $adresse->adresse_ligne_1."+".$adresse->adresse_cp."+".$adresse->adresse_ville."+".$adresse->adresse_pays)
@endif
<x-filament::dropdown.list.item
href="https://www.google.fr/maps/place/{{ $adresseToLink }}"
target="_blank"
tag="a">
{{ $adresse->adresse_ligne_1}}
<br>
@if(!empty($adresse->adresse_ligne_2) && !is_null($adresse->adresse_ligne_2))
{{ $adresse->adresse_ligne_2}}
<br>
@endif
{{ $adresse->adresse_cp }} {{$adresse->adresse_ville}}
<br>
{{$adresse->adresse_pays}}
</x-filament::dropdown.list.item>
@endforeach
</x-filament::dropdown.list>
</x-filament::dropdown>
<x-filament::dropdown>
<x-slot name="trigger">
<x-filament::icon-button
icon="heroicon-o-ellipsis-vertical"
/>
</x-slot>
<x-filament::dropdown.list>
<x-filament::dropdown.list.item icon="heroicon-s-eye"
href="{{route('filament.admin.resources.leads.view',['record'=>$record['id']])}}"
target="_blank"
tag="a">
{{__("View")}}
</x-filament::dropdown.list.item>
<x-filament::dropdown.list.item icon="heroicon-o-pencil-square"
href="{{route('filament.admin.resources.leads.edit',['record'=>$record['id']])}}"
target="_blank"
tag="a">
{{__("Edit")}}
</x-filament::dropdown.list.item>
</x-filament::dropdown.list>
</x-filament::dropdown>
</td>
<td>
<span class="text-gray-600 text-left">
{{ ucfirst($record['title']) }}
</span>
</td>
</tr>
<tr>
<td>
<span class="text-sm text-gray-900 text-left italic">
{!! $record["leads_description"] !!}
</span>
</td>
</tr>
</table>
</x-filament::section>
</div>
If you need i can provide an access to my online production test server to test it.
Just add more than two status to your application
1.11.0
8.3.0
10.45.0
macOS, Windows, Linux
Chrome
No response
i do what exacly in video but not showing any record
thx u
im using version 2.2 and filament 3.25
2.0
8.3
10
Linux
Chrome
No response
> @vutasoftvn have you watched the video tutorials? https://github.com/mokhosh/filament-kanban#video-tutorial
How to customize icon for view? Thank you!
Originally posted by @vutasoftvn in #6 (comment)
Hi, this is not a bug rather a question, on how I can possibly add a limit. Right now, My cards in done panel are more than 20. I want to only display 10 of it per panel. Are there any work arounds?
none
2
8.2
10
No response
No response
No response
Manage team relationship members is not working when using an select.
Call to a member function isRelation() on null
public static function getSchema(): array
{
return [
TextInput::make('title')
->required(),
Textarea::make('description')
->autosize(),
Toggle::make('urgent'),
TextInput::make('progress')->numeric(),
Select::make('team')
->relationship('team', 'name')
->multiple()
->options(Filament::getTenant()->allUsers()->pluck('name', 'id'))
->searchable(['name', 'email'])
->preload()
];
}
<?php
namespace App\Filament\Pages;
use App\Enums\TaskStatus;
use App\Models\Task;
use Filament\Actions\CreateAction;
use Illuminate\Database\Eloquent\Model;
use Mokhosh\FilamentKanban\Pages\KanbanBoard;
class TasksKanbanBoard extends KanbanBoard
{
public bool $disableEditModal = false;
protected static ?string $title = 'Tasks';
protected static ?string $slug = 'tasks';
protected static string $model = Task::class;
protected static string $statusEnum = TaskStatus::class;
protected static string $headerView = 'tasks-kanban.kanban-header';
protected static string $recordView = 'tasks-kanban.kanban-record';
protected static string $statusView = 'tasks-kanban.kanban-status';
public function mount(): void
{
$this->form->model(static::$model);
}
public function getHeaderActions(): array
{
return [
CreateAction::make()
->model(self::$model)
->form(self::$model::getSchema())
->mutateFormDataUsing(fn ($data) => array_merge($data, [
'user_id' => auth()->id(),
])),
];
}
protected function getEditModalFormSchema(?int $recordId): array
{
return self::$model::getSchema();
}
protected function getModel(int $recordId): Model
{
return self::$model::findOrFail($recordId);
}
protected function getEditModalRecordData(int $recordId, array $data): array
{
return $this->getModel($recordId)->toArray();
}
protected function editRecord(int $recordId, array $data, array $state): void
{
static::$model::find($recordId)->update($data);
}
}
2.4.0
8.3
10
Linux
Chrome
No response
Hi Mokhosh!!
I am using ULID in the model, so when I try to drag the item to another status or edit it, an error occurs...
I found that the problem is that all declarations of the $recordId parameter were defined as integer
int $recordId
This way I found all the declarations and changed it to:
int|string $recordId
The changed methods were in the following files:
"src/Concerns/HasStatusChange.php"
"src/Concerns/HasEditRecordModal.php"
In this second file, we have the variable
public ?int $editModalRecordId = null;
Changed to:
public null|int|string $editModalRecordId = null;
And the method:
getEditModalFormSchema(?int $recordId)
Changed to
getEditModalFormSchema(null|int|string $recordId)
Important:
I also had to change the function calls:
onStatusChanged(int|string $recordId, ...)
getEditModalFormSchema(null|int|string $recordId, ...)
In the page generated through the "make" command.
Reminder:
It is necessary to change the test files and functions generated in the "make" command
Well, I believe that as the use of UUID or ULID is common, this correction is interesting...
Use UUID or ULID as record IDs. Then try to change them...
v2.3.0
8.1.10
10.10
Windows
Chrome
No response
Using a content security policy, any <script> tags need to also provide a nonce in the tag. It's created with the built-in csp_nonce() function. I found where your style sheet is added with Css::make() but it doesn't look like there's any provision for a nonce there.
Is this just a current limitation of Filament? Do you think there are any easy alternatives for your package if so?
Thank you!
You need to have an active content security policy in place (which is pretty easy to do with Spatie's CSP package).
1.10
8.3
10.48
No response
No response
No response
Hello Mokhosh!!
Congratulations on the work, really good and simple to implement in the project.
I have a Model where the "Status" field is an integer. I use an Enum (int) to define the values. Even so, I configured it and, initially, everything worked normally. However, I identified and fixed some problems:
1º - Status Title:
I saw that it shows the status title based on the Enum value, as in my case it is an integer, it shows the ID.
Correction: Observing the "IsKanbanStatus" trait, I saw that it was enough to implement the getTitle() method passing the titles, which shows the Status correctly... See the code below...
enum statusEnum:int implements HasLabel
{
use IsKanbanStatus;
case TODO = 0;
case DOING = 1;
case DONE = 2;
public function getLabel(): ?string {
return match ($this) {
self::TODO => 'To-Do',
self::DOING => 'In Progress',
self::DONE => 'Done',
};
}
public function getTitle(): ?string {
return $this->getLabel();
}
}
2º - Conflict between Status and Record
When I tried to drag an item to another status, I saw that I could drag internal elements from Records separately. I was able to drag a div from a record into another record or to another status and, obviously, this generated an error...
I noticed that the problem is that the code identifies the "Status" container by the tag ID, and the tag ID is generated from the status value, which in my case, is an integer.
This generates a <div id=1>
conflict, as the record tag ID is also loaded from the record ID in the database
So, depending on the value of the item, we can have 2 elements with the same id. See the image below...
Of course this problem will not occur with items with larger IDs. Ex: id="185466", But if you test with few items, it may occur...
Correction: As the objective is to load the status ID from a string, so as not to conflict with the record ID, I added a prefix to the status ID tag.
[Final Step-by-step]
File [pages/KanbanBoard.php #37]
Add prefix variable:
protected static string $statusPrefix = 'status';
So I can change the prefix if necessary
File: [kanban-status.blade.php #7]
I changed from:
id="{{ $status['id'] }}"
for
id="{{ self::$statusPrefix }}-{{ $status['id'] }}"
File: [kanban-scripts.blade.php #32]
I changed from:
const statuses = @js($statuses->map(fn ($status) => $status['id']))
for
const statuses = @js($statuses->map(fn ($status) => self::$statusPrefix.'-'.$status['id']))
Then, for the system to pick up the correct Status, I just remove the prefix where necessary
File: [concerns/HasStatusChange.php #12 / #30]
Add this line: $status = str_replace($statusPrefix.'-', '', $status);
public function statusChanged(int $recordId, string $status, array $fromOrderedIds, array $toOrderedIds): void
{
$status = str_replace($statusPrefix.'-', '', $status);
$this->onStatusChanged($recordId, $status, $fromOrderedIds, $toOrderedIds);
}
public function sortChanged(int $recordId, string $status, array $orderedIds): void
{
$status = str_replace($statusPrefix.'-', '', $status);
$this->onSortChanged($recordId, $status, $orderedIds);
}
Use an Enum (int) to define the Status values
2.3.0
8.1.10
10.10
Windows
Chrome
No response
I can't install in a fresh Laravel 11 instalation
Just run composer install
latest
8.2.10
11.0.0
Linux
No response
No response
I can't open the kanban board without getting an exception when the view renders if I use Model::preventAccessingMissingAttributes()
. The crash occurs in resources/views/kanban-record.blade.php
due to calling the blade directive @if($record->just_updated)
checking if just_updated
exists on the model or not.
Model::preventAccessingMissingAttributes()
is a setting that makes sure that one can't use attributes that don't exist on a model, this of course helps prevent bugs. Call the method in your AppServiceProvider
.
2.2.0
8.3.2-1+ubuntu22.04.1+deb.sury.org+1
10.45.1
No response
No response
The bug is easily fixed by using isset
in the @if
condition rather than attempting to access the property directly.
Hi thanks for this awesome free plugin, but I am wondring how to use this as this does not comes with a migration. So I was wondering how to get started and what table structures it needed.
none
1
8.2
10
No response
No response
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.