From c0aa22adb095192fb8a6b888df65f327cfbe0ab5 Mon Sep 17 00:00:00 2001 From: Chen Asraf Date: Wed, 18 Mar 2026 11:11:53 +0200 Subject: [PATCH] feat: editable templates --- lib/Controller/TemplateController.php | 171 ++++++ lib/Db/Template.php | 69 +++ lib/Db/TemplateMapper.php | 86 +++ lib/Migration/Version25Date20260318000000.php | 70 +++ openapi-full.json | 474 +++++++++++++++ openapi.json | 474 +++++++++++++++ src/components/BBCodeEditor/BBCodeEditor.vue | 7 + .../BBCodeToolbar/BBCodeToolbar.test.ts | 11 + .../BBCodeToolbar/BBCodeToolbar.vue | 55 +- src/components/PostEditForm/PostEditForm.vue | 1 + .../PostReplyForm/PostReplyForm.vue | 1 + .../TemplateModal/TemplateModal.test.ts | 563 ++++++++++++++++++ .../TemplateModal/TemplateModal.vue | 478 +++++++++++++++ src/components/TemplateModal/index.ts | 2 + .../ThreadCreateForm/ThreadCreateForm.vue | 1 + src/test-mocks.ts | 24 +- src/types/models.ts | 11 + .../Controller/TemplateControllerTest.php | 340 +++++++++++ tests/unit/Db/TemplateMapperTest.php | 142 +++++ 19 files changed, 2977 insertions(+), 3 deletions(-) create mode 100644 lib/Controller/TemplateController.php create mode 100644 lib/Db/Template.php create mode 100644 lib/Db/TemplateMapper.php create mode 100644 lib/Migration/Version25Date20260318000000.php create mode 100644 src/components/TemplateModal/TemplateModal.test.ts create mode 100644 src/components/TemplateModal/TemplateModal.vue create mode 100644 src/components/TemplateModal/index.ts create mode 100644 tests/unit/Controller/TemplateControllerTest.php create mode 100644 tests/unit/Db/TemplateMapperTest.php diff --git a/lib/Controller/TemplateController.php b/lib/Controller/TemplateController.php new file mode 100644 index 0000000..41c4174 --- /dev/null +++ b/lib/Controller/TemplateController.php @@ -0,0 +1,171 @@ + +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace OCA\Forum\Controller; + +use OCA\Forum\Db\Template; +use OCA\Forum\Db\TemplateMapper; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\ApiRoute; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\OCSController; +use OCP\IRequest; +use Psr\Log\LoggerInterface; + +class TemplateController extends OCSController { + public function __construct( + string $appName, + IRequest $request, + private TemplateMapper $templateMapper, + private LoggerInterface $logger, + private ?string $userId, + ) { + parent::__construct($appName, $request); + } + + /** + * List current user's templates + * + * @param string|null $visibility Optional visibility filter (threads, replies) + * @return DataResponse>, array{}> + * + * 200: Templates returned + */ + #[NoAdminRequired] + #[ApiRoute(verb: 'GET', url: '/api/templates')] + public function index(?string $visibility = null): DataResponse { + try { + $templates = $this->templateMapper->findByUserId($this->userId, $visibility); + return new DataResponse(array_map(fn ($t) => $t->jsonSerialize(), $templates)); + } catch (\Exception $e) { + $this->logger->error('Error fetching templates: ' . $e->getMessage()); + return new DataResponse(['error' => 'Failed to fetch templates'], Http::STATUS_INTERNAL_SERVER_ERROR); + } + } + + /** + * Create a template + * + * @param string $name Template name + * @param string $content Template content (BBCode) + * @param string $visibility Visibility setting + * @param int $sortOrder Sort order + * @return DataResponse, array{}> + * + * 201: Template created + */ + #[NoAdminRequired] + #[ApiRoute(verb: 'POST', url: '/api/templates')] + public function create( + string $name, + string $content, + string $visibility = 'both', + int $sortOrder = 0, + ): DataResponse { + try { + $now = time(); + $template = new Template(); + $template->setUserId($this->userId); + $template->setName($name); + $template->setContent($content); + $template->setVisibility($visibility); + $template->setSortOrder($sortOrder); + $template->setCreatedAt($now); + $template->setUpdatedAt($now); + + /** @var Template */ + $created = $this->templateMapper->insert($template); + return new DataResponse($created->jsonSerialize(), Http::STATUS_CREATED); + } catch (\Exception $e) { + $this->logger->error('Error creating template: ' . $e->getMessage()); + return new DataResponse(['error' => 'Failed to create template'], Http::STATUS_INTERNAL_SERVER_ERROR); + } + } + + /** + * Update a template + * + * @param int $id Template ID + * @param string|null $name Template name + * @param string|null $content Template content + * @param string|null $visibility Visibility setting + * @param int|null $sortOrder Sort order + * @return DataResponse, array{}> + * + * 200: Template updated + */ + #[NoAdminRequired] + #[ApiRoute(verb: 'PUT', url: '/api/templates/{id}')] + public function update( + int $id, + ?string $name = null, + ?string $content = null, + ?string $visibility = null, + ?int $sortOrder = null, + ): DataResponse { + try { + $template = $this->templateMapper->find($id); + + if ($template->getUserId() !== $this->userId) { + return new DataResponse(['error' => 'Forbidden'], Http::STATUS_FORBIDDEN); + } + + if ($name !== null) { + $template->setName($name); + } + if ($content !== null) { + $template->setContent($content); + } + if ($visibility !== null) { + $template->setVisibility($visibility); + } + if ($sortOrder !== null) { + $template->setSortOrder($sortOrder); + } + $template->setUpdatedAt(time()); + + /** @var Template */ + $updated = $this->templateMapper->update($template); + return new DataResponse($updated->jsonSerialize()); + } catch (DoesNotExistException $e) { + return new DataResponse(['error' => 'Template not found'], Http::STATUS_NOT_FOUND); + } catch (\Exception $e) { + $this->logger->error('Error updating template: ' . $e->getMessage()); + return new DataResponse(['error' => 'Failed to update template'], Http::STATUS_INTERNAL_SERVER_ERROR); + } + } + + /** + * Delete a template + * + * @param int $id Template ID + * @return DataResponse + * + * 200: Template deleted + */ + #[NoAdminRequired] + #[ApiRoute(verb: 'DELETE', url: '/api/templates/{id}')] + public function destroy(int $id): DataResponse { + try { + $template = $this->templateMapper->find($id); + + if ($template->getUserId() !== $this->userId) { + return new DataResponse(['error' => 'Forbidden'], Http::STATUS_FORBIDDEN); + } + + $this->templateMapper->delete($template); + return new DataResponse(['success' => true]); + } catch (DoesNotExistException $e) { + return new DataResponse(['error' => 'Template not found'], Http::STATUS_NOT_FOUND); + } catch (\Exception $e) { + $this->logger->error('Error deleting template: ' . $e->getMessage()); + return new DataResponse(['error' => 'Failed to delete template'], Http::STATUS_INTERNAL_SERVER_ERROR); + } + } +} diff --git a/lib/Db/Template.php b/lib/Db/Template.php new file mode 100644 index 0000000..2315f40 --- /dev/null +++ b/lib/Db/Template.php @@ -0,0 +1,69 @@ + +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace OCA\Forum\Db; + +use JsonSerializable; + +use OCP\AppFramework\Db\Entity; + +/** + * @method int getId() + * @method void setId(int $value) + * @method string getUserId() + * @method void setUserId(string $value) + * @method string getName() + * @method void setName(string $value) + * @method string getContent() + * @method void setContent(string $value) + * @method string getVisibility() + * @method void setVisibility(string $value) + * @method int getSortOrder() + * @method void setSortOrder(int $value) + * @method int getCreatedAt() + * @method void setCreatedAt(int $value) + * @method int getUpdatedAt() + * @method void setUpdatedAt(int $value) + */ +class Template extends Entity implements JsonSerializable { + public const VISIBILITY_THREADS = 'threads'; + public const VISIBILITY_REPLIES = 'replies'; + public const VISIBILITY_BOTH = 'both'; + public const VISIBILITY_NEITHER = 'neither'; + + protected $userId; + protected $name; + protected $content; + protected $visibility; + protected $sortOrder; + protected $createdAt; + protected $updatedAt; + + public function __construct() { + $this->addType('id', 'integer'); + $this->addType('userId', 'string'); + $this->addType('name', 'string'); + $this->addType('content', 'string'); + $this->addType('visibility', 'string'); + $this->addType('sortOrder', 'integer'); + $this->addType('createdAt', 'integer'); + $this->addType('updatedAt', 'integer'); + } + + public function jsonSerialize(): array { + return [ + 'id' => $this->getId(), + 'userId' => $this->getUserId(), + 'name' => $this->getName(), + 'content' => $this->getContent(), + 'visibility' => $this->getVisibility(), + 'sortOrder' => $this->getSortOrder(), + 'createdAt' => $this->getCreatedAt(), + 'updatedAt' => $this->getUpdatedAt(), + ]; + } +} diff --git a/lib/Db/TemplateMapper.php b/lib/Db/TemplateMapper.php new file mode 100644 index 0000000..e2aca58 --- /dev/null +++ b/lib/Db/TemplateMapper.php @@ -0,0 +1,86 @@ + +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace OCA\Forum\Db; + +use OCA\Forum\AppInfo\Application; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Db\QBMapper; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; + +/** + * @template-extends QBMapper