Skip to content

Conversation

@AllanKoder
Copy link
Owner

No description provided.

Copilot AI review requested due to automatic review settings December 26, 2025 19:06
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a new Resource Edits index page and refactors the polymorphic relationship handling by replacing the custom ModelResolverService with Laravel's built-in morph map functionality. It also implements automatic upvoting for newly created content (resources, reviews, edits, and comments).

Key changes:

  • Introduces a Resource Edits index page with sorting and pagination functionality
  • Replaces custom ModelResolverService with Laravel's native Relation::enforceMorphMap() for cleaner polymorphic relationship handling
  • Implements auto-upvoting feature for all newly created upvotable content
  • Adds comprehensive test coverage for auto-upvote functionality

Reviewed changes

Copilot reviewed 37 out of 38 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
app/Providers/AppServiceProvider.php Configures Laravel's morph map with aliases for polymorphic relationships
app/Services/ModelResolverService.php Removed - replaced by Laravel's native morph map functionality
app/Providers/ModelResolverServiceProvider.php Removed - no longer needed after service removal
app/Services/ResourceEditsService.php Added methods for creating edits, merging edits, and retrieving index data with sorting
app/Services/ComputerScienceResourceService.php Removed database transaction wrapping (now handled in controller)
app/Services/CommentService.php Updated to use Relation::getMorphedModel() instead of ModelResolverService
app/Services/UpvoteService.php Updated to use morph map for model resolution
app/Traits/HasVotes.php Updated to use getMorphClass() for consistent morph type handling
app/Traits/HasComments.php Updated to use getMorphClass() for polymorphic relationships
app/SortingStrategies/VoteSortingStrategy.php Updated to use morph class instead of full class name
app/Http/Controllers/ResourceEditsController.php Refactored to delegate business logic to service layer and added index method
app/Http/Controllers/ComputerScienceResourceController.php Added transaction handling and auto-upvote call after resource creation
app/Http/Controllers/ResourceReviewController.php Added auto-upvote call after review creation
app/Http/Controllers/CommentController.php Added auto-upvote call after comment creation
app/Http/Requests/StoreCommentRequest.php Removed unnecessary ModelResolverService dependency
database/migrations/2025_12_25_000000_convert_morph_types_to_aliases.php Migration to convert existing database records from full class names to morph aliases
database/factories/CommentFactory.php Updated to use morph map for polymorphic type generation
bootstrap/providers.php Removed ModelResolverServiceProvider registration
routes/web.php Added route for resource edits index page
resources/js/Pages/ResourceEdits/Index.vue New page component for browsing and sorting resource edits
resources/js/Pages/AboutUs.vue Removed FAQ about open source status and added link to rules page
resources/js/Components/Resources/ResourceEdit/ResourceEditsTable.vue New component displaying resource edits with pagination and voting
resources/js/Components/Upvote/UpvoteSorting.vue Refactored to be reusable across different pages with configurable routes
resources/js/Components/Resources/ResourceTabs.vue Updated to use refactored UpvoteSorting component
resources/js/Components/Resources/FilterBar.vue Fixed indentation inconsistencies
resources/js/Components/Navigation/Navbar.vue Added Resource Edits to navigation menu with dropdown
resources/js/Components/FrequentlyAskedQuestion.vue Changed heading level from h3 to h2 for better semantic structure
resources/js/Components/ApplicationTitleLogo.vue New component for displaying application logo
app/Events/CommentService.php Duplicate CommentService file (should be removed)
tests/Feature/* Updated all tests to use morph aliases instead of full class names and added auto-upvote tests

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 1 to 266
<?php

namespace App\Services;

use App\Http\Resources\CommentResource;
use App\Http\Resources\UserResource;
use App\Models\Comment;
use App\Services\SortingManagers\GeneralVotesSortingManager;
use Exception;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

class CommentService
{
/**
* Get paginated comments with custom logic.
*
* @param string $commentableType
*/
public function getPaginatedComments(string $commentableKey, int $commentableId, int $index, int $paginationLimit = -1, string $sortBy = 'top'): array
{
if ($paginationLimit == -1) {
$paginationLimit = config('comment.default_pagination_limit');
}

Validator::validate([
'index' => $index,
'commentable_key' => $commentableKey,
'pagination_limit' => $paginationLimit,
'sort_by' => $sortBy,
], [
'index' => ['required', 'integer', 'min:0'],
'commentable_key' => ['required', Rule::in(config('comment.commentable_keys'))],
'pagination_limit' => ['required', 'integer', 'max:'.config('comment.pagination_limit')],
'sort_by' => ['required', 'string'],
]);

$commentableType = Relation::getMorphedModel($commentableKey);
Log::debug('Getting paginated comments', [
'commentable_type' => $commentableType,
'commentable_id' => $commentableId,
'index' => $index,
'sort_by' => $sortBy,
'pagination_limit' => $paginationLimit,
]);

// Get the root comments:
$query = Comment::where([
'commentable_type' => $commentableKey,
'commentable_id' => $commentableId,
'depth' => 1,
]);

// Apply sorting on the comments
$query = app(GeneralVotesSortingManager::class)->applySort($query, $sortBy);

$rootComments = $query->get();
Log::debug('Root comments retrieved', [
'count' => $rootComments->count(),
'commentable_type' => $commentableType,
'commentable_id' => $commentableId,
]);

// Initialize variables
$currentCommentsSum = 0;
$resultingPaginatedComments = [];
$currentIndex = 0;
$hasMoreComments = false;

foreach ($rootComments as $comment) {
$childrenCount = $comment->children_count + 1;

// Handle comments that exceed MAX when alone in a page
if ($currentCommentsSum + $childrenCount > $paginationLimit) {
if ($currentCommentsSum === 0) {
// Force include oversized comment
if ($currentIndex === $index) {
$resultingPaginatedComments[] = $comment;
$currentCommentsSum += $childrenCount;
}
$currentIndex++;

continue;
}

$currentIndex++;
$currentCommentsSum = 0;
}

// Now we know that there exists more comments to load later
if ($currentIndex > $index) {
$hasMoreComments = true;
break;
}
// Only add comments for the desired index
elseif ($currentIndex === $index) {
$resultingPaginatedComments[] = $comment;
}
$currentCommentsSum += $childrenCount;
}

$nestedComments = new Collection($resultingPaginatedComments);
// Lazy eager load the user for the root comment and for each reply.
$nestedComments->load(['user', 'replies.user']);

// Flatten the comments and replies into the desired format.
$flattenedComments = collect();

foreach ($nestedComments as $comment) {
// Transform the root comment.
$flattenedComments->push(
new CommentResource($comment)
);

// Transform any loaded replies.
if ($comment->relationLoaded('replies')) {
foreach ($comment->replies as $reply) {
$flattenedComments->push(
new CommentResource($reply)
);
}
}
}

// Extract unique users into a separate collection.
$users = collect();

foreach ($flattenedComments as $comment) {
if ($comment->relationLoaded('user') && $comment->user) {
$users->put($comment->user->id, new UserResource($comment->user));
}
}

Log::debug('Returning paginated comments', [
'comments_count' => $flattenedComments->count(),
'users_count' => $users->count(),
'has_more_comments' => $hasMoreComments,
'current_index' => $index,
]);

return [
'comments' => $flattenedComments,
'users' => $users->values(),
'has_more_comments' => $hasMoreComments,
];
}

