diff options
41 files changed, 3215 insertions, 52 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' + ], + ], + ], + ], + ]; + } +} diff --git a/dist/settings-declarative-settings-forms.js b/dist/settings-declarative-settings-forms.js new file mode 100644 index 00000000000..11418dc983d --- /dev/null +++ b/dist/settings-declarative-settings-forms.js @@ -0,0 +1,2 @@ +(()=>{"use strict";var e,a,r,i={27565:(e,a,r)=>{var i=r(85471),l=r(38613),o=r(53334),n=r(26287),d=r(99498),s=r(85168),c=r(17334),p=r.n(c),u=r(88837),f=r(44492),v=r(67607),m=r(32073),h=r(96763);const A={name:"DeclarativeSection",components:{NcSettingsSection:u.A,NcInputField:f.A,NcSelect:v.A,NcCheckboxRadioSwitch:m.A},props:{form:{type:Object,required:!0}},data:()=>({formFieldsData:{}}),beforeMount(){this.initFormFieldsData()},computed:{formApp(){return this.form.app||""},formFields(){return this.form.fields||[]}},methods:{initFormFieldsData(){this.form.fields.forEach((e=>{"checkbox"===e.type&&this.$set(e,"value",+e.value),"multi-checkbox"===e.type&&(""===e.value?(this.$set(e,"value",{}),e.options.forEach((t=>{this.$set(e.value,t.value,!1)}))):(this.$set(e,"value",JSON.parse(e.value)),e.options.forEach((t=>{e.value.hasOwnProperty(t.value)||this.$set(e.value,t.value,!1)})),Object.keys(e.value).forEach((t=>{e.options.find((e=>e.value===t))||delete e.value[t]})))),"multi-select"===e.type&&(""===e.value?this.$set(e,"value",[]):this.$set(e,"value",JSON.parse(e.value))),this.$set(this.formFieldsData,e.id,{value:e.value})}))},updateFormFieldDataValue(e,t){let a=arguments.length>2&&void 0!==arguments[2]&&arguments[2];this.formFieldsData[t.id].value=e,a&&this.updateDeclarativeSettingsValue(t)},updateDeclarativeSettingsValue(e){let a=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;try{return n.A.post((0,d.KT)("settings/api/declarative/value"),{app:this.formApp,formId:this.form.id.replace(this.formApp+"_",""),fieldId:e.id,value:null===a?this.formFieldsData[e.id].value:a})}catch(e){h.debug(e),(0,s.Qg)(t("settings","Failed to save setting"))}},onChangeDebounced:p()((function(e){this.updateDeclarativeSettingsValue(e)}),1e3),isTextFormField:e=>["text","password","email","tel","url","number"].includes(e.type)}};var b=r(85072),_=r.n(b),g=r(97825),C=r.n(g),y=r(77659),x=r.n(y),k=r(55056),D=r.n(k),F=r(10540),w=r.n(F),S=r(41113),O=r.n(S),N=r(46613),E={};E.styleTagTransform=O(),E.setAttributes=D(),E.insert=x().bind(null,"head"),E.domAPI=C(),E.insertStyleElement=w(),_()(N.A,E),N.A&&N.A.locals&&N.A.locals;const T=(0,r(14486).A)(A,(function(){var e=this,t=e._self._c;return t("NcSettingsSection",{staticClass:"declarative-settings-section",attrs:{name:e.t(e.formApp,e.form.title),description:e.t(e.formApp,e.form.description),"doc-url":e.form.doc_url||""}},e._l(e.formFields,(function(a){return t("div",{key:a.id,staticClass:"declarative-form-field",class:{"declarative-form-field-text":e.isTextFormField(a),"declarative-form-field-select":"select"===a.type,"declarative-form-field-multi-select":"multi-select"===a.type,"declarative-form-field-checkbox":"checkbox"===a.type,"declarative-form-field-multi_checkbox":"multi-checkbox"===a.type,"declarative-form-field-radio":"radio"===a.type},attrs:{"aria-label":e.t("settings","{app}'s declarative setting field: {name}",{app:e.formApp,name:e.t(e.formApp,a.title)})}},[e.isTextFormField(a)?[t("div",{staticClass:"input-wrapper"},[t("NcInputField",{attrs:{type:a.type,label:e.t(e.formApp,a.title),value:e.formFieldsData[a.id].value,placeholder:e.t(e.formApp,a.placeholder)},on:{"update:value":[function(t){return e.$set(e.formFieldsData[a.id],"value",t)},function(t){return e.onChangeDebounced(a)}],submit:function(t){return e.updateDeclarativeSettingsValue(a)}}})],1),e._v(" "),t("span",{staticClass:"hint"},[e._v(e._s(e.t(e.formApp,a.description)))])]:e._e(),e._v(" "),"select"===a.type?[t("label",{attrs:{for:a.id+"_field"}},[e._v(e._s(e.t(e.formApp,a.title)))]),e._v(" "),t("div",{staticClass:"input-wrapper"},[t("NcSelect",{attrs:{id:a.id+"_field",options:a.options,placeholder:e.t(e.formApp,a.placeholder),"label-outside":!0,value:e.formFieldsData[a.id].value},on:{input:t=>e.updateFormFieldDataValue(t,a,!0)}})],1),e._v(" "),t("span",{staticClass:"hint"},[e._v(e._s(e.t(e.formApp,a.description)))])]:e._e(),e._v(" "),"multi-select"===a.type?[t("label",{attrs:{for:a.id+"_field"}},[e._v(e._s(e.t(e.formApp,a.title)))]),e._v(" "),t("div",{staticClass:"input-wrapper"},[t("NcSelect",{attrs:{id:a.id+"_field",options:a.options,placeholder:e.t(e.formApp,a.placeholder),multiple:!0,"label-outside":!0,value:e.formFieldsData[a.id].value},on:{input:t=>{e.formFieldsData[a.id].value=t,e.updateDeclarativeSettingsValue(a,JSON.stringify(e.formFieldsData[a.id].value))}}})],1),e._v(" "),t("span",{staticClass:"hint"},[e._v(e._s(e.t(e.formApp,a.description)))])]:e._e(),e._v(" "),"checkbox"===a.type?[t("label",{attrs:{for:a.id+"_field"}},[e._v(e._s(e.t(e.formApp,a.title)))]),e._v(" "),t("NcCheckboxRadioSwitch",{attrs:{id:a.id+"_field",checked:Boolean(e.formFieldsData[a.id].value)},on:{"update:checked":t=>{a.value=t,e.updateFormFieldDataValue(+t,a,!0)}}},[e._v("\n\t\t\t\t"+e._s(e.t(e.formApp,a.label))+"\n\t\t\t")]),e._v(" "),t("span",{staticClass:"hint"},[e._v(e._s(e.t(e.formApp,a.description)))])]:e._e(),e._v(" "),"multi-checkbox"===a.type?[t("label",{attrs:{for:a.id+"_field"}},[e._v(e._s(e.t(e.formApp,a.title)))]),e._v(" "),e._l(a.options,(function(r){return t("NcCheckboxRadioSwitch",{key:r.value,attrs:{id:a.id+"_field_"+r.value,checked:e.formFieldsData[a.id].value[r.value]},on:{"update:checked":t=>{e.formFieldsData[a.id].value[r.value]=t,e.updateDeclarativeSettingsValue(a,JSON.stringify(e.formFieldsData[a.id].value))}}},[e._v("\n\t\t\t\t"+e._s(e.t(e.formApp,r.name))+"\n\t\t\t")])})),e._v(" "),t("span",{staticClass:"hint"},[e._v(e._s(e.t(e.formApp,a.description)))])]:e._e(),e._v(" "),"radio"===a.type?[t("label",{attrs:{for:a.id+"_field"}},[e._v(e._s(e.t(e.formApp,a.title)))]),e._v(" "),e._l(a.options,(function(r){return t("NcCheckboxRadioSwitch",{key:r.value,attrs:{value:r.value,type:"radio",checked:e.formFieldsData[a.id].value},on:{"update:checked":t=>e.updateFormFieldDataValue(t,a,!0)}},[e._v("\n\t\t\t\t"+e._s(e.t(e.formApp,r.name))+"\n\t\t\t")])})),e._v(" "),t("span",{staticClass:"hint"},[e._v(e._s(e.t(e.formApp,a.description)))])]:e._e()],2)})),0)}),[],!1,null,"49d8d244",null).exports;var j=r(96763);const $=(0,l.C)("settings","declarative-settings-forms",[]);j.debug("Loaded declarative forms:",$),document.addEventListener("DOMContentLoaded",(()=>{!function(e){i.Ay.mixin({methods:{t:o.Tl,n:o.zw}});const t=i.Ay.extend(T);for(const a of e)new t({el:"#".concat(a.app,"_").concat(a.id),propsData:{form:a}})}($)}))},46613:(e,t,a)=>{a.d(t,{A:()=>n});var r=a(71354),i=a.n(r),l=a(76314),o=a.n(l)()(i());o.push([e.id,".declarative-form-field[data-v-49d8d244]{margin:20px 0;padding:10px 0}.declarative-form-field .input-wrapper[data-v-49d8d244]{width:100%;max-width:400px}.declarative-form-field[data-v-49d8d244]:last-child{border-bottom:none}.declarative-form-field .hint[data-v-49d8d244]{display:inline-block;color:var(--color-text-maxcontrast);margin-left:8px;padding-top:5px}.declarative-form-field-radio[data-v-49d8d244],.declarative-form-field-multi_checkbox[data-v-49d8d244]{max-height:250px;overflow-y:auto}.declarative-form-field-multi-select[data-v-49d8d244],.declarative-form-field-select[data-v-49d8d244]{display:flex;flex-direction:column}.declarative-form-field-multi-select label[data-v-49d8d244],.declarative-form-field-select label[data-v-49d8d244]{margin-bottom:5px}","",{version:3,sources:["webpack://./apps/settings/src/components/DeclarativeSettings/DeclarativeSection.vue"],names:[],mappings:"AACA,yCACC,aAAA,CACA,cAAA,CAEA,wDACC,UAAA,CACA,eAAA,CAGD,oDACC,kBAAA,CAGD,+CACC,oBAAA,CACA,mCAAA,CACA,eAAA,CACA,eAAA,CAGD,uGACC,gBAAA,CACA,eAAA,CAGD,sGACC,YAAA,CACA,qBAAA,CAEA,kHACC,iBAAA",sourcesContent:["\r\n.declarative-form-field {\r\n\tmargin: 20px 0;\r\n\tpadding: 10px 0;\r\n\r\n\t.input-wrapper {\r\n\t\twidth: 100%;\r\n\t\tmax-width: 400px;\r\n\t}\r\n\r\n\t&:last-child {\r\n\t\tborder-bottom: none;\r\n\t}\r\n\r\n\t.hint {\r\n\t\tdisplay: inline-block;\r\n\t\tcolor: var(--color-text-maxcontrast);\r\n\t\tmargin-left: 8px;\r\n\t\tpadding-top: 5px;\r\n\t}\r\n\r\n\t&-radio, &-multi_checkbox {\r\n\t\tmax-height: 250px;\r\n\t\toverflow-y: auto;\r\n\t}\r\n\r\n\t&-multi-select, &-select {\r\n\t\tdisplay: flex;\r\n\t\tflex-direction: column;\r\n\r\n\t\tlabel {\r\n\t\t\tmargin-bottom: 5px;\r\n\t\t}\r\n\t}\r\n}\r\n"],sourceRoot:""}]);const n=o}},l={};function o(e){var t=l[e];if(void 0!==t)return t.exports;var a=l[e]={id:e,loaded:!1,exports:{}};return i[e].call(a.exports,a,a.exports,o),a.loaded=!0,a.exports}o.m=i,e=[],o.O=(t,a,r,i)=>{if(!a){var l=1/0;for(c=0;c<e.length;c++){a=e[c][0],r=e[c][1],i=e[c][2];for(var n=!0,d=0;d<a.length;d++)(!1&i||l>=i)&&Object.keys(o.O).every((e=>o.O[e](a[d])))?a.splice(d--,1):(n=!1,i<l&&(l=i));if(n){e.splice(c--,1);var s=r();void 0!==s&&(t=s)}}return t}i=i||0;for(var c=e.length;c>0&&e[c-1][2]>i;c--)e[c]=e[c-1];e[c]=[a,r,i]},o.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return o.d(t,{a:t}),t},o.d=(e,t)=>{for(var a in t)o.o(t,a)&&!o.o(e,a)&&Object.defineProperty(e,a,{enumerable:!0,get:t[a]})},o.f={},o.e=e=>Promise.all(Object.keys(o.f).reduce(((t,a)=>(o.f[a](e,t),t)),[])),o.u=e=>e+"-"+e+".js?v="+{1359:"79a120e5671b1b5ba537",8618:"1e8f15db3b14455fef8f"}[e],o.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),o.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),a={},r="nextcloud:",o.l=(e,t,i,l)=>{if(a[e])a[e].push(t);else{var n,d;if(void 0!==i)for(var s=document.getElementsByTagName("script"),c=0;c<s.length;c++){var p=s[c];if(p.getAttribute("src")==e||p.getAttribute("data-webpack")==r+i){n=p;break}}n||(d=!0,(n=document.createElement("script")).charset="utf-8",n.timeout=120,o.nc&&n.setAttribute("nonce",o.nc),n.setAttribute("data-webpack",r+i),n.src=e),a[e]=[t];var u=(t,r)=>{n.onerror=n.onload=null,clearTimeout(f);var i=a[e];if(delete a[e],n.parentNode&&n.parentNode.removeChild(n),i&&i.forEach((e=>e(r))),t)return t(r)},f=setTimeout(u.bind(null,void 0,{type:"timeout",target:n}),12e4);n.onerror=u.bind(null,n.onerror),n.onload=u.bind(null,n.onload),d&&document.head.appendChild(n)}},o.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},o.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),o.j=6085,(()=>{var e;o.g.importScripts&&(e=o.g.location+"");var t=o.g.document;if(!e&&t&&(t.currentScript&&(e=t.currentScript.src),!e)){var a=t.getElementsByTagName("script");if(a.length)for(var r=a.length-1;r>-1&&(!e||!/^http(s?):/.test(e));)e=a[r--].src}if(!e)throw new Error("Automatic publicPath is not supported in this browser");e=e.replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),o.p=e})(),(()=>{o.b=document.baseURI||self.location.href;var e={6085:0};o.f.j=(t,a)=>{var r=o.o(e,t)?e[t]:void 0;if(0!==r)if(r)a.push(r[2]);else{var i=new Promise(((a,i)=>r=e[t]=[a,i]));a.push(r[2]=i);var l=o.p+o.u(t),n=new Error;o.l(l,(a=>{if(o.o(e,t)&&(0!==(r=e[t])&&(e[t]=void 0),r)){var i=a&&("load"===a.type?"missing":a.type),l=a&&a.target&&a.target.src;n.message="Loading chunk "+t+" failed.\n("+i+": "+l+")",n.name="ChunkLoadError",n.type=i,n.request=l,r[1](n)}}),"chunk-"+t,t)}},o.O.j=t=>0===e[t];var t=(t,a)=>{var r,i,l=a[0],n=a[1],d=a[2],s=0;if(l.some((t=>0!==e[t]))){for(r in n)o.o(n,r)&&(o.m[r]=n[r]);if(d)var c=d(o)}for(t&&t(a);s<l.length;s++)i=l[s],o.o(e,i)&&e[i]&&e[i][0](),e[i]=0;return o.O(c)},a=self.webpackChunknextcloud=self.webpackChunknextcloud||[];a.forEach(t.bind(null,0)),a.push=t.bind(null,a.push.bind(a))})(),o.nc=void 0;var n=o.O(void 0,[4208],(()=>o(27565)));n=o.O(n)})(); +//# sourceMappingURL=settings-declarative-settings-forms.js.map?v=d27f2b431a59171ca710
\ No newline at end of file diff --git a/dist/settings-declarative-settings-forms.js.map b/dist/settings-declarative-settings-forms.js.map new file mode 100644 index 00000000000..d7f345bd335 --- /dev/null +++ b/dist/settings-declarative-settings-forms.js.map @@ -0,0 +1 @@ +{"version":3,"file":"settings-declarative-settings-forms.js?v=d27f2b431a59171ca710","mappings":"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","sources":["webpack:///nextcloud/webpack/runtime/chunk loaded","webpack:///nextcloud/webpack/runtime/load script","webpack:///nextcloud/apps/settings/src/components/DeclarativeSettings/DeclarativeSection.vue","webpack:///nextcloud/apps/settings/src/components/DeclarativeSettings/DeclarativeSection.vue?vue&type=script&lang=js","webpack://nextcloud/./apps/settings/src/components/DeclarativeSettings/DeclarativeSection.vue?dafe","webpack://nextcloud/./apps/settings/src/components/DeclarativeSettings/DeclarativeSection.vue?6f30","webpack://nextcloud/./apps/settings/src/components/DeclarativeSettings/DeclarativeSection.vue?d806","webpack:///nextcloud/apps/settings/src/main-declarative-settings-forms.ts","webpack:///nextcloud/apps/settings/src/components/DeclarativeSettings/DeclarativeSection.vue?vue&type=style&index=0&id=49d8d244&prod&lang=scss&scoped=true","webpack:///nextcloud/webpack/bootstrap","webpack:///nextcloud/webpack/runtime/compat get default export","webpack:///nextcloud/webpack/runtime/define property getters","webpack:///nextcloud/webpack/runtime/ensure chunk","webpack:///nextcloud/webpack/runtime/get javascript chunk filename","webpack:///nextcloud/webpack/runtime/global","webpack:///nextcloud/webpack/runtime/hasOwnProperty shorthand","webpack:///nextcloud/webpack/runtime/make namespace object","webpack:///nextcloud/webpack/runtime/node module decorator","webpack:///nextcloud/webpack/runtime/runtimeId","webpack:///nextcloud/webpack/runtime/publicPath","webpack:///nextcloud/webpack/runtime/jsonp chunk loading","webpack:///nextcloud/webpack/runtime/nonce","webpack:///nextcloud/webpack/startup"],"sourcesContent":["var deferred = [];\n__webpack_require__.O = (result, chunkIds, fn, priority) => {\n\tif(chunkIds) {\n\t\tpriority = priority || 0;\n\t\tfor(var i = deferred.length; i > 0 && deferred[i - 1][2] > priority; i--) deferred[i] = deferred[i - 1];\n\t\tdeferred[i] = [chunkIds, fn, priority];\n\t\treturn;\n\t}\n\tvar notFulfilled = Infinity;\n\tfor (var i = 0; i < deferred.length; i++) {\n\t\tvar chunkIds = deferred[i][0];\n\t\tvar fn = deferred[i][1];\n\t\tvar priority = deferred[i][2];\n\t\tvar fulfilled = true;\n\t\tfor (var j = 0; j < chunkIds.length; j++) {\n\t\t\tif ((priority & 1 === 0 || notFulfilled >= priority) && Object.keys(__webpack_require__.O).every((key) => (__webpack_require__.O[key](chunkIds[j])))) {\n\t\t\t\tchunkIds.splice(j--, 1);\n\t\t\t} else {\n\t\t\t\tfulfilled = false;\n\t\t\t\tif(priority < notFulfilled) notFulfilled = priority;\n\t\t\t}\n\t\t}\n\t\tif(fulfilled) {\n\t\t\tdeferred.splice(i--, 1)\n\t\t\tvar r = fn();\n\t\t\tif (r !== undefined) result = r;\n\t\t}\n\t}\n\treturn result;\n};","var inProgress = {};\nvar dataWebpackPrefix = \"nextcloud:\";\n// loadScript function to load a script via script tag\n__webpack_require__.l = (url, done, key, chunkId) => {\n\tif(inProgress[url]) { inProgress[url].push(done); return; }\n\tvar script, needAttach;\n\tif(key !== undefined) {\n\t\tvar scripts = document.getElementsByTagName(\"script\");\n\t\tfor(var i = 0; i < scripts.length; i++) {\n\t\t\tvar s = scripts[i];\n\t\t\tif(s.getAttribute(\"src\") == url || s.getAttribute(\"data-webpack\") == dataWebpackPrefix + key) { script = s; break; }\n\t\t}\n\t}\n\tif(!script) {\n\t\tneedAttach = true;\n\t\tscript = document.createElement('script');\n\n\t\tscript.charset = 'utf-8';\n\t\tscript.timeout = 120;\n\t\tif (__webpack_require__.nc) {\n\t\t\tscript.setAttribute(\"nonce\", __webpack_require__.nc);\n\t\t}\n\t\tscript.setAttribute(\"data-webpack\", dataWebpackPrefix + key);\n\n\t\tscript.src = url;\n\t}\n\tinProgress[url] = [done];\n\tvar onScriptComplete = (prev, event) => {\n\t\t// avoid mem leaks in IE.\n\t\tscript.onerror = script.onload = null;\n\t\tclearTimeout(timeout);\n\t\tvar doneFns = inProgress[url];\n\t\tdelete inProgress[url];\n\t\tscript.parentNode && script.parentNode.removeChild(script);\n\t\tdoneFns && doneFns.forEach((fn) => (fn(event)));\n\t\tif(prev) return prev(event);\n\t}\n\tvar timeout = setTimeout(onScriptComplete.bind(null, undefined, { type: 'timeout', target: script }), 120000);\n\tscript.onerror = onScriptComplete.bind(null, script.onerror);\n\tscript.onload = onScriptComplete.bind(null, script.onload);\n\tneedAttach && document.head.appendChild(script);\n};","<template>\r\n\t<NcSettingsSection\r\n\t\tclass=\"declarative-settings-section\"\r\n\t\t:name=\"t(formApp, form.title)\"\r\n\t\t:description=\"t(formApp, form.description)\"\r\n\t\t:doc-url=\"form.doc_url || ''\">\r\n\t\t<div v-for=\"formField in formFields\"\r\n\t\t\t :key=\"formField.id\"\r\n\t\t\t class=\"declarative-form-field\"\r\n\t\t\t:aria-label=\"t('settings', '{app}\\'s declarative setting field: {name}', { app: formApp, name: t(formApp, formField.title) })\"\r\n\t\t\t:class=\"{\r\n\t\t\t\t'declarative-form-field-text': isTextFormField(formField),\r\n\t\t\t\t'declarative-form-field-select': formField.type === 'select',\r\n\t\t\t\t'declarative-form-field-multi-select': formField.type === 'multi-select',\r\n\t\t\t\t'declarative-form-field-checkbox': formField.type === 'checkbox',\r\n\t\t\t\t'declarative-form-field-multi_checkbox': formField.type === 'multi-checkbox',\r\n\t\t\t\t'declarative-form-field-radio': formField.type === 'radio'\r\n\t\t\t}\">\r\n\r\n\t\t\t<template v-if=\"isTextFormField(formField)\">\r\n\t\t\t\t<div class=\"input-wrapper\">\r\n\t\t\t\t\t<NcInputField\r\n\t\t\t\t\t\t:type=\"formField.type\"\r\n\t\t\t\t\t\t:label=\"t(formApp, formField.title)\"\r\n\t\t\t\t\t\t:value.sync=\"formFieldsData[formField.id].value\"\r\n\t\t\t\t\t\t:placeholder=\"t(formApp, formField.placeholder)\"\r\n\t\t\t\t\t\t@update:value=\"onChangeDebounced(formField)\"\r\n\t\t\t\t\t\t@submit=\"updateDeclarativeSettingsValue(formField)\"/>\r\n\t\t\t\t</div>\r\n\t\t\t\t<span class=\"hint\">{{ t(formApp, formField.description) }}</span>\r\n\t\t\t</template>\r\n\r\n\t\t\t<template v-if=\"formField.type === 'select'\">\r\n\t\t\t\t<label :for=\"formField.id + '_field'\">{{ t(formApp, formField.title) }}</label>\r\n\t\t\t\t<div class=\"input-wrapper\">\r\n\t\t\t\t\t<NcSelect\r\n\t\t\t\t\t\t:id=\"formField.id + '_field'\"\r\n\t\t\t\t\t\t:options=\"formField.options\"\r\n\t\t\t\t\t\t:placeholder=\"t(formApp, formField.placeholder)\"\r\n\t\t\t\t\t\t:label-outside=\"true\"\r\n\t\t\t\t\t\t:value=\"formFieldsData[formField.id].value\"\r\n\t\t\t\t\t\t@input=\"(value) => updateFormFieldDataValue(value, formField, true)\"/>\r\n\t\t\t\t</div>\r\n\t\t\t\t<span class=\"hint\">{{ t(formApp, formField.description) }}</span>\r\n\t\t\t</template>\r\n\r\n\t\t\t<template v-if=\"formField.type === 'multi-select'\">\r\n\t\t\t\t<label :for=\"formField.id + '_field'\">{{ t(formApp, formField.title) }}</label>\r\n\t\t\t\t<div class=\"input-wrapper\">\r\n\t\t\t\t\t<NcSelect\r\n\t\t\t\t\t\t:id=\"formField.id + '_field'\"\r\n\t\t\t\t\t\t:options=\"formField.options\"\r\n\t\t\t\t\t\t:placeholder=\"t(formApp, formField.placeholder)\"\r\n\t\t\t\t\t\t:multiple=\"true\"\r\n\t\t\t\t\t\t:label-outside=\"true\"\r\n\t\t\t\t\t\t:value=\"formFieldsData[formField.id].value\"\r\n\t\t\t\t\t\t@input=\"(value) => {\r\n\t\t\t\t\t\t\tformFieldsData[formField.id].value = value\r\n\t\t\t\t\t\t\tupdateDeclarativeSettingsValue(formField, JSON.stringify(formFieldsData[formField.id].value))\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\"/>\r\n\t\t\t\t</div>\r\n\t\t\t\t<span class=\"hint\">{{ t(formApp, formField.description) }}</span>\r\n\t\t\t</template>\r\n\r\n\t\t\t<template v-if=\"formField.type === 'checkbox'\">\r\n\t\t\t\t<label :for=\"formField.id + '_field'\">{{ t(formApp, formField.title) }}</label>\r\n\t\t\t\t<NcCheckboxRadioSwitch\r\n\t\t\t\t\t:id=\"formField.id + '_field'\"\r\n\t\t\t\t\t:checked=\"Boolean(formFieldsData[formField.id].value)\"\r\n\t\t\t\t\t@update:checked=\"(value) => {\r\n\t\t\t\t\t\tformField.value = value\r\n\t\t\t\t\t\tupdateFormFieldDataValue(+value, formField, true)\r\n\t\t\t\t\t}\r\n\t\t\t\t\">\r\n\t\t\t\t\t{{ t(formApp, formField.label) }}\r\n\t\t\t\t</NcCheckboxRadioSwitch>\r\n\t\t\t\t<span class=\"hint\">{{ t(formApp, formField.description) }}</span>\r\n\t\t\t</template>\r\n\r\n\t\t\t<template v-if=\"formField.type === 'multi-checkbox'\">\r\n\t\t\t\t<label :for=\"formField.id + '_field'\">{{ t(formApp, formField.title) }}</label>\r\n\t\t\t\t<NcCheckboxRadioSwitch\r\n\t\t\t\t\tv-for=\"option in formField.options\"\r\n\t\t\t\t\t:id=\"formField.id + '_field_' + option.value\"\r\n\t\t\t\t\t:key=\"option.value\"\r\n\t\t\t\t\t:checked=\"formFieldsData[formField.id].value[option.value]\"\r\n\t\t\t\t\t@update:checked=\"(value) => {\r\n\t\t\t\t\t\tformFieldsData[formField.id].value[option.value] = value\r\n\t\t\t\t\t\t// Update without re-generating initial formFieldsData.value object as the link to components are lost\r\n\t\t\t\t\t\tupdateDeclarativeSettingsValue(formField, JSON.stringify(formFieldsData[formField.id].value))\r\n\t\t\t\t\t}\r\n\t\t\t\t\">\r\n\t\t\t\t\t{{ t(formApp, option.name) }}\r\n\t\t\t\t</NcCheckboxRadioSwitch>\r\n\t\t\t\t<span class=\"hint\">{{ t(formApp, formField.description) }}</span>\r\n\t\t\t</template>\r\n\r\n\t\t\t<template v-if=\"formField.type === 'radio'\">\r\n\t\t\t\t<label :for=\"formField.id + '_field'\">{{ t(formApp, formField.title) }}</label>\r\n\t\t\t\t<NcCheckboxRadioSwitch\r\n\t\t\t\t\tv-for=\"option in formField.options\"\r\n\t\t\t\t\t:key=\"option.value\"\r\n\t\t\t\t\t:value=\"option.value\"\r\n\t\t\t\t\ttype=\"radio\"\r\n\t\t\t\t\t:checked=\"formFieldsData[formField.id].value\"\r\n\t\t\t\t\t@update:checked=\"(value) => updateFormFieldDataValue(value, formField, true)\">\r\n\t\t\t\t\t{{ t(formApp, option.name) }}\r\n\t\t\t\t</NcCheckboxRadioSwitch>\r\n\t\t\t\t<span class=\"hint\">{{ t(formApp, formField.description) }}</span>\r\n\t\t\t</template>\r\n\t\t</div>\r\n\t</NcSettingsSection>\r\n</template>\r\n\r\n<script>\r\nimport axios from '@nextcloud/axios'\r\nimport { generateOcsUrl } from '@nextcloud/router'\r\nimport { showError } from '@nextcloud/dialogs'\r\nimport debounce from 'debounce'\r\nimport NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection.js'\r\nimport NcInputField from '@nextcloud/vue/dist/Components/NcInputField.js'\r\nimport NcSelect from '@nextcloud/vue/dist/Components/NcSelect.js'\r\nimport NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'\r\n\r\nexport default {\r\n\tname: 'DeclarativeSection',\r\n\tcomponents: {\r\n\t\tNcSettingsSection,\r\n\t\tNcInputField,\r\n\t\tNcSelect,\r\n\t\tNcCheckboxRadioSwitch,\r\n\t},\r\n\tprops: {\r\n\t\tform: {\r\n\t\t\ttype: Object,\r\n\t\t\trequired: true,\r\n\t\t},\r\n\t},\r\n\tdata() {\r\n\t\treturn {\r\n\t\t\tformFieldsData: {},\r\n\t\t}\r\n\t},\r\n\tbeforeMount() {\r\n\t\tthis.initFormFieldsData()\r\n\t},\r\n\tcomputed: {\r\n\t\tformApp() {\r\n\t\t\treturn this.form.app || ''\r\n\t\t},\r\n\t\tformFields() {\r\n\t\t\treturn this.form.fields || []\r\n\t\t},\r\n\t},\r\n\tmethods: {\r\n\t\tinitFormFieldsData() {\r\n\t\t\tthis.form.fields.forEach((formField) => {\r\n\t\t\t\tif (formField.type === 'checkbox') {\r\n\t\t\t\t\t// convert bool to number using unary plus (+) operator\r\n\t\t\t\t\tthis.$set(formField, 'value', +formField.value)\r\n\t\t\t\t}\r\n\t\t\t\tif (formField.type === 'multi-checkbox') {\r\n\t\t\t\t\tif (formField.value === '') {\r\n\t\t\t\t\t\t// Init formFieldsData from options\r\n\t\t\t\t\t\tthis.$set(formField, 'value', {})\r\n\t\t\t\t\t\tformField.options.forEach(option => {\r\n\t\t\t\t\t\t\tthis.$set(formField.value, option.value, false)\r\n\t\t\t\t\t\t})\r\n\t\t\t\t\t} else {\r\n\t\t\t\t\t\tthis.$set(formField, 'value', JSON.parse(formField.value))\r\n\t\t\t\t\t\t// Merge possible new options\r\n\t\t\t\t\t\tformField.options.forEach(option => {\r\n\t\t\t\t\t\t\tif (!formField.value.hasOwnProperty(option.value)) {\r\n\t\t\t\t\t\t\t\tthis.$set(formField.value, option.value, false)\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t})\r\n\t\t\t\t\t\t// Remove options that are not in the form anymore\r\n\t\t\t\t\t\tObject.keys(formField.value).forEach(key => {\r\n\t\t\t\t\t\t\tif (!formField.options.find(option => option.value === key)) {\r\n\t\t\t\t\t\t\t\tdelete formField.value[key]\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t})\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t\tif (formField.type === 'multi-select') {\r\n\t\t\t\t\tif (formField.value === '') {\r\n\t\t\t\t\t\t// Init empty array for multi-select\r\n\t\t\t\t\t\tthis.$set(formField, 'value', [])\r\n\t\t\t\t\t} else {\r\n\t\t\t\t\t\t// JSON decode an array of multiple values set\r\n\t\t\t\t\t\tthis.$set(formField, 'value', JSON.parse(formField.value))\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t\tthis.$set(this.formFieldsData, formField.id, {\r\n\t\t\t\t\tvalue: formField.value,\r\n\t\t\t\t})\r\n\t\t\t})\r\n\t\t},\r\n\r\n\t\tupdateFormFieldDataValue(value, formField, update = false) {\r\n\t\t\tthis.formFieldsData[formField.id].value = value\r\n\t\t\tif (update) {\r\n\t\t\t\tthis.updateDeclarativeSettingsValue(formField)\r\n\t\t\t}\r\n\t\t},\r\n\r\n\t\tupdateDeclarativeSettingsValue(formField, value = null) {\r\n\t\t\ttry {\r\n\t\t\t\treturn axios.post(generateOcsUrl('settings/api/declarative/value'), {\r\n\t\t\t\t\tapp: this.formApp,\r\n\t\t\t\t\tformId: this.form.id.replace(this.formApp + '_', ''), // Remove app prefix to send clean form id\r\n\t\t\t\t\tfieldId: formField.id,\r\n\t\t\t\t\tvalue: value === null ? this.formFieldsData[formField.id].value : value,\r\n\t\t\t\t});\r\n\t\t\t} catch (err) {\r\n\t\t\t\tconsole.debug(err)\r\n\t\t\t\tshowError(t('settings', 'Failed to save setting'))\r\n\t\t\t}\r\n\t\t},\r\n\r\n\t\tonChangeDebounced: debounce(function(formField) {\r\n\t\t\tthis.updateDeclarativeSettingsValue(formField)\r\n\t\t}, 1000),\r\n\r\n\t\tisTextFormField(formField) {\r\n\t\t\treturn ['text', 'password', 'email', 'tel', 'url', 'number'].includes(formField.type)\r\n\t\t},\r\n\t},\r\n}\r\n</script>\r\n\r\n<style lang=\"scss\" scoped>\r\n.declarative-form-field {\r\n\tmargin: 20px 0;\r\n\tpadding: 10px 0;\r\n\r\n\t.input-wrapper {\r\n\t\twidth: 100%;\r\n\t\tmax-width: 400px;\r\n\t}\r\n\r\n\t&:last-child {\r\n\t\tborder-bottom: none;\r\n\t}\r\n\r\n\t.hint {\r\n\t\tdisplay: inline-block;\r\n\t\tcolor: var(--color-text-maxcontrast);\r\n\t\tmargin-left: 8px;\r\n\t\tpadding-top: 5px;\r\n\t}\r\n\r\n\t&-radio, &-multi_checkbox {\r\n\t\tmax-height: 250px;\r\n\t\toverflow-y: auto;\r\n\t}\r\n\r\n\t&-multi-select, &-select {\r\n\t\tdisplay: flex;\r\n\t\tflex-direction: column;\r\n\r\n\t\tlabel {\r\n\t\t\tmargin-bottom: 5px;\r\n\t\t}\r\n\t}\r\n}\r\n</style>\r\n","import mod from \"-!../../../../../node_modules/babel-loader/lib/index.js!../../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./DeclarativeSection.vue?vue&type=script&lang=js\"; export default mod; export * from \"-!../../../../../node_modules/babel-loader/lib/index.js!../../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./DeclarativeSection.vue?vue&type=script&lang=js\"","\n import API from \"!../../../../../node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js\";\n import domAPI from \"!../../../../../node_modules/style-loader/dist/runtime/styleDomAPI.js\";\n import insertFn from \"!../../../../../node_modules/style-loader/dist/runtime/insertBySelector.js\";\n import setAttributes from \"!../../../../../node_modules/style-loader/dist/runtime/setAttributesWithoutAttributes.js\";\n import insertStyleElement from \"!../../../../../node_modules/style-loader/dist/runtime/insertStyleElement.js\";\n import styleTagTransformFn from \"!../../../../../node_modules/style-loader/dist/runtime/styleTagTransform.js\";\n import content, * as namedExport from \"!!../../../../../node_modules/css-loader/dist/cjs.js!../../../../../node_modules/vue-loader/lib/loaders/stylePostLoader.js!../../../../../node_modules/sass-loader/dist/cjs.js!../../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./DeclarativeSection.vue?vue&type=style&index=0&id=49d8d244&prod&lang=scss&scoped=true\";\n \n \n\nvar options = {};\n\noptions.styleTagTransform = styleTagTransformFn;\noptions.setAttributes = setAttributes;\n\n options.insert = insertFn.bind(null, \"head\");\n \noptions.domAPI = domAPI;\noptions.insertStyleElement = insertStyleElement;\n\nvar update = API(content, options);\n\n\n\nexport * from \"!!../../../../../node_modules/css-loader/dist/cjs.js!../../../../../node_modules/vue-loader/lib/loaders/stylePostLoader.js!../../../../../node_modules/sass-loader/dist/cjs.js!../../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./DeclarativeSection.vue?vue&type=style&index=0&id=49d8d244&prod&lang=scss&scoped=true\";\n export default content && content.locals ? content.locals : undefined;\n","import { render, staticRenderFns } from \"./DeclarativeSection.vue?vue&type=template&id=49d8d244&scoped=true\"\nimport script from \"./DeclarativeSection.vue?vue&type=script&lang=js\"\nexport * from \"./DeclarativeSection.vue?vue&type=script&lang=js\"\nimport style0 from \"./DeclarativeSection.vue?vue&type=style&index=0&id=49d8d244&prod&lang=scss&scoped=true\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"49d8d244\",\n null\n \n)\n\nexport default component.exports","var render = function render(){var _vm=this,_c=_vm._self._c;return _c('NcSettingsSection',{staticClass:\"declarative-settings-section\",attrs:{\"name\":_vm.t(_vm.formApp, _vm.form.title),\"description\":_vm.t(_vm.formApp, _vm.form.description),\"doc-url\":_vm.form.doc_url || ''}},_vm._l((_vm.formFields),function(formField){return _c('div',{key:formField.id,staticClass:\"declarative-form-field\",class:{\n\t\t\t'declarative-form-field-text': _vm.isTextFormField(formField),\n\t\t\t'declarative-form-field-select': formField.type === 'select',\n\t\t\t'declarative-form-field-multi-select': formField.type === 'multi-select',\n\t\t\t'declarative-form-field-checkbox': formField.type === 'checkbox',\n\t\t\t'declarative-form-field-multi_checkbox': formField.type === 'multi-checkbox',\n\t\t\t'declarative-form-field-radio': formField.type === 'radio'\n\t\t},attrs:{\"aria-label\":_vm.t('settings', '{app}\\'s declarative setting field: {name}', { app: _vm.formApp, name: _vm.t(_vm.formApp, formField.title) })}},[(_vm.isTextFormField(formField))?[_c('div',{staticClass:\"input-wrapper\"},[_c('NcInputField',{attrs:{\"type\":formField.type,\"label\":_vm.t(_vm.formApp, formField.title),\"value\":_vm.formFieldsData[formField.id].value,\"placeholder\":_vm.t(_vm.formApp, formField.placeholder)},on:{\"update:value\":[function($event){return _vm.$set(_vm.formFieldsData[formField.id], \"value\", $event)},function($event){return _vm.onChangeDebounced(formField)}],\"submit\":function($event){return _vm.updateDeclarativeSettingsValue(formField)}}})],1),_vm._v(\" \"),_c('span',{staticClass:\"hint\"},[_vm._v(_vm._s(_vm.t(_vm.formApp, formField.description)))])]:_vm._e(),_vm._v(\" \"),(formField.type === 'select')?[_c('label',{attrs:{\"for\":formField.id + '_field'}},[_vm._v(_vm._s(_vm.t(_vm.formApp, formField.title)))]),_vm._v(\" \"),_c('div',{staticClass:\"input-wrapper\"},[_c('NcSelect',{attrs:{\"id\":formField.id + '_field',\"options\":formField.options,\"placeholder\":_vm.t(_vm.formApp, formField.placeholder),\"label-outside\":true,\"value\":_vm.formFieldsData[formField.id].value},on:{\"input\":(value) => _vm.updateFormFieldDataValue(value, formField, true)}})],1),_vm._v(\" \"),_c('span',{staticClass:\"hint\"},[_vm._v(_vm._s(_vm.t(_vm.formApp, formField.description)))])]:_vm._e(),_vm._v(\" \"),(formField.type === 'multi-select')?[_c('label',{attrs:{\"for\":formField.id + '_field'}},[_vm._v(_vm._s(_vm.t(_vm.formApp, formField.title)))]),_vm._v(\" \"),_c('div',{staticClass:\"input-wrapper\"},[_c('NcSelect',{attrs:{\"id\":formField.id + '_field',\"options\":formField.options,\"placeholder\":_vm.t(_vm.formApp, formField.placeholder),\"multiple\":true,\"label-outside\":true,\"value\":_vm.formFieldsData[formField.id].value},on:{\"input\":(value) => {\n\t\t\t\t\t\t_vm.formFieldsData[formField.id].value = value\n\t\t\t\t\t\t_vm.updateDeclarativeSettingsValue(formField, JSON.stringify(_vm.formFieldsData[formField.id].value))\n\t\t\t\t\t}}})],1),_vm._v(\" \"),_c('span',{staticClass:\"hint\"},[_vm._v(_vm._s(_vm.t(_vm.formApp, formField.description)))])]:_vm._e(),_vm._v(\" \"),(formField.type === 'checkbox')?[_c('label',{attrs:{\"for\":formField.id + '_field'}},[_vm._v(_vm._s(_vm.t(_vm.formApp, formField.title)))]),_vm._v(\" \"),_c('NcCheckboxRadioSwitch',{attrs:{\"id\":formField.id + '_field',\"checked\":Boolean(_vm.formFieldsData[formField.id].value)},on:{\"update:checked\":(value) => {\n\t\t\t\t\tformField.value = value\n\t\t\t\t\t_vm.updateFormFieldDataValue(+value, formField, true)\n\t\t\t\t}}},[_vm._v(\"\\n\\t\\t\\t\\t\"+_vm._s(_vm.t(_vm.formApp, formField.label))+\"\\n\\t\\t\\t\")]),_vm._v(\" \"),_c('span',{staticClass:\"hint\"},[_vm._v(_vm._s(_vm.t(_vm.formApp, formField.description)))])]:_vm._e(),_vm._v(\" \"),(formField.type === 'multi-checkbox')?[_c('label',{attrs:{\"for\":formField.id + '_field'}},[_vm._v(_vm._s(_vm.t(_vm.formApp, formField.title)))]),_vm._v(\" \"),_vm._l((formField.options),function(option){return _c('NcCheckboxRadioSwitch',{key:option.value,attrs:{\"id\":formField.id + '_field_' + option.value,\"checked\":_vm.formFieldsData[formField.id].value[option.value]},on:{\"update:checked\":(value) => {\n\t\t\t\t\t_vm.formFieldsData[formField.id].value[option.value] = value\n\t\t\t\t\t// Update without re-generating initial formFieldsData.value object as the link to components are lost\n\t\t\t\t\t_vm.updateDeclarativeSettingsValue(formField, JSON.stringify(_vm.formFieldsData[formField.id].value))\n\t\t\t\t}}},[_vm._v(\"\\n\\t\\t\\t\\t\"+_vm._s(_vm.t(_vm.formApp, option.name))+\"\\n\\t\\t\\t\")])}),_vm._v(\" \"),_c('span',{staticClass:\"hint\"},[_vm._v(_vm._s(_vm.t(_vm.formApp, formField.description)))])]:_vm._e(),_vm._v(\" \"),(formField.type === 'radio')?[_c('label',{attrs:{\"for\":formField.id + '_field'}},[_vm._v(_vm._s(_vm.t(_vm.formApp, formField.title)))]),_vm._v(\" \"),_vm._l((formField.options),function(option){return _c('NcCheckboxRadioSwitch',{key:option.value,attrs:{\"value\":option.value,\"type\":\"radio\",\"checked\":_vm.formFieldsData[formField.id].value},on:{\"update:checked\":(value) => _vm.updateFormFieldDataValue(value, formField, true)}},[_vm._v(\"\\n\\t\\t\\t\\t\"+_vm._s(_vm.t(_vm.formApp, option.name))+\"\\n\\t\\t\\t\")])}),_vm._v(\" \"),_c('span',{staticClass:\"hint\"},[_vm._v(_vm._s(_vm.t(_vm.formApp, formField.description)))])]:_vm._e()],2)}),0)\n}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","import Vue from 'vue';\nimport { loadState } from '@nextcloud/initial-state';\nimport { translate as t, translatePlural as n } from '@nextcloud/l10n';\nimport DeclarativeSection from './components/DeclarativeSettings/DeclarativeSection.vue';\nconst forms = loadState('settings', 'declarative-settings-forms', []);\nconsole.debug('Loaded declarative forms:', forms);\nfunction renderDeclarativeSettingsSections(forms) {\n Vue.mixin({ methods: { t, n } });\n const DeclarativeSettingsSection = Vue.extend(DeclarativeSection);\n for (const form of forms) {\n const el = `#${form.app}_${form.id}`;\n new DeclarativeSettingsSection({\n el: el,\n propsData: {\n form,\n },\n });\n }\n}\ndocument.addEventListener('DOMContentLoaded', () => {\n renderDeclarativeSettingsSections(forms);\n});\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.declarative-form-field[data-v-49d8d244]{margin:20px 0;padding:10px 0}.declarative-form-field .input-wrapper[data-v-49d8d244]{width:100%;max-width:400px}.declarative-form-field[data-v-49d8d244]:last-child{border-bottom:none}.declarative-form-field .hint[data-v-49d8d244]{display:inline-block;color:var(--color-text-maxcontrast);margin-left:8px;padding-top:5px}.declarative-form-field-radio[data-v-49d8d244],.declarative-form-field-multi_checkbox[data-v-49d8d244]{max-height:250px;overflow-y:auto}.declarative-form-field-multi-select[data-v-49d8d244],.declarative-form-field-select[data-v-49d8d244]{display:flex;flex-direction:column}.declarative-form-field-multi-select label[data-v-49d8d244],.declarative-form-field-select label[data-v-49d8d244]{margin-bottom:5px}`, \"\",{\"version\":3,\"sources\":[\"webpack://./apps/settings/src/components/DeclarativeSettings/DeclarativeSection.vue\"],\"names\":[],\"mappings\":\"AACA,yCACC,aAAA,CACA,cAAA,CAEA,wDACC,UAAA,CACA,eAAA,CAGD,oDACC,kBAAA,CAGD,+CACC,oBAAA,CACA,mCAAA,CACA,eAAA,CACA,eAAA,CAGD,uGACC,gBAAA,CACA,eAAA,CAGD,sGACC,YAAA,CACA,qBAAA,CAEA,kHACC,iBAAA\",\"sourcesContent\":[\"\\r\\n.declarative-form-field {\\r\\n\\tmargin: 20px 0;\\r\\n\\tpadding: 10px 0;\\r\\n\\r\\n\\t.input-wrapper {\\r\\n\\t\\twidth: 100%;\\r\\n\\t\\tmax-width: 400px;\\r\\n\\t}\\r\\n\\r\\n\\t&:last-child {\\r\\n\\t\\tborder-bottom: none;\\r\\n\\t}\\r\\n\\r\\n\\t.hint {\\r\\n\\t\\tdisplay: inline-block;\\r\\n\\t\\tcolor: var(--color-text-maxcontrast);\\r\\n\\t\\tmargin-left: 8px;\\r\\n\\t\\tpadding-top: 5px;\\r\\n\\t}\\r\\n\\r\\n\\t&-radio, &-multi_checkbox {\\r\\n\\t\\tmax-height: 250px;\\r\\n\\t\\toverflow-y: auto;\\r\\n\\t}\\r\\n\\r\\n\\t&-multi-select, &-select {\\r\\n\\t\\tdisplay: flex;\\r\\n\\t\\tflex-direction: column;\\r\\n\\r\\n\\t\\tlabel {\\r\\n\\t\\t\\tmargin-bottom: 5px;\\r\\n\\t\\t}\\r\\n\\t}\\r\\n}\\r\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tvar cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\tid: moduleId,\n\t\tloaded: false,\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n\t// Flag the module as loaded\n\tmodule.loaded = true;\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n// expose the modules object (__webpack_modules__)\n__webpack_require__.m = __webpack_modules__;\n\n","// getDefaultExport function for compatibility with non-harmony modules\n__webpack_require__.n = (module) => {\n\tvar getter = module && module.__esModule ?\n\t\t() => (module['default']) :\n\t\t() => (module);\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.f = {};\n// This file contains only the entry chunk.\n// The chunk loading function for additional chunks\n__webpack_require__.e = (chunkId) => {\n\treturn Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {\n\t\t__webpack_require__.f[key](chunkId, promises);\n\t\treturn promises;\n\t}, []));\n};","// This function allow to reference async chunks\n__webpack_require__.u = (chunkId) => {\n\t// return url for filenames based on template\n\treturn \"\" + chunkId + \"-\" + chunkId + \".js?v=\" + {\"1359\":\"79a120e5671b1b5ba537\",\"8618\":\"1e8f15db3b14455fef8f\"}[chunkId] + \"\";\n};","__webpack_require__.g = (function() {\n\tif (typeof globalThis === 'object') return globalThis;\n\ttry {\n\t\treturn this || new Function('return this')();\n\t} catch (e) {\n\t\tif (typeof window === 'object') return window;\n\t}\n})();","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","__webpack_require__.nmd = (module) => {\n\tmodule.paths = [];\n\tif (!module.children) module.children = [];\n\treturn module;\n};","__webpack_require__.j = 6085;","var scriptUrl;\nif (__webpack_require__.g.importScripts) scriptUrl = __webpack_require__.g.location + \"\";\nvar document = __webpack_require__.g.document;\nif (!scriptUrl && document) {\n\tif (document.currentScript)\n\t\tscriptUrl = document.currentScript.src;\n\tif (!scriptUrl) {\n\t\tvar scripts = document.getElementsByTagName(\"script\");\n\t\tif(scripts.length) {\n\t\t\tvar i = scripts.length - 1;\n\t\t\twhile (i > -1 && (!scriptUrl || !/^http(s?):/.test(scriptUrl))) scriptUrl = scripts[i--].src;\n\t\t}\n\t}\n}\n// When supporting browsers where an automatic publicPath is not supported you must specify an output.publicPath manually via configuration\n// or pass an empty string (\"\") and set the __webpack_public_path__ variable from your code to use your own logic.\nif (!scriptUrl) throw new Error(\"Automatic publicPath is not supported in this browser\");\nscriptUrl = scriptUrl.replace(/#.*$/, \"\").replace(/\\?.*$/, \"\").replace(/\\/[^\\/]+$/, \"/\");\n__webpack_require__.p = scriptUrl;","__webpack_require__.b = document.baseURI || self.location.href;\n\n// object to store loaded and loading chunks\n// undefined = chunk not loaded, null = chunk preloaded/prefetched\n// [resolve, reject, Promise] = chunk loading, 0 = chunk loaded\nvar installedChunks = {\n\t6085: 0\n};\n\n__webpack_require__.f.j = (chunkId, promises) => {\n\t\t// JSONP chunk loading for javascript\n\t\tvar installedChunkData = __webpack_require__.o(installedChunks, chunkId) ? installedChunks[chunkId] : undefined;\n\t\tif(installedChunkData !== 0) { // 0 means \"already installed\".\n\n\t\t\t// a Promise means \"currently loading\".\n\t\t\tif(installedChunkData) {\n\t\t\t\tpromises.push(installedChunkData[2]);\n\t\t\t} else {\n\t\t\t\tif(true) { // all chunks have JS\n\t\t\t\t\t// setup Promise in chunk cache\n\t\t\t\t\tvar promise = new Promise((resolve, reject) => (installedChunkData = installedChunks[chunkId] = [resolve, reject]));\n\t\t\t\t\tpromises.push(installedChunkData[2] = promise);\n\n\t\t\t\t\t// start chunk loading\n\t\t\t\t\tvar url = __webpack_require__.p + __webpack_require__.u(chunkId);\n\t\t\t\t\t// create error before stack unwound to get useful stacktrace later\n\t\t\t\t\tvar error = new Error();\n\t\t\t\t\tvar loadingEnded = (event) => {\n\t\t\t\t\t\tif(__webpack_require__.o(installedChunks, chunkId)) {\n\t\t\t\t\t\t\tinstalledChunkData = installedChunks[chunkId];\n\t\t\t\t\t\t\tif(installedChunkData !== 0) installedChunks[chunkId] = undefined;\n\t\t\t\t\t\t\tif(installedChunkData) {\n\t\t\t\t\t\t\t\tvar errorType = event && (event.type === 'load' ? 'missing' : event.type);\n\t\t\t\t\t\t\t\tvar realSrc = event && event.target && event.target.src;\n\t\t\t\t\t\t\t\terror.message = 'Loading chunk ' + chunkId + ' failed.\\n(' + errorType + ': ' + realSrc + ')';\n\t\t\t\t\t\t\t\terror.name = 'ChunkLoadError';\n\t\t\t\t\t\t\t\terror.type = errorType;\n\t\t\t\t\t\t\t\terror.request = realSrc;\n\t\t\t\t\t\t\t\tinstalledChunkData[1](error);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\t\t\t\t\t__webpack_require__.l(url, loadingEnded, \"chunk-\" + chunkId, chunkId);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n};\n\n// no prefetching\n\n// no preloaded\n\n// no HMR\n\n// no HMR manifest\n\n__webpack_require__.O.j = (chunkId) => (installedChunks[chunkId] === 0);\n\n// install a JSONP callback for chunk loading\nvar webpackJsonpCallback = (parentChunkLoadingFunction, data) => {\n\tvar chunkIds = data[0];\n\tvar moreModules = data[1];\n\tvar runtime = data[2];\n\t// add \"moreModules\" to the modules object,\n\t// then flag all \"chunkIds\" as loaded and fire callback\n\tvar moduleId, chunkId, i = 0;\n\tif(chunkIds.some((id) => (installedChunks[id] !== 0))) {\n\t\tfor(moduleId in moreModules) {\n\t\t\tif(__webpack_require__.o(moreModules, moduleId)) {\n\t\t\t\t__webpack_require__.m[moduleId] = moreModules[moduleId];\n\t\t\t}\n\t\t}\n\t\tif(runtime) var result = runtime(__webpack_require__);\n\t}\n\tif(parentChunkLoadingFunction) parentChunkLoadingFunction(data);\n\tfor(;i < chunkIds.length; i++) {\n\t\tchunkId = chunkIds[i];\n\t\tif(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {\n\t\t\tinstalledChunks[chunkId][0]();\n\t\t}\n\t\tinstalledChunks[chunkId] = 0;\n\t}\n\treturn __webpack_require__.O(result);\n}\n\nvar chunkLoadingGlobal = self[\"webpackChunknextcloud\"] = self[\"webpackChunknextcloud\"] || [];\nchunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));\nchunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));","__webpack_require__.nc = undefined;","// startup\n// Load entry module and return exports\n// This entry module depends on other loaded chunks and execution need to be delayed\nvar __webpack_exports__ = __webpack_require__.O(undefined, [4208], () => (__webpack_require__(27565)))\n__webpack_exports__ = __webpack_require__.O(__webpack_exports__);\n"],"names":["deferred","inProgress","dataWebpackPrefix","name","components","NcSettingsSection","NcInputField","NcSelect","NcCheckboxRadioSwitch","props","form","type","Object","required","data","formFieldsData","beforeMount","initFormFieldsData","computed","formApp","app","formFields","fields","methods","forEach","formField","$set","value","options","option","JSON","parse","hasOwnProperty","keys","key","find","id","updateFormFieldDataValue","update","arguments","length","undefined","updateDeclarativeSettingsValue","axios","post","generateOcsUrl","formId","replace","fieldId","err","console","debug","showError","t","onChangeDebounced","debounce","isTextFormField","includes","styleTagTransform","setAttributes","insert","domAPI","insertStyleElement","locals","_vm","this","_c","_self","staticClass","attrs","title","description","doc_url","_l","class","placeholder","on","$event","_v","_s","_e","stringify","Boolean","label","forms","loadState","document","addEventListener","Vue","mixin","n","DeclarativeSettingsSection","extend","DeclarativeSection","el","concat","propsData","renderDeclarativeSettingsSections","___CSS_LOADER_EXPORT___","push","module","__webpack_module_cache__","__webpack_require__","moduleId","cachedModule","exports","loaded","__webpack_modules__","call","m","O","result","chunkIds","fn","priority","notFulfilled","Infinity","i","fulfilled","j","every","splice","r","getter","__esModule","d","a","definition","o","defineProperty","enumerable","get","f","e","chunkId","Promise","all","reduce","promises","u","g","globalThis","Function","window","obj","prop","prototype","l","url","done","script","needAttach","scripts","getElementsByTagName","s","getAttribute","createElement","charset","timeout","nc","setAttribute","src","onScriptComplete","prev","event","onerror","onload","clearTimeout","doneFns","parentNode","removeChild","setTimeout","bind","target","head","appendChild","Symbol","toStringTag","nmd","paths","children","scriptUrl","importScripts","location","currentScript","test","Error","p","b","baseURI","self","href","installedChunks","installedChunkData","promise","resolve","reject","error","errorType","realSrc","message","request","webpackJsonpCallback","parentChunkLoadingFunction","moreModules","runtime","some","chunkLoadingGlobal","__webpack_exports__"],"sourceRoot":""}
\ No newline at end of file diff --git a/lib/composer/composer/LICENSE b/lib/composer/composer/LICENSE index f27399a042d..62ecfd8d004 100644 --- a/lib/composer/composer/LICENSE +++ b/lib/composer/composer/LICENSE @@ -1,4 +1,3 @@ - Copyright (c) Nils Adermann, Jordi Boggiano Permission is hereby granted, free of charge, to any person obtaining a copy @@ -18,4 +17,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 8cedc1a5c0b..4742c6c3054 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -631,6 +631,12 @@ return array( 'OCP\\Security\\VerificationToken\\InvalidTokenException' => $baseDir . '/lib/public/Security/VerificationToken/InvalidTokenException.php', 'OCP\\Server' => $baseDir . '/lib/public/Server.php', 'OCP\\Session\\Exceptions\\SessionNotAvailableException' => $baseDir . '/lib/public/Session/Exceptions/SessionNotAvailableException.php', + 'OCP\\Settings\\DeclarativeSettingsTypes' => $baseDir . '/lib/public/Settings/DeclarativeSettingsTypes.php', + 'OCP\\Settings\\Events\\DeclarativeSettingsGetValueEvent' => $baseDir . '/lib/public/Settings/Events/DeclarativeSettingsGetValueEvent.php', + 'OCP\\Settings\\Events\\DeclarativeSettingsRegisterFormEvent' => $baseDir . '/lib/public/Settings/Events/DeclarativeSettingsRegisterFormEvent.php', + 'OCP\\Settings\\Events\\DeclarativeSettingsSetValueEvent' => $baseDir . '/lib/public/Settings/Events/DeclarativeSettingsSetValueEvent.php', + 'OCP\\Settings\\IDeclarativeManager' => $baseDir . '/lib/public/Settings/IDeclarativeManager.php', + 'OCP\\Settings\\IDeclarativeSettingsForm' => $baseDir . '/lib/public/Settings/IDeclarativeSettingsForm.php', 'OCP\\Settings\\IDelegatedSettings' => $baseDir . '/lib/public/Settings/IDelegatedSettings.php', 'OCP\\Settings\\IIconSection' => $baseDir . '/lib/public/Settings/IIconSection.php', 'OCP\\Settings\\IManager' => $baseDir . '/lib/public/Settings/IManager.php', @@ -1755,6 +1761,7 @@ return array( 'OC\\Session\\Session' => $baseDir . '/lib/private/Session/Session.php', 'OC\\Settings\\AuthorizedGroup' => $baseDir . '/lib/private/Settings/AuthorizedGroup.php', 'OC\\Settings\\AuthorizedGroupMapper' => $baseDir . '/lib/private/Settings/AuthorizedGroupMapper.php', + 'OC\\Settings\\DeclarativeManager' => $baseDir . '/lib/private/Settings/DeclarativeManager.php', 'OC\\Settings\\Manager' => $baseDir . '/lib/private/Settings/Manager.php', 'OC\\Settings\\Section' => $baseDir . '/lib/private/Settings/Section.php', 'OC\\Setup' => $baseDir . '/lib/private/Setup.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 643b2031adb..f9b1d2eec8c 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -672,6 +672,12 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OCP\\Security\\VerificationToken\\InvalidTokenException' => __DIR__ . '/../../..' . '/lib/public/Security/VerificationToken/InvalidTokenException.php', 'OCP\\Server' => __DIR__ . '/../../..' . '/lib/public/Server.php', 'OCP\\Session\\Exceptions\\SessionNotAvailableException' => __DIR__ . '/../../..' . '/lib/public/Session/Exceptions/SessionNotAvailableException.php', + 'OCP\\Settings\\DeclarativeSettingsTypes' => __DIR__ . '/../../..' . '/lib/public/Settings/DeclarativeSettingsTypes.php', + 'OCP\\Settings\\Events\\DeclarativeSettingsGetValueEvent' => __DIR__ . '/../../..' . '/lib/public/Settings/Events/DeclarativeSettingsGetValueEvent.php', + 'OCP\\Settings\\Events\\DeclarativeSettingsRegisterFormEvent' => __DIR__ . '/../../..' . '/lib/public/Settings/Events/DeclarativeSettingsRegisterFormEvent.php', + 'OCP\\Settings\\Events\\DeclarativeSettingsSetValueEvent' => __DIR__ . '/../../..' . '/lib/public/Settings/Events/DeclarativeSettingsSetValueEvent.php', + 'OCP\\Settings\\IDeclarativeManager' => __DIR__ . '/../../..' . '/lib/public/Settings/IDeclarativeManager.php', + 'OCP\\Settings\\IDeclarativeSettingsForm' => __DIR__ . '/../../..' . '/lib/public/Settings/IDeclarativeSettingsForm.php', 'OCP\\Settings\\IDelegatedSettings' => __DIR__ . '/../../..' . '/lib/public/Settings/IDelegatedSettings.php', 'OCP\\Settings\\IIconSection' => __DIR__ . '/../../..' . '/lib/public/Settings/IIconSection.php', 'OCP\\Settings\\IManager' => __DIR__ . '/../../..' . '/lib/public/Settings/IManager.php', @@ -1796,6 +1802,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Session\\Session' => __DIR__ . '/../../..' . '/lib/private/Session/Session.php', 'OC\\Settings\\AuthorizedGroup' => __DIR__ . '/../../..' . '/lib/private/Settings/AuthorizedGroup.php', 'OC\\Settings\\AuthorizedGroupMapper' => __DIR__ . '/../../..' . '/lib/private/Settings/AuthorizedGroupMapper.php', + 'OC\\Settings\\DeclarativeManager' => __DIR__ . '/../../..' . '/lib/private/Settings/DeclarativeManager.php', 'OC\\Settings\\Manager' => __DIR__ . '/../../..' . '/lib/private/Settings/Manager.php', 'OC\\Settings\\Section' => __DIR__ . '/../../..' . '/lib/private/Settings/Section.php', 'OC\\Setup' => __DIR__ . '/../../..' . '/lib/private/Setup.php', diff --git a/lib/composer/composer/installed.php b/lib/composer/composer/installed.php index 88d9c302532..8189e7e4e35 100644 --- a/lib/composer/composer/installed.php +++ b/lib/composer/composer/installed.php @@ -3,7 +3,7 @@ 'name' => '__root__', 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => 'b6abfc4cba2d1ef4fdd8f2c22bbff46796b9485e', + 'reference' => '4ff660ca2e0baa02440ba07296ed7e75fa544c0e', 'type' => 'library', 'install_path' => __DIR__ . '/../../../', 'aliases' => array(), @@ -13,7 +13,7 @@ '__root__' => array( 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => 'b6abfc4cba2d1ef4fdd8f2c22bbff46796b9485e', + 'reference' => '4ff660ca2e0baa02440ba07296ed7e75fa544c0e', 'type' => 'library', 'install_path' => __DIR__ . '/../../../', 'aliases' => array(), diff --git a/lib/private/AppFramework/Bootstrap/RegistrationContext.php b/lib/private/AppFramework/Bootstrap/RegistrationContext.php index 6c51aafff9b..95f59d243c6 100644 --- a/lib/private/AppFramework/Bootstrap/RegistrationContext.php +++ b/lib/private/AppFramework/Bootstrap/RegistrationContext.php @@ -49,6 +49,7 @@ use OCP\Http\WellKnown\IHandler; use OCP\Notification\INotifier; use OCP\Profile\ILinkAction; use OCP\Search\IProvider; +use OCP\Settings\IDeclarativeSettingsForm; use OCP\SetupCheck\ISetupCheck; use OCP\Share\IPublicShareTemplateProvider; use OCP\SpeechToText\ISpeechToTextProvider; @@ -142,9 +143,6 @@ class RegistrationContext { /** @var ServiceRegistration<\OCP\TextToImage\IProvider>[] */ private $textToImageProviders = []; - - - /** @var ParameterRegistration[] */ private $sensitiveMethods = []; @@ -159,6 +157,9 @@ class RegistrationContext { /** @var PreviewProviderRegistration[] */ private array $previewProviders = []; + /** @var ServiceRegistration<IDeclarativeSettingsForm>[] */ + private array $declarativeSettings = []; + /** @var ServiceRegistration<ITeamResourceProvider>[] */ private array $teamResourceProviders = []; @@ -403,6 +404,13 @@ class RegistrationContext { $setupCheckClass ); } + + public function registerDeclarativeSettings(string $declarativeSettingsClass): void { + $this->context->registerDeclarativeSettings( + $this->appId, + $declarativeSettingsClass + ); + } }; } @@ -542,7 +550,6 @@ class RegistrationContext { ); } - /** * @psalm-param class-string<ITeamResourceProvider> $class */ @@ -577,6 +584,13 @@ class RegistrationContext { } /** + * @psalm-param class-string<IDeclarativeSettingsForm> $declarativeSettingsClass + */ + public function registerDeclarativeSettings(string $appId, string $declarativeSettingsClass): void { + $this->declarativeSettings[] = new ServiceRegistration($appId, $declarativeSettingsClass); + } + + /** * @param App[] $apps */ public function delegateCapabilityRegistrations(array $apps): void { @@ -893,11 +907,10 @@ class RegistrationContext { return $this->setupChecks; } - /** - * @return ServiceRegistration<ITeamResourceProvider>[] + * @return ServiceRegistration<IDeclarativeSettingsForm>[] */ - public function getTeamResourceProviders(): array { - return $this->teamResourceProviders; + public function getDeclarativeSettings(): array { + return $this->declarativeSettings; } } diff --git a/lib/private/Server.php b/lib/private/Server.php index cb5086acfc6..76c73383b2e 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -150,6 +150,7 @@ use OC\Security\SecureRandom; use OC\Security\TrustedDomainHelper; use OC\Security\VerificationToken\VerificationToken; use OC\Session\CryptoWrapper; +use OC\Settings\DeclarativeManager; use OC\SetupCheck\SetupCheckManager; use OC\Share20\ProviderFactory; use OC\Share20\ShareHelper; @@ -259,6 +260,7 @@ use OCP\Security\ISecureRandom; use OCP\Security\ITrustedDomainHelper; use OCP\Security\RateLimiting\ILimiter; use OCP\Security\VerificationToken\IVerificationToken; +use OCP\Settings\IDeclarativeManager; use OCP\SetupCheck\ISetupCheckManager; use OCP\Share\IProviderFactory; use OCP\Share\IShareHelper; @@ -1430,6 +1432,8 @@ class Server extends ServerContainer implements IServerContainer { $this->registerAlias(IAvailabilityCoordinator::class, AvailabilityCoordinator::class); + $this->registerAlias(IDeclarativeManager::class, DeclarativeManager::class); + $this->connectDispatcher(); } diff --git a/lib/private/Settings/DeclarativeManager.php b/lib/private/Settings/DeclarativeManager.php new file mode 100644 index 00000000000..8f788aa3cc5 --- /dev/null +++ b/lib/private/Settings/DeclarativeManager.php @@ -0,0 +1,402 @@ +<?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 OC\Settings; + +use Exception; +use OC\AppFramework\Bootstrap\Coordinator; +use OC\AppFramework\Middleware\Security\Exceptions\NotAdminException; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\IAppConfig; +use OCP\IConfig; +use OCP\IGroupManager; +use OCP\IUser; +use OCP\Server; +use OCP\Settings\DeclarativeSettingsTypes; +use OCP\Settings\Events\DeclarativeSettingsGetValueEvent; +use OCP\Settings\Events\DeclarativeSettingsRegisterFormEvent; +use OCP\Settings\Events\DeclarativeSettingsSetValueEvent; +use OCP\Settings\IDeclarativeManager; +use OCP\Settings\IDeclarativeSettingsForm; +use Psr\Log\LoggerInterface; + +/** + * @psalm-import-type DeclarativeSettingsValueTypes from IDeclarativeSettingsForm + * @psalm-import-type DeclarativeSettingsStorageType from IDeclarativeSettingsForm + * @psalm-import-type DeclarativeSettingsSectionType from IDeclarativeSettingsForm + * @psalm-import-type DeclarativeSettingsFormSchemaWithValues from IDeclarativeSettingsForm + * @psalm-import-type DeclarativeSettingsFormSchemaWithoutValues from IDeclarativeSettingsForm + */ +class DeclarativeManager implements IDeclarativeManager { + public function __construct( + private IEventDispatcher $eventDispatcher, + private IGroupManager $groupManager, + private Coordinator $coordinator, + private IConfig $config, + private IAppConfig $appConfig, + private LoggerInterface $logger, + ) { + } + + /** + * @var array<string, list<DeclarativeSettingsFormSchemaWithoutValues>> + */ + private array $appSchemas = []; + + /** + * @inheritdoc + */ + public function registerSchema(string $app, array $schema): void { + $this->appSchemas[$app] ??= []; + + if (!$this->validateSchema($app, $schema)) { + throw new Exception('Invalid schema. Please check the logs for more details.'); + } + + foreach ($this->appSchemas[$app] as $otherSchema) { + if ($otherSchema['id'] === $schema['id']) { + throw new Exception('Duplicate form IDs detected: ' . $schema['id']); + } + } + + $fieldIDs = array_map(fn ($field) => $field['id'], $schema['fields']); + $otherFieldIDs = array_merge(...array_map(fn ($schema) => array_map(fn ($field) => $field['id'], $schema['fields']), $this->appSchemas[$app])); + $intersectionFieldIDs = array_intersect($fieldIDs, $otherFieldIDs); + if (count($intersectionFieldIDs) > 0) { + throw new Exception('Non unique field IDs detected: ' . join(', ', $intersectionFieldIDs)); + } + + $this->appSchemas[$app][] = $schema; + } + + /** + * @inheritdoc + */ + public function loadSchemas(): void { + $declarativeSettings = $this->coordinator->getRegistrationContext()->getDeclarativeSettings(); + foreach ($declarativeSettings as $declarativeSetting) { + /** @var IDeclarativeSettingsForm $declarativeSettingObject */ + $declarativeSettingObject = Server::get($declarativeSetting->getService()); + $this->registerSchema($declarativeSetting->getAppId(), $declarativeSettingObject->getSchema()); + } + + $this->eventDispatcher->dispatchTyped(new DeclarativeSettingsRegisterFormEvent($this)); + } + + /** + * @inheritdoc + */ + public function getFormIDs(IUser $user, string $type, string $section): array { + $isAdmin = $this->groupManager->isAdmin($user->getUID()); + /** @var array<string, list<string>> $formIds */ + $formIds = []; + + foreach ($this->appSchemas as $app => $schemas) { + $ids = []; + usort($schemas, [$this, 'sortSchemasByPriorityCallback']); + foreach ($schemas as $schema) { + if ($schema['section_type'] === DeclarativeSettingsTypes::SECTION_TYPE_ADMIN && !$isAdmin) { + continue; + } + if ($schema['section_type'] === $type && $schema['section_id'] === $section) { + $ids[] = $schema['id']; + } + } + + if (!empty($ids)) { + $formIds[$app] = array_merge($formIds[$app] ?? [], $ids); + } + } + + return $formIds; + } + + /** + * @inheritdoc + * @throws Exception + */ + public function getFormsWithValues(IUser $user, ?string $type, ?string $section): array { + $isAdmin = $this->groupManager->isAdmin($user->getUID()); + $forms = []; + + foreach ($this->appSchemas as $app => $schemas) { + foreach ($schemas as $schema) { + if ($type !== null && $schema['section_type'] !== $type) { + continue; + } + if ($section !== null && $schema['section_id'] !== $section) { + continue; + } + // If listing all fields skip the admin fields which a non-admin user has no access to + if ($type === null && $schema['section_type'] === 'admin' && !$isAdmin) { + continue; + } + + $s = $schema; + $s['app'] = $app; + + foreach ($s['fields'] as &$field) { + $field['value'] = $this->getValue($user, $app, $schema['id'], $field['id']); + } + unset($field); + + /** @var DeclarativeSettingsFormSchemaWithValues $s */ + $forms[] = $s; + } + } + + usort($forms, [$this, 'sortSchemasByPriorityCallback']); + + return $forms; + } + + private function sortSchemasByPriorityCallback(mixed $a, mixed $b): int { + if ($a['priority'] === $b['priority']) { + return 0; + } + return $a['priority'] > $b['priority'] ? -1 : 1; + } + + /** + * @return DeclarativeSettingsStorageType + */ + private function getStorageType(string $app, string $fieldId): string { + if (array_key_exists($app, $this->appSchemas)) { + foreach ($this->appSchemas[$app] as $schema) { + foreach ($schema['fields'] as $field) { + if ($field['id'] == $fieldId) { + if (array_key_exists('storage_type', $field)) { + return $field['storage_type']; + } + } + } + + if (array_key_exists('storage_type', $schema)) { + return $schema['storage_type']; + } + } + } + + return DeclarativeSettingsTypes::STORAGE_TYPE_INTERNAL; + } + + /** + * @return DeclarativeSettingsSectionType + * @throws Exception + */ + private function getSectionType(string $app, string $fieldId): string { + if (array_key_exists($app, $this->appSchemas)) { + foreach ($this->appSchemas[$app] as $schema) { + foreach ($schema['fields'] as $field) { + if ($field['id'] == $fieldId) { + return $schema['section_type']; + } + } + } + } + + throw new Exception('Unknown fieldId "' . $fieldId . '"'); + } + + /** + * @psalm-param DeclarativeSettingsSectionType $sectionType + * @throws NotAdminException + */ + private function assertAuthorized(IUser $user, string $sectionType): void { + if ($sectionType === 'admin' && !$this->groupManager->isAdmin($user->getUID())) { + throw new NotAdminException('Logged in user does not have permission to access these settings.'); + } + } + + /** + * @return DeclarativeSettingsValueTypes + * @throws Exception + * @throws NotAdminException + */ + private function getValue(IUser $user, string $app, string $formId, string $fieldId): mixed { + $sectionType = $this->getSectionType($app, $fieldId); + $this->assertAuthorized($user, $sectionType); + + $storageType = $this->getStorageType($app, $fieldId); + switch ($storageType) { + case DeclarativeSettingsTypes::STORAGE_TYPE_EXTERNAL: + $event = new DeclarativeSettingsGetValueEvent($user, $app, $formId, $fieldId); + $this->eventDispatcher->dispatchTyped($event); + return $event->getValue(); + case DeclarativeSettingsTypes::STORAGE_TYPE_INTERNAL: + return $this->getInternalValue($user, $app, $formId, $fieldId); + default: + throw new Exception('Unknown storage type "' . $storageType . '"'); + } + } + + /** + * @inheritdoc + */ + public function setValue(IUser $user, string $app, string $formId, string $fieldId, mixed $value): void { + $sectionType = $this->getSectionType($app, $fieldId); + $this->assertAuthorized($user, $sectionType); + + $storageType = $this->getStorageType($app, $fieldId); + switch ($storageType) { + case DeclarativeSettingsTypes::STORAGE_TYPE_EXTERNAL: + $this->eventDispatcher->dispatchTyped(new DeclarativeSettingsSetValueEvent($user, $app, $formId, $fieldId, $value)); + break; + case DeclarativeSettingsTypes::STORAGE_TYPE_INTERNAL: + $this->saveInternalValue($user, $app, $fieldId, $value); + break; + default: + throw new Exception('Unknown storage type "' . $storageType . '"'); + } + } + + private function getInternalValue(IUser $user, string $app, string $formId, string $fieldId): mixed { + $sectionType = $this->getSectionType($app, $fieldId); + $defaultValue = $this->getDefaultValue($app, $formId, $fieldId); + switch ($sectionType) { + case DeclarativeSettingsTypes::SECTION_TYPE_ADMIN: + return $this->config->getAppValue($app, $fieldId, $defaultValue); + case DeclarativeSettingsTypes::SECTION_TYPE_PERSONAL: + return $this->config->getUserValue($user->getUID(), $app, $fieldId, $defaultValue); + default: + throw new Exception('Unknown section type "' . $sectionType . '"'); + } + } + + private function saveInternalValue(IUser $user, string $app, string $fieldId, mixed $value): void { + $sectionType = $this->getSectionType($app, $fieldId); + switch ($sectionType) { + case DeclarativeSettingsTypes::SECTION_TYPE_ADMIN: + $this->appConfig->setValueString($app, $fieldId, $value); + break; + case DeclarativeSettingsTypes::SECTION_TYPE_PERSONAL: + $this->config->setUserValue($user->getUID(), $app, $fieldId, $value); + break; + default: + throw new Exception('Unknown section type "' . $sectionType . '"'); + } + } + + private function getDefaultValue(string $app, string $formId, string $fieldId): mixed { + foreach ($this->appSchemas[$app] as $schema) { + if ($schema['id'] === $formId) { + foreach ($schema['fields'] as $field) { + if ($field['id'] === $fieldId) { + if (isset($field['default'])) { + if (is_array($field['default'])) { + return json_encode($field['default']); + } + return $field['default']; + } + } + } + } + } + return null; + } + + private function validateSchema(string $appId, array $schema): bool { + if (!isset($schema['id'])) { + $this->logger->warning('Attempt to register a declarative settings schema with no id', ['app' => $appId]); + return false; + } + $formId = $schema['id']; + if (!isset($schema['section_type'])) { + $this->logger->warning('Declarative settings: missing section_type', ['app' => $appId, 'form_id' => $formId]); + return false; + } + if (!in_array($schema['section_type'], [DeclarativeSettingsTypes::SECTION_TYPE_ADMIN, DeclarativeSettingsTypes::SECTION_TYPE_PERSONAL])) { + $this->logger->warning('Declarative settings: invalid section_type', ['app' => $appId, 'form_id' => $formId, 'section_type' => $schema['section_type']]); + return false; + } + if (!isset($schema['section_id'])) { + $this->logger->warning('Declarative settings: missing section_id', ['app' => $appId, 'form_id' => $formId]); + return false; + } + if (!isset($schema['storage_type'])) { + $this->logger->warning('Declarative settings: missing storage_type', ['app' => $appId, 'form_id' => $formId]); + return false; + } + if (!in_array($schema['storage_type'], [DeclarativeSettingsTypes::STORAGE_TYPE_EXTERNAL, DeclarativeSettingsTypes::STORAGE_TYPE_INTERNAL])) { + $this->logger->warning('Declarative settings: invalid storage_type', ['app' => $appId, 'form_id' => $formId, 'storage_type' => $schema['storage_type']]); + return false; + } + if (!isset($schema['title'])) { + $this->logger->warning('Declarative settings: missing title', ['app' => $appId, 'form_id' => $formId]); + return false; + } + if (!isset($schema['fields']) || !is_array($schema['fields'])) { + $this->logger->warning('Declarative settings: missing or invalid fields', ['app' => $appId, 'form_id' => $formId]); + return false; + } + foreach ($schema['fields'] as $field) { + if (!isset($field['id'])) { + $this->logger->warning('Declarative settings: missing field id', ['app' => $appId, 'form_id' => $formId, 'field' => $field]); + return false; + } + $fieldId = $field['id']; + if (!isset($field['title'])) { + $this->logger->warning('Declarative settings: missing field title', ['app' => $appId, 'form_id' => $formId, 'field_id' => $fieldId]); + return false; + } + if (!isset($field['type'])) { + $this->logger->warning('Declarative settings: missing field type', ['app' => $appId, 'form_id' => $formId, 'field_id' => $fieldId]); + return false; + } + if (!in_array($field['type'], [ + DeclarativeSettingsTypes::MULTI_SELECT, DeclarativeSettingsTypes::MULTI_CHECKBOX, DeclarativeSettingsTypes::RADIO, + DeclarativeSettingsTypes::SELECT, DeclarativeSettingsTypes::CHECKBOX, + DeclarativeSettingsTypes::URL, DeclarativeSettingsTypes::EMAIL, DeclarativeSettingsTypes::NUMBER, + DeclarativeSettingsTypes::TEL, DeclarativeSettingsTypes::TEXT, DeclarativeSettingsTypes::PASSWORD, + ])) { + $this->logger->warning('Declarative settings: invalid field type', [ + 'app' => $appId, 'form_id' => $formId, 'field_id' => $fieldId, 'type' => $field['type'], + ]); + return false; + } + if (!$this->validateField($appId, $formId, $field)) { + return false; + } + } + + return true; + } + + private function validateField(string $appId, string $formId, array $field): bool { + $fieldId = $field['id']; + if (in_array($field['type'], [ + DeclarativeSettingsTypes::MULTI_SELECT, DeclarativeSettingsTypes::MULTI_CHECKBOX, DeclarativeSettingsTypes::RADIO, + DeclarativeSettingsTypes::SELECT + ])) { + if (!isset($field['options'])) { + $this->logger->warning('Declarative settings: missing field options', ['app' => $appId, 'form_id' => $formId, 'field_id' => $fieldId]); + return false; + } + if (!is_array($field['options'])) { + $this->logger->warning('Declarative settings: field options should be an array', ['app' => $appId, 'form_id' => $formId, 'field_id' => $fieldId]); + return false; + } + } + return true; + } +} diff --git a/lib/public/AppFramework/Bootstrap/IRegistrationContext.php b/lib/public/AppFramework/Bootstrap/IRegistrationContext.php index f515180cef5..09bc703e0a4 100644 --- a/lib/public/AppFramework/Bootstrap/IRegistrationContext.php +++ b/lib/public/AppFramework/Bootstrap/IRegistrationContext.php @@ -399,4 +399,15 @@ interface IRegistrationContext { * @since 28.0.0 */ public function registerSetupCheck(string $setupCheckClass): void; + + /** + * Register an implementation of \OCP\Settings\IDeclarativeSettings that + * will handle the implementation of declarative settings + * + * @param string $declarativeSettingsClass + * @psalm-param class-string<\OCP\Settings\IDeclarativeSettingsForm> $declarativeSettingsClass + * @return void + * @since 29.0.0 + */ + public function registerDeclarativeSettings(string $declarativeSettingsClass): void; } diff --git a/lib/public/Settings/DeclarativeSettingsTypes.php b/lib/public/Settings/DeclarativeSettingsTypes.php new file mode 100644 index 00000000000..01e20ee7cc9 --- /dev/null +++ b/lib/public/Settings/DeclarativeSettingsTypes.php @@ -0,0 +1,145 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2023 Andrey Borysenko <andrey.borysenko@nextcloud.com> + * + * @author Andrey Borysenko <andrey.borysenko@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 OCP\Settings; + +/** + * Declarative settings types supported in the IDeclarativeSettingsForm forms + * + * @since 29.0.0 + */ +final class DeclarativeSettingsTypes { + /** + * IDeclarativeSettingsForm section_type which is determines where the form is displayed + * + * @since 29.0.0 + */ + public const SECTION_TYPE_ADMIN = 'admin'; + + /** + * IDeclarativeSettingsForm section_type which is determines where the form is displayed + * + * @since 29.0.0 + */ + public const SECTION_TYPE_PERSONAL = 'personal'; + + /** + * IDeclarativeSettingsForm storage_type which is determines where and how the config value is stored + * + * + * For `external` storage_type the app implementing \OCP\Settings\SetDeclarativeSettingsValueEvent and \OCP\Settings\GetDeclarativeSettingsValueEvent events is responsible for storing and retrieving the config value. + * + * @since 29.0.0 + */ + public const STORAGE_TYPE_EXTERNAL = 'external'; + + /** + * IDeclarativeSettingsForm storage_type which is determines where and how the config value is stored + * + * For `internal` storage_type the config value is stored in default `appconfig` and `preferences` tables. + * For `external` storage_type the app implementing \OCP\Settings\SetDeclarativeSettingsValueEvent and \OCP\Settings\GetDeclarativeSettingsValueEvent events is responsible for storing and retrieving the config value. + * + * @since 29.0.0 + */ + public const STORAGE_TYPE_INTERNAL = 'internal'; + + /** + * NcInputField type text + * + * @since 29.0.0 + */ + public const TEXT = 'text'; + + /** + * NcInputField type password + * + * @since 29.0.0 + */ + public const PASSWORD = 'password'; + + /** + * NcInputField type email + * + * @since 29.0.0 + */ + public const EMAIL = 'email'; + + /** + * NcInputField type tel + * + * @since 29.0.0 + */ + public const TEL = 'tel'; + + /** + * NcInputField type url + * + * @since 29.0.0 + */ + public const URL = 'url'; + + /** + * NcInputField type number + * + * @since 29.0.0 + */ + public const NUMBER = 'number'; + + /** + * NcCheckboxRadioSwitch type checkbox + * + * @since 29.0.0 + */ + public const CHECKBOX = 'checkbox'; + + /** + * Multiple NcCheckboxRadioSwitch type checkbox representing a one config value (saved as JSON object) + * + * @since 29.0.0 + */ + public const MULTI_CHECKBOX = 'multi-checkbox'; + + /** + * NcCheckboxRadioSwitch type radio + * + * @since 29.0.0 + */ + public const RADIO = 'radio'; + + /** + * NcSelect + * + * @since 29.0.0 + */ + public const SELECT = 'select'; + + /** + * Multiple NcSelect representing a one config value (saved as JSON array) + * + * @since 29.0.0 + */ + public const MULTI_SELECT = 'multi-select'; +} diff --git a/lib/public/Settings/Events/DeclarativeSettingsGetValueEvent.php b/lib/public/Settings/Events/DeclarativeSettingsGetValueEvent.php new file mode 100644 index 00000000000..c7224f761fd --- /dev/null +++ b/lib/public/Settings/Events/DeclarativeSettingsGetValueEvent.php @@ -0,0 +1,81 @@ +<?php + +namespace OCP\Settings\Events; + +use Exception; +use OCP\EventDispatcher\Event; +use OCP\IUser; +use OCP\Settings\IDeclarativeSettingsForm; + +/** + * @psalm-import-type DeclarativeSettingsValueTypes from IDeclarativeSettingsForm + * + * @since 29.0.0 + */ +class DeclarativeSettingsGetValueEvent extends Event { + /** + * @var ?DeclarativeSettingsValueTypes + */ + private mixed $value = null; + + /** + * @since 29.0.0 + */ + public function __construct( + private IUser $user, + private string $app, + private string $formId, + private string $fieldId, + ) { + parent::__construct(); + } + + /** + * @since 29.0.0 + */ + public function getUser(): IUser { + return $this->user; + } + + /** + * @since 29.0.0 + */ + public function getApp(): string { + return $this->app; + } + + /** + * @since 29.0.0 + */ + public function getFormId(): string { + return $this->formId; + } + + /** + * @since 29.0.0 + */ + public function getFieldId(): string { + return $this->fieldId; + } + + /** + * @since 29.0.0 + */ + public function setValue(mixed $value): void { + $this->value = $value; + } + + /** + * @return DeclarativeSettingsValueTypes + * @throws Exception + * + * @since 29.0.0 + */ + public function getValue(): mixed { + if ($this->value === null) { + throw new Exception('Value not set'); + } + + return $this->value; + } +} diff --git a/lib/public/Settings/Events/DeclarativeSettingsRegisterFormEvent.php b/lib/public/Settings/Events/DeclarativeSettingsRegisterFormEvent.php new file mode 100644 index 00000000000..d017596e545 --- /dev/null +++ b/lib/public/Settings/Events/DeclarativeSettingsRegisterFormEvent.php @@ -0,0 +1,29 @@ +<?php + +namespace OCP\Settings\Events; + +use OCP\EventDispatcher\Event; +use OCP\Settings\IDeclarativeManager; +use OCP\Settings\IDeclarativeSettingsForm; + +/** + * @psalm-import-type DeclarativeSettingsFormSchemaWithoutValues from IDeclarativeSettingsForm + * + * @since 29.0.0 + */ +class DeclarativeSettingsRegisterFormEvent extends Event { + /** + * @since 29.0.0 + */ + public function __construct(private IDeclarativeManager $manager) { + parent::__construct(); + } + + /** + * @param DeclarativeSettingsFormSchemaWithoutValues $schema + * @since 29.0.0 + */ + public function registerSchema(string $app, array $schema): void { + $this->manager->registerSchema($app, $schema); + } +} diff --git a/lib/public/Settings/Events/DeclarativeSettingsSetValueEvent.php b/lib/public/Settings/Events/DeclarativeSettingsSetValueEvent.php new file mode 100644 index 00000000000..d298c1ec9b4 --- /dev/null +++ b/lib/public/Settings/Events/DeclarativeSettingsSetValueEvent.php @@ -0,0 +1,63 @@ +<?php + +namespace OCP\Settings\Events; + +use OCP\EventDispatcher\Event; +use OCP\IUser; +use OCP\Settings\IDeclarativeSettingsForm; + +/** + * @psalm-import-type DeclarativeSettingsValueTypes from IDeclarativeSettingsForm + * + * @since 29.0.0 + */ +class DeclarativeSettingsSetValueEvent extends Event { + /** + * @param DeclarativeSettingsValueTypes $value + * @since 29.0.0 + */ + public function __construct( + private IUser $user, + private string $app, + private string $formId, + private string $fieldId, + private mixed $value, + ) { + parent::__construct(); + } + + /** + * @since 29.0.0 + */ + public function getUser(): IUser { + return $this->user; + } + + /** + * @since 29.0.0 + */ + public function getApp(): string { + return $this->app; + } + + /** + * @since 29.0.0 + */ + public function getFormId(): string { + return $this->formId; + } + + /** + * @since 29.0.0 + */ + public function getFieldId(): string { + return $this->fieldId; + } + + /** + * @since 29.0.0 + */ + public function getValue(): mixed { + return $this->value; + } +} diff --git a/lib/public/Settings/IDeclarativeManager.php b/lib/public/Settings/IDeclarativeManager.php new file mode 100644 index 00000000000..586296bac13 --- /dev/null +++ b/lib/public/Settings/IDeclarativeManager.php @@ -0,0 +1,89 @@ +<?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 OCP\Settings; + +use Exception; +use OC\AppFramework\Middleware\Security\Exceptions\NotAdminException; +use OCP\IUser; + +/** + * @since 29.0.0 + * + * @psalm-import-type DeclarativeSettingsValueTypes from IDeclarativeSettingsForm + * @psalm-import-type DeclarativeSettingsSectionType from IDeclarativeSettingsForm + * @psalm-import-type DeclarativeSettingsFormSchemaWithValues from IDeclarativeSettingsForm + * @psalm-import-type DeclarativeSettingsFormSchemaWithoutValues from IDeclarativeSettingsForm + */ +interface IDeclarativeManager { + /** + * Registers a new declarative settings schema. + * + * @param DeclarativeSettingsFormSchemaWithoutValues $schema + * @since 29.0.0 + */ + public function registerSchema(string $app, array $schema): void; + + /** + * Load all schemas from the registration context and events. + * + * @since 29.0.0 + */ + public function loadSchemas(): void; + + /** + * Gets the IDs of the forms for the given type and section. + * + * @param DeclarativeSettingsSectionType $type + * @param string $section + * @return array<string, list<string>> + * + * @since 29.0.0 + */ + public function getFormIDs(IUser $user, string $type, string $section): array; + + /** + * Gets the forms including the field values for the given type and section. + * + * @param IUser $user Used for reading values from the personal section or for authorization for the admin section. + * @param ?DeclarativeSettingsSectionType $type If it is null the forms will not be filtered by type. + * @param ?string $section If it is null the forms will not be filtered by section. + * @return list<DeclarativeSettingsFormSchemaWithValues> + * + * @since 29.0.0 + */ + public function getFormsWithValues(IUser $user, ?string $type, ?string $section): array; + + /** + * Sets a value for the given field ID. + * + * @param IUser $user Used for storing values in the personal section or for authorization for the admin section. + * @param DeclarativeSettingsValueTypes $value + * + * @throws Exception + * @throws NotAdminException + * + * @since 29.0.0 + */ + public function setValue(IUser $user, string $app, string $formId, string $fieldId, mixed $value): void; +} diff --git a/lib/public/Settings/IDeclarativeSettingsForm.php b/lib/public/Settings/IDeclarativeSettingsForm.php new file mode 100644 index 00000000000..fd9fe120f6f --- /dev/null +++ b/lib/public/Settings/IDeclarativeSettingsForm.php @@ -0,0 +1,78 @@ +<?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 OCP\Settings; + +/** + * @since 29.0.0 + * + * @psalm-type DeclarativeSettingsSectionType = 'admin'|'personal' + * + * @psalm-type DeclarativeSettingsStorageType = 'internal'|'external' + * + * @psalm-type DeclarativeSettingsValueTypes = string|int|float|bool|list<string> + * + * @psalm-type DeclarativeSettingsFormField = 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}>, + * } + * + * @psalm-type DeclarativeSettingsFormFieldWithValue = DeclarativeSettingsFormField&array{ + * value: DeclarativeSettingsValueTypes, + * } + * + * @psalm-type DeclarativeSettingsFormSchema = array{ + * id: string, + * priority: int, + * section_type: DeclarativeSettingsSectionType, + * section_id: string, + * storage_type: DeclarativeSettingsStorageType, + * title: string, + * description?: string, + * doc_url?: string, + * } + * + * @psalm-type DeclarativeSettingsFormSchemaWithValues = DeclarativeSettingsFormSchema&array{ + * app: string, + * fields: list<DeclarativeSettingsFormFieldWithValue>, + * } + * + * @psalm-type DeclarativeSettingsFormSchemaWithoutValues = DeclarativeSettingsFormSchema&array{ + * fields: list<DeclarativeSettingsFormField>, + * } + */ +interface IDeclarativeSettingsForm { + /** + * Gets the schema that defines the declarative settings form + * + * @return DeclarativeSettingsFormSchemaWithoutValues + * @since 29.0.0 + */ + public function getSchema(): array; +} diff --git a/tests/lib/Settings/DeclarativeManagerTest.php b/tests/lib/Settings/DeclarativeManagerTest.php new file mode 100644 index 00000000000..7df519d984b --- /dev/null +++ b/tests/lib/Settings/DeclarativeManagerTest.php @@ -0,0 +1,536 @@ +<?php + +/** + * @copyright Copyright (c) 2023 Andrey Borysenko <andrey.borysenko@nextcloud.com> + * + * @author Andrey Borysenko <andrey.borysenko@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 Test\Settings; + +use OC\AppFramework\Bootstrap\Coordinator; +use OC\Settings\DeclarativeManager; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\IAppConfig; +use OCP\IConfig; +use OCP\IGroupManager; +use OCP\IUser; +use OCP\Settings\DeclarativeSettingsTypes; +use OCP\Settings\Events\DeclarativeSettingsSetValueEvent; +use OCP\Settings\IDeclarativeManager; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Test\TestCase; + +class DeclarativeManagerTest extends TestCase { + + /** @var IDeclarativeManager|MockObject */ + private $declarativeManager; + + /** @var IEventDispatcher|MockObject */ + private $eventDispatcher; + + /** @var IGroupManager|MockObject */ + private $groupManager; + + /** @var Coordinator|MockObject */ + private $coordinator; + + /** @var IConfig|MockObject */ + private $config; + + /** @var IAppConfig|MockObject */ + private $appConfig; + + /** @var LoggerInterface|MockObject */ + private $logger; + + /** @var IUser|MockObject */ + private $user; + + /** @var IUser|MockObject */ + private $adminUser; + + public const validSchemaAllFields = [ + 'id' => 'test_form_1', + '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', // NcSettingsSection name + 'description' => 'These fields are rendered dynamically from declarative schema', // NcSettingsSection description + 'doc_url' => '', // NcSettingsSection doc_url for documentation or help page, empty string if not needed + 'fields' => [ + [ + 'id' => 'test_field_7', // configkey + 'title' => 'Multi-selection', // name or label + 'description' => 'Select some option setting', // hint + 'type' => DeclarativeSettingsTypes::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' => 'Select single option', + 'description' => 'Single option radio buttons', + 'type' => DeclarativeSettingsTypes::RADIO, // radio (NcCheckboxRadioSwitch type radio) + 'placeholder' => 'Select single option, test 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_field_1', // configkey + 'title' => 'Default text field', // label + 'description' => 'Set some simple text setting', // hint + 'type' => DeclarativeSettingsTypes::TEXT, + 'placeholder' => 'Enter text setting', // placeholder + 'default' => 'foo', + ], + [ + 'id' => 'test_field_1_1', + 'title' => 'Email field', + 'description' => 'Set email config', + 'type' => DeclarativeSettingsTypes::EMAIL, + 'placeholder' => 'Enter email', + 'default' => '', + ], + [ + 'id' => 'test_field_1_2', + 'title' => 'Tel field', + 'description' => 'Set tel config', + 'type' => DeclarativeSettingsTypes::TEL, + 'placeholder' => 'Enter your tel', + 'default' => '', + ], + [ + 'id' => 'test_field_1_3', + 'title' => 'Url (website) field', + 'description' => 'Set url config', + 'type' => 'url', + 'placeholder' => 'Enter url', + 'default' => '', + ], + [ + 'id' => 'test_field_1_4', + 'title' => 'Number field', + 'description' => 'Set number config', + 'type' => DeclarativeSettingsTypes::NUMBER, + 'placeholder' => 'Enter number value', + 'default' => 0, + ], + [ + 'id' => 'test_field_2', + 'title' => 'Password', + 'description' => 'Set some secure value setting', + 'type' => 'password', + 'placeholder' => 'Set secure value', + 'default' => '', + ], + [ + 'id' => 'test_field_3', + 'title' => 'Selection', + 'description' => 'Select some option setting', + 'type' => DeclarativeSettingsTypes::SELECT, + 'options' => ['foo', 'bar', 'baz'], + 'placeholder' => 'Select some option setting', + 'default' => 'foo', + ], + [ + 'id' => 'test_field_4', + 'title' => 'Toggle something', + 'description' => 'Select checkbox option setting', + 'type' => DeclarativeSettingsTypes::CHECKBOX, + 'label' => 'Verify something if enabled', + 'default' => false, + ], + [ + 'id' => 'test_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, + 'default' => ['foo' => true, 'bar' => true], + 'options' => [ + [ + 'name' => 'Foo', + 'value' => 'foo', // multiple-checkbox configkey + ], + [ + 'name' => 'Bar', + 'value' => 'bar', + ], + [ + 'name' => 'Baz', + 'value' => 'baz', + ], + [ + 'name' => 'Qux', + 'value' => 'qux', + ], + ], + ], + [ + 'id' => 'test_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' => 'Second radio', + 'value' => 'baz' + ], + ], + ], + ], + ]; + + public static bool $testSetInternalValueAfterChange = false; + + protected function setUp(): void { + parent::setUp(); + + $this->eventDispatcher = $this->createMock(IEventDispatcher::class); + $this->groupManager = $this->createMock(IGroupManager::class); + $this->coordinator = $this->createMock(Coordinator::class); + $this->config = $this->createMock(IConfig::class); + $this->appConfig = $this->createMock(IAppConfig::class); + $this->logger = $this->createMock(LoggerInterface::class); + + $this->declarativeManager = new DeclarativeManager( + $this->eventDispatcher, + $this->groupManager, + $this->coordinator, + $this->config, + $this->appConfig, + $this->logger + ); + + $this->user = $this->createMock(IUser::class); + $this->user->expects($this->any()) + ->method('getUID') + ->willReturn('test_user'); + + $this->adminUser = $this->createMock(IUser::class); + $this->adminUser->expects($this->any()) + ->method('getUID') + ->willReturn('admin_test_user'); + + $this->groupManager->expects($this->any()) + ->method('isAdmin') + ->willReturnCallback(function ($userId) { + return $userId === 'admin_test_user'; + }); + } + + public function testRegisterSchema(): void { + $app = 'testing'; + $schema = self::validSchemaAllFields; + $this->declarativeManager->registerSchema($app, $schema); + $formIds = $this->declarativeManager->getFormIDs($this->adminUser, $schema['section_type'], $schema['section_id']); + $this->assertTrue(isset($formIds[$app]) && in_array($schema['id'], $formIds[$app])); + } + + /** + * Simple test to verify that exception is thrown when trying to register schema with duplicate id + */ + public function testRegisterDuplicateSchema(): void { + $this->declarativeManager->registerSchema('testing', self::validSchemaAllFields); + $this->expectException(\Exception::class); + $this->declarativeManager->registerSchema('testing', self::validSchemaAllFields); + } + + /** + * It's not allowed to register schema with duplicate fields ids for the same app + */ + public function testRegisterSchemaWithDuplicateFields(): void { + // Register first valid schema + $this->declarativeManager->registerSchema('testing', self::validSchemaAllFields); + // Register second schema with duplicate fields, but different schema id + $this->expectException(\Exception::class); + $schema = self::validSchemaAllFields; + $schema['id'] = 'test_form_2'; + $this->declarativeManager->registerSchema('testing', $schema); + } + + public function testRegisterMultipleSchemasAndDuplicate(): void { + $app = 'testing'; + $schema = self::validSchemaAllFields; + $this->declarativeManager->registerSchema($app, $schema); + $formIds = $this->declarativeManager->getFormIDs($this->adminUser, $schema['section_type'], $schema['section_id']); + // 1. Check that form is registered for the app + $this->assertTrue(isset($formIds[$app]) && in_array($schema['id'], $formIds[$app])); + $app = 'testing2'; + $this->declarativeManager->registerSchema($app, $schema); + $formIds = $this->declarativeManager->getFormIDs($this->adminUser, $schema['section_type'], $schema['section_id']); + // 2. Check that form is registered for the second app + $this->assertTrue(isset($formIds[$app]) && in_array($schema['id'], $formIds[$app])); + $app = 'testing'; + $this->expectException(\Exception::class); // expecting duplicate form id and duplicate fields ids exception + $this->declarativeManager->registerSchema($app, $schema); + $schemaDuplicateFields = self::validSchemaAllFields; + $schemaDuplicateFields['id'] = 'test_form_2'; // change form id to test duplicate fields + $this->declarativeManager->registerSchema($app, $schemaDuplicateFields); + // 3. Check that not valid form with duplicate fields is not registered + $formIds = $this->declarativeManager->getFormIDs($this->adminUser, $schemaDuplicateFields['section_type'], $schemaDuplicateFields['section_id']); + $this->assertFalse(isset($formIds[$app]) && in_array($schemaDuplicateFields['id'], $formIds[$app])); + } + + /** + * @dataProvider dataValidateSchema + */ + public function testValidateSchema(bool $expected, bool $expectException, string $app, array $schema): void { + if ($expectException) { + $this->expectException(\Exception::class); + } + $this->declarativeManager->registerSchema($app, $schema); + $formIds = $this->declarativeManager->getFormIDs($this->adminUser, $schema['section_type'], $schema['section_id']); + $this->assertEquals($expected, isset($formIds[$app]) && in_array($schema['id'], $formIds[$app])); + } + + public static function dataValidateSchema(): array { + return [ + 'valid schema with all supported fields' => [ + true, + false, + 'testing', + self::validSchemaAllFields, + ], + 'invalid schema with missing id' => [ + false, + true, + 'testing', + [ + 'priority' => 10, + 'section_type' => DeclarativeSettingsTypes::SECTION_TYPE_ADMIN, + 'section_id' => 'additional', + 'storage_type' => DeclarativeSettingsTypes::STORAGE_TYPE_INTERNAL, + 'title' => 'Test declarative settings', + 'description' => 'These fields are rendered dynamically from declarative schema', + 'doc_url' => '', + 'fields' => [ + [ + 'id' => 'test_field_7', + 'title' => 'Multi-selection', + 'description' => 'Select some option setting', + 'type' => DeclarativeSettingsTypes::MULTI_SELECT, + 'options' => ['foo', 'bar', 'baz'], + 'placeholder' => 'Select some multiple options', + 'default' => ['foo', 'bar'], + ], + ], + ], + ], + 'invalid schema with invalid field' => [ + false, + true, + 'testing', + [ + 'id' => 'test_form_1', + 'priority' => 10, + 'section_type' => DeclarativeSettingsTypes::SECTION_TYPE_ADMIN, + 'section_id' => 'additional', + 'storage_type' => DeclarativeSettingsTypes::STORAGE_TYPE_INTERNAL, + 'title' => 'Test declarative settings', + 'description' => 'These fields are rendered dynamically from declarative schema', + 'doc_url' => '', + 'fields' => [ + [ + 'id' => 'test_invalid_field', + 'title' => 'Invalid field', + 'description' => 'Some invalid setting description', + 'type' => 'some_invalid_type', + 'placeholder' => 'Some invalid field placeholder', + 'default' => null, + ], + ], + ], + ], + ]; + } + + public function testGetFormIDs(): void { + $app = 'testing'; + $schema = self::validSchemaAllFields; + $this->declarativeManager->registerSchema($app, $schema); + $formIds = $this->declarativeManager->getFormIDs($this->adminUser, $schema['section_type'], $schema['section_id']); + $this->assertTrue(isset($formIds[$app]) && in_array($schema['id'], $formIds[$app])); + $app = 'testing2'; + $this->declarativeManager->registerSchema($app, $schema); + $formIds = $this->declarativeManager->getFormIDs($this->adminUser, $schema['section_type'], $schema['section_id']); + $this->assertTrue(isset($formIds[$app]) && in_array($schema['id'], $formIds[$app])); + } + + /** + * Check that form with default values is returned with internal storage_type + */ + public function testGetFormsWithDefaultValues(): void { + $app = 'testing'; + $schema = self::validSchemaAllFields; + $this->declarativeManager->registerSchema($app, $schema); + + $this->config->expects($this->any()) + ->method('getAppValue') + ->willReturnCallback(fn ($app, $configkey, $default) => $default); + + $forms = $this->declarativeManager->getFormsWithValues($this->adminUser, $schema['section_type'], $schema['section_id']); + $this->assertNotEmpty($forms); + $this->assertTrue(array_search($schema['id'], array_column($forms, 'id')) !== false); + // Check some_real_setting field default value + $someRealSettingField = array_values(array_filter(array_filter($forms, fn ($form) => $form['id'] === $schema['id'])[0]['fields'], fn ($field) => $field['id'] === 'some_real_setting'))[0]; + $schemaSomeRealSettingField = array_values(array_filter($schema['fields'], fn ($field) => $field['id'] === 'some_real_setting'))[0]; + $this->assertEquals($schemaSomeRealSettingField['default'], $someRealSettingField['default']); + } + + /** + * Check values in json format to ensure that they are properly encoded + */ + public function testGetFormsWithDefaultValuesJson(): void { + $app = 'testing'; + $schema = [ + 'id' => 'test_form_1', + 'priority' => 10, + 'section_type' => DeclarativeSettingsTypes::SECTION_TYPE_PERSONAL, + 'section_id' => 'additional', + 'storage_type' => DeclarativeSettingsTypes::STORAGE_TYPE_INTERNAL, + 'title' => 'Test declarative settings', + 'description' => 'These fields are rendered dynamically from declarative schema', + 'doc_url' => '', + 'fields' => [ + [ + 'id' => 'test_field_json', + 'title' => 'Multi-selection', + 'description' => 'Select some option setting', + 'type' => DeclarativeSettingsTypes::MULTI_SELECT, + 'options' => ['foo', 'bar', 'baz'], + 'placeholder' => 'Select some multiple options', + 'default' => ['foo', 'bar'], + ], + ], + ]; + $this->declarativeManager->registerSchema($app, $schema); + + // config->getUserValue() should be called with json encoded default value + $this->config->expects($this->once()) + ->method('getUserValue') + ->with($this->adminUser->getUID(), $app, 'test_field_json', json_encode($schema['fields'][0]['default'])) + ->willReturn(json_encode($schema['fields'][0]['default'])); + + $forms = $this->declarativeManager->getFormsWithValues($this->adminUser, $schema['section_type'], $schema['section_id']); + $this->assertNotEmpty($forms); + $this->assertTrue(array_search($schema['id'], array_column($forms, 'id')) !== false); + $testFieldJson = array_values(array_filter(array_filter($forms, fn ($form) => $form['id'] === $schema['id'])[0]['fields'], fn ($field) => $field['id'] === 'test_field_json'))[0]; + $this->assertEquals(json_encode($schema['fields'][0]['default']), $testFieldJson['value']); + } + + /** + * Check that saving value for field with internal storage_type is handled by core + */ + public function testSetInternalValue(): void { + $app = 'testing'; + $schema = self::validSchemaAllFields; + $this->declarativeManager->registerSchema($app, $schema); + self::$testSetInternalValueAfterChange = false; + + $this->config->expects($this->any()) + ->method('getAppValue') + ->willReturnCallback(function ($app, $configkey, $default) { + if ($configkey === 'some_real_setting' && self::$testSetInternalValueAfterChange) { + return '120m'; + } + return $default; + }); + + $this->appConfig->expects($this->once()) + ->method('setValueString') + ->with($app, 'some_real_setting', '120m'); + + $forms = $this->declarativeManager->getFormsWithValues($this->adminUser, $schema['section_type'], $schema['section_id']); + $someRealSettingField = array_values(array_filter(array_filter($forms, fn ($form) => $form['id'] === $schema['id'])[0]['fields'], fn ($field) => $field['id'] === 'some_real_setting'))[0]; + $this->assertEquals('40m', $someRealSettingField['value']); // first check that default value (40m) is returned + + // Set new value for some_real_setting field + $this->declarativeManager->setValue($this->adminUser, $app, $schema['id'], 'some_real_setting', '120m'); + self::$testSetInternalValueAfterChange = true; + + $forms = $this->declarativeManager->getFormsWithValues($this->adminUser, $schema['section_type'], $schema['section_id']); + $this->assertNotEmpty($forms); + $this->assertTrue(array_search($schema['id'], array_column($forms, 'id')) !== false); + // Check some_real_setting field default value + $someRealSettingField = array_values(array_filter(array_filter($forms, fn ($form) => $form['id'] === $schema['id'])[0]['fields'], fn ($field) => $field['id'] === 'some_real_setting'))[0]; + $this->assertEquals('120m', $someRealSettingField['value']); + } + + public function testSetExternalValue(): void { + $app = 'testing'; + $schema = self::validSchemaAllFields; + // Change storage_type to external and section_type to personal + $schema['storage_type'] = DeclarativeSettingsTypes::STORAGE_TYPE_EXTERNAL; + $schema['section_type'] = DeclarativeSettingsTypes::SECTION_TYPE_PERSONAL; + $this->declarativeManager->registerSchema($app, $schema); + + $setDeclarativeSettingsValueEvent = new DeclarativeSettingsSetValueEvent( + $this->adminUser, + $app, + $schema['id'], + 'some_real_setting', + '120m' + ); + + $this->eventDispatcher->expects($this->once()) + ->method('dispatchTyped') + ->with($setDeclarativeSettingsValueEvent); + $this->declarativeManager->setValue($this->adminUser, $app, $schema['id'], 'some_real_setting', '120m'); + } + + public function testAdminFormUserUnauthorized(): void { + $app = 'testing'; + $schema = self::validSchemaAllFields; + $this->declarativeManager->registerSchema($app, $schema); + + $this->expectException(\Exception::class); + $this->declarativeManager->getFormsWithValues($this->user, $schema['section_type'], $schema['section_id']); + } +} diff --git a/webpack.modules.js b/webpack.modules.js index 09be290eb10..95ad048eed8 100644 --- a/webpack.modules.js +++ b/webpack.modules.js @@ -98,6 +98,7 @@ module.exports = { 'vue-settings-personal-password': path.join(__dirname, 'apps/settings/src', 'main-personal-password.js'), 'vue-settings-personal-security': path.join(__dirname, 'apps/settings/src', 'main-personal-security.js'), 'vue-settings-personal-webauthn': path.join(__dirname, 'apps/settings/src', 'main-personal-webauth.js'), + 'declarative-settings-forms': path.join(__dirname, 'apps/settings/src', 'main-declarative-settings-forms.ts'), }, sharebymail: { 'vue-settings-admin-sharebymail': path.join(__dirname, 'apps/sharebymail/src', 'main-admin.js'), |