diff --git a/app/Http/Controllers/Admin/FeedbackController.php b/app/Http/Controllers/Admin/FeedbackController.php index 942d625..f778961 100644 --- a/app/Http/Controllers/Admin/FeedbackController.php +++ b/app/Http/Controllers/Admin/FeedbackController.php @@ -53,9 +53,33 @@ public function index(Request $request) ]); } + public function search(Request $request) + { + $search = $request->input('search'); + $parent_id = $request->input('parent_id') ?? null; + if (!$request->has('search')) { + return response()->json([]); + } + + $query = Post::where('merged_with_post', null); + $query->where('title', 'like', '%' . $search . '%'); + if ($parent_id) { + $query->where('id', '!=', $parent_id); + } + + $query->orderBy('vote', 'desc'); + + $response = $query->get(); + + return response()->json($response); + } + public function show(Post $post) { $post->load('creator', 'board', 'status', 'by'); + if ($post->merged_with_post) { + $post->merged_with_post = Post::find($post->merged_with_post); + } $boards = Board::select('id', 'name', 'posts', 'slug')->get(); $statuses = Status::select('id', 'name', 'color')->get(); @@ -141,10 +165,6 @@ public function update(Request $request, Post $post) 'body' => $request->input('comment') ?? '', 'status_id' => $request->input('status_id'), ]); - - if ($request->input('notify') === true) { - $this->notify($post); - } } $post->update([ @@ -152,6 +172,10 @@ public function update(Request $request, Post $post) 'status_id' => $request->input('status_id'), ]); + if ($request->input('notify') === true) { + $this->notify($post); + } + return redirect()->back()->with('success', 'Feedback updated successfully.'); } @@ -195,4 +219,32 @@ public function addVote(Post $post, Request $request) return redirect()->back()->with('success', 'Vote added successfully.'); } + + public function merge(Request $request) + { + $request->validate([ + 'post_id' => 'required|exists:posts,id', + 'merge_ids' => 'required|array|exists:posts,id', + 'status_id' => 'required|exists:statuses,id', + ]); + + try { + $post = Post::where('id', $request->post_id); + $post->increment('comments', Post::whereIn('id', $request->merge_ids)->pluck( 'comments' )->sum() ); + $post->increment('vote', Post::whereIn('id', $request->merge_ids)->pluck( 'vote' )->sum() ); + + Post::whereIn('id', $request->merge_ids) + ->update([ + 'merged_with_post' => $request->post_id, + 'status_id' => $request->status_id, + 'comments' => 0, + 'vote' => 0, + ]); + + return redirect()->back()->with('success', 'Posts merged successfully.'); + } catch (\Exception $e) { + return redirect()->back()->with('error', 'Post not found.'); + + } + } } diff --git a/app/Http/Controllers/Frontend/BoardController.php b/app/Http/Controllers/Frontend/BoardController.php index 8d82253..d2317b9 100644 --- a/app/Http/Controllers/Frontend/BoardController.php +++ b/app/Http/Controllers/Frontend/BoardController.php @@ -27,8 +27,8 @@ public function show(Request $request, Board $board) $postsQuery = Post::where('board_id', $board->id); $statuses = Status::select('id') - ->inFrontend() - ->get(); + ->inFrontend() + ->get(); // Only show posts with statuses that are in the frontend // or have no status (waiting to be reviewed) @@ -47,12 +47,16 @@ public function show(Request $request, Board $board) ]); } + if ($request->has('search') && $request->search) { + $postsQuery->where('title', 'like', '%' . $request->search . '%'); + } + if ($request->has('sort') && in_array($request->sort, array_keys($sortFields))) { $orderBy = $sortFields[$request->sort]; } $postsQuery->orderBy($orderBy, $request->sort === 'oldest' ? 'asc' : 'desc'); - $posts = $postsQuery->cursorPaginate(20); + $posts = $postsQuery->paginate(20); $data = [ 'board' => $board, diff --git a/app/Http/Controllers/Frontend/CommentController.php b/app/Http/Controllers/Frontend/CommentController.php index d936b0a..806c76d 100644 --- a/app/Http/Controllers/Frontend/CommentController.php +++ b/app/Http/Controllers/Frontend/CommentController.php @@ -19,7 +19,13 @@ public function index(Request $request, Post $post) $sort = $request->has('sort') && in_array($request->sort, ['latest', 'oldest']) ? $request->sort : 'oldest'; $orderBy = ($sort === 'latest') ? 'desc' : 'asc'; - $comments = $post->comments()->with('user', 'status')->orderBy('created_at', $orderBy)->get(); + // Search if there are any merged posts with the post id + $mergedPost = Post::where('merged_with_post', $post->id)->get(); + $postIds = $mergedPost->pluck('id')->push($post->id); + $comments = Comment::whereIn('post_id', $postIds) + ->with('user', 'status') + ->orderBy('created_at', $orderBy) + ->get(); // Group comments by parent_id $groupedComments = $comments->groupBy('parent_id'); diff --git a/app/Http/Controllers/Frontend/PostController.php b/app/Http/Controllers/Frontend/PostController.php index f59ce7c..269077d 100644 --- a/app/Http/Controllers/Frontend/PostController.php +++ b/app/Http/Controllers/Frontend/PostController.php @@ -18,6 +18,9 @@ public function show(Board $board, $post) $post->load('creator'); $post->body = Formatting::transformBody($post->body); + if ($post->merged_with_post) { + $post->merged_with_post = Post::with('board')->find($post->merged_with_post); + } $data = [ 'post' => $post, diff --git a/app/Models/Post.php b/app/Models/Post.php index 7ef238d..eaa3639 100644 --- a/app/Models/Post.php +++ b/app/Models/Post.php @@ -23,7 +23,8 @@ class Post extends Model 'eta', 'impact', 'effort', - 'created_by' + 'created_by', + 'merged_with_post', ]; protected static function boot() @@ -63,7 +64,7 @@ public function board() public function status() { - return $this->hasOne(Status::class, 'id', 'status_id'); + return $this->hasOne(Status::class, 'id', 'status_id')->withDefault(); } public function votes() @@ -98,7 +99,7 @@ public function scopeWithVote($query) 'has_voted' => Vote::selectRaw('count(*)') ->whereColumn('post_id', 'posts.id') ->where('user_id', $userId) - ->take(1) + ->take(1), ]); } } diff --git a/database/migrations/2024_05_07_091836_add_archived_by_post_to_posts_table.php b/database/migrations/2024_05_07_091836_add_archived_by_post_to_posts_table.php new file mode 100644 index 0000000..8716e37 --- /dev/null +++ b/database/migrations/2024_05_07_091836_add_archived_by_post_to_posts_table.php @@ -0,0 +1,28 @@ +foreignId('merged_with_post')->nullable()->constrained('posts')->onDelete('set null'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('posts', function (Blueprint $table) { + $table->dropConstrainedForeignId('merged_with_post'); + }); + } +}; diff --git a/database/seeders/PostSeeder.php b/database/seeders/PostSeeder.php index c76e104..fabdddc 100644 --- a/database/seeders/PostSeeder.php +++ b/database/seeders/PostSeeder.php @@ -26,5 +26,14 @@ public function run(): void 'created_by' => User::inRandomOrder()->first()->id, ]); } + + // foreach (range(1, 200) as $n) { + // Post::factory()->create([ + // 'title' => 'post-' . $n, + // 'board_id' => $boards->first()->id, + // ]); + + // sleep(1); + // } } } diff --git a/phpunit.xml b/phpunit.xml index bc86714..ed8872f 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -21,8 +21,8 @@ - - + + diff --git a/resources/js/Components/ActionMenu.tsx b/resources/js/Components/ActionMenu.tsx new file mode 100644 index 0000000..8224df8 --- /dev/null +++ b/resources/js/Components/ActionMenu.tsx @@ -0,0 +1,56 @@ +import { Fragment } from 'react' +import { Menu, Transition } from '@headlessui/react' +import { EllipsisVerticalIcon } from '@heroicons/react/20/solid' +import classNames from 'classnames'; + +type ActionMenuProps = { + menuItems: { + label: string; + onClick: () => void; + }[]; + menuName?: string; +}; + +export default function ActionMenu({ menuItems, menuName = 'Actions' }: ActionMenuProps) { + return ( + +
+ + Open options + {menuName} + +
+ + + +
+ {menuItems.map((item) => ( + + {({ active }) => ( + + )} + + ))} +
+
+
+
+ ) +} diff --git a/resources/js/Components/Comments.tsx b/resources/js/Components/Comments.tsx index 40569d5..6792657 100644 --- a/resources/js/Components/Comments.tsx +++ b/resources/js/Components/Comments.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { Link, usePage } from '@inertiajs/react'; import CommentBox from './CommentBox'; @@ -16,29 +16,32 @@ const Comments: React.FC = ({ post }) => { const [sort, setSort] = useState<'latest' | 'oldest'>('oldest'); const [isFetching, setIsFetching] = useState(false); - const fetchComments = async () => { - setIsFetching(true); + const fetchComments = useCallback( async () => { + setIsFetching(true); - try { - const response = await fetch( - route('post.comments.index', { - post: post.slug, - sort: sort, - }) - ); + if (post.merged_with_post){ + return; + } + try { + const response = await fetch( + route('post.comments.index', { + post: post.slug, + sort: sort, + }) + ); - const data = await response.json(); - setIsFetching(false); - setComments(data); - } catch (error) { - console.error('Error fetching comments:', error); - setIsFetching(false); - } - }; + const data = await response.json(); + setIsFetching(false); + setComments(data); + } catch (error) { + console.error('Error fetching comments:', error); + setIsFetching(false); + } + }, [sort, post]); useEffect(() => { fetchComments(); - }, [sort]); + }, [sort, post]); const appendToComments = (comment: CommentType) => { setComments([...comments, comment]); @@ -70,7 +73,7 @@ const Comments: React.FC = ({ post }) => {
Sort By
{ + if (event.target.checked) { + form.clearErrors(); + form.setData('mergedPosts', [...form.data.mergedPosts, feedback.id]); + } else { + form.setData('mergedPosts', form.data.mergedPosts.filter(item => item !== feedback.id)); + } + }} + /> +
+
+ + + {feedback.body} - {feedback.by?.name} + +
+ + ))} + + + + + + {searchedData && searchedData.length > 0 && ( +
+ status.id === post.status_id)?.id.toString()} + options={statusOptions} + onChange={(option) => { + form.clearErrors(); + form.setData('status_id', Number(option.key)); + }} + /> +
+ + +
+
+ )} +
+ + + ); +}; + +export default MergeFeedback; diff --git a/resources/js/Pages/Admin/Feedbacks/Show.tsx b/resources/js/Pages/Admin/Feedbacks/Show.tsx index 8a4754e..1a217fd 100644 --- a/resources/js/Pages/Admin/Feedbacks/Show.tsx +++ b/resources/js/Pages/Admin/Feedbacks/Show.tsx @@ -27,6 +27,8 @@ import classNames from 'classnames'; import UserSearchDropdown from '@/Components/UserSearchDropdown'; import CreateUserModal from '@/Components/CreateUserModal'; import EditFeedback from './EditFeedback'; +import ActionMenu from '@/Components/ActionMenu'; +import MergeFeedback from '@/Pages/Admin/Feedbacks/MergeFeedback'; type Props = { post: PostType; @@ -43,6 +45,7 @@ type VoteProps = { const FeedbackShow = ({ post, statuses, boards, votes }: Props) => { const [showEditForm, setShowEditForm] = useState(false); + const [showMergeForm, setShowMergeForm] = useState(false); const [localPost, setLocalPost] = useState(post); const [showVoteModal, setShowVoteModal] = useState(false); const form = useForm({ @@ -96,6 +99,20 @@ const FeedbackShow = ({ post, statuses, boards, votes }: Props) => {
{localPost.title} + {post.merged_with_post && ( + + (Merged with{' '} + + {post.merged_with_post.title} + + ) + + )}
@@ -168,35 +185,34 @@ const FeedbackShow = ({ post, statuses, boards, votes }: Props) => {
- - + setShowMergeForm(true), + }, + { + label: 'Edit', + onClick: () => setShowEditForm(true), + }, + { + label: 'Delete', + onClick: () => { + if ( + confirm( + 'Are you sure you want to delete this feedback? This action cannot be undone.' + ) + ) { + form.delete( + route('admin.feedbacks.destroy', { + post: post.slug, + }) + ); + } + }, + }, + ]} + />
@@ -297,6 +313,13 @@ const FeedbackShow = ({ post, statuses, boards, votes }: Props) => { post={localPost} /> + setShowMergeForm(false)} + /> + setShowEditForm(false)} diff --git a/resources/js/Pages/Admin/Status.tsx b/resources/js/Pages/Admin/Status.tsx index e923451..50541d1 100644 --- a/resources/js/Pages/Admin/Status.tsx +++ b/resources/js/Pages/Admin/Status.tsx @@ -1,9 +1,9 @@ import React, { useEffect, useState } from 'react'; -import { Head, Link, useForm } from '@inertiajs/react'; +import { Head, useForm } from '@inertiajs/react'; import { CirclePicker } from 'react-color'; import * as Popover from '@radix-ui/react-popover'; -import { PageProps, StatusType } from '@/types'; +import { StatusType } from '@/types'; import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout'; import { Button, @@ -15,7 +15,6 @@ import { TextField, } from '@wedevs/tail-react'; import { PlusIcon } from '@heroicons/react/24/outline'; -import useSearchParams from '@/hooks/useSearchParams'; type Props = { statuses: StatusType[]; @@ -211,7 +210,7 @@ const StatusPage = ({ statuses }: Props) => { variant="danger" style="outline" size="small" - className="ml-3 dark:bg-transparent dark:text-red-500" + className="ml-3 dark:text-red-400 dark:hover:text-red-500 dark:bg-gray-900 dark:hover:bg-gray-800" onClick={() => confirmDelete(status.id)} > Delete diff --git a/resources/js/Pages/Admin/User/Index.tsx b/resources/js/Pages/Admin/User/Index.tsx index 90207fb..4334a24 100644 --- a/resources/js/Pages/Admin/User/Index.tsx +++ b/resources/js/Pages/Admin/User/Index.tsx @@ -126,6 +126,7 @@ const UserIndex = ({ users }: Props) => { variant="secondary" size="small" onClick={() => editUser(user)} + className="dark:bg-gray-700" > Edit @@ -134,7 +135,7 @@ const UserIndex = ({ users }: Props) => { variant="danger" size="small" style="outline" - className="ml-2" + className="ml-2 dark:text-red-400 dark:hover:text-red-500 dark:bg-gray-900 dark:hover:bg-gray-800" onClick={() => deleteUser(user.id)} > Delete diff --git a/resources/js/Pages/Frontend/Board/Show.tsx b/resources/js/Pages/Frontend/Board/Show.tsx index 2c38d01..b9d3ebb 100644 --- a/resources/js/Pages/Frontend/Board/Show.tsx +++ b/resources/js/Pages/Frontend/Board/Show.tsx @@ -1,8 +1,9 @@ -import React, { useState, useRef, useCallback, useEffect } from 'react'; +import React, { useCallback, useRef, useState, useEffect } from 'react'; import { Head, Link, router } from '@inertiajs/react'; import { MagnifyingGlassIcon, ChatBubbleLeftIcon, + ArrowPathIcon, } from '@heroicons/react/24/outline'; import FrontendLayout from '@/Layouts/FrontendLayout'; @@ -10,6 +11,7 @@ import { BoardType, PageProps, PostType } from '@/types'; import PostForm from './PostForm'; import axios from 'axios'; import VoteButton from '@/Components/VoteButton'; +import { debounce } from 'lodash'; import BackToTop from '@/Components/BackToTop'; type Props = { @@ -20,6 +22,12 @@ type Props = { board: BoardType; }; +type UrlParams = { + board: string; + search?: string; + sort?: string; +}; + const ShowBoard = ({ auth, posts, board }: PageProps) => { const [allPosts, setAllPosts] = useState(posts.data); const [nextPageUrl, setNextPageUrl] = useState( @@ -31,7 +39,11 @@ const ShowBoard = ({ auth, posts, board }: PageProps) => { // Get sort key from URL param const urlParams = new URLSearchParams(window.location.search); const sort = urlParams.get('sort'); - const [sortKey, setSortKey] = useState(sort || 'voted'); + const search = urlParams.get('search'); + const [searchUrlParam, setSearchUrlParam] = useState({ + search: search || '', + sort: sort || 'voted', + }); const toggleVote = (post: PostType) => { if (!auth.user) return; @@ -53,19 +65,55 @@ const ShowBoard = ({ auth, posts, board }: PageProps) => { const handleSortChange = (e: React.ChangeEvent) => { const value = e.target.value; - setSortKey(value); - router.visit(route('board.show', { board: board.slug, sort: value }), { + + setSearchUrlParam({ + ...searchUrlParam, + sort: value, + }); + let params: UrlParams = { + board: board.slug, + sort: value, + search: searchUrlParam.search, + }; + + if (searchUrlParam.search.length === 0) { + delete params['search']; + } + + router.visit(route('board.show', params), { replace: true, }); }; + const handleSearch = useCallback( + debounce((search: string) => { + let params: UrlParams = { + board: board.slug, + sort: searchUrlParam.sort, + search: search, + }; + if (search.length === 0) { + delete params['search']; + } + router.visit(route('board.show', params), { + replace: true, + }); + }, 500), + [] + ); + const loadMorePosts = useCallback(() => { if (loading || !nextPageUrl) return; setLoading(true); + console.log('Loading more posts...', { + loading, + nextPageUrl, + }); + const url = new URL(nextPageUrl); // @TODO: Need to add search query later - url.searchParams.set('sort', sortKey); + // url.searchParams.set('sort', sortKey); axios.get(url.toString()).then((response) => { setAllPosts((prevPosts) => [...prevPosts, ...response.data.posts.data]); @@ -110,7 +158,7 @@ const ShowBoard = ({ auth, posts, board }: PageProps) => { { + setSearchUrlParam({ + ...searchUrlParam, + search: e.target.value, + }); + handleSearch(e.target.value); + }} + className="px-4 pl-9 py-2 dark:bg-gray-800 rounded border-0 text-sm ring-1 ring-indigo-50 dark:ring-gray-700 focus:outline-none focus:ring-1 dark:text-gray-300" + autoFocus={searchUrlParam.search.length > 0} />
+ {search || + searchUrlParam.search.length > 0 || + searchUrlParam.sort !== 'voted' ? ( +
+ +
+ ) : null}
{allPosts.map((post, index) => { - if (allPosts.length === index + 1) { - return ( -
+ - -
- {post.title} -
-
- {post.body} -
-
- - {post.comments} -
- -
-
- -
+
+ {post.title}
-
- ); - } else { - return ( -
- -
- {post.title} -
-
- {post.body} -
-
- - {post.comments} -
- -
-
- -
+
+ {post.body} +
+
+ + {post.comments} +
+ +
+
+
- ); - } +
+ ); })} {allPosts.length === 0 && ( diff --git a/resources/js/Pages/Frontend/Post.tsx b/resources/js/Pages/Frontend/Post.tsx index e084ef9..c484d64 100644 --- a/resources/js/Pages/Frontend/Post.tsx +++ b/resources/js/Pages/Frontend/Post.tsx @@ -67,6 +67,21 @@ const Post = ({ post, status, board, votes }: PageProps) => {
{post.title} + {post.merged_with_post && ( + + (Merged with{' '} + + {post.merged_with_post.title} + + ) + + )}
{status && ( diff --git a/resources/js/types/index.d.ts b/resources/js/types/index.d.ts index 8a574fe..aeb1f14 100644 --- a/resources/js/types/index.d.ts +++ b/resources/js/types/index.d.ts @@ -86,6 +86,7 @@ export interface PostType { board: BoardType; status?: StatusType; by: User | null; + merged_with_post?: PostType; } export interface CommentType { diff --git a/routes/web.php b/routes/web.php index b97066f..db522c1 100644 --- a/routes/web.php +++ b/routes/web.php @@ -38,8 +38,10 @@ Route::redirect('/', '/admin/feedbacks'); Route::get('/feedbacks', [FeedbackController::class, 'index'])->name('admin.feedbacks.index'); + Route::get('/feedbacks/search', [FeedbackController::class, 'search'])->name('admin.feedbacks.search'); Route::get('/feedbacks/{post}', [FeedbackController::class, 'show'])->name('admin.feedbacks.show'); Route::post('/feedbacks', [FeedbackController::class, 'store'])->name('admin.feedbacks.store'); + Route::post( '/feedbacks/merge', [FeedbackController::class, 'merge'])->name('admin.feedbacks.merge'); Route::post('/feedbacks/{post}', [FeedbackController::class, 'update'])->name('admin.feedbacks.update'); Route::put('/feedbacks/{post}/update', [FeedbackController::class, 'updateContent'])->name('admin.feedbacks.update-content'); Route::post('/feedbacks/{post}/vote', [FeedbackController::class, 'addVote'])->name('admin.feedbacks.vote'); diff --git a/tests/Feature/Posts/MergedPostsTest.php b/tests/Feature/Posts/MergedPostsTest.php new file mode 100644 index 0000000..085a82e --- /dev/null +++ b/tests/Feature/Posts/MergedPostsTest.php @@ -0,0 +1,77 @@ +user = User::factory()->create( [ + 'name' => 'Admin User', + 'email' => 'admin@example.com', + 'password' => bcrypt( 'password' ), + 'role' => 'admin', + ] ); + $this->board = Board::create([ + 'name' => 'Feedback', + 'slug' => 'feedback', + 'order' => 1, + 'privacy' => 'public', + 'allow_posts' => true, + 'settings' => [], + ]); + $this->status = Status::create([ + 'name' => 'Open', + 'color' => 'green', + ]); +}); + +test('post can be merged with another post', function () { + $post = Post::factory()->create([ + 'board_id' => $this->board->id, + 'created_by' => $this->user->id, + ]); + $mergedPost = Post::factory()->create([ + 'board_id' => $this->board->id, + 'created_by' => $this->user->id, + ]); + + $response = $this->actingAs($this->user) + ->post('admin/feedbacks/merge', [ + 'post_id' => $post->id, + 'merge_ids' => [$mergedPost->id], + 'status_id' => $this->status->id, + ]); + + $response->assertSessionHasNoErrors(); + $this->assertNotNull($mergedPost->refresh()->merged_with_post); + $this->assertEquals($post->id, $mergedPost->merged_with_post); +}); + +test('post can be merged with one or more posts', function () { + $post = Post::factory()->create([ + 'board_id' => $this->board->id, + 'created_by' => $this->user->id, + ]); + $mergedPosts = Post::factory()->count(2)->create([ + 'board_id' => $this->board->id, + 'created_by' => $this->user->id, + ]); + + $response = $this + ->actingAs($this->user) + ->post('/admin/feedbacks/merge', [ + 'post_id' => $post->id, + 'merge_ids' => $mergedPosts->pluck('id')->toArray(), + 'status_id' => $this->status->id, + ]); + + $response->assertSessionHasNoErrors(); + $this->assertNotNull($mergedPosts->first()->refresh()->merged_with_post); + $this->assertNotNull($mergedPosts->last()->refresh()->merged_with_post); + $this->assertEquals($mergedPosts->first()->merged_with_post, $post->id); + $this->assertEquals($mergedPosts->last()->merged_with_post, $post->id); +}); diff --git a/tests/Feature/Posts/SearchPostTest.php b/tests/Feature/Posts/SearchPostTest.php new file mode 100644 index 0000000..4e4df8d --- /dev/null +++ b/tests/Feature/Posts/SearchPostTest.php @@ -0,0 +1,95 @@ +user = User::factory()->create( [ + 'name' => 'Admin User', + 'email' => 'admin@example.com', + 'password' => bcrypt( 'password' ), + 'role' => 'admin', + ] ); + $this->board = Board::create( [ + 'name' => 'Feedback', + 'slug' => 'feedback', + 'order' => 1, + 'privacy' => 'public', + 'allow_posts' => true, + 'settings' => [], + ] ); +} ); + +it( 'returns empty array when search parameter is missing', function () { + $response = $this->actingAs( $this->user ) + ->get( '/admin/feedbacks/search' ); + + $response->assertStatus( 200 ) + ->assertJson( [] ); +} ); + +it( 'returns posts matching the search query', function () { + // Create posts for testing + Post::factory()->create( [ + 'title' => 'Post One', + 'vote' => 10, + 'board_id' => $this->board->id, + 'created_by' => $this->user->id, + ] ); + Post::factory()->create( [ + 'title' => 'Another', + 'vote' => 5, + 'board_id' => $this->board->id, + 'created_by' => $this->user->id, + ] ); + Post::factory()->create( [ + 'title' => 'Post Two', + 'vote' => 15, + 'board_id' => $this->board->id, + 'created_by' => $this->user->id, + ] ); + + // Add search to the request + $response = $this->actingAs( $this->user ) + ->get( '/admin/feedbacks/search?search=Post' ); + + $response->assertStatus( 200 ) + ->assertJsonCount( 2 ) + ->assertJsonFragment( [ 'title' => 'Post One' ] ) + ->assertJsonFragment( [ 'title' => 'Post Two' ] ) + ->assertJsonMissing( [ 'title' => 'Another' ] ); +} ); + +it( 'excludes post with parent_id from results', function () { + // Create posts for testing + $post1 = Post::factory()->create( [ 'title' => 'Post One', 'vote' => 10, 'board_id' => $this->board->id, 'created_by' => $this->user->id ] ); + $post2 = Post::factory()->create( [ 'title' => 'Another Post', 'vote' => 5, 'board_id' => $this->board->id, 'created_by' => $this->user->id ] ); + + $response = $this->actingAs( $this->user ) + ->get( '/admin/feedbacks/search?search=Post&parent_id=' . $post1->id ); + + $response->assertStatus(200) + ->assertJsonCount(1) + ->assertJsonFragment(['title' => 'Another Post']) + ->assertJsonMissing(['title' => 'Post One']); +} ); + +it( 'orders results by vote in descending order', function () { + // Create posts for testing + Post::factory()->create( [ 'title' => 'Low Vote Post', 'vote' => 1, 'board_id' => $this->board->id, 'created_by' => $this->user->id ] ); + Post::factory()->create( [ 'title' => 'High Vote Post', 'vote' => 10, 'board_id' => $this->board->id, 'created_by' => $this->user->id ] ); + Post::factory()->create( [ 'title' => 'Medium Vote Post', 'vote' => 5, 'board_id' => $this->board->id, 'created_by' => $this->user->id ] ); + + $response = $this->actingAs( $this->user ) + ->get( '/admin/feedbacks/search?search=Post' ); + + $response->assertStatus( 200 ) + ->assertJsonPath( '0.vote', 10 ) + ->assertJsonPath( '1.vote', 5 ) + ->assertJsonPath( '2.vote', 1 ); +} ); diff --git a/tests/TestCase.php b/tests/TestCase.php index 2932d4a..1252394 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,9 +2,18 @@ namespace Tests; +use App\Http\Middleware\VerifyCsrfToken; use Illuminate\Foundation\Testing\TestCase as BaseTestCase; abstract class TestCase extends BaseTestCase { use CreatesApplication; + + protected function setUp(): void + { + parent::setUp(); + + // Disable CSRF protection + $this->withoutMiddleware( VerifyCsrfToken::class ); + } }