Skip to content

Commit 617c551

Browse files
authored
Merge pull request #339 from ploi/feature/board-column-sort-and-search
feat: add sort and search dropdown to board columns on project page
2 parents 84d42e8 + 46c146d commit 617c551

File tree

11 files changed

+397
-77
lines changed

11 files changed

+397
-77
lines changed

app/Http/Controllers/ProjectController.php

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,7 @@ public function show($id)
1212

1313
return view('project', [
1414
'project' => $project,
15-
'boards' => $project->boards()
16-
->visible()
17-
->with(['items' => function ($query) {
18-
return $query
19-
->orderBy('pinned', 'desc')
20-
->visibleForCurrentUser()
21-
->popular() // TODO: This needs to be fixed to respect the sorting setting from the board itself (sort_items_by)
22-
->withCount('votes');
23-
}])
24-
->get(),
15+
'boards' => $project->boards()->visible()->get(),
2516
]);
2617
}
2718
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
namespace App\Livewire\Project;
4+
5+
use App\Models\Board;
6+
use App\Models\Project;
7+
use Livewire\Component;
8+
9+
class BoardColumn extends Component
10+
{
11+
public Project $project;
12+
public Board $board;
13+
public string $sortBy = 'created_at';
14+
public string $search = '';
15+
16+
protected $listeners = [
17+
'item-created' => '$refresh',
18+
];
19+
20+
public function mount(): void
21+
{
22+
$this->sortBy = 'created_at';
23+
}
24+
25+
public function setSortBy(string $sort): void
26+
{
27+
if (! in_array($sort, ['created_at', 'total_votes', 'last_commented'])) {
28+
return;
29+
}
30+
31+
$this->sortBy = $sort;
32+
}
33+
34+
public function render()
35+
{
36+
$query = $this->board->items()
37+
->visibleForCurrentUser()
38+
->when($this->search, fn ($q) => $q->where('title', 'like', '%' . $this->search . '%'));
39+
40+
if ($this->sortBy === 'last_commented') {
41+
$query->withMax('comments', 'created_at')
42+
->orderBy('pinned', 'desc')
43+
->orderByDesc('comments_max_created_at');
44+
} else {
45+
$query->orderBy('pinned', 'desc')
46+
->orderByDesc($this->sortBy);
47+
}
48+
49+
$items = $query->get();
50+
51+
return view('livewire.project.board-column', [
52+
'items' => $items,
53+
]);
54+
}
55+
}

lang/en/general.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,10 @@
3030

3131
'close' => 'Close',
3232
'save' => 'Save',
33+
34+
'search-items' => 'Search items...',
35+
'sort-by' => 'Sort by',
36+
'sort-newest' => 'Newest',
37+
'sort-most-voted' => 'Most voted',
38+
'sort-last-commented' => 'Last commented',
3339
];

lang/es/general.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,10 @@
99

1010
'close' => 'Cerrar',
1111
'save' => 'Guardar',
12+
13+
'search-items' => 'Buscar elementos...',
14+
'sort-by' => 'Ordenar por',
15+
'sort-newest' => 'Más recientes',
16+
'sort-most-voted' => 'Más votados',
17+
'sort-last-commented' => 'Último comentado',
1218
];

lang/hu/general.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,11 @@
3131
'close' => 'Bezárás',
3232
'save' => 'Mentés',
3333

34+
'search-items' => 'Elemek keresése...',
35+
'sort-by' => 'Rendezés',
36+
'sort-newest' => 'Legújabb',
37+
'sort-most-voted' => 'Legtöbb szavazat',
38+
'sort-last-commented' => 'Utoljára hozzászólt',
39+
3440
'public-user' => 'Nyilvános felhasználó',
3541
];

lang/it/general.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,11 @@
1010
'close' => 'Chiudi',
1111
'save' => 'Salva',
1212