/**
* Create and save a comment
*
* @throws Exception
*/
public function createComment(array $validatedData): Comment
{
DB::beginTransaction();

$comment = new Comment;
$comment->content = $validatedData['content'];
$comment->user_id = Auth::id();

$commentableKey = $validatedData['commentable_key'];
$commentableModel = Relation::getMorphedModel($commentableKey);
$commentableId = $validatedData['commentable_id'];

// Ensure that the model exists
$model = $commentableModel::find($commentableId);
if (! $model) {
throw new NotFoundHttpException;
}

// Set the commentable type
$comment->commentable_type = $commentableKey;
$comment->commentable_id = $commentableId;

// Top level comment
$parentCommentId = $validatedData['parent_comment_id'];
if (! $parentCommentId) {
$comment->parent_comment_id = null;
$comment->depth = 1;
$comment->children_count = 0;
}
// Is reply to a comment
else {
$parent = Comment::find($parentCommentId);
$new_comment_depth = $parent->depth + 1;

// Check if the parent is the root comment
if ($parent->depth == 1) {
$root_comment = $parent;
$root_comment_id = $parent->id;
} else {
// If not, fetch the root comment
$root_comment_id = $parent->root_comment_id;
$root_comment = Comment::find($root_comment_id);
}

$replies_count = $root_comment->children_count ?? 0;

// Ensure that they are commenting to the same root
// And the depth is not exceeded
Validator::validate(
[
'commentable_id' => $commentableId,
'commentable_type' => $commentableKey,
'depth' => $new_comment_depth,
'replies_count' => $replies_count,
],
[
'commentable_id' => [
'required',
Rule::in([$parent->commentable_id]),
],
'commentable_type' => [
'required',
Rule::in([$parent->commentable_type]),
],
// Cannot exceed the max depth
'depth' => [
'required',
'integer',
'lte:'.(config('comment.max_depth')),
],
// Cannot exceed max replies
'replies_count' => [
'required',
'integer',
'lt:'.(config('comment.max_replies')),
],
]
);

// Set the parent id
$comment->parent_comment_id = $parentCommentId;

// Set the parent's root as this comment's root, unless it is the root itself.
$comment->root_comment_id = $root_comment_id;

// Set the new depth
$comment->depth = $new_comment_depth;

// Update the children count for root
$root_comment->children_count = $root_comment->children_count + 1;
$root_comment->save();
}

$comment->save();

Log::debug('New comment saved', [
'comment_id' => $comment->id,
'user_id' => $comment->user_id,
'commentable_type' => $comment->commentable_type,
'commentable_id' => $comment->commentable_id,
'depth' => $comment->depth,
]);

DB::commit();

return $comment;
}
}
Copy link

