namespace BookStack\Activity\Controllers;
use BookStack\Activity\CommentRepo;
+use BookStack\Activity\Models\Comment;
use BookStack\Http\ApiController;
use Illuminate\Http\JsonResponse;
+/**
+ * The comment data model has a 'local_id' property, which is a unique integer ID
+ * scoped to the page which the comment is on. The 'parent_id' is used for replies
+ * and refers to the 'local_id' of the parent comment on the same page, not the main
+ * globally unique 'id'.
+ */
class CommentApiController extends ApiController
{
// TODO - Add tree-style comment listing to page-show responses.
- // TODO - list
// TODO - create
- // TODO - read
// TODO - update
// TODO - delete
// TODO - Test visibility controls
// TODO - Test permissions of each action
- // TODO - Support intro block for API docs so we can explain the
- // properties for comments in a shared kind of way?
-
public function __construct(
protected CommentRepo $commentRepo,
) {
}
-
/**
* Get a listing of comments visible to the user.
*/
'id', 'commentable_id', 'commentable_type', 'parent_id', 'local_id', 'content_ref', 'created_by', 'updated_by', 'created_at', 'updated_at'
]);
}
+
+ /**
+ * Read the details of a single comment, along with its direct replies.
+ */
+ public function read(string $id): JsonResponse
+ {
+ $comment = $this->commentRepo->getQueryForVisible()
+ ->where('id', '=', $id)->firstOrFail();
+
+ $replies = $this->commentRepo->getQueryForVisible()
+ ->where('parent_id', '=', $comment->local_id)
+ ->where('commentable_id', '=', $comment->commentable_id)
+ ->where('commentable_type', '=', $comment->commentable_type)
+ ->get();
+
+ /** @var Comment[] $toProcess */
+ $toProcess = [$comment, ...$replies];
+ foreach ($toProcess as $commentToProcess) {
+ $commentToProcess->setAttribute('html', $commentToProcess->safeHtml());
+ $commentToProcess->makeVisible('html');
+ }
+
+ $comment->setRelation('replies', $replies);
+
+ return response()->json($comment);
+ }
}
/**
* @property int $id
- * @property string $text - Deprecated & now unused (#4821)
* @property string $html
* @property int|null $parent_id - Relates to local_id, not id
* @property int $local_id
use HasCreatorAndUpdater;
protected $fillable = ['parent_id'];
+ protected $hidden = ['html'];
+
+ protected $casts = [
+ 'archived' => 'boolean',
+ ];
/**
* Get the entity that this comment belongs to.
protected function loadDetailsFromControllers(Collection $routes): Collection
{
return $routes->map(function (array $route) {
+ $class = $this->getReflectionClass($route['controller']);
$method = $this->getReflectionMethod($route['controller'], $route['controller_method']);
$comment = $method->getDocComment();
- $route['description'] = $comment ? $this->parseDescriptionFromMethodComment($comment) : null;
+ $route['description'] = $comment ? $this->parseDescriptionFromDocBlockComment($comment) : null;
$route['body_params'] = $this->getBodyParamsFromClass($route['controller'], $route['controller_method']);
+ // Load class description for the model
+ // Not ideal to have it here on each route, but adding it in a more structured manner would break
+ // docs resulting JSON format and therefore be an API break.
+ // Save refactoring for a more significant set of changes.
+ $classComment = $class->getDocComment();
+ $route['model_description'] = $classComment ? $this->parseDescriptionFromDocBlockComment($classComment) : null;
+
return $route;
});
}
/**
* Parse out the description text from a class method comment.
*/
- protected function parseDescriptionFromMethodComment(string $comment): string
+ protected function parseDescriptionFromDocBlockComment(string $comment): string
{
$matches = [];
preg_match_all('/^\s*?\*\s?($|((?![\/@\s]).*?))$/m', $comment, $matches);
* @throws ReflectionException
*/
protected function getReflectionMethod(string $className, string $methodName): ReflectionMethod
+ {
+ return $this->getReflectionClass($className)->getMethod($methodName);
+ }
+
+ /**
+ * Get a reflection class from the given class name.
+ *
+ * @throws ReflectionException
+ */
+ protected function getReflectionClass(string $className): ReflectionClass
{
$class = $this->reflectionClasses[$className] ?? null;
if ($class === null) {
$this->reflectionClasses[$className] = $class;
}
- return $class->getMethod($methodName);
+ return $class;
}
/**
/**
* View the details of a single book.
- * The response data will contain 'content' property listing the chapter and pages directly within, in
+ * The response data will contain a 'content' property listing the chapter and pages directly within, in
* the same structure as you'd see within the BookStack interface when viewing a book. Top-level
* contents will have a 'type' property to distinguish between pages & chapters.
*/
@foreach($docs as $model => $endpoints)
<section class="card content-wrap auto-height">
<h1 class="list-heading text-capitals">{{ $model }}</h1>
-
+ @if($endpoints[0]['model_description'])
+ <p>{{ $endpoints[0]['model_description'] }}</p>
+ @endif
@foreach($endpoints as $endpoint)
@include('api-docs.parts.endpoint', ['endpoint' => $endpoint, 'loop' => $loop])
@endforeach
Route::get('search', [SearchApiController::class, 'all']);
Route::get('comments', [ActivityControllers\CommentApiController::class, 'list']);
+Route::post('comments', [ActivityControllers\CommentApiController::class, 'create']);
+Route::get('comments/{id}', [ActivityControllers\CommentApiController::class, 'read']);
+Route::put('comments/{id}', [ActivityControllers\CommentApiController::class, 'update']);
+Route::delete('comments/{id}', [ActivityControllers\CommentApiController::class, 'delete']);
Route::get('shelves', [EntityControllers\BookshelfApiController::class, 'list']);
Route::post('shelves', [EntityControllers\BookshelfApiController::class, 'create']);