From 69a4fc338c60e1dabad0e587da9524a7e5542252 Mon Sep 17 00:00:00 2001 From: Christian Hartmann Date: Mon, 11 May 2026 15:42:54 +0200 Subject: [PATCH] feat: add support for comments in forms Signed-off-by: Christian Hartmann --- lib/AppInfo/Application.php | 3 + lib/Constants.php | 2 + lib/Controller/PageController.php | 6 +- lib/Db/Form.php | 6 + lib/FormsMigrator.php | 1 + lib/Listener/CommentsEntityListener.php | 43 +++++ .../Version050300Date20260511121033.php | 44 +++++ lib/ResponseDefinitions.php | 1 + lib/Service/ConfigService.php | 5 + openapi.json | 6 +- src/Forms.vue | 7 +- src/FormsSettings.vue | 15 ++ .../SidebarTabs/SettingsSidebarTab.vue | 12 ++ src/views/Sidebar.vue | 182 +++++++++++++++++- tests/Integration/Api/ApiV3Test.php | 5 + .../Api/RespectAdminSettingsTest.php | 4 + tests/Integration/DB/SharedFormsTest.php | 5 + tests/Integration/DB/SubmissionMapperTest.php | 2 + tests/Unit/Controller/ApiControllerTest.php | 5 +- tests/Unit/Controller/PageControllerTest.php | 4 + tests/Unit/FormsMigratorTest.php | 16 +- tests/Unit/Service/ConfigServiceTest.php | 7 +- tests/Unit/Service/FormsServiceTest.php | 2 + 23 files changed, 364 insertions(+), 19 deletions(-) create mode 100644 lib/Listener/CommentsEntityListener.php create mode 100644 lib/Migration/Version050300Date20260511121033.php diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 82c17f12f..37737da59 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -13,6 +13,7 @@ use OCA\Forms\Capabilities; use OCA\Forms\FormsMigrator; use OCA\Forms\Listener\AnalyticsDatasourceListener; +use OCA\Forms\Listener\CommentsEntityListener; use OCA\Forms\Listener\UserDeletedListener; use OCA\Forms\Middleware\ThrottleFormAccessMiddleware; use OCA\Forms\Search\SearchProvider; @@ -20,6 +21,7 @@ use OCP\AppFramework\Bootstrap\IBootContext; use OCP\AppFramework\Bootstrap\IBootstrap; use OCP\AppFramework\Bootstrap\IRegistrationContext; +use OCP\Comments\CommentsEntityEvent; use OCP\User\Events\UserDeletedEvent; class Application extends App implements IBootstrap { @@ -44,6 +46,7 @@ public function register(IRegistrationContext $context): void { $context->registerCapability(Capabilities::class); $context->registerEventListener(UserDeletedEvent::class, UserDeletedListener::class); $context->registerEventListener(DatasourceEvent::class, AnalyticsDatasourceListener::class); + $context->registerEventListener(CommentsEntityEvent::class, CommentsEntityListener::class); $context->registerMiddleware(ThrottleFormAccessMiddleware::class); $context->registerSearchProvider(SearchProvider::class); $context->registerUserMigrator(FormsMigrator::class); diff --git a/lib/Constants.php b/lib/Constants.php index fa0c347db..d16a8bad0 100644 --- a/lib/Constants.php +++ b/lib/Constants.php @@ -20,6 +20,7 @@ class Constants { public const CONFIG_KEY_RESTRICTCREATION = 'restrictCreation'; public const CONFIG_KEY_ALLOWCONFIRMATIONEMAIL = 'allowConfirmationEmail'; public const CONFIG_KEY_CONFIRMATIONEMAILRATELIMIT = 'confirmationEmailRateLimit'; + public const CONFIG_KEY_ALLOWCOMMENTS = 'allowComments'; public const CONFIG_KEYS = [ self::CONFIG_KEY_ALLOWPERMITALL, self::CONFIG_KEY_ALLOWPUBLICLINK, @@ -28,6 +29,7 @@ class Constants { self::CONFIG_KEY_RESTRICTCREATION, self::CONFIG_KEY_ALLOWCONFIRMATIONEMAIL, self::CONFIG_KEY_CONFIRMATIONEMAILRATELIMIT, + self::CONFIG_KEY_ALLOWCOMMENTS, ]; /** diff --git a/lib/Controller/PageController.php b/lib/Controller/PageController.php index 43db2366a..91892f01a 100644 --- a/lib/Controller/PageController.php +++ b/lib/Controller/PageController.php @@ -29,6 +29,7 @@ use OCP\AppFramework\Http\Template\PublicTemplateResponse; use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Services\IInitialState; +use OCP\Comments\ICommentsManager; use OCP\IL10N; use OCP\IRequest; use OCP\IURLGenerator; @@ -51,6 +52,7 @@ public function __construct( private FormsService $formsService, private IAccountManager $accountManager, private IInitialState $initialState, + private ICommentsManager $commentsManager, private IL10N $l10n, private IUrlGenerator $urlGenerator, private IUserManager $userManager, @@ -66,7 +68,9 @@ public function __construct( #[NoCSRFRequired()] #[FrontpageRoute(verb: 'GET', url: '/')] public function index(?string $hash = null, ?int $submissionId = null): TemplateResponse { - Util::addScript($this->appName, 'forms-main'); + // Ensure the Comments client is available and load comments resources + $this->commentsManager->load(); + Util::addScript($this->appName, 'forms-main', 'comments'); Util::addStyle($this->appName, 'forms'); Util::addStyle($this->appName, 'forms-style'); $this->insertHeaderOnIos(); diff --git a/lib/Db/Form.php b/lib/Db/Form.php index fe035cfe1..239f9635c 100644 --- a/lib/Db/Form.php +++ b/lib/Db/Form.php @@ -61,6 +61,8 @@ * @method void setConfirmationEmailBody(string|null $value) * @method int|null getConfirmationEmailQuestionId() * @method void setConfirmationEmailQuestionId(int|null $value) + * @method bool getAllowComments() + * @method void setAllowComments(bool $value) */ class Form extends Entity { protected $hash; @@ -86,6 +88,7 @@ class Form extends Entity { protected $confirmationEmailSubject; protected $confirmationEmailBody; protected $confirmationEmailQuestionId; + protected $allowComments; /** * Form constructor. @@ -104,6 +107,7 @@ public function __construct() { $this->addType('maxSubmissions', 'integer'); $this->addType('confirmationEmailEnabled', 'boolean'); $this->addType('confirmationEmailQuestionId', 'integer'); + $this->addType('allowComments', 'boolean'); } // JSON-Decoding of access-column. @@ -182,6 +186,7 @@ public function setAccess(array $access): void { * confirmationEmailSubject: ?string, * confirmationEmailBody: ?string, * confirmationEmailQuestionId: ?int, + * allowComments: bool, * } */ public function read() { @@ -210,6 +215,7 @@ public function read() { 'confirmationEmailSubject' => $this->getConfirmationEmailSubject(), 'confirmationEmailBody' => $this->getConfirmationEmailBody(), 'confirmationEmailQuestionId' => $this->getConfirmationEmailQuestionId(), + 'allowComments' => $this->getAllowComments(), ]; } } diff --git a/lib/FormsMigrator.php b/lib/FormsMigrator.php index e21f25701..3b8f78f19 100644 --- a/lib/FormsMigrator.php +++ b/lib/FormsMigrator.php @@ -153,6 +153,7 @@ public function import(IUser $user, IImportSource $importSource, OutputInterface $form->setConfirmationEmailSubject($formData['confirmationEmailSubject'] ?? null); $form->setConfirmationEmailBody($formData['confirmationEmailBody'] ?? null); $form->setConfirmationEmailQuestionId(null); // Set to null initially, updated after questions are imported + $form->setAllowComments($formData['allowComments'] ?? false); $this->formMapper->insert($form); diff --git a/lib/Listener/CommentsEntityListener.php b/lib/Listener/CommentsEntityListener.php new file mode 100644 index 000000000..68ac82c4c --- /dev/null +++ b/lib/Listener/CommentsEntityListener.php @@ -0,0 +1,43 @@ + + */ +class CommentsEntityListener implements IEventListener { + public function __construct( + protected FormMapper $formMapper, + ) { + } + + #[\Override] + public function handle(Event $event): void { + if (!$event instanceof CommentsEntityEvent) { + return; + } + + // Register the 'forms' entity collection so the Comments app can + // check whether a given form id allows comments. + $event->addEntityCollection('forms', function ($formId) { + try { + $form = $this->formMapper->findById((int)$formId); + } catch (DoesNotExistException $e) { + return false; + } + return (bool)$form->getAllowComments(); + }); + } +} diff --git a/lib/Migration/Version050300Date20260511121033.php b/lib/Migration/Version050300Date20260511121033.php new file mode 100644 index 000000000..1301567ba --- /dev/null +++ b/lib/Migration/Version050300Date20260511121033.php @@ -0,0 +1,44 @@ +getTable('forms_v2_forms'); + $changed = false; + + if (!$table->hasColumn('allow_comments')) { + $table->addColumn('allow_comments', Types::BOOLEAN, [ + 'notnull' => false, + 'default' => 0, + ]); + $changed = true; + } + + return $changed ? $schema : null; + } +} diff --git a/lib/ResponseDefinitions.php b/lib/ResponseDefinitions.php index e71b0e51b..47f4e0739 100644 --- a/lib/ResponseDefinitions.php +++ b/lib/ResponseDefinitions.php @@ -145,6 +145,7 @@ * confirmationEmailSubject: ?string, * confirmationEmailBody: ?string, * confirmationEmailQuestionId: ?int, + * allowComments: bool, * } * * @psalm-type FormsUploadedFile = array{ diff --git a/lib/Service/ConfigService.php b/lib/Service/ConfigService.php index 9bb6a19b8..db12750cd 100644 --- a/lib/Service/ConfigService.php +++ b/lib/Service/ConfigService.php @@ -64,6 +64,10 @@ public function getConfirmationEmailRateLimit(): int { return $this->appConfig->getAppValueInt(Constants::CONFIG_KEY_CONFIRMATIONEMAILRATELIMIT, 3); } + public function getAllowComments(): bool { + return $this->appConfig->getAppValueBool(Constants::CONFIG_KEY_ALLOWCOMMENTS, false); + } + /** * Provide the full AppConfig */ @@ -77,6 +81,7 @@ public function getAppConfig(): array { Constants::CONFIG_KEY_ALLOWCONFIRMATIONEMAIL => $this->getAllowConfirmationEmail(), Constants::CONFIG_KEY_CONFIRMATIONEMAILRATELIMIT => $this->getConfirmationEmailRateLimit(), 'isMailConfigured' => $this->isMailConfigured(), + Constants::CONFIG_KEY_ALLOWCOMMENTS => $this->getAllowComments(), // Additional, calculated information out of Config 'canCreateForms' => $this->canCreateForms() diff --git a/openapi.json b/openapi.json index a423c4546..9381509fc 100644 --- a/openapi.json +++ b/openapi.json @@ -123,7 +123,8 @@ "confirmationEmailEnabled", "confirmationEmailSubject", "confirmationEmailBody", - "confirmationEmailQuestionId" + "confirmationEmailQuestionId", + "allowComments" ], "properties": { "id": { @@ -252,6 +253,9 @@ "type": "integer", "format": "int64", "nullable": true + }, + "allowComments": { + "type": "boolean" } } }, diff --git a/src/Forms.vue b/src/Forms.vue index af83b7e7e..324ab85b0 100644 --- a/src/Forms.vue +++ b/src/Forms.vue @@ -126,7 +126,10 @@ @update:sidebarOpened="sidebarOpened = $event" @openSharing="openSharing" /> + + + {{ t('forms', 'Allow comments') }} + + @@ -192,6 +201,12 @@ export default { await this.saveAppConfig('confirmationEmailRateLimit', value) }, + async onAllowCommentsChange(newVal) { + this.loading.allowComments = true + await this.saveAppConfig('allowComments', newVal) + this.loading.allowComments = false + }, + /** * Save a key-value pair to the appConfig. * diff --git a/src/components/SidebarTabs/SettingsSidebarTab.vue b/src/components/SidebarTabs/SettingsSidebarTab.vue index 723a37ee2..df5dc5dde 100644 --- a/src/components/SidebarTabs/SettingsSidebarTab.vue +++ b/src/components/SidebarTabs/SettingsSidebarTab.vue @@ -49,6 +49,14 @@ @update:modelValue="onAllowEditSubmissionsChange"> {{ t('forms', 'Allow editing own responses') }} + + {{ t('forms', 'Allow comments') }} + - + @@ -25,6 +29,7 @@ @@ -37,25 +42,42 @@ :lockedUntil="lockedUntilFormatted" @update:formProp="onPropertyChange" /> + + + + +
+