13+
'search-items' => 'Cerca elementi...',
14+
'sort-by' => 'Ordina per',
15+
'sort-newest' => 'Più recenti',
16+
'sort-most-voted' => 'Più votati',
17+
'sort-last-commented' => 'Ultimo commento',
18+
1319
'public-user' => 'Utente pubblico',
1420
];

lang/nl/general.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,10 @@
3030

3131
'close' => 'Sluiten',
3232
'save' => 'Opslaan',
33+
34+
'search-items' => 'Zoek items...',
35+
'sort-by' => 'Sorteren op',
36+
'sort-newest' => 'Nieuwste',
37+
'sort-most-voted' => 'Meeste stemmen',
38+
'sort-last-commented' => 'Laatst becommentarieerd',
3339
];
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
<section class="h-full">
2+
<div class="bg-gray-100 rounded-xl min-w-[18rem] lg:w-92 flex flex-col max-h-full dark:bg-white/5">
3+
<div
4+
class="p-2 font-semibold border-b border-gray-200 bg-gray-100/80 rounded-t-xl backdrop-blur-xl backdrop-saturate-150 dark:bg-gray-900 dark:text-white dark:border-b-gray-800">
5+
<div class="flex items-center justify-between">
6+
<a
7+
href="{{ route('projects.boards.show', [$project, $board]) }}"
8+
class="border-b border-dotted border-black text-gray-800 dark:text-white">
9+
{{ $board->title }}
10+
</a>
11+
12+
<div x-data="{ open: false }" @click.away="open = false" class="relative">
13+
<button
14+
type="button"
15+
@click="open = !open"
16+
class="flex items-center justify-center w-8 h-8 rounded-lg text-gray-500 hover:text-gray-700 hover:bg-gray-200 dark:text-gray-400 dark:hover:text-gray-200 dark:hover:bg-gray-700 transition"
17+
>
18+
<x-heroicon-o-adjustments-vertical class="w-4 h-4" />
19+
</button>
20+
21+
<div
22+
x-show="open"
23+
x-cloak
24+
class="absolute right-0 z-50 mt-1 w-52 origin-top-right rounded-lg bg-white dark:bg-gray-800 shadow-lg ring-1 ring-black/5 dark:ring-white/10"
25+
>
26+
<div class="p-2">
27+
<input
28+
type="text"
29+
wire:model.live.debounce.300ms="search"
30+
placeholder="{{ trans('general.search-items') }}"
31+
class="w-full rounded-md border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-700 px-3 py-1.5 text-sm text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-1 focus:ring-brand-500 focus:border-brand-500"
32+
/>
33+
</div>
34+
<div class="border-t border-gray-100 dark:border-gray-700">
35+
<div class="py-1">
36+
<button
37+
type="button"
38+
wire:click="setSortBy('created_at')"
39+
@click="open = false"
40+
@class([
41+
'flex w-full items-center gap-2 px-4 py-2 text-sm whitespace-nowrap hover:bg-gray-100 dark:hover:bg-gray-700',
42+
'text-brand-500 font-medium' => $sortBy === 'created_at',
43+
'text-gray-700 dark:text-gray-300' => $sortBy !== 'created_at',
44+
])
45+
>
46+
{{ trans('general.sort-newest') }}
47+
</button>
48+
<button
49+
type="button"
50+
wire:click="setSortBy('total_votes')"
51+
@click="open = false"
52+
@class([
53+
'flex w-full items-center gap-2 px-4 py-2 text-sm whitespace-nowrap hover:bg-gray-100 dark:hover:bg-gray-700',
54+
'text-brand-500 font-medium' => $sortBy === 'total_votes',
55+
'text-gray-700 dark:text-gray-300' => $sortBy !== 'total_votes',
56+
])
57+
>
58+
{{ trans('general.sort-most-voted') }}
59+
</button>
60+
<button
61+
type="button"
62+
wire:click="setSortBy('last_commented')"
63+
@click="open = false"
64+
@class([
65+
'flex w-full items-center gap-2 px-4 py-2 text-sm whitespace-nowrap hover:bg-gray-100 dark:hover:bg-gray-700',
66+
'text-brand-500 font-medium' => $sortBy === 'last_commented',
67+
'text-gray-700 dark:text-gray-300' => $sortBy !== 'last_commented',
68+
])
69+
>
70+
{{ trans('general.sort-last-commented') }}
71+
</button>
72+
</div>
73+
</div>
74+
</div>
75+
</div>
76+
</div>
77+
</div>
78+
<ul class="p-2 space-y-2 o overflow-y-auto flex-1 min-h-0">
79+
@forelse($items as $item)
80+
<li>
81+
<a href="{{ route('projects.items.show', [$project, $item]) }}"
82+
class="block p-4 space-y-4 bg-white shadow rounded-xl hover:bg-gray-50 dark:bg-gray-900 dark:hover:bg-gray-950">
83+
<div class="flex justify-between">
84+
<p>
85+
{{ $item->title }}
86+
</p>
87+
88+
<div class="flex items-center">
89+
@if($item->isPrivate())
90+
<span x-data x-tooltip.raw="{{ trans('items.item-private') }}">
91+
<x-heroicon-s-lock-closed class="text-gray-500 fill-gray-500 w-5 h-5" />
92+
</span>
93+
@endif
94+
@if($item->isPinned())
95+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20"
96+
x-data
97+
x-tooltip.raw="{{ trans('items.item-pinned') }}"
98+
class="text-gray-500 fill-gray-500">
99+
<path
100+
d="M15 11.586V6h2V4a2 2 0 0 0-2-2H9a2 2 0 0 0-2 2v2h2v5.586l-2.707 1.707A.996.996 0 0 0 6 14v2a1 1 0 0 0 1 1h4v3l1 2 1-2v-3h4a1 1 0 0 0 1-1v-2a.996.996 0 0 0-.293-.707L15 11.586z"></path>
101+
</svg>
102+
@endif
103+
</div>
104+
</div>
105+
106+
<footer class="flex items-end justify-between">
107+
<span
108+
class="inline-flex items-center justify-center h-6 px-2 text-sm font-semibold tracking-tight text-gray-700 dark:text-gray-300 rounded-full bg-gray-50 dark:bg-gray-600">
109+
{{ $item->created_at->isoFormat('ll') }}
110+
</span>
111+
112+
<div class="text-gray-500 text-sm">
113+
{{ $item->total_votes }} {{ trans_choice('messages.votes', $item->total_votes) }}
114+
</div>
115+
</footer>
116+
</a>
117+
</li>
118+
@empty
119+
<li>
120+
<div
121+
class="p-3 font-medium text-center text-gray-500 dark:text-gray-400 border border-gray-300 dark:border-gray-400 border-dashed rounded-xl opacity-70">
122+
<p>{{ trans('items.no-items') }}</p>
123+
</div>
124+
</li>
125+
@endforelse
126+
</ul>
127+
</div>
128+
</section>

resources/views/project.blade.php

Lines changed: 1 addition & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -18,67 +18,7 @@
1818
])
1919
>
2020
@forelse($boards as $board)
21-
<section class="h-full">
22-
<div class="bg-gray-100 rounded-xl min-w-[18rem] lg:w-92 flex flex-col max-h-full dark:bg-white/5">
23-
<div
24-
class="p-2 font-semibold text-center text-gray-800 border-b border-gray-200 bg-gray-100/80 rounded-t-xl backdrop-blur-xl backdrop-saturate-150 dark:bg-gray-900 dark:text-white dark:border-b-gray-800">
25-
<a
26-
href="{{ route('projects.boards.show', [$project, $board]) }}"
27-
class="border-b border-dotted border-black">
28-
{{ $board->title }}
29-
</a>
30-
</div>
31-
<ul class="p-2 space-y-2 o overflow-y-auto flex-1 min-h-0">
32-
@forelse($board->items as $item)
33-
<li>
34-
<a href="{{ route('projects.items.show', [$project, $item]) }}"
35-
class="block p-4 space-y-4 bg-white shadow rounded-xl hover:bg-gray-50 dark:bg-gray-900 dark:hover:bg-gray-950">
36-
<div class="flex justify-between">
37-
<p>
38-
{{ $item->title }}
39-
</p>
40-
41-
<div class="flex items-center">
42-
@if($item->isPrivate())
43-
<span x-data x-tooltip.raw="{{ trans('items.item-private') }}">
44-
<x-heroicon-s-lock-closed class="text-gray-500 fill-gray-500 w-5 h-5" />
45-
</span>
46-
@endif
47-
@if($item->isPinned())
48-
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20"
49-
x-data
50-
x-tooltip.raw="{{ trans('items.item-pinned') }}"
51-
class="text-gray-500 fill-gray-500">
52-
<path
53-
d="M15 11.586V6h2V4a2 2 0 0 0-2-2H9a2 2 0 0 0-2 2v2h2v5.586l-2.707 1.707A.996.996 0 0 0 6 14v2a1 1 0 0 0 1 1h4v3l1 2 1-2v-3h4a1 1 0 0 0 1-1v-2a.996.996 0 0 0-.293-.707L15 11.586z"></path>
54-
</svg>
55-
@endif
56-
</div>
57-
</div>
58-
59-
<footer class="flex items-end justify-between">
60-
<span
61-
class="inline-flex items-center justify-center h-6 px-2 text-sm font-semibold tracking-tight text-gray-700 dark:text-gray-300 rounded-full bg-gray-50 dark:bg-gray-600">
62-
{{ $item->created_at->isoFormat('ll') }}
63-
</span>
64-
65-
<div class="text-gray-500 text-sm">
66-
{{ $item->total_votes }} {{ trans_choice('messages.votes', $item->total_votes) }}
67-
</div>
68-
</footer>
69-
</a>
70-
</li>
71-
@empty
72-
<li>
73-
<div
74-
class="p-3 font-medium text-center text-gray-500 dark:text-gray-400 border border-gray-300 dark:border-gray-400 border-dashed rounded-xl opacity-70">
75-
<p>{{ trans('items.no-items') }}</p>
76-
</div>
77-
</li>
78-
@endforelse
79-
</ul>
80-
</div>
81-
</section>
21+
<livewire:project.board-column :project="$project" :board="$board" :key="$board->id" />
8222
@empty
8323
<div class="w-full">
8424
<div

tests/Feature/Controllers/ProjectControllerTest.php

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,14 @@
2525
it('includes the items', function () {
2626
$project = Project::factory()
2727
->has(
28-
Board::factory(5)
29-
->has(Item::factory(10))
28+
Board::factory()
29+
->has(Item::factory(3))
3030
)
3131
->create();
3232

33-
get(route('projects.show', $project))->assertSeeInOrder(Item::all()->pluck('title')->toArray());
33+
$response = get(route('projects.show', $project));
34+
35+
Item::all()->each(fn (Item $item) => $response->assertSeeText($item->title));
3436
});
3537

3638
it('includes votes for items', function () {
@@ -71,14 +73,14 @@
7173
get(route('projects.show', $project))->assertSeeInOrder(['item 2' , 'item 1']);
7274
});
7375

74-
test('items are sorted by vote count', function () {
76+
test('items are sorted by newest by default', function () {
7577
$project = Project::factory()
7678
->has(
7779
Board::factory()
7880
->has(
7981
Item::factory(2)->state(new Sequence(
80-
['title' => 'item 1', 'total_votes' => 1],
81-
['title' => 'item 2', 'total_votes' => 10]
82+
['title' => 'item 1', 'created_at' => now()->subDay()],
83+
['title' => 'item 2', 'created_at' => now()]
8284
))
8385
)
8486
)->create();

0 commit comments

Comments
 (0)