Copilot AI Dec 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A CommentService.php file has been created in the app/Events directory, but this appears to be a duplicate of the existing CommentService in app/Services. Event classes should be placed in the Events directory, while service classes belong in the Services directory. This file should be removed as it's in the wrong location.

Copilot uses AI. Check for mistakes.

DB::commit();

$this->upvoteService->upvote('resource', $resource->id);
Copy link

Copilot AI Dec 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The upvote call is made outside of the database transaction. If the upvote operation fails, the resource will still be created but without its automatic upvote. Consider moving the upvote call inside the transaction before the commit, or handle the failure scenario appropriately.

Copilot uses AI. Check for mistakes.
preserve-state
prefetch
cache-for="10s"
cache-for="2s"
Copy link

Copilot AI Dec 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cache duration has been reduced from "10s" to "2s". This change could impact performance by causing more frequent server requests. Consider whether this aggressive cache invalidation is necessary or if it could be optimized differently.

Suggested change
cache-for="2s"
cache-for="10s"

Copilot uses AI. Check for mistakes.
Comment on lines 220 to 227
<ResponsiveNavLink
:href="route('resources.index', { tab: 'edits' })"
:active="route().current('resources.index') && route().params.tab === 'edits'"
class="text-primaryDark dark:text-primary hover:text-primary dark:hover:text-primaryLight"
>
<Icon icon="mdi:pencil-box-multiple" class="mr-2" />
Resource Edits
</ResponsiveNavLink>
Copy link

Copilot AI Dec 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mobile navigation menu links to a different route than the desktop navigation. The desktop navigation links to route('resource_edits.index') while the mobile navigation links to route('resources.index', { tab: 'edits' }). This inconsistency could confuse users when switching between devices.

Copilot uses AI. Check for mistakes.
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback

Comment on lines 45 to 49
DB::commit();

$this->upvoteService->upvote('comment', $comment->id);

Copy link

Copilot AI Dec 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The upvote call is made outside of the database transaction. If the upvote operation fails, the comment will still be created but without its automatic upvote. Consider moving the upvote call inside the transaction before the commit, or handle the failure scenario appropriately.

Suggested change
DB::commit();
$this->upvoteService->upvote('comment', $comment->id);
$this->upvoteService->upvote('comment', $comment->id);
DB::commit();

Copilot uses AI. Check for mistakes.
'review_id' => $review->id,
]);

$this->upvoteService->upvote('review', $review->id);
Copy link

Copilot AI Dec 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The upvote call is made outside of any database transaction. If the upvote operation fails, the review will still be created but without its automatic upvote. Consider wrapping both operations in a transaction or handling the failure scenario appropriately.

Copilot uses AI. Check for mistakes.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copy link
Contributor

Copilot AI commented Dec 26, 2025

@AllanKoder I've opened a new pull request, #53, to work on those changes. Once the pull request is ready, I'll request review from you.

AllanKoder and others added 4 commits December 26, 2025 14:06
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Initial plan

* Fix mobile navigation to use consistent route for Resource Edits

Co-authored-by: AllanKoder <74692833+AllanKoder@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: AllanKoder <74692833+AllanKoder@users.noreply.github.com>
@AllanKoder AllanKoder merged commit a6f6904 into master Dec 26, 2025
3 checks passed
@AllanKoder AllanKoder deleted the resource-edits-tab branch December 26, 2025 21:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants