aboutsummaryrefslogtreecommitdiffstats
path: root/apps
diff options
context:
space:
mode:
authorjld3103 <jld3103yt@gmail.com>2023-12-07 16:39:16 +0100
committerAndrey Borysenko <andrey18106x@gmail.com>2024-03-12 13:56:54 +0200
commit4ac2375ca2082750432ccc9cff46bf5888b4db30 (patch)
treebca24a21f4dfa0184f8e400e9508fc5600ade8d4 /apps
parentc42397358f05aa60ae91ed11e7754fddba182cce (diff)
downloadnextcloud-server-4ac2375ca2082750432ccc9cff46bf5888b4db30.tar.gz
nextcloud-server-4ac2375ca2082750432ccc9cff46bf5888b4db30.zip
feat: Add declarative settings
Signed-off-by: jld3103 <jld3103yt@gmail.com> Signed-off-by: Julien Veyssier <julien-nc@posteo.net> Signed-off-by: Andrey Borysenko <andrey18106x@gmail.com>
Diffstat (limited to 'apps')
-rw-r--r--apps/settings/appinfo/routes.php6
-rw-r--r--apps/settings/composer/composer/autoload_classmap.php2
-rw-r--r--apps/settings/composer/composer/autoload_static.php2
-rw-r--r--apps/settings/composer/composer/installed.php4
-rw-r--r--apps/settings/lib/Controller/AdminSettingsController.php11
-rw-r--r--apps/settings/lib/Controller/CommonSettingsTrait.php63
-rw-r--r--apps/settings/lib/Controller/DeclarativeSettingsController.php105
-rw-r--r--apps/settings/lib/Controller/PersonalSettingsController.php8
-rw-r--r--apps/settings/lib/ResponseDefinitions.php56
-rw-r--r--apps/settings/openapi-administration.json65
-rw-r--r--apps/settings/openapi-full.json433
-rw-r--r--apps/settings/openapi.json358
-rw-r--r--apps/settings/src/components/DeclarativeSettings/DeclarativeSection.vue268
-rw-r--r--apps/settings/src/main-declarative-settings-forms.ts50
-rw-r--r--apps/settings/tests/Controller/AdminSettingsControllerTest.php17
-rw-r--r--apps/testing/composer/composer/autoload_classmap.php4
-rw-r--r--apps/testing/composer/composer/autoload_static.php4
-rw-r--r--apps/testing/composer/composer/installed.php4
-rw-r--r--apps/testing/lib/AppInfo/Application.php12
-rw-r--r--apps/testing/lib/Listener/GetDeclarativeSettingsValueListener.php32
-rw-r--r--apps/testing/lib/Listener/RegisterDeclarativeSettingsListener.php68
-rw-r--r--apps/testing/lib/Listener/SetDeclarativeSettingsValueListener.php32
-rw-r--r--apps/testing/lib/Settings/DeclarativeSettingsForm.php172
23 files changed, 1736 insertions, 40 deletions
diff --git a/apps/settings/appinfo/routes.php b/apps/settings/appinfo/routes.php
index e8a3869fe11..7bb946d1934 100644
--- a/apps/settings/appinfo/routes.php
+++ b/apps/settings/appinfo/routes.php
@@ -81,5 +81,9 @@ return [
['name' => 'WebAuthn#deleteRegistration', 'url' => '/settings/api/personal/webauthn/registration/{id}', 'verb' => 'DELETE' , 'root' => ''],
['name' => 'Reasons#getPdf', 'url' => '/settings/download/reasons', 'verb' => 'GET', 'root' => ''],
- ]
+ ],
+ 'ocs' => [
+ ['name' => 'DeclarativeSettings#setValue', 'url' => '/settings/api/declarative/value', 'verb' => 'POST', 'root' => ''],
+ ['name' => 'DeclarativeSettings#getForms', 'url' => '/settings/api/declarative/forms', 'verb' => 'GET', 'root' => ''],
+ ],
];
diff --git a/apps/settings/composer/composer/autoload_classmap.php b/apps/settings/composer/composer/autoload_classmap.php
index c3cf0763ed9..b9709c8ad28 100644
--- a/apps/settings/composer/composer/autoload_classmap.php
+++ b/apps/settings/composer/composer/autoload_classmap.php
@@ -27,6 +27,7 @@ return array(
'OCA\\Settings\\Controller\\ChangePasswordController' => $baseDir . '/../lib/Controller/ChangePasswordController.php',
'OCA\\Settings\\Controller\\CheckSetupController' => $baseDir . '/../lib/Controller/CheckSetupController.php',
'OCA\\Settings\\Controller\\CommonSettingsTrait' => $baseDir . '/../lib/Controller/CommonSettingsTrait.php',
+ 'OCA\\Settings\\Controller\\DeclarativeSettingsController' => $baseDir . '/../lib/Controller/DeclarativeSettingsController.php',
'OCA\\Settings\\Controller\\HelpController' => $baseDir . '/../lib/Controller/HelpController.php',
'OCA\\Settings\\Controller\\LogSettingsController' => $baseDir . '/../lib/Controller/LogSettingsController.php',
'OCA\\Settings\\Controller\\MailSettingsController' => $baseDir . '/../lib/Controller/MailSettingsController.php',
@@ -43,6 +44,7 @@ return array(
'OCA\\Settings\\Listener\\UserRemovedFromGroupActivityListener' => $baseDir . '/../lib/Listener/UserRemovedFromGroupActivityListener.php',
'OCA\\Settings\\Mailer\\NewUserMailHelper' => $baseDir . '/../lib/Mailer/NewUserMailHelper.php',
'OCA\\Settings\\Middleware\\SubadminMiddleware' => $baseDir . '/../lib/Middleware/SubadminMiddleware.php',
+ 'OCA\\Settings\\ResponseDefinitions' => $baseDir . '/../lib/ResponseDefinitions.php',
'OCA\\Settings\\Search\\AppSearch' => $baseDir . '/../lib/Search/AppSearch.php',
'OCA\\Settings\\Search\\SectionSearch' => $baseDir . '/../lib/Search/SectionSearch.php',
'OCA\\Settings\\Search\\UserSearch' => $baseDir . '/../lib/Search/UserSearch.php',
diff --git a/apps/settings/composer/composer/autoload_static.php b/apps/settings/composer/composer/autoload_static.php
index 88f71b026f5..67808ad23f2 100644
--- a/apps/settings/composer/composer/autoload_static.php
+++ b/apps/settings/composer/composer/autoload_static.php
@@ -42,6 +42,7 @@ class ComposerStaticInitSettings
'OCA\\Settings\\Controller\\ChangePasswordController' => __DIR__ . '/..' . '/../lib/Controller/ChangePasswordController.php',
'OCA\\Settings\\Controller\\CheckSetupController' => __DIR__ . '/..' . '/../lib/Controller/CheckSetupController.php',
'OCA\\Settings\\Controller\\CommonSettingsTrait' => __DIR__ . '/..' . '/../lib/Controller/CommonSettingsTrait.php',
+ 'OCA\\Settings\\Controller\\DeclarativeSettingsController' => __DIR__ . '/..' . '/../lib/Controller/DeclarativeSettingsController.php',
'OCA\\Settings\\Controller\\HelpController' => __DIR__ . '/..' . '/../lib/Controller/HelpController.php',
'OCA\\Settings\\Controller\\LogSettingsController' => __DIR__ . '/..' . '/../lib/Controller/LogSettingsController.php',
'OCA\\Settings\\Controller\\MailSettingsController' => __DIR__ . '/..' . '/../lib/Controller/MailSettingsController.php',
@@ -58,6 +59,7 @@ class ComposerStaticInitSettings
'OCA\\Settings\\Listener\\UserRemovedFromGroupActivityListener' => __DIR__ . '/..' . '/../lib/Listener/UserRemovedFromGroupActivityListener.php',
'OCA\\Settings\\Mailer\\NewUserMailHelper' => __DIR__ . '/..' . '/../lib/Mailer/NewUserMailHelper.php',
'OCA\\Settings\\Middleware\\SubadminMiddleware' => __DIR__ . '/..' . '/../lib/Middleware/SubadminMiddleware.php',
+ 'OCA\\Settings\\ResponseDefinitions' => __DIR__ . '/..' . '/../lib/ResponseDefinitions.php',
'OCA\\Settings\\Search\\AppSearch' => __DIR__ . '/..' . '/../lib/Search/AppSearch.php',
'OCA\\Settings\\Search\\SectionSearch' => __DIR__ . '/..' . '/../lib/Search/SectionSearch.php',
'OCA\\Settings\\Search\\UserSearch' => __DIR__ . '/..' . '/../lib/Search/UserSearch.php',
diff --git a/apps/settings/composer/composer/installed.php b/apps/settings/composer/composer/installed.php
index 1a66c7f2416..d2b87e1bdfd 100644
--- a/apps/settings/composer/composer/installed.php
+++ b/apps/settings/composer/composer/installed.php
@@ -3,7 +3,7 @@
'name' => '__root__',
'pretty_version' => 'dev-master',
'version' => 'dev-master',
- 'reference' => 'b1797842784b250fb01ed5e3bf130705eb94751b',
+ 'reference' => '4ff660ca2e0baa02440ba07296ed7e75fa544c0e',
'type' => 'library',
'install_path' => __DIR__ . '/../',
'aliases' => array(),
@@ -13,7 +13,7 @@
'__root__' => array(
'pretty_version' => 'dev-master',
'version' => 'dev-master',
- 'reference' => 'b1797842784b250fb01ed5e3bf130705eb94751b',
+ 'reference' => '4ff660ca2e0baa02440ba07296ed7e75fa544c0e',
'type' => 'library',
'install_path' => __DIR__ . '/../',
'aliases' => array(),
diff --git a/apps/settings/lib/Controller/AdminSettingsController.php b/apps/settings/lib/Controller/AdminSettingsController.php
index 7b0313c9fa7..8da03607a79 100644
--- a/apps/settings/lib/Controller/AdminSettingsController.php
+++ b/apps/settings/lib/Controller/AdminSettingsController.php
@@ -30,12 +30,14 @@ use OC\AppFramework\Middleware\Security\Exceptions\NotAdminException;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\Attribute\OpenAPI;
use OCP\AppFramework\Http\TemplateResponse;
+use OCP\AppFramework\Services\IInitialState;
use OCP\Group\ISubAdmin;
use OCP\IGroupManager;
use OCP\INavigationManager;
use OCP\IRequest;
use OCP\IUser;
use OCP\IUserSession;
+use OCP\Settings\IDeclarativeManager;
use OCP\Settings\IManager as ISettingsManager;
use OCP\Template;
@@ -50,7 +52,9 @@ class AdminSettingsController extends Controller {
ISettingsManager $settingsManager,
IUserSession $userSession,
IGroupManager $groupManager,
- ISubAdmin $subAdmin
+ ISubAdmin $subAdmin,
+ IDeclarativeManager $declarativeSettingsManager,
+ IInitialState $initialState,
) {
parent::__construct($appName, $request);
$this->navigationManager = $navigationManager;
@@ -58,6 +62,8 @@ class AdminSettingsController extends Controller {
$this->userSession = $userSession;
$this->groupManager = $groupManager;
$this->subAdmin = $subAdmin;
+ $this->declarativeSettingsManager = $declarativeSettingsManager;
+ $this->initialState = $initialState;
}
/**
@@ -80,7 +86,8 @@ class AdminSettingsController extends Controller {
$user = $this->userSession->getUser();
$isSubAdmin = !$this->groupManager->isAdmin($user->getUID()) && $this->subAdmin->isSubAdmin($user);
$settings = $this->settingsManager->getAllowedAdminSettings($section, $user);
- if (empty($settings)) {
+ $declarativeFormIDs = $this->declarativeSettingsManager->getFormIDs($user, 'admin', $section);
+ if (empty($settings) && empty($declarativeFormIDs)) {
throw new NotAdminException("Logged in user doesn't have permission to access these settings.");
}
$formatted = $this->formatSettings($settings);
diff --git a/apps/settings/lib/Controller/CommonSettingsTrait.php b/apps/settings/lib/Controller/CommonSettingsTrait.php
index 5d683d7d824..ab51deadfc3 100644
--- a/apps/settings/lib/Controller/CommonSettingsTrait.php
+++ b/apps/settings/lib/Controller/CommonSettingsTrait.php
@@ -26,17 +26,26 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
+
namespace OCA\Settings\Controller;
+use OCA\Settings\AppInfo\Application;
use OCP\AppFramework\Http\TemplateResponse;
+use OCP\AppFramework\Services\IInitialState;
use OCP\Group\ISubAdmin;
use OCP\IGroupManager;
use OCP\INavigationManager;
use OCP\IUserSession;
+use OCP\Settings\IDeclarativeManager;
+use OCP\Settings\IDeclarativeSettingsForm;
use OCP\Settings\IIconSection;
use OCP\Settings\IManager as ISettingsManager;
use OCP\Settings\ISettings;
+use OCP\Util;
+/**
+ * @psalm-import-type DeclarativeSettingsFormField from IDeclarativeSettingsForm
+ */
trait CommonSettingsTrait {
/** @var ISettingsManager */
@@ -54,28 +63,26 @@ trait CommonSettingsTrait {
/** @var ISubAdmin */
private $subAdmin;
+ private IDeclarativeManager $declarativeSettingsManager;
+
+ /** @var IInitialState */
+ private $initialState;
+
/**
* @return array{forms: array{personal: array, admin: array}}
*/
private function getNavigationParameters(string $currentType, string $currentSection): array {
- $templateParameters = [
- 'personal' => $this->formatPersonalSections($currentType, $currentSection),
- 'admin' => []
- ];
-
- $templateParameters['admin'] = $this->formatAdminSections(
- $currentType,
- $currentSection
- );
-
return [
- 'forms' => $templateParameters
+ 'forms' => [
+ 'personal' => $this->formatPersonalSections($currentType, $currentSection),
+ 'admin' => $this->formatAdminSections($currentType, $currentSection),
+ ],
];
}
/**
* @param IIconSection[][] $sections
- * @psam-param 'admin'|'personal' $type
+ * @psalm-param 'admin'|'personal' $type
* @return list<array{anchor: string, section-name: string, active: bool, icon: string}>
*/
protected function formatSections(array $sections, string $currentSection, string $type, string $currentType): array {
@@ -87,7 +94,11 @@ trait CommonSettingsTrait {
} elseif ($type === 'personal') {
$settings = $this->settingsManager->getPersonalSettings($section->getID());
}
- if (empty($settings) && !($section->getID() === 'additional' && count(\OC_App::getForms('admin')) > 0)) {
+
+ /** @psalm-suppress PossiblyNullArgument */
+ $declarativeFormIDs = $this->declarativeSettingsManager->getFormIDs($this->userSession->getUser(), $type, $section->getID());
+
+ if (empty($settings) && empty($declarativeFormIDs) && !($section->getID() === 'additional' && count(\OC_App::getForms('admin')) > 0)) {
continue;
}
@@ -107,14 +118,14 @@ trait CommonSettingsTrait {
return $templateParameters;
}
- protected function formatPersonalSections(string $currentType, string $currentSections): array {
+ protected function formatPersonalSections(string $currentType, string $currentSection): array {
$sections = $this->settingsManager->getPersonalSections();
- return $this->formatSections($sections, $currentSections, 'personal', $currentType);
+ return $this->formatSections($sections, $currentSection, 'personal', $currentType);
}
- protected function formatAdminSections(string $currentType, string $currentSections): array {
+ protected function formatAdminSections(string $currentType, string $currentSection): array {
$sections = $this->settingsManager->getAdminSections();
- return $this->formatSections($sections, $currentSections, 'admin', $currentType);
+ return $this->formatSections($sections, $currentSection, 'admin', $currentType);
}
/**
@@ -133,6 +144,9 @@ trait CommonSettingsTrait {
return ['content' => $html];
}
+ /**
+ * @psalm-param 'admin'|'personal' $type
+ */
private function getIndexResponse(string $type, string $section): TemplateResponse {
if ($type === 'personal') {
if ($section === 'theming') {
@@ -144,9 +158,24 @@ trait CommonSettingsTrait {
$this->navigationManager->setActiveEntry('admin_settings');
}
+ $this->declarativeSettingsManager->loadSchemas();
+
$templateParams = [];
$templateParams = array_merge($templateParams, $this->getNavigationParameters($type, $section));
$templateParams = array_merge($templateParams, $this->getSettings($section));
+
+ /** @psalm-suppress PossiblyNullArgument */
+ $declarativeFormIDs = $this->declarativeSettingsManager->getFormIDs($this->userSession->getUser(), $type, $section);
+ if (!empty($declarativeFormIDs)) {
+ foreach ($declarativeFormIDs as $app => $ids) {
+ /** @psalm-suppress PossiblyUndefinedArrayOffset */
+ $templateParams['content'] .= join(array_map(fn (string $id) => '<div id="' . $app . '_' . $id . '"></div>', $ids));
+ }
+ Util::addScript(Application::APP_ID, 'declarative-settings-forms');
+ /** @psalm-suppress PossiblyNullArgument */
+ $this->initialState->provideInitialState('declarative-settings-forms', $this->declarativeSettingsManager->getFormsWithValues($this->userSession->getUser(), $type, $section));
+ }
+
$activeSection = $this->settingsManager->getSection($type, $section);
if ($activeSection) {
$templateParams['pageTitle'] = $activeSection->getName();
diff --git a/apps/settings/lib/Controller/DeclarativeSettingsController.php b/apps/settings/lib/Controller/DeclarativeSettingsController.php
new file mode 100644
index 00000000000..71347066290
--- /dev/null
+++ b/apps/settings/lib/Controller/DeclarativeSettingsController.php
@@ -0,0 +1,105 @@
+<?php
+/**
+ * @copyright Copyright (c) 2023 Kate Döen <kate.doeen@nextcloud.com>
+ *
+ * @author Kate Döen <kate.doeen@nextcloud.com>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCA\Settings\Controller;
+
+use Exception;
+use OC\AppFramework\Middleware\Security\Exceptions\NotAdminException;
+use OC\AppFramework\Middleware\Security\Exceptions\NotLoggedInException;
+use OCA\Settings\ResponseDefinitions;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\Attribute\NoAdminRequired;
+use OCP\AppFramework\Http\DataResponse;
+use OCP\AppFramework\OCS\OCSBadRequestException;
+use OCP\AppFramework\OCSController;
+use OCP\IRequest;
+use OCP\IUserSession;
+use OCP\Settings\IDeclarativeManager;
+use Psr\Log\LoggerInterface;
+
+/**
+ * @psalm-import-type SettingsDeclarativeForm from ResponseDefinitions
+ */
+class DeclarativeSettingsController extends OCSController {
+ public function __construct(
+ string $appName,
+ IRequest $request,
+ private IUserSession $userSession,
+ private IDeclarativeManager $declarativeManager,
+ private LoggerInterface $logger,
+ ) {
+ parent::__construct($appName, $request);
+ }
+
+ /**
+ * Sets a declarative settings value
+ *
+ * @param string $app ID of the app
+ * @param string $formId ID of the form
+ * @param string $fieldId ID of the field
+ * @param mixed $value Value to be saved
+ * @return DataResponse<Http::STATUS_OK, null, array{}>
+ * @throws NotLoggedInException Not logged in or not an admin user
+ * @throws NotAdminException Not logged in or not an admin user
+ * @throws OCSBadRequestException Invalid arguments to save value
+ *
+ * 200: Value set successfully
+ */
+ #[NoAdminRequired]
+ public function setValue(string $app, string $formId, string $fieldId, mixed $value): DataResponse {
+ $user = $this->userSession->getUser();
+ if ($user === null) {
+ throw new NotLoggedInException();
+ }
+
+ try {
+ $this->declarativeManager->loadSchemas();
+ $this->declarativeManager->setValue($user, $app, $formId, $fieldId, $value);
+ return new DataResponse(null);
+ } catch (NotAdminException $e) {
+ throw $e;
+ } catch (Exception $e) {
+ $this->logger->error('Failed to set declarative settings value: ' . $e->getMessage());
+ throw new OCSBadRequestException();
+ }
+ }
+
+ /**
+ * Gets all declarative forms with the values prefilled.
+ *
+ * @return DataResponse<Http::STATUS_OK, list<SettingsDeclarativeForm>, array{}>
+ * @throws NotLoggedInException
+ * @NoSubAdminRequired
+ *
+ * 200: Forms returned
+ */
+ #[NoAdminRequired]
+ public function getForms(): DataResponse {
+ $user = $this->userSession->getUser();
+ if ($user === null) {
+ throw new NotLoggedInException();
+ }
+ $this->declarativeManager->loadSchemas();
+ return new DataResponse($this->declarativeManager->getFormsWithValues($user, null, null));
+ }
+}
diff --git a/apps/settings/lib/Controller/PersonalSettingsController.php b/apps/settings/lib/Controller/PersonalSettingsController.php
index 7d219f5c165..57da74cd99a 100644
--- a/apps/settings/lib/Controller/PersonalSettingsController.php
+++ b/apps/settings/lib/Controller/PersonalSettingsController.php
@@ -29,11 +29,13 @@ namespace OCA\Settings\Controller;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\Attribute\OpenAPI;
use OCP\AppFramework\Http\TemplateResponse;
+use OCP\AppFramework\Services\IInitialState;
use OCP\Group\ISubAdmin;
use OCP\IGroupManager;
use OCP\INavigationManager;
use OCP\IRequest;
use OCP\IUserSession;
+use OCP\Settings\IDeclarativeManager;
use OCP\Settings\IManager as ISettingsManager;
use OCP\Template;
@@ -48,7 +50,9 @@ class PersonalSettingsController extends Controller {
ISettingsManager $settingsManager,
IUserSession $userSession,
IGroupManager $groupManager,
- ISubAdmin $subAdmin
+ ISubAdmin $subAdmin,
+ IDeclarativeManager $declarativeSettingsManager,
+ IInitialState $initialState,
) {
parent::__construct($appName, $request);
$this->navigationManager = $navigationManager;
@@ -56,6 +60,8 @@ class PersonalSettingsController extends Controller {
$this->userSession = $userSession;
$this->subAdmin = $subAdmin;
$this->groupManager = $groupManager;
+ $this->declarativeSettingsManager = $declarativeSettingsManager;
+ $this->initialState = $initialState;
}
/**
diff --git a/apps/settings/lib/ResponseDefinitions.php b/apps/settings/lib/ResponseDefinitions.php
new file mode 100644
index 00000000000..1887a04de08
--- /dev/null
+++ b/apps/settings/lib/ResponseDefinitions.php
@@ -0,0 +1,56 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright Copyright (c) 2024 Kate Döen <kate.doeen@nextcloud.com>
+ *
+ * @author Kate Döen <kate.doeen@nextcloud.com>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCA\Settings;
+
+/**
+ * @psalm-type SettingsDeclarativeFormField = array{
+ * id: string,
+ * title: string,
+ * description?: string,
+ * type: 'text'|'password'|'email'|'tel'|'url'|'number'|'checkbox'|'multi-checkbox'|'radio'|'select'|'multi-select',
+ * placeholder?: string,
+ * label?: string,
+ * default: mixed,
+ * options?: list<string|array{name: string, value: mixed}>,
+ * value: string|int|float|bool|list<string>,
+ * }
+ *
+ * @psalm-type SettingsDeclarativeForm = array{
+ * id: string,
+ * priority: int,
+ * section_type: 'admin'|'personal',
+ * section_id: string,
+ * storage_type: 'internal'|'external',
+ * title: string,
+ * description?: string,
+ * doc_url?: string,
+ * app: string,
+ * fields: list<SettingsDeclarativeFormField>,
+ * }
+ */
+class ResponseDefinitions {
+}
diff --git a/apps/settings/openapi-administration.json b/apps/settings/openapi-administration.json
new file mode 100644
index 00000000000..5d39237779a
--- /dev/null
+++ b/apps/settings/openapi-administration.json
@@ -0,0 +1,65 @@
+{
+ "openapi": "3.0.3",
+ "info": {
+ "title": "settings-administration",
+ "version": "0.0.1",
+ "description": "Nextcloud settings",
+ "license": {
+ "name": "agpl"
+ }
+ },
+ "components": {
+ "securitySchemes": {
+ "basic_auth": {
+ "type": "http",
+ "scheme": "basic"
+ },
+ "bearer_auth": {
+ "type": "http",
+ "scheme": "bearer"
+ }
+ },
+ "schemas": {}
+ },
+ "paths": {
+ "/index.php/settings/admin/log/download": {
+ "get": {
+ "operationId": "log_settings-download",
+ "summary": "download logfile",
+ "description": "This endpoint requires admin access",
+ "tags": [
+ "log_settings"
+ ],
+ "security": [
+ {
+ "bearer_auth": []
+ },
+ {
+ "basic_auth": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Logfile returned",
+ "headers": {
+ "Content-Disposition": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ },
+ "content": {
+ "application/octet-stream": {
+ "schema": {
+ "type": "string",
+ "format": "binary"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "tags": []
+} \ No newline at end of file
diff --git a/apps/settings/openapi-full.json b/apps/settings/openapi-full.json
new file mode 100644
index 00000000000..99a2b8b97c1
--- /dev/null
+++ b/apps/settings/openapi-full.json
@@ -0,0 +1,433 @@
+{
+ "openapi": "3.0.3",
+ "info": {
+ "title": "settings-full",
+ "version": "0.0.1",
+ "description": "Nextcloud settings",
+ "license": {
+ "name": "agpl"
+ }
+ },
+ "components": {
+ "securitySchemes": {
+ "basic_auth": {
+ "type": "http",
+ "scheme": "basic"
+ },
+ "bearer_auth": {
+ "type": "http",
+ "scheme": "bearer"
+ }
+ },
+ "schemas": {
+ "DeclarativeForm": {
+ "type": "object",
+ "required": [
+ "id",
+ "priority",
+ "section_type",
+ "section_id",
+ "storage_type",
+ "title",
+ "app",
+ "fields"
+ ],
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "priority": {
+ "type": "integer",
+ "format": "int64"
+ },
+ "section_type": {
+ "type": "string",
+ "enum": [
+ "admin",
+ "personal"
+ ]
+ },
+ "section_id": {
+ "type": "string"
+ },
+ "storage_type": {
+ "type": "string",
+ "enum": [
+ "internal",
+ "external"
+ ]
+ },
+ "title": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "doc_url": {
+ "type": "string"
+ },
+ "app": {
+ "type": "string"
+ },
+ "fields": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/DeclarativeFormField"
+ }
+ }
+ }
+ },
+ "DeclarativeFormField": {
+ "type": "object",
+ "required": [
+ "id",
+ "title",
+ "type",
+ "default",
+ "value"
+ ],
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "enum": [
+ "text",
+ "password",
+ "email",
+ "tel",
+ "url",
+ "number",
+ "checkbox",
+ "multi-checkbox",
+ "radio",
+ "select",
+ "multi-select"
+ ]
+ },
+ "placeholder": {
+ "type": "string"
+ },
+ "label": {
+ "type": "string"
+ },
+ "default": {
+ "type": "object"
+ },
+ "options": {
+ "type": "array",
+ "items": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "object",
+ "required": [
+ "name",
+ "value"
+ ],
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "value": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "value": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "integer",
+ "format": "int64"
+ },
+ {
+ "type": "number",
+ "format": "float"
+ },
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ }
+ }
+ },
+ "OCSMeta": {
+ "type": "object",
+ "required": [
+ "status",
+ "statuscode"
+ ],
+ "properties": {
+ "status": {
+ "type": "string"
+ },
+ "statuscode": {
+ "type": "integer"
+ },
+ "message": {
+ "type": "string"
+ },
+ "totalitems": {
+ "type": "string"
+ },
+ "itemsperpage": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "paths": {
+ "/index.php/settings/admin/log/download": {
+ "get": {
+ "operationId": "log_settings-download",
+ "summary": "download logfile",
+ "description": "This endpoint requires admin access",
+ "tags": [
+ "log_settings"
+ ],
+ "security": [
+ {
+ "bearer_auth": []
+ },
+ {
+ "basic_auth": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Logfile returned",
+ "headers": {
+ "Content-Disposition": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ },
+ "content": {
+ "application/octet-stream": {
+ "schema": {
+ "type": "string",
+ "format": "binary"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/ocs/v2.php/settings/api/declarative/value": {
+ "post": {
+ "operationId": "declarative_settings-set-value",
+ "summary": "Sets a declarative settings value",
+ "tags": [
+ "declarative_settings"
+ ],
+ "security": [
+ {
+ "bearer_auth": []
+ },
+ {
+ "basic_auth": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "app",
+ "in": "query",
+ "description": "ID of the app",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "formId",
+ "in": "query",
+ "description": "ID of the form",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "fieldId",
+ "in": "query",
+ "description": "ID of the field",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "value",
+ "in": "query",
+ "description": "Value to be saved",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "OCS-APIRequest",
+ "in": "header",
+ "description": "Required to be true for the API request to pass",
+ "required": true,
+ "schema": {
+ "type": "boolean",
+ "default": true
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Value set successfully",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": [
+ "ocs"
+ ],
+ "properties": {
+ "ocs": {
+ "type": "object",
+ "required": [
+ "meta",
+ "data"
+ ],
+ "properties": {
+ "meta": {
+ "$ref": "#/components/schemas/OCSMeta"
+ },
+ "data": {
+ "nullable": true
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Not logged in or not an admin user",
+ "content": {
+ "text/plain": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid arguments to save value",
+ "content": {
+ "text/plain": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/ocs/v2.php/settings/api/declarative/forms": {
+ "get": {
+ "operationId": "declarative_settings-get-forms",
+ "summary": "Gets all declarative forms with the values prefilled.",
+ "tags": [
+ "declarative_settings"
+ ],
+ "security": [
+ {
+ "bearer_auth": []
+ },
+ {
+ "basic_auth": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "OCS-APIRequest",
+ "in": "header",
+ "description": "Required to be true for the API request to pass",
+ "required": true,
+ "schema": {
+ "type": "boolean",
+ "default": true
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Forms returned",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": [
+ "ocs"
+ ],
+ "properties": {
+ "ocs": {
+ "type": "object",
+ "required": [
+ "meta",
+ "data"
+ ],
+ "properties": {
+ "meta": {
+ "$ref": "#/components/schemas/OCSMeta"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/DeclarativeForm"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "",
+ "content": {
+ "text/plain": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "tags": []
+} \ No newline at end of file
diff --git a/apps/settings/openapi.json b/apps/settings/openapi.json
index 217a0fae9f7..e9591eaf346 100644
--- a/apps/settings/openapi.json
+++ b/apps/settings/openapi.json
@@ -19,16 +19,192 @@
"scheme": "bearer"
}
},
- "schemas": {}
+ "schemas": {
+ "DeclarativeForm": {
+ "type": "object",
+ "required": [
+ "id",
+ "priority",
+ "section_type",
+ "section_id",
+ "storage_type",
+ "title",
+ "app",
+ "fields"
+ ],
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "priority": {
+ "type": "integer",
+ "format": "int64"
+ },
+ "section_type": {
+ "type": "string",
+ "enum": [
+ "admin",
+ "personal"
+ ]
+ },
+ "section_id": {
+ "type": "string"
+ },
+ "storage_type": {
+ "type": "string",
+ "enum": [
+ "internal",
+ "external"
+ ]
+ },
+ "title": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "doc_url": {
+ "type": "string"
+ },
+ "app": {
+ "type": "string"
+ },
+ "fields": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/DeclarativeFormField"
+ }
+ }
+ }
+ },
+ "DeclarativeFormField": {
+ "type": "object",
+ "required": [
+ "id",
+ "title",
+ "type",
+ "default",
+ "value"
+ ],
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "enum": [
+ "text",
+ "password",
+ "email",
+ "tel",
+ "url",
+ "number",
+ "checkbox",
+ "multi-checkbox",
+ "radio",
+ "select",
+ "multi-select"
+ ]
+ },
+ "placeholder": {
+ "type": "string"
+ },
+ "label": {
+ "type": "string"
+ },
+ "default": {
+ "type": "object"
+ },
+ "options": {
+ "type": "array",
+ "items": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "object",
+ "required": [
+ "name",
+ "value"
+ ],
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "value": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "value": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "integer",
+ "format": "int64"
+ },
+ {
+ "type": "number",
+ "format": "float"
+ },
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ }
+ }
+ },
+ "OCSMeta": {
+ "type": "object",
+ "required": [
+ "status",
+ "statuscode"
+ ],
+ "properties": {
+ "status": {
+ "type": "string"
+ },
+ "statuscode": {
+ "type": "integer"
+ },
+ "message": {
+ "type": "string"
+ },
+ "totalitems": {
+ "type": "string"
+ },
+ "itemsperpage": {
+ "type": "string"
+ }
+ }
+ }
+ }
},
"paths": {
- "/index.php/settings/admin/log/download": {
- "get": {
- "operationId": "log_settings-download",
- "summary": "download logfile",
- "description": "This endpoint requires admin access",
+ "/ocs/v2.php/settings/api/declarative/value": {
+ "post": {
+ "operationId": "declarative_settings-set-value",
+ "summary": "Sets a declarative settings value",
"tags": [
- "log_settings"
+ "declarative_settings"
],
"security": [
{
@@ -38,21 +214,175 @@
"basic_auth": []
}
],
+ "parameters": [
+ {
+ "name": "app",
+ "in": "query",
+ "description": "ID of the app",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "formId",
+ "in": "query",
+ "description": "ID of the form",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "fieldId",
+ "in": "query",
+ "description": "ID of the field",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "value",
+ "in": "query",
+ "description": "Value to be saved",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "OCS-APIRequest",
+ "in": "header",
+ "description": "Required to be true for the API request to pass",
+ "required": true,
+ "schema": {
+ "type": "boolean",
+ "default": true
+ }
+ }
+ ],
"responses": {
"200": {
- "description": "Logfile returned",
- "headers": {
- "Content-Disposition": {
+ "description": "Value set successfully",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": [
+ "ocs"
+ ],
+ "properties": {
+ "ocs": {
+ "type": "object",
+ "required": [
+ "meta",
+ "data"
+ ],
+ "properties": {
+ "meta": {
+ "$ref": "#/components/schemas/OCSMeta"
+ },
+ "data": {
+ "nullable": true
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Not logged in or not an admin user",
+ "content": {
+ "text/plain": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid arguments to save value",
+ "content": {
+ "text/plain": {
"schema": {
"type": "string"
}
}
- },
+ }
+ }
+ }
+ }
+ },
+ "/ocs/v2.php/settings/api/declarative/forms": {
+ "get": {
+ "operationId": "declarative_settings-get-forms",
+ "summary": "Gets all declarative forms with the values prefilled.",
+ "tags": [
+ "declarative_settings"
+ ],
+ "security": [
+ {
+ "bearer_auth": []
+ },
+ {
+ "basic_auth": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "OCS-APIRequest",
+ "in": "header",
+ "description": "Required to be true for the API request to pass",
+ "required": true,
+ "schema": {
+ "type": "boolean",
+ "default": true
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Forms returned",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": [
+ "ocs"
+ ],
+ "properties": {
+ "ocs": {
+ "type": "object",
+ "required": [
+ "meta",
+ "data"
+ ],
+ "properties": {
+ "meta": {
+ "$ref": "#/components/schemas/OCSMeta"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/DeclarativeForm"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "",
"content": {
- "application/octet-stream": {
+ "text/plain": {
"schema": {
- "type": "string",
- "format": "binary"
+ "type": "string"
}
}
}
diff --git a/apps/settings/src/components/DeclarativeSettings/DeclarativeSection.vue b/apps/settings/src/components/DeclarativeSettings/DeclarativeSection.vue
new file mode 100644
index 00000000000..879eb8e62d8
--- /dev/null
+++ b/apps/settings/src/components/DeclarativeSettings/DeclarativeSection.vue
@@ -0,0 +1,268 @@
+<template>
+ <NcSettingsSection
+ class="declarative-settings-section"
+ :name="t(formApp, form.title)"
+ :description="t(formApp, form.description)"
+ :doc-url="form.doc_url || ''">
+ <div v-for="formField in formFields"
+ :key="formField.id"
+ class="declarative-form-field"
+ :aria-label="t('settings', '{app}\'s declarative setting field: {name}', { app: formApp, name: t(formApp, formField.title) })"
+ :class="{
+ 'declarative-form-field-text': isTextFormField(formField),
+ 'declarative-form-field-select': formField.type === 'select',
+ 'declarative-form-field-multi-select': formField.type === 'multi-select',
+ 'declarative-form-field-checkbox': formField.type === 'checkbox',
+ 'declarative-form-field-multi_checkbox': formField.type === 'multi-checkbox',
+ 'declarative-form-field-radio': formField.type === 'radio'
+ }">
+
+ <template v-if="isTextFormField(formField)">
+ <div class="input-wrapper">
+ <NcInputField
+ :type="formField.type"
+ :label="t(formApp, formField.title)"
+ :value.sync="formFieldsData[formField.id].value"
+ :placeholder="t(formApp, formField.placeholder)"
+ @update:value="onChangeDebounced(formField)"
+ @submit="updateDeclarativeSettingsValue(formField)"/>
+ </div>
+ <span class="hint">{{ t(formApp, formField.description) }}</span>
+ </template>
+
+ <template v-if="formField.type === 'select'">
+ <label :for="formField.id + '_field'">{{ t(formApp, formField.title) }}</label>
+ <div class="input-wrapper">
+ <NcSelect
+ :id="formField.id + '_field'"
+ :options="formField.options"
+ :placeholder="t(formApp, formField.placeholder)"
+ :label-outside="true"
+ :value="formFieldsData[formField.id].value"
+ @input="(value) => updateFormFieldDataValue(value, formField, true)"/>
+ </div>
+ <span class="hint">{{ t(formApp, formField.description) }}</span>
+ </template>
+
+ <template v-if="formField.type === 'multi-select'">
+ <label :for="formField.id + '_field'">{{ t(formApp, formField.title) }}</label>
+ <div class="input-wrapper">
+ <NcSelect
+ :id="formField.id + '_field'"
+ :options="formField.options"
+ :placeholder="t(formApp, formField.placeholder)"
+ :multiple="true"
+ :label-outside="true"
+ :value="formFieldsData[formField.id].value"
+ @input="(value) => {
+ formFieldsData[formField.id].value = value
+ updateDeclarativeSettingsValue(formField, JSON.stringify(formFieldsData[formField.id].value))
+ }
+ "/>
+ </div>
+ <span class="hint">{{ t(formApp, formField.description) }}</span>
+ </template>
+
+ <template v-if="formField.type === 'checkbox'">
+ <label :for="formField.id + '_field'">{{ t(formApp, formField.title) }}</label>
+ <NcCheckboxRadioSwitch
+ :id="formField.id + '_field'"
+ :checked="Boolean(formFieldsData[formField.id].value)"
+ @update:checked="(value) => {
+ formField.value = value
+ updateFormFieldDataValue(+value, formField, true)
+ }
+ ">
+ {{ t(formApp, formField.label) }}
+ </NcCheckboxRadioSwitch>
+ <span class="hint">{{ t(formApp, formField.description) }}</span>
+ </template>
+
+ <template v-if="formField.type === 'multi-checkbox'">
+ <label :for="formField.id + '_field'">{{ t(formApp, formField.title) }}</label>
+ <NcCheckboxRadioSwitch
+ v-for="option in formField.options"
+ :id="formField.id + '_field_' + option.value"
+ :key="option.value"
+ :checked="formFieldsData[formField.id].value[option.value]"
+ @update:checked="(value) => {
+ formFieldsData[formField.id].value[option.value] = value
+ // Update without re-generating initial formFieldsData.value object as the link to components are lost
+ updateDeclarativeSettingsValue(formField, JSON.stringify(formFieldsData[formField.id].value))
+ }
+ ">
+ {{ t(formApp, option.name) }}
+ </NcCheckboxRadioSwitch>
+ <span class="hint">{{ t(formApp, formField.description) }}</span>
+ </template>
+
+ <template v-if="formField.type === 'radio'">
+ <label :for="formField.id + '_field'">{{ t(formApp, formField.title) }}</label>
+ <NcCheckboxRadioSwitch
+ v-for="option in formField.options"
+ :key="option.value"
+ :value="option.value"
+ type="radio"
+ :checked="formFieldsData[formField.id].value"
+ @update:checked="(value) => updateFormFieldDataValue(value, formField, true)">
+ {{ t(formApp, option.name) }}
+ </NcCheckboxRadioSwitch>
+ <span class="hint">{{ t(formApp, formField.description) }}</span>
+ </template>
+ </div>
+ </NcSettingsSection>
+</template>
+
+<script>
+import axios from '@nextcloud/axios'
+import { generateOcsUrl } from '@nextcloud/router'
+import { showError } from '@nextcloud/dialogs'
+import debounce from 'debounce'
+import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection.js'
+import NcInputField from '@nextcloud/vue/dist/Components/NcInputField.js'
+import NcSelect from '@nextcloud/vue/dist/Components/NcSelect.js'
+import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
+
+export default {
+ name: 'DeclarativeSection',
+ components: {
+ NcSettingsSection,
+ NcInputField,
+ NcSelect,
+ NcCheckboxRadioSwitch,
+ },
+ props: {
+ form: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ formFieldsData: {},
+ }
+ },
+ beforeMount() {
+ this.initFormFieldsData()
+ },
+ computed: {
+ formApp() {
+ return this.form.app || ''
+ },
+ formFields() {
+ return this.form.fields || []
+ },
+ },
+ methods: {
+ initFormFieldsData() {
+ this.form.fields.forEach((formField) => {
+ if (formField.type === 'checkbox') {
+ // convert bool to number using unary plus (+) operator
+ this.$set(formField, 'value', +formField.value)
+ }
+ if (formField.type === 'multi-checkbox') {
+ if (formField.value === '') {
+ // Init formFieldsData from options
+ this.$set(formField, 'value', {})
+ formField.options.forEach(option => {
+ this.$set(formField.value, option.value, false)
+ })
+ } else {
+ this.$set(formField, 'value', JSON.parse(formField.value))
+ // Merge possible new options
+ formField.options.forEach(option => {
+ if (!formField.value.hasOwnProperty(option.value)) {
+ this.$set(formField.value, option.value, false)
+ }
+ })
+ // Remove options that are not in the form anymore
+ Object.keys(formField.value).forEach(key => {
+ if (!formField.options.find(option => option.value === key)) {
+ delete formField.value[key]
+ }
+ })
+ }
+ }
+ if (formField.type === 'multi-select') {
+ if (formField.value === '') {
+ // Init empty array for multi-select
+ this.$set(formField, 'value', [])
+ } else {
+ // JSON decode an array of multiple values set
+ this.$set(formField, 'value', JSON.parse(formField.value))
+ }
+ }
+ this.$set(this.formFieldsData, formField.id, {
+ value: formField.value,
+ })
+ })
+ },
+
+ updateFormFieldDataValue(value, formField, update = false) {
+ this.formFieldsData[formField.id].value = value
+ if (update) {
+ this.updateDeclarativeSettingsValue(formField)
+ }
+ },
+
+ updateDeclarativeSettingsValue(formField, value = null) {
+ try {
+ return axios.post(generateOcsUrl('settings/api/declarative/value'), {
+ app: this.formApp,
+ formId: this.form.id.replace(this.formApp + '_', ''), // Remove app prefix to send clean form id
+ fieldId: formField.id,
+ value: value === null ? this.formFieldsData[formField.id].value : value,
+ });
+ } catch (err) {
+ console.debug(err)
+ showError(t('settings', 'Failed to save setting'))
+ }
+ },
+
+ onChangeDebounced: debounce(function(formField) {
+ this.updateDeclarativeSettingsValue(formField)
+ }, 1000),
+
+ isTextFormField(formField) {
+ return ['text', 'password', 'email', 'tel', 'url', 'number'].includes(formField.type)
+ },
+ },
+}
+</script>
+
+<style lang="scss" scoped>
+.declarative-form-field {
+ margin: 20px 0;
+ padding: 10px 0;
+
+ .input-wrapper {
+ width: 100%;
+ max-width: 400px;
+ }
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ .hint {
+ display: inline-block;
+ color: var(--color-text-maxcontrast);
+ margin-left: 8px;
+ padding-top: 5px;
+ }
+
+ &-radio, &-multi_checkbox {
+ max-height: 250px;
+ overflow-y: auto;
+ }
+
+ &-multi-select, &-select {
+ display: flex;
+ flex-direction: column;
+
+ label {
+ margin-bottom: 5px;
+ }
+ }
+}
+</style>
diff --git a/apps/settings/src/main-declarative-settings-forms.ts b/apps/settings/src/main-declarative-settings-forms.ts
new file mode 100644
index 00000000000..0b37fee476e
--- /dev/null
+++ b/apps/settings/src/main-declarative-settings-forms.ts
@@ -0,0 +1,50 @@
+import Vue from 'vue';
+import { loadState } from '@nextcloud/initial-state';
+import { translate as t, translatePlural as n } from '@nextcloud/l10n';
+import DeclarativeSection from './components/DeclarativeSettings/DeclarativeSection.vue';
+
+interface DeclarativeFormField {
+ id: string,
+ title: string,
+ description: string,
+ type: string,
+ placeholder: string,
+ label: string,
+ options: Array<any>|null,
+ value: any,
+ default: any,
+}
+
+interface DeclarativeForm {
+ id: number,
+ priority: number,
+ section_type: string,
+ section_id: string,
+ storage_type: string,
+ title: string,
+ description: string,
+ doc_url: string,
+ app: string,
+ fields: Array<DeclarativeFormField>,
+}
+
+const forms = loadState('settings', 'declarative-settings-forms', []) as Array<DeclarativeForm>;
+console.debug('Loaded declarative forms:', forms);
+
+function renderDeclarativeSettingsSections(forms: Array<DeclarativeForm>): void {
+ Vue.mixin({ methods: { t, n } })
+ const DeclarativeSettingsSection = Vue.extend(<any>DeclarativeSection);
+ for (const form of forms) {
+ const el = `#${form.app}_${form.id}`
+ new DeclarativeSettingsSection({
+ el: el,
+ propsData: {
+ form,
+ },
+ })
+ }
+}
+
+document.addEventListener('DOMContentLoaded', () => {
+ renderDeclarativeSettingsSections(forms);
+});
diff --git a/apps/settings/tests/Controller/AdminSettingsControllerTest.php b/apps/settings/tests/Controller/AdminSettingsControllerTest.php
index acdcaa136aa..6a11ceb9fca 100644
--- a/apps/settings/tests/Controller/AdminSettingsControllerTest.php
+++ b/apps/settings/tests/Controller/AdminSettingsControllerTest.php
@@ -29,12 +29,14 @@ namespace OCA\Settings\Tests\Controller;
use OCA\Settings\Controller\AdminSettingsController;
use OCA\Settings\Settings\Personal\ServerDevNotice;
use OCP\AppFramework\Http\TemplateResponse;
+use OCP\AppFramework\Services\IInitialState;
use OCP\Group\ISubAdmin;
use OCP\IGroupManager;
use OCP\INavigationManager;
use OCP\IRequest;
use OCP\IUser;
use OCP\IUserSession;
+use OCP\Settings\IDeclarativeManager;
use OCP\Settings\IManager;
use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
@@ -62,6 +64,10 @@ class AdminSettingsControllerTest extends TestCase {
private $groupManager;
/** @var ISubAdmin|MockObject */
private $subAdmin;
+ /** @var IDeclarativeManager|MockObject */
+ private $declarativeSettingsManager;
+ /** @var IInitialState|MockObject */
+ private $initialState;
/** @var string */
private $adminUid = 'lololo';
@@ -74,6 +80,8 @@ class AdminSettingsControllerTest extends TestCase {
$this->userSession = $this->createMock(IUserSession::class);
$this->groupManager = $this->createMock(IGroupManager::class);
$this->subAdmin = $this->createMock(ISubAdmin::class);
+ $this->declarativeSettingsManager = $this->createMock(IDeclarativeManager::class);
+ $this->initialState = $this->createMock(IInitialState::class);
$this->adminSettingsController = new AdminSettingsController(
'settings',
@@ -82,7 +90,9 @@ class AdminSettingsControllerTest extends TestCase {
$this->settingsManager,
$this->userSession,
$this->groupManager,
- $this->subAdmin
+ $this->subAdmin,
+ $this->declarativeSettingsManager,
+ $this->initialState,
);
$user = \OC::$server->getUserManager()->createUser($this->adminUid, 'mylongrandompassword');
@@ -123,6 +133,11 @@ class AdminSettingsControllerTest extends TestCase {
->method('getAllowedAdminSettings')
->with('test')
->willReturn([5 => $this->createMock(ServerDevNotice::class)]);
+ $this->declarativeSettingsManager
+ ->expects($this->any())
+ ->method('getFormIDs')
+ ->with($user, 'admin', 'test')
+ ->willReturn([]);
$idx = $this->adminSettingsController->index('test');
diff --git a/apps/testing/composer/composer/autoload_classmap.php b/apps/testing/composer/composer/autoload_classmap.php
index cd2a052bbac..079f8877881 100644
--- a/apps/testing/composer/composer/autoload_classmap.php
+++ b/apps/testing/composer/composer/autoload_classmap.php
@@ -12,9 +12,13 @@ return array(
'OCA\\Testing\\Controller\\ConfigController' => $baseDir . '/../lib/Controller/ConfigController.php',
'OCA\\Testing\\Controller\\LockingController' => $baseDir . '/../lib/Controller/LockingController.php',
'OCA\\Testing\\Controller\\RateLimitTestController' => $baseDir . '/../lib/Controller/RateLimitTestController.php',
+ 'OCA\\Testing\\Listener\\GetDeclarativeSettingsValueListener' => $baseDir . '/../lib/Listener/GetDeclarativeSettingsValueListener.php',
+ 'OCA\\Testing\\Listener\\RegisterDeclarativeSettingsListener' => $baseDir . '/../lib/Listener/RegisterDeclarativeSettingsListener.php',
+ 'OCA\\Testing\\Listener\\SetDeclarativeSettingsValueListener' => $baseDir . '/../lib/Listener/SetDeclarativeSettingsValueListener.php',
'OCA\\Testing\\Locking\\FakeDBLockingProvider' => $baseDir . '/../lib/Locking/FakeDBLockingProvider.php',
'OCA\\Testing\\Provider\\FakeText2ImageProvider' => $baseDir . '/../lib/Provider/FakeText2ImageProvider.php',
'OCA\\Testing\\Provider\\FakeTextProcessingProvider' => $baseDir . '/../lib/Provider/FakeTextProcessingProvider.php',
'OCA\\Testing\\Provider\\FakeTextProcessingProviderSync' => $baseDir . '/../lib/Provider/FakeTextProcessingProviderSync.php',
'OCA\\Testing\\Provider\\FakeTranslationProvider' => $baseDir . '/../lib/Provider/FakeTranslationProvider.php',
+ 'OCA\\Testing\\Settings\\DeclarativeSettingsForm' => $baseDir . '/../lib/Settings/DeclarativeSettingsForm.php',
);
diff --git a/apps/testing/composer/composer/autoload_static.php b/apps/testing/composer/composer/autoload_static.php
index ce07262200a..2332da70da9 100644
--- a/apps/testing/composer/composer/autoload_static.php
+++ b/apps/testing/composer/composer/autoload_static.php
@@ -27,11 +27,15 @@ class ComposerStaticInitTesting
'OCA\\Testing\\Controller\\ConfigController' => __DIR__ . '/..' . '/../lib/Controller/ConfigController.php',
'OCA\\Testing\\Controller\\LockingController' => __DIR__ . '/..' . '/../lib/Controller/LockingController.php',
'OCA\\Testing\\Controller\\RateLimitTestController' => __DIR__ . '/..' . '/../lib/Controller/RateLimitTestController.php',
+ 'OCA\\Testing\\Listener\\GetDeclarativeSettingsValueListener' => __DIR__ . '/..' . '/../lib/Listener/GetDeclarativeSettingsValueListener.php',
+ 'OCA\\Testing\\Listener\\RegisterDeclarativeSettingsListener' => __DIR__ . '/..' . '/../lib/Listener/RegisterDeclarativeSettingsListener.php',
+ 'OCA\\Testing\\Listener\\SetDeclarativeSettingsValueListener' => __DIR__ . '/..' . '/../lib/Listener/SetDeclarativeSettingsValueListener.php',
'OCA\\Testing\\Locking\\FakeDBLockingProvider' => __DIR__ . '/..' . '/../lib/Locking/FakeDBLockingProvider.php',
'OCA\\Testing\\Provider\\FakeText2ImageProvider' => __DIR__ . '/..' . '/../lib/Provider/FakeText2ImageProvider.php',
'OCA\\Testing\\Provider\\FakeTextProcessingProvider' => __DIR__ . '/..' . '/../lib/Provider/FakeTextProcessingProvider.php',
'OCA\\Testing\\Provider\\FakeTextProcessingProviderSync' => __DIR__ . '/..' . '/../lib/Provider/FakeTextProcessingProviderSync.php',
'OCA\\Testing\\Provider\\FakeTranslationProvider' => __DIR__ . '/..' . '/../lib/Provider/FakeTranslationProvider.php',
+ 'OCA\\Testing\\Settings\\DeclarativeSettingsForm' => __DIR__ . '/..' . '/../lib/Settings/DeclarativeSettingsForm.php',
);
public static function getInitializer(ClassLoader $loader)
diff --git a/apps/testing/composer/composer/installed.php b/apps/testing/composer/composer/installed.php
index 1a66c7f2416..d2b87e1bdfd 100644
--- a/apps/testing/composer/composer/installed.php
+++ b/apps/testing/composer/composer/installed.php
@@ -3,7 +3,7 @@
'name' => '__root__',
'pretty_version' => 'dev-master',
'version' => 'dev-master',
- 'reference' => 'b1797842784b250fb01ed5e3bf130705eb94751b',
+ 'reference' => '4ff660ca2e0baa02440ba07296ed7e75fa544c0e',
'type' => 'library',
'install_path' => __DIR__ . '/../',
'aliases' => array(),
@@ -13,7 +13,7 @@
'__root__' => array(
'pretty_version' => 'dev-master',
'version' => 'dev-master',
- 'reference' => 'b1797842784b250fb01ed5e3bf130705eb94751b',
+ 'reference' => '4ff660ca2e0baa02440ba07296ed7e75fa544c0e',
'type' => 'library',
'install_path' => __DIR__ . '/../',
'aliases' => array(),
diff --git a/apps/testing/lib/AppInfo/Application.php b/apps/testing/lib/AppInfo/Application.php
index cbbbf6fc4ea..7b51a8469db 100644
--- a/apps/testing/lib/AppInfo/Application.php
+++ b/apps/testing/lib/AppInfo/Application.php
@@ -25,14 +25,21 @@
namespace OCA\Testing\AppInfo;
use OCA\Testing\AlternativeHomeUserBackend;
+use OCA\Testing\Listener\GetDeclarativeSettingsValueListener;
+use OCA\Testing\Listener\RegisterDeclarativeSettingsListener;
+use OCA\Testing\Listener\SetDeclarativeSettingsValueListener;
use OCA\Testing\Provider\FakeText2ImageProvider;
use OCA\Testing\Provider\FakeTextProcessingProvider;
use OCA\Testing\Provider\FakeTextProcessingProviderSync;
use OCA\Testing\Provider\FakeTranslationProvider;
+use OCA\Testing\Settings\DeclarativeSettingsForm;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext;
use OCP\AppFramework\Bootstrap\IBootstrap;
use OCP\AppFramework\Bootstrap\IRegistrationContext;
+use OCP\Settings\Events\DeclarativeSettingsGetValueEvent;
+use OCP\Settings\Events\DeclarativeSettingsRegisterFormEvent;
+use OCP\Settings\Events\DeclarativeSettingsSetValueEvent;
class Application extends App implements IBootstrap {
public function __construct(array $urlParams = []) {
@@ -44,6 +51,11 @@ class Application extends App implements IBootstrap {
$context->registerTextProcessingProvider(FakeTextProcessingProvider::class);
$context->registerTextProcessingProvider(FakeTextProcessingProviderSync::class);
$context->registerTextToImageProvider(FakeText2ImageProvider::class);
+
+ $context->registerDeclarativeSettings(DeclarativeSettingsForm::class);
+ $context->registerEventListener(DeclarativeSettingsRegisterFormEvent::class, RegisterDeclarativeSettingsListener::class);
+ $context->registerEventListener(DeclarativeSettingsGetValueEvent::class, GetDeclarativeSettingsValueListener::class);
+ $context->registerEventListener(DeclarativeSettingsSetValueEvent::class, SetDeclarativeSettingsValueListener::class);
}
public function boot(IBootContext $context): void {
diff --git a/apps/testing/lib/Listener/GetDeclarativeSettingsValueListener.php b/apps/testing/lib/Listener/GetDeclarativeSettingsValueListener.php
new file mode 100644
index 00000000000..ff55ba77104
--- /dev/null
+++ b/apps/testing/lib/Listener/GetDeclarativeSettingsValueListener.php
@@ -0,0 +1,32 @@
+<?php
+
+declare(strict_types=1);
+
+namespace OCA\Testing\Listener;
+
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventListener;
+use OCP\IConfig;
+use OCP\Settings\Events\DeclarativeSettingsGetValueEvent;
+
+/**
+ * @template-implements IEventListener<DeclarativeSettingsGetValueEvent>
+ */
+class GetDeclarativeSettingsValueListener implements IEventListener {
+
+ public function __construct(private IConfig $config) {
+ }
+
+ public function handle(Event $event): void {
+ if (!$event instanceof DeclarativeSettingsGetValueEvent) {
+ return;
+ }
+
+ if ($event->getApp() !== 'testing') {
+ return;
+ }
+
+ $value = $this->config->getUserValue($event->getUser()->getUID(), $event->getApp(), $event->getFieldId());
+ $event->setValue($value);
+ }
+}
diff --git a/apps/testing/lib/Listener/RegisterDeclarativeSettingsListener.php b/apps/testing/lib/Listener/RegisterDeclarativeSettingsListener.php
new file mode 100644
index 00000000000..e11205904e7
--- /dev/null
+++ b/apps/testing/lib/Listener/RegisterDeclarativeSettingsListener.php
@@ -0,0 +1,68 @@
+<?php
+
+declare(strict_types=1);
+
+namespace OCA\Testing\Listener;
+
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventListener;
+use OCP\Settings\DeclarativeSettingsTypes;
+use OCP\Settings\Events\DeclarativeSettingsRegisterFormEvent;
+
+/**
+ * @template-implements IEventListener<DeclarativeSettingsRegisterFormEvent>
+ */
+class RegisterDeclarativeSettingsListener implements IEventListener {
+
+ public function __construct() {
+ }
+
+ public function handle(Event $event): void {
+ if (!($event instanceof DeclarativeSettingsRegisterFormEvent)) {
+ // Unrelated
+ return;
+ }
+
+ $event->registerSchema('testing', [
+ 'id' => 'test_declarative_form_event',
+ 'priority' => 20,
+ 'section_type' => DeclarativeSettingsTypes::SECTION_TYPE_ADMIN,
+ 'section_id' => 'additional',
+ 'storage_type' => DeclarativeSettingsTypes::STORAGE_TYPE_INTERNAL,
+ 'title' => 'Test declarative settings event', // NcSettingsSection name
+ 'description' => 'This form is registered via the RegisterDeclarativeSettingsFormEvent', // NcSettingsSection description
+ 'fields' => [
+ [
+ 'id' => 'event_field_1',
+ 'title' => 'Why is 42 this answer to all questions?',
+ 'description' => 'Hint: It\'s not',
+ 'type' => DeclarativeSettingsTypes::TEXT,
+ 'placeholder' => 'Enter your answer',
+ 'default' => 'Because it is',
+ ],
+ [
+ 'id' => 'feature_rating',
+ 'title' => 'How would you rate this feature?',
+ 'description' => 'Your vote is not anonymous',
+ 'type' => DeclarativeSettingsTypes::RADIO, // radio, radio-button (NcCheckboxRadioSwitch button-variant)
+ 'label' => 'Select single toggle',
+ 'default' => '3',
+ 'options' => [
+ [
+ 'name' => 'Awesome', // NcCheckboxRadioSwitch display name
+ 'value' => '1' // NcCheckboxRadioSwitch value
+ ],
+ [
+ 'name' => 'Very awesome',
+ 'value' => '2'
+ ],
+ [
+ 'name' => 'Super awesome',
+ 'value' => '3'
+ ],
+ ],
+ ],
+ ],
+ ]);
+ }
+}
diff --git a/apps/testing/lib/Listener/SetDeclarativeSettingsValueListener.php b/apps/testing/lib/Listener/SetDeclarativeSettingsValueListener.php
new file mode 100644
index 00000000000..d9931455934
--- /dev/null
+++ b/apps/testing/lib/Listener/SetDeclarativeSettingsValueListener.php
@@ -0,0 +1,32 @@
+<?php
+
+declare(strict_types=1);
+
+namespace OCA\Testing\Listener;
+
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventListener;
+use OCP\IConfig;
+use OCP\Settings\Events\DeclarativeSettingsSetValueEvent;
+
+/**
+ * @template-implements IEventListener<DeclarativeSettingsSetValueEvent>
+ */
+class SetDeclarativeSettingsValueListener implements IEventListener {
+
+ public function __construct(private IConfig $config) {
+ }
+
+ public function handle(Event $event): void {
+ if (!$event instanceof DeclarativeSettingsSetValueEvent) {
+ return;
+ }
+
+ if ($event->getApp() !== 'testing') {
+ return;
+ }
+
+ error_log('Testing app wants to store ' . $event->getValue() . ' for field ' . $event->getFieldId() . ' for user ' . $event->getUser()->getUID());
+ $this->config->setUserValue($event->getUser()->getUID(), $event->getApp(), $event->getFieldId(), $event->getValue());
+ }
+}
diff --git a/apps/testing/lib/Settings/DeclarativeSettingsForm.php b/apps/testing/lib/Settings/DeclarativeSettingsForm.php
new file mode 100644
index 00000000000..a717de59ea1
--- /dev/null
+++ b/apps/testing/lib/Settings/DeclarativeSettingsForm.php
@@ -0,0 +1,172 @@
+<?php
+
+declare(strict_types=1);
+
+namespace OCA\Testing\Settings;
+
+use OCP\Settings\DeclarativeSettingsTypes;
+use OCP\Settings\IDeclarativeSettingsForm;
+
+class DeclarativeSettingsForm implements IDeclarativeSettingsForm {
+ public function getSchema(): array {
+ return [
+ 'id' => 'test_declarative_form',
+ 'priority' => 10,
+ 'section_type' => DeclarativeSettingsTypes::SECTION_TYPE_ADMIN, // admin, personal
+ 'section_id' => 'additional',
+ 'storage_type' => DeclarativeSettingsTypes::STORAGE_TYPE_INTERNAL, // external, internal (handled by core to store in appconfig and preferences)
+ 'title' => 'Test declarative settings class', // NcSettingsSection name
+ 'description' => 'This form is registered with a DeclarativeSettingsForm class', // NcSettingsSection description
+ 'doc_url' => '', // NcSettingsSection doc_url for documentation or help page, empty string if not needed
+ 'fields' => [
+ [
+ 'id' => 'test_ex_app_field_7', // configkey
+ 'title' => 'Multi-selection', // name or label
+ 'description' => 'Select some option setting', // hint
+ 'type' => DeclarativeSettingsTypes::MULTI_SELECT, // select, radio, multi-select
+ 'options' => ['foo', 'bar', 'baz'], // simple options for select, radio, multi-select
+ 'placeholder' => 'Select some multiple options', // input placeholder
+ 'default' => ['foo', 'bar'],
+ ],
+ [
+ 'id' => 'some_real_setting',
+ 'title' => 'Choose init status check background job interval',
+ 'description' => 'How often AppAPI should check for initialization status',
+ 'type' => DeclarativeSettingsTypes::RADIO, // radio (NcCheckboxRadioSwitch type radio)
+ 'placeholder' => 'Choose init status check background job interval',
+ 'default' => '40m',
+ 'options' => [
+ [
+ 'name' => 'Each 40 minutes', // NcCheckboxRadioSwitch display name
+ 'value' => '40m' // NcCheckboxRadioSwitch value
+ ],
+ [
+ 'name' => 'Each 60 minutes',
+ 'value' => '60m'
+ ],
+ [
+ 'name' => 'Each 120 minutes',
+ 'value' => '120m'
+ ],
+ [
+ 'name' => 'Each day',
+ 'value' => 60 * 24 . 'm'
+ ],
+ ],
+ ],
+ [
+ 'id' => 'test_ex_app_field_1', // configkey
+ 'title' => 'Default text field', // label
+ 'description' => 'Set some simple text setting', // hint
+ 'type' => DeclarativeSettingsTypes::TEXT, // text, password, email, tel, url, number
+ 'placeholder' => 'Enter text setting', // placeholder
+ 'default' => 'foo',
+ ],
+ [
+ 'id' => 'test_ex_app_field_1_1',
+ 'title' => 'Email field',
+ 'description' => 'Set email config',
+ 'type' => DeclarativeSettingsTypes::EMAIL,
+ 'placeholder' => 'Enter email',
+ 'default' => '',
+ ],
+ [
+ 'id' => 'test_ex_app_field_1_2',
+ 'title' => 'Tel field',
+ 'description' => 'Set tel config',
+ 'type' => DeclarativeSettingsTypes::TEL,
+ 'placeholder' => 'Enter your tel',
+ 'default' => '',
+ ],
+ [
+ 'id' => 'test_ex_app_field_1_3',
+ 'title' => 'Url (website) field',
+ 'description' => 'Set url config',
+ 'type' => 'url',
+ 'placeholder' => 'Enter url',
+ 'default' => '',
+ ],
+ [
+ 'id' => 'test_ex_app_field_1_4',
+ 'title' => 'Number field',
+ 'description' => 'Set number config',
+ 'type' => DeclarativeSettingsTypes::NUMBER,
+ 'placeholder' => 'Enter number value',
+ 'default' => 0,
+ ],
+ [
+ 'id' => 'test_ex_app_field_2',
+ 'title' => 'Password',
+ 'description' => 'Set some secure value setting',
+ 'type' => 'password',
+ 'placeholder' => 'Set secure value',
+ 'default' => '',
+ ],
+ [
+ 'id' => 'test_ex_app_field_3',
+ 'title' => 'Selection',
+ 'description' => 'Select some option setting',
+ 'type' => DeclarativeSettingsTypes::SELECT, // select, radio, multi-select
+ 'options' => ['foo', 'bar', 'baz'],
+ 'placeholder' => 'Select some option setting',
+ 'default' => 'foo',
+ ],
+ [
+ 'id' => 'test_ex_app_field_4',
+ 'title' => 'Toggle something',
+ 'description' => 'Select checkbox option setting',
+ 'type' => DeclarativeSettingsTypes::CHECKBOX, // checkbox, multiple-checkbox
+ 'label' => 'Verify something if enabled',
+ 'default' => false,
+ ],
+ [
+ 'id' => 'test_ex_app_field_5',
+ 'title' => 'Multiple checkbox toggles, describing one setting, checked options are saved as an JSON object {foo: true, bar: false}',
+ 'description' => 'Select checkbox option setting',
+ 'type' => DeclarativeSettingsTypes::MULTI_CHECKBOX, // checkbox, multi-checkbox
+ 'default' => ['foo' => true, 'bar' => true, 'baz' => true],
+ 'options' => [
+ [
+ 'name' => 'Foo',
+ 'value' => 'foo', // multiple-checkbox configkey
+ ],
+ [
+ 'name' => 'Bar',
+ 'value' => 'bar',
+ ],
+ [
+ 'name' => 'Baz',
+ 'value' => 'baz',
+ ],
+ [
+ 'name' => 'Qux',
+ 'value' => 'qux',
+ ],
+ ],
+ ],
+ [
+ 'id' => 'test_ex_app_field_6',
+ 'title' => 'Radio toggles, describing one setting like single select',
+ 'description' => 'Select radio option setting',
+ 'type' => DeclarativeSettingsTypes::RADIO, // radio (NcCheckboxRadioSwitch type radio)
+ 'label' => 'Select single toggle',
+ 'default' => 'foo',
+ 'options' => [
+ [
+ 'name' => 'First radio', // NcCheckboxRadioSwitch display name
+ 'value' => 'foo' // NcCheckboxRadioSwitch value
+ ],
+ [
+ 'name' => 'Second radio',
+ 'value' => 'bar'
+ ],
+ [
+ 'name' => 'Third radio',
+ 'value' => 'baz'
+ ],
+ ],
+ ],
+ ],
+ ];
+ }
+}