From dae7adec2a1c027d186484498e15c1c4e4e97c44 Mon Sep 17 00:00:00 2001
From: Ruben van der Linde
Date: Wed, 30 Sep 2026 07:34:18 +0200
Subject: [PATCH 01/60] feat(people): custom profile fields, expertise tags and
a directory-wide people search that respects profile visibility
Administrators define up to ten fields (text or tags, filled by the person or read
from LDAP); people fill their own on their personal settings; GET /api/people?q=
matches name, email, role, headline, biography and searchable fields across the
directory; a private standard field neither shows nor matches for others, and a
group-only field only for colleagues who share a group.
REQ-PEX-001..004 (widgets-people-expertise-and-fields)
---
appinfo/info.xml | 5 +-
appinfo/routes.php | 7 +
lib/AppInfo/Application.php | 19 +
lib/BackgroundJob/ProfileFieldsSyncJob.php | 86 +++
lib/Controller/PeopleWidgetController.php | 4 +
lib/Controller/ProfileFieldsController.php | 205 ++++++
lib/Db/ProfileValue.php | 127 ++++
lib/Db/ProfileValueMapper.php | 192 +++++
lib/Listener/ProfileFieldsListener.php | 88 +++
.../Version002012Date20260930080000.php | 101 +++
lib/Service/PeopleWidgetService.php | 131 +++-
lib/Service/ProfileFieldService.php | 674 ++++++++++++++++++
lib/Settings/LaunchPadPersonal.php | 90 +++
lib/actions.seed.json | 4 +
templates/settings/personal.php | 8 +
.../ProfileFieldsControllerTest.php | 119 ++++
.../Database/ProfileValuesDatabaseTest.php | 55 ++
tests/Unit/Service/ActionAuthBaselineTest.php | 5 +
.../Unit/Service/PeopleWidgetServiceTest.php | 136 +++-
.../Unit/Service/ProfileFieldServiceTest.php | 318 +++++++++
tests/Unit/Support/InMemoryProfileValues.php | 137 ++++
21 files changed, 2500 insertions(+), 11 deletions(-)
create mode 100644 lib/BackgroundJob/ProfileFieldsSyncJob.php
create mode 100644 lib/Controller/ProfileFieldsController.php
create mode 100644 lib/Db/ProfileValue.php
create mode 100644 lib/Db/ProfileValueMapper.php
create mode 100644 lib/Listener/ProfileFieldsListener.php
create mode 100644 lib/Migration/Version002012Date20260930080000.php
create mode 100644 lib/Service/ProfileFieldService.php
create mode 100644 lib/Settings/LaunchPadPersonal.php
create mode 100644 templates/settings/personal.php
create mode 100644 tests/Unit/Controller/ProfileFieldsControllerTest.php
create mode 100644 tests/Unit/Database/ProfileValuesDatabaseTest.php
create mode 100644 tests/Unit/Service/ProfileFieldServiceTest.php
create mode 100644 tests/Unit/Support/InMemoryProfileValues.php
diff --git a/appinfo/info.xml b/appinfo/info.xml
index 7c699720c..c268a4903 100644
--- a/appinfo/info.xml
+++ b/appinfo/info.xml
@@ -52,7 +52,7 @@ Ideaal voor organisaties die consistente, samengestelde dashboards willen voor h
Vrij en open source onder de EUPL-1.2-licentie.
]]>
- 1.1.6-unstable.20260930050000
+ 1.1.6-unstable.20260930080000EUPL-1.2ConductionLaunchPad
@@ -95,6 +95,8 @@ Vrij en open source onder de EUPL-1.2-licentie.
OCA\LaunchPad\BackgroundJob\OrphanedDataCleanupJobOCA\LaunchPad\BackgroundJob\HealthPingRefreshJob
+
+ OCA\LaunchPad\BackgroundJob\ProfileFieldsSyncJob
@@ -169,6 +171,7 @@ Vrij en open source onder de EUPL-1.2-licentie.
OCA\LaunchPad\Settings\LaunchPadAdminOCA\LaunchPad\Settings\LaunchPadAdminSection
+ OCA\LaunchPad\Settings\LaunchPadPersonal
diff --git a/appinfo/routes.php b/appinfo/routes.php
index d747b1403..8b77a0048 100644
--- a/appinfo/routes.php
+++ b/appinfo/routes.php
@@ -455,6 +455,13 @@
// returns `{users, total, hasMore}` with offset-based pagination.
['name' => 'peopleWidget#getUsers', 'url' => '/api/people', 'verb' => 'GET'],
+ // Custom profile fields (REQ-PEX-001, REQ-PEX-002): own values for
+ // every signed-in person, definitions for administrators.
+ ['name' => 'profileFields#getOwn', 'url' => '/api/profile-fields/me', 'verb' => 'GET'],
+ ['name' => 'profileFields#saveOwn', 'url' => '/api/profile-fields/me', 'verb' => 'PUT'],
+ ['name' => 'profileFields#getDefinitions', 'url' => '/api/profile-fields/definitions', 'verb' => 'GET'],
+ ['name' => 'profileFields#saveDefinitions', 'url' => '/api/profile-fields/definitions', 'verb' => 'PUT'],
+
// Setup wizard endpoints (REQ-WIZ-008, REQ-WIZ-009, REQ-WIZ-003).
// Admin-only via runtime `IGroupManager::isAdmin` check inside the
// controller. The state endpoint also drives the "Run setup wizard"
diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php
index e90b899d6..0b7860e23 100644
--- a/lib/AppInfo/Application.php
+++ b/lib/AppInfo/Application.php
@@ -27,6 +27,7 @@
use OCA\LaunchPad\Listener\GroupDeletedListener;
use OCA\LaunchPad\Listener\LocksListener;
use OCA\LaunchPad\Listener\MetadataValuesListener;
+use OCA\LaunchPad\Listener\ProfileFieldsListener;
use OCA\LaunchPad\Listener\PublicSharesListener;
use OCA\LaunchPad\Listener\ReactionsListener;
use OCA\LaunchPad\Listener\TranslationsListener;
@@ -41,6 +42,7 @@
use OCA\LaunchPad\Service\ImportService;
use OCA\LaunchPad\Service\PublicShareContext;
use OCA\LaunchPad\Service\StoreService;
+use OCP\Accounts\UserUpdatedEvent;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext;
use OCP\AppFramework\Bootstrap\IBootstrap;
@@ -48,6 +50,7 @@
use OCP\Group\Events\GroupDeletedEvent;
use OCP\Security\CSP\AddContentSecurityPolicyEvent;
use OCP\User\Events\UserDeletedEvent;
+use OCP\User\Events\UserLoggedInEvent;
/**
* Application bootstrap.
@@ -125,6 +128,22 @@ private function registerUserLifecycle(IRegistrationContext $context): void {
event: UserDeletedEvent::class,
listener: UserDeletedListener::class
);
+
+ // Custom profile fields (REQ-PEX-001, REQ-PEX-004): LDAP sync and the
+ // standard-field mirror on sign-in and profile edits; values go with
+ // the user.
+ $context->registerEventListener(
+ event: UserLoggedInEvent::class,
+ listener: ProfileFieldsListener::class
+ );
+ $context->registerEventListener(
+ event: UserUpdatedEvent::class,
+ listener: ProfileFieldsListener::class
+ );
+ $context->registerEventListener(
+ event: UserDeletedEvent::class,
+ listener: ProfileFieldsListener::class
+ );
}//end registerUserLifecycle()
/**
diff --git a/lib/BackgroundJob/ProfileFieldsSyncJob.php b/lib/BackgroundJob/ProfileFieldsSyncJob.php
new file mode 100644
index 000000000..6757f4e97
--- /dev/null
+++ b/lib/BackgroundJob/ProfileFieldsSyncJob.php
@@ -0,0 +1,86 @@
+
+ * @copyright 2026 Conduction b.v.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ * @version GIT:auto
+ * @link https://conduction.nl
+ *
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
+ * SPDX-License-Identifier: EUPL-1.2
+ */
+
+declare(strict_types=1);
+
+namespace OCA\LaunchPad\BackgroundJob;
+
+use OCA\LaunchPad\Service\ProfileFieldService;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\BackgroundJob\TimedJob;
+use OCP\IUser;
+use OCP\IUserManager;
+use Psr\Log\LoggerInterface;
+use Throwable;
+
+/**
+ * Daily profile field sync.
+ *
+ * @spec openspec/specs/people-widget/spec.md
+ */
+class ProfileFieldsSyncJob extends TimedJob {
+ /**
+ * Constructor.
+ *
+ * @param ITimeFactory $time Time factory.
+ * @param IUserManager $userManager Users.
+ * @param ProfileFieldService $profileFields Profile field service.
+ * @param LoggerInterface $logger Logger.
+ *
+ * @spec openspec/specs/people-widget/spec.md
+ */
+ public function __construct(
+ ITimeFactory $time,
+ private readonly IUserManager $userManager,
+ private readonly ProfileFieldService $profileFields,
+ private readonly LoggerInterface $logger,
+ ) {
+ parent::__construct(time: $time);
+ $this->setInterval(seconds: 86400);
+ }//end __construct()
+
+ /**
+ * Refresh every user that has signed in; one failure does not stop the rest.
+ *
+ * @param mixed $argument Unused.
+ *
+ * @return void
+ *
+ * @spec openspec/specs/people-widget/spec.md
+ */
+ protected function run($argument): void {
+ $failed = 0;
+ $this->userManager->callForSeenUsers(
+ function (IUser $user) use (&$failed): void {
+ try {
+ $this->profileFields->syncLdapFields(user: $user);
+ $this->profileFields->mirrorStandardFields(user: $user);
+ } catch (Throwable) {
+ $failed++;
+ }
+ }
+ );
+
+ if ($failed > 0) {
+ $this->logger->warning(message: 'ProfileFieldsSyncJob: ' . $failed . ' user(s) could not be refreshed.');
+ }
+ }//end run()
+}//end class
diff --git a/lib/Controller/PeopleWidgetController.php b/lib/Controller/PeopleWidgetController.php
index 49157559f..2adcd7ad4 100644
--- a/lib/Controller/PeopleWidgetController.php
+++ b/lib/Controller/PeopleWidgetController.php
@@ -88,6 +88,7 @@ public function __construct(
* @param string|null $sortBy Sort key.
* @param int|null $limit Page size.
* @param int|null $offset Page offset.
+ * @param string|null $q Search text; 2 or more characters search the whole directory by name, email and profile fields (REQ-PEX-003).
*
* @return JSONResponse
*
@@ -102,6 +103,7 @@ public function getUsers(
?string $sortBy = 'displayName',
?int $limit = PeopleWidgetService::DEFAULT_LIMIT,
?int $offset = 0,
+ ?string $q = null,
): JSONResponse {
if ($this->userId === null) {
return new JSONResponse(['error' => 'Not authenticated'], Http::STATUS_UNAUTHORIZED);
@@ -137,6 +139,8 @@ public function getUsers(
sortBy: ($sortBy ?? 'displayName'),
limit: ($limit ?? PeopleWidgetService::DEFAULT_LIMIT),
offset: ($offset ?? 0),
+ query: $q,
+ viewerId: $user->getUID(),
);
} catch (InvalidArgumentException $e) {
return ResponseHelper::error(
diff --git a/lib/Controller/ProfileFieldsController.php b/lib/Controller/ProfileFieldsController.php
new file mode 100644
index 000000000..219206ac9
--- /dev/null
+++ b/lib/Controller/ProfileFieldsController.php
@@ -0,0 +1,205 @@
+
+ * @copyright 2026 Conduction b.v.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ * @version GIT:auto
+ * @link https://conduction.nl
+ *
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
+ * SPDX-License-Identifier: EUPL-1.2
+ */
+
+declare(strict_types=1);
+
+namespace OCA\LaunchPad\Controller;
+
+use InvalidArgumentException;
+use OCA\LaunchPad\AppInfo\Application;
+use OCA\LaunchPad\Service\ActionAuthService;
+use OCA\LaunchPad\Service\ProfileFieldService;
+use OCA\LaunchPad\Settings\LaunchPadAdmin;
+use OCP\AppFramework\Controller;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\Attribute\AuthorizedAdminSetting;
+use OCP\AppFramework\Http\Attribute\NoAdminRequired;
+use OCP\AppFramework\Http\JSONResponse;
+use OCP\AppFramework\OCS\OCSForbiddenException;
+use OCP\IGroupManager;
+use OCP\IRequest;
+use OCP\IUser;
+use OCP\IUserSession;
+
+/**
+ * Custom profile field endpoints.
+ *
+ * @spec openspec/specs/people-widget/spec.md
+ */
+class ProfileFieldsController extends Controller {
+ /**
+ * Constructor.
+ *
+ * @param IRequest $request HTTP request.
+ * @param ProfileFieldService $profileFields Profile field service.
+ * @param ActionAuthService $actionAuth ADR-023 action authorization.
+ * @param IUserSession $userSession Current user.
+ * @param IGroupManager $groupManager Admin check.
+ *
+ * @spec openspec/specs/people-widget/spec.md
+ */
+ public function __construct(
+ IRequest $request,
+ private readonly ProfileFieldService $profileFields,
+ private readonly ActionAuthService $actionAuth,
+ private readonly IUserSession $userSession,
+ private readonly IGroupManager $groupManager,
+ ) {
+ parent::__construct(
+ appName: Application::APP_ID,
+ request: $request
+ );
+ }//end __construct()
+
+ /**
+ * The caller's own fields and values. Each person reads only their own.
+ *
+ * @return JSONResponse `{fields: [...]}`, 401 or 403.
+ *
+ * @spec openspec/specs/people-widget/spec.md
+ */
+ #[NoAdminRequired]
+ public function getOwn(): JSONResponse {
+ $user = $this->userSession->getUser();
+ $guard = $this->guard(user: $user, action: 'profile-fields.get-own');
+ if ($guard !== null || $user === null) {
+ return $guard ?? self::unauthenticated();
+ }
+
+ return new JSONResponse(data: ['fields' => $this->profileFields->getOwnFields(userId: $user->getUID())]);
+ }//end getOwn()
+
+ /**
+ * Save the caller's own values. Each person writes only their own; LDAP
+ * fields stay read-only.
+ *
+ * @param array|null $values Map of field key to a string or a list of tags.
+ *
+ * @return JSONResponse `{fields: [...]}`, 400, 401 or 403.
+ *
+ * @spec openspec/specs/people-widget/spec.md
+ */
+ #[NoAdminRequired]
+ public function saveOwn(?array $values = null): JSONResponse {
+ $user = $this->userSession->getUser();
+ $guard = $this->guard(user: $user, action: 'profile-fields.save-own');
+ if ($guard !== null || $user === null) {
+ return $guard ?? self::unauthenticated();
+ }
+
+ try {
+ $fields = $this->profileFields->saveOwnValues(userId: $user->getUID(), values: ($values ?? []));
+ } catch (InvalidArgumentException $e) {
+ return new JSONResponse(data: ['error' => $e->getMessage()], statusCode: Http::STATUS_BAD_REQUEST);
+ }
+
+ return new JSONResponse(data: ['fields' => $fields]);
+ }//end saveOwn()
+
+ /**
+ * The field definitions (administrators).
+ *
+ * @return JSONResponse `{fields: [...]}`, 401 or 403.
+ *
+ * @spec openspec/specs/people-widget/spec.md
+ */
+ #[AuthorizedAdminSetting(LaunchPadAdmin::class)]
+ public function getDefinitions(): JSONResponse {
+ $guard = $this->adminGuard(action: 'profile-fields.get-definitions');
+ if ($guard !== null) {
+ return $guard;
+ }
+
+ return new JSONResponse(data: ['fields' => $this->profileFields->getDefinitions()]);
+ }//end getDefinitions()
+
+ /**
+ * Replace the field definitions (administrators).
+ *
+ * @param array|null $fields The definitions.
+ *
+ * @return JSONResponse `{fields: [...]}`, 400, 401 or 403.
+ *
+ * @spec openspec/specs/people-widget/spec.md
+ */
+ #[AuthorizedAdminSetting(LaunchPadAdmin::class)]
+ public function saveDefinitions(?array $fields = null): JSONResponse {
+ $guard = $this->adminGuard(action: 'profile-fields.save-definitions');
+ if ($guard !== null) {
+ return $guard;
+ }
+
+ try {
+ $saved = $this->profileFields->saveDefinitions(raw: ($fields ?? []));
+ } catch (InvalidArgumentException $e) {
+ return new JSONResponse(data: ['error' => $e->getMessage()], statusCode: Http::STATUS_BAD_REQUEST);
+ }
+
+ return new JSONResponse(data: ['fields' => $saved]);
+ }//end saveDefinitions()
+
+ /**
+ * Signed-in and allowed to call the action.
+ *
+ * @param IUser|null $user The caller.
+ * @param string $action ADR-023 action id.
+ *
+ * @return JSONResponse|null An error response, or null when allowed.
+ */
+ private function guard(?IUser $user, string $action): ?JSONResponse {
+ if ($user === null) {
+ return self::unauthenticated();
+ }
+
+ try {
+ $this->actionAuth->requireAction($user, $action);
+ } catch (OCSForbiddenException) {
+ return new JSONResponse(data: ['error' => 'Forbidden'], statusCode: Http::STATUS_FORBIDDEN);
+ }
+
+ return null;
+ }//end guard()
+
+ /**
+ * Signed-in administrator allowed to call the action.
+ *
+ * @param string $action ADR-023 action id.
+ *
+ * @return JSONResponse|null An error response, or null when allowed.
+ */
+ private function adminGuard(string $action): ?JSONResponse {
+ $user = $this->userSession->getUser();
+ if ($user !== null && $this->groupManager->isAdmin(userId: $user->getUID()) === false) {
+ return new JSONResponse(data: ['error' => 'Admin required'], statusCode: Http::STATUS_FORBIDDEN);
+ }
+
+ return $this->guard(user: $user, action: $action);
+ }//end adminGuard()
+
+ /**
+ * The 401 response.
+ *
+ * @return JSONResponse
+ */
+ private static function unauthenticated(): JSONResponse {
+ return new JSONResponse(data: ['error' => 'Not authenticated'], statusCode: Http::STATUS_UNAUTHORIZED);
+ }//end unauthenticated()
+}//end class
diff --git a/lib/Db/ProfileValue.php b/lib/Db/ProfileValue.php
new file mode 100644
index 000000000..c3a9a01d9
--- /dev/null
+++ b/lib/Db/ProfileValue.php
@@ -0,0 +1,127 @@
+
+ * @copyright 2026 Conduction b.v.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ * @version GIT:auto
+ * @link https://conduction.nl
+ *
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
+ * SPDX-License-Identifier: EUPL-1.2
+ */
+
+declare(strict_types=1);
+
+namespace OCA\LaunchPad\Db;
+
+use JsonSerializable;
+use OCP\AppFramework\Db\Entity;
+
+/**
+ * Profile value entity.
+ *
+ * @method string getUserId()
+ * @method void setUserId(string $userId)
+ * @method string getFieldKey()
+ * @method void setFieldKey(string $fieldKey)
+ * @method string getValue()
+ * @method void setValue(string $value)
+ * @method string getValueSearch()
+ * @method void setValueSearch(string $valueSearch)
+ * @method string getSource()
+ * @method void setSource(string $source)
+ * @method string|null getScope()
+ * @method void setScope(?string $scope)
+ * @method string|null getUpdatedAt()
+ * @method void setUpdatedAt(?string $updatedAt)
+ *
+ * @spec openspec/specs/people-widget/spec.md
+ */
+class ProfileValue extends Entity implements JsonSerializable {
+
+ /**
+ * Owner user id.
+ *
+ * @var string
+ */
+ protected string $userId = '';
+
+ /**
+ * Field key: a custom field key, or nc:role, nc:headline, nc:biography.
+ *
+ * @var string
+ */
+ protected string $fieldKey = '';
+
+ /**
+ * The value as entered or read.
+ *
+ * @var string
+ */
+ protected string $value = '';
+
+ /**
+ * Lower-cased value for the search.
+ *
+ * @var string
+ */
+ protected string $valueSearch = '';
+
+ /**
+ * Where the value came from: self, ldap or nextcloud.
+ *
+ * @var string
+ */
+ protected string $source = '';
+
+ /**
+ * Nextcloud visibility scope of a mirrored standard field.
+ *
+ * @var string|null
+ */
+ protected ?string $scope = null;
+
+ /**
+ * Last write.
+ *
+ * @var string|null
+ */
+ protected ?string $updatedAt = null;
+
+ /**
+ * Register the column types.
+ *
+ * @spec openspec/specs/people-widget/spec.md
+ */
+ public function __construct() {
+ $this->addType(fieldName: 'id', type: 'integer');
+ }//end __construct()
+
+ /**
+ * Serialise for the API.
+ *
+ * @return array
+ *
+ * @spec openspec/specs/people-widget/spec.md
+ */
+ public function jsonSerialize(): array {
+ return [
+ 'id' => $this->getId(),
+ 'userId' => $this->userId,
+ 'fieldKey' => $this->fieldKey,
+ 'value' => $this->value,
+ 'source' => $this->source,
+ 'scope' => $this->scope,
+ ];
+ }//end jsonSerialize()
+}//end class
diff --git a/lib/Db/ProfileValueMapper.php b/lib/Db/ProfileValueMapper.php
new file mode 100644
index 000000000..aeef45b13
--- /dev/null
+++ b/lib/Db/ProfileValueMapper.php
@@ -0,0 +1,192 @@
+
+ * @copyright 2026 Conduction b.v.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ * @version GIT:auto
+ * @link https://conduction.nl
+ *
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
+ * SPDX-License-Identifier: EUPL-1.2
+ */
+
+declare(strict_types=1);
+
+namespace OCA\LaunchPad\Db;
+
+use DateTime;
+use OCP\AppFramework\Db\QBMapper;
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\IDBConnection;
+
+/**
+ * Mapper for profile values.
+ *
+ * @extends QBMapper
+ *
+ * @spec openspec/specs/people-widget/spec.md
+ */
+class ProfileValueMapper extends QBMapper {
+ /**
+ * Most rows one search returns before the caller filters them.
+ *
+ * @var int
+ */
+ public const SEARCH_ROW_CAP = 1000;
+
+ /**
+ * Constructor.
+ *
+ * @param IDBConnection $db Database connection.
+ *
+ * @spec openspec/specs/people-widget/spec.md
+ */
+ public function __construct(IDBConnection $db) {
+ parent::__construct(
+ db: $db,
+ tableName: 'launchpad_profile_values',
+ entityClass: ProfileValue::class
+ );
+ }//end __construct()
+
+ /**
+ * Every value of the given people.
+ *
+ * @param string[] $userIds User ids.
+ *
+ * @return ProfileValue[]
+ *
+ * @spec openspec/specs/people-widget/spec.md
+ */
+ public function findByUsers(array $userIds): array {
+ if ($userIds === []) {
+ return [];
+ }
+
+ $qb = $this->db->getQueryBuilder();
+ $qb->select('*')
+ ->from(from: $this->getTableName())
+ ->where(
+ $qb->expr()->in(
+ x: 'user_id',
+ y: $qb->createNamedParameter(
+ value: array_values(array: $userIds),
+ type: IQueryBuilder::PARAM_STR_ARRAY
+ )
+ )
+ )
+ ->orderBy(sort: 'id');
+
+ return $this->findEntities(query: $qb);
+ }//end findByUsers()
+
+ /**
+ * Values whose lower-cased text contains the needle.
+ *
+ * @param string $needle Lower-cased search text.
+ *
+ * @return ProfileValue[] At most SEARCH_ROW_CAP rows.
+ *
+ * @spec openspec/specs/people-widget/spec.md
+ */
+ public function search(string $needle): array {
+ $qb = $this->db->getQueryBuilder();
+ $qb->select('*')
+ ->from(from: $this->getTableName())
+ ->where(
+ $qb->expr()->like(
+ x: 'value_search',
+ y: $qb->createNamedParameter(
+ value: '%' . $this->db->escapeLikeParameter(param: $needle) . '%'
+ )
+ )
+ )
+ ->setMaxResults(maxResults: self::SEARCH_ROW_CAP);
+
+ return $this->findEntities(query: $qb);
+ }//end search()
+
+ /**
+ * Replace one person's values of one field with a new list.
+ *
+ * @param string $userId Owner.
+ * @param string $fieldKey Field key.
+ * @param string[] $values New values (already cleaned).
+ * @param string $source self, ldap or nextcloud.
+ * @param string|null $scope Visibility scope of a mirrored standard field.
+ *
+ * @return void
+ *
+ * @spec openspec/specs/people-widget/spec.md
+ */
+ public function replaceValues(
+ string $userId,
+ string $fieldKey,
+ array $values,
+ string $source,
+ ?string $scope = null,
+ ): void {
+ $this->deleteField(userId: $userId, fieldKey: $fieldKey);
+
+ $now = (new DateTime())->format(format: 'Y-m-d H:i:s');
+ foreach ($values as $value) {
+ $row = new ProfileValue();
+ // Entity setters take their argument positionally.
+ // phpcs:disable CustomSniffs.Functions.NamedParameters.RequireNamedParameters
+ $row->setUserId($userId);
+ $row->setFieldKey($fieldKey);
+ $row->setValue($value);
+ $row->setValueSearch(mb_strtolower(string: $value));
+ $row->setSource($source);
+ $row->setScope($scope);
+ $row->setUpdatedAt($now);
+ // phpcs:enable CustomSniffs.Functions.NamedParameters.RequireNamedParameters
+ $this->insert(entity: $row);
+ }
+ }//end replaceValues()
+
+ /**
+ * Delete one person's values of one field.
+ *
+ * @param string $userId Owner.
+ * @param string $fieldKey Field key.
+ *
+ * @return int Rows deleted.
+ *
+ * @spec openspec/specs/people-widget/spec.md
+ */
+ public function deleteField(string $userId, string $fieldKey): int {
+ $qb = $this->db->getQueryBuilder();
+ $qb->delete(delete: $this->getTableName())
+ ->where(
+ $qb->expr()->eq(x: 'user_id', y: $qb->createNamedParameter(value: $userId)),
+ $qb->expr()->eq(x: 'field_key', y: $qb->createNamedParameter(value: $fieldKey))
+ );
+
+ return $qb->executeStatement();
+ }//end deleteField()
+
+ /**
+ * Delete every value of a person (user deletion).
+ *
+ * @param string $userId Owner.
+ *
+ * @return int Rows deleted.
+ *
+ * @spec openspec/specs/people-widget/spec.md
+ */
+ public function deleteByUser(string $userId): int {
+ $qb = $this->db->getQueryBuilder();
+ $qb->delete(delete: $this->getTableName())
+ ->where($qb->expr()->eq(x: 'user_id', y: $qb->createNamedParameter(value: $userId)));
+
+ return $qb->executeStatement();
+ }//end deleteByUser()
+}//end class
diff --git a/lib/Listener/ProfileFieldsListener.php b/lib/Listener/ProfileFieldsListener.php
new file mode 100644
index 000000000..8f820298d
--- /dev/null
+++ b/lib/Listener/ProfileFieldsListener.php
@@ -0,0 +1,88 @@
+
+ * @copyright 2026 Conduction b.v.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ * @version GIT:auto
+ * @link https://conduction.nl
+ *
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
+ * SPDX-License-Identifier: EUPL-1.2
+ */
+
+declare(strict_types=1);
+
+namespace OCA\LaunchPad\Listener;
+
+use OCA\LaunchPad\Service\ProfileFieldService;
+use OCP\Accounts\UserUpdatedEvent;
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventListener;
+use OCP\User\Events\UserDeletedEvent;
+use OCP\User\Events\UserLoggedInEvent;
+use Psr\Log\LoggerInterface;
+use Throwable;
+
+/**
+ * Profile field sync on sign-in, profile edit and user deletion.
+ *
+ * @template-implements IEventListener
+ *
+ * @spec openspec/specs/people-widget/spec.md
+ */
+class ProfileFieldsListener implements IEventListener {
+ /**
+ * Constructor.
+ *
+ * @param ProfileFieldService $profileFields Profile field service.
+ * @param LoggerInterface $logger Logger.
+ *
+ * @spec openspec/specs/people-widget/spec.md
+ */
+ public function __construct(
+ private readonly ProfileFieldService $profileFields,
+ private readonly LoggerInterface $logger,
+ ) {
+ }//end __construct()
+
+ /**
+ * Handle the event. A failure is logged and never breaks the sign-in or
+ * the profile edit that raised it.
+ *
+ * @param Event $event The event.
+ *
+ * @return void
+ *
+ * @spec openspec/specs/people-widget/spec.md
+ */
+ public function handle(Event $event): void {
+ try {
+ if ($event instanceof UserLoggedInEvent) {
+ $this->profileFields->syncLdapFields(user: $event->getUser());
+ $this->profileFields->mirrorStandardFields(user: $event->getUser());
+ return;
+ }
+
+ if ($event instanceof UserUpdatedEvent) {
+ $this->profileFields->mirrorStandardFields(user: $event->getUser());
+ return;
+ }
+
+ if ($event instanceof UserDeletedEvent) {
+ $this->profileFields->deleteUser(userId: $event->getUser()->getUID());
+ }
+ } catch (Throwable $e) {
+ $this->logger->warning(message: 'Profile field sync failed: ' . $e->getMessage());
+ }
+ }//end handle()
+}//end class
diff --git a/lib/Migration/Version002012Date20260930080000.php b/lib/Migration/Version002012Date20260930080000.php
new file mode 100644
index 000000000..9216d0efc
--- /dev/null
+++ b/lib/Migration/Version002012Date20260930080000.php
@@ -0,0 +1,101 @@
+
+ * @copyright 2026 Conduction B.V.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * @link https://conduction.nl
+ *
+ * @spec openspec/specs/people-widget/spec.md
+ */
+
+declare(strict_types=1);
+
+namespace OCA\LaunchPad\Migration;
+
+use Closure;
+use OCP\DB\ISchemaWrapper;
+use OCP\DB\Types;
+use OCP\Migration\IOutput;
+use OCP\Migration\SimpleMigrationStep;
+
+/**
+ * Create the profile values table.
+ *
+ * @spec openspec/specs/people-widget/spec.md
+ */
+class Version002012Date20260930080000 extends SimpleMigrationStep {
+ /**
+ * Create the table when it is not already there.
+ *
+ * @param IOutput $output Migration output.
+ * @param Closure $schemaClosure Schema closure (provides ISchemaWrapper).
+ * @param array $options Migration options (unused).
+ *
+ * @return ISchemaWrapper|null The modified schema, or null when unchanged.
+ *
+ * @spec openspec/specs/people-widget/spec.md
+ */
+ public function changeSchema(
+ IOutput $output,
+ Closure $schemaClosure,
+ array $options,
+ ): ?ISchemaWrapper {
+ // @var ISchemaWrapper $schema.
+ $schema = $schemaClosure();
+
+ if ($schema->hasTable('launchpad_profile_values') === true) {
+ return null;
+ }
+
+ $table = $schema->createTable('launchpad_profile_values');
+ $table->addColumn('id', Types::BIGINT, ['autoincrement' => true, 'notnull' => true, 'unsigned' => true]);
+ $table->addColumn('user_id', Types::STRING, ['notnull' => true, 'length' => 64]);
+ $table->addColumn('field_key', Types::STRING, ['notnull' => true, 'length' => 64]);
+ $table->addColumn('value', Types::TEXT, ['notnull' => true]);
+ $table->addColumn(
+ 'value_search',
+ Types::TEXT,
+ [
+ 'notnull' => true,
+ 'comment' => 'Lower-cased value, matched with LIKE by the people search.',
+ ]
+ );
+ $table->addColumn(
+ 'source',
+ Types::STRING,
+ [
+ 'notnull' => true,
+ 'length' => 16,
+ 'comment' => 'self, ldap or nextcloud (a mirrored standard field).',
+ ]
+ );
+ $table->addColumn(
+ 'scope',
+ Types::STRING,
+ [
+ 'notnull' => false,
+ 'length' => 32,
+ 'comment' => 'Nextcloud visibility scope of a mirrored standard field; NULL for custom fields.',
+ ]
+ );
+ $table->addColumn('updated_at', Types::DATETIME, ['notnull' => false]);
+
+ $table->setPrimaryKey(['id']);
+ $table->addIndex(['user_id'], 'launchpad_pv_user');
+ $table->addIndex(['field_key'], 'launchpad_pv_field');
+
+ return $schema;
+ }//end changeSchema()
+}//end class
diff --git a/lib/Service/PeopleWidgetService.php b/lib/Service/PeopleWidgetService.php
index 9ed9beea0..0a85d8846 100644
--- a/lib/Service/PeopleWidgetService.php
+++ b/lib/Service/PeopleWidgetService.php
@@ -10,10 +10,10 @@
* the single contact point between the {@see PeopleWidgetController} HTTP
* layer and Nextcloud's user / group / account managers.
*
- * Visibility model (REQ-PPL-004): v1 returns every non-empty profile field
- * unconditionally — scope-based (`$prop->getScope()`) filtering is a
- * documented follow-up. Empty / null values are still omitted from the
- * response, never returned as `null` keys.
+ * Visibility model (REQ-PPL-004, REQ-PEX-004): a standard profile field
+ * whose Nextcloud scope is private shows to its owner only; custom fields
+ * follow their definition's audience. Empty / null values are omitted from
+ * the response, never returned as `null` keys.
*
* @category Service
* @package OCA\LaunchPad\Service
@@ -88,6 +88,28 @@ class PeopleWidgetService {
*
* @var string[]
*/
+ /**
+ * Most name and email matches one search reads (REQ-PEX-003).
+ *
+ * @var int
+ */
+ public const SEARCH_CANDIDATE_CAP = 500;
+
+ /**
+ * Shortest query that asks the server (REQ-PEX-003).
+ *
+ * @var int
+ */
+ public const MIN_QUERY_LENGTH = 2;
+
+ /**
+ * The user the current listing is for; decides which fields they may
+ * see and match (REQ-PEX-004). Set at the start of every listUsers() call.
+ *
+ * @var string|null
+ */
+ private ?string $viewerId = null;
+
private const STANDARD_PROPERTIES = [
IAccountManager::PROPERTY_PHONE,
IAccountManager::PROPERTY_ADDRESS,
@@ -112,6 +134,7 @@ class PeopleWidgetService {
* @param AdminTemplateService $adminTemplateService Single-source-of-truth wrapper around
* `IGroupManager::getUserGroupIds`
* (REQ-TMPL-013 grep guard).
+ * @param ProfileFieldService $profileFields Custom profile fields and their visibility.
*/
public function __construct(
private readonly IUserManager $userManager,
@@ -119,6 +142,7 @@ public function __construct(
private readonly IAccountManager $accountManager,
private readonly IURLGenerator $urlGenerator,
private readonly AdminTemplateService $adminTemplateService,
+ private readonly ProfileFieldService $profileFields,
) {
}//end __construct()
@@ -142,12 +166,18 @@ public function __construct(
* `group`, or `recent-activity`.
* @param int $limit Page size (1..MAX_LIMIT).
* @param int $offset Page offset (>=0).
+ * @param string|null $query Search text; 2 or more characters search the
+ * whole directory by name, email and profile
+ * fields (REQ-PEX-003).
+ * @param string|null $viewerId The signed-in user the list is for; decides
+ * which fields show and match (REQ-PEX-004).
*
* @return array Pagination envelope with keys `users`, `total`, `hasMore`.
*
* @throws InvalidArgumentException When `limit` exceeds MAX_LIMIT, is
* non-positive, or `offset` is negative;
- * or when `sortBy` is `recent-activity`.
+ * when `sortBy` is `recent-activity`; or
+ * when the query is shorter than 2 characters.
*
* @spec openspec/specs/people-widget/spec.md
*/
@@ -158,7 +188,11 @@ public function listUsers(
string $sortBy = 'displayName',
int $limit = self::DEFAULT_LIMIT,
int $offset = 0,
+ ?string $query = null,
+ ?string $viewerId = null,
): array {
+ $this->viewerId = $viewerId;
+
if ($limit < 1 || $limit > self::MAX_LIMIT) {
throw new InvalidArgumentException(
message: 'limit must be between 1 and ' . self::MAX_LIMIT
@@ -177,6 +211,22 @@ public function listUsers(
$groupFilter = $this->extractGroupFilter(filters: $filters);
+ $query = trim(string: (string)$query);
+ if ($query !== '') {
+ if (mb_strlen(string: $query) < self::MIN_QUERY_LENGTH) {
+ throw new InvalidArgumentException(message: 'q must be at least ' . self::MIN_QUERY_LENGTH . ' characters');
+ }
+
+ return $this->paginateCandidates(
+ candidates: $this->searchCandidates(query: $query, groupFilter: $groupFilter),
+ excludeDisabled: $excludeDisabled,
+ showBirthdays: $showBirthdays,
+ sortBy: 'displayName',
+ limit: $limit,
+ offset: $offset
+ );
+ }
+
// No `group` filter AND the default `displayName` sort: page
// directly from the backend in display-name order so we never
// materialize more IUser objects than the requested window
@@ -253,7 +303,7 @@ private function listDirectoryPageByDisplayName(
}
return [
- 'users' => $users,
+ 'users' => $this->attachCustomFields(users: $users),
'total' => $total,
'hasMore' => ($offset + count(value: $page)) < $total,
];
@@ -397,12 +447,73 @@ private function paginateCandidates(
}
return [
- 'users' => $users,
+ 'users' => $this->attachCustomFields(users: $users),
'total' => $total,
'hasMore' => ($offset + count(value: $page)) < $total,
];
}//end paginateCandidates()
+ /**
+ * Name, email and profile-field matches for a search (REQ-PEX-003),
+ * bounded to the configured groups when the widget has a group filter.
+ *
+ * @param string $query The search text.
+ * @param array|null $groupFilter The widget's group filter, or null.
+ *
+ * @return IUser[] Matching users, unsorted.
+ *
+ * @spec openspec/specs/people-widget/spec.md
+ */
+ private function searchCandidates(string $query, ?array $groupFilter): array {
+ $byUid = [];
+ foreach ($this->userManager->search(pattern: $query, limit: self::SEARCH_CANDIDATE_CAP) as $user) {
+ $byUid[$user->getUID()] = $user;
+ }
+
+ $fieldMatches = $this->profileFields->findMatchingUserIds(viewerId: $this->viewerId, query: $query);
+ foreach ($fieldMatches as $uid) {
+ if (isset($byUid[$uid]) === true) {
+ continue;
+ }
+
+ $user = $this->userManager->get(uid: $uid);
+ if ($user !== null) {
+ $byUid[$uid] = $user;
+ }
+ }
+
+ if ($groupFilter === null) {
+ return array_values(array: $byUid);
+ }
+
+ $members = [];
+ foreach ($this->collectGroupMembers(groupIds: (array)($groupFilter['values'] ?? [])) as $member) {
+ $members[$member->getUID()] = true;
+ }
+
+ return array_values(array: array_intersect_key($byUid, $members));
+ }//end searchCandidates()
+
+ /**
+ * Add the custom fields the viewer may see to every profile on the page (REQ-PEX-004).
+ *
+ * @param array $users Projected profiles.
+ *
+ * @return array The profiles with `customFields`.
+ *
+ * @spec openspec/specs/people-widget/spec.md
+ */
+ private function attachCustomFields(array $users): array {
+ $uids = array_map(callback: static fn (array $user): string => $user['uid'], array: $users);
+ $fields = $this->profileFields->visibleFieldsFor(viewerId: $this->viewerId, userIds: $uids);
+
+ foreach ($users as $index => $user) {
+ $users[$index]['customFields'] = $fields[$user['uid']] ?? [];
+ }
+
+ return $users;
+ }//end attachCustomFields()
+
/**
* Compute the number of days between today (UTC) and the user's next
* birthday. Returns `null` for invalid / missing input.
@@ -673,10 +784,16 @@ private function buildAccountFields(IUser $user, bool $showBirthdays): array {
try {
$prop = $account->getProperty(property: $property);
$value = $prop->getValue();
+ $scope = $prop->getScope();
} catch (Exception $e) {
continue;
}
+ // REQ-PEX-004: a private field shows to its owner only.
+ if (ProfileFieldService::scopeAllows(scope: $scope, viewerId: $this->viewerId, ownerId: $user->getUID()) === false) {
+ continue;
+ }
+
if ($value === '') {
continue;
}
diff --git a/lib/Service/ProfileFieldService.php b/lib/Service/ProfileFieldService.php
new file mode 100644
index 000000000..8aa8f9c16
--- /dev/null
+++ b/lib/Service/ProfileFieldService.php
@@ -0,0 +1,674 @@
+
+ * @copyright 2026 Conduction b.v.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ * @link https://conduction.nl
+ *
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
+ * SPDX-License-Identifier: EUPL-1.2
+ */
+
+declare(strict_types=1);
+
+namespace OCA\LaunchPad\Service;
+
+use Exception;
+use InvalidArgumentException;
+use OCA\LaunchPad\Db\AdminSettingMapper;
+use OCA\LaunchPad\Db\ProfileValue;
+use OCA\LaunchPad\Db\ProfileValueMapper;
+use OCP\Accounts\IAccountManager;
+use OCP\IUser;
+use OCP\LDAP\ILDAPProviderFactory;
+use Psr\Log\LoggerInterface;
+
+/**
+ * Definitions, values and visibility of custom profile fields.
+ *
+ * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) Definition validation,
+ * self edits, the LDAP sync, the standard-field mirror and the visibility
+ * rule share one data model; splitting them would scatter that rule.
+ *
+ * @spec openspec/specs/people-widget/spec.md
+ */
+class ProfileFieldService {
+ /**
+ * Admin setting key holding the field definitions.
+ *
+ * @var string
+ */
+ public const SETTING_KEY = 'profile_fields';
+
+ /**
+ * Most custom fields an administrator may define.
+ *
+ * @var int
+ */
+ public const MAX_FIELDS = 10;
+
+ /**
+ * Most tags one person may list in one field.
+ *
+ * @var int
+ */
+ public const MAX_TAGS = 25;
+
+ /**
+ * Longest value, in characters.
+ *
+ * @var int
+ */
+ public const MAX_VALUE_LENGTH = 255;
+
+ /**
+ * Standard fields mirrored for the search, keyed by their row key.
+ *
+ * @var array
+ */
+ public const MIRRORED_STANDARD_FIELDS = [
+ 'nc:role' => IAccountManager::PROPERTY_ROLE,
+ 'nc:headline' => IAccountManager::PROPERTY_HEADLINE,
+ 'nc:biography' => IAccountManager::PROPERTY_BIOGRAPHY,
+ ];
+
+ private const TYPES = ['text', 'tags'];
+ private const SOURCES = ['self', 'ldap'];
+ private const VISIBILITIES = ['everyone', 'groups'];
+
+ /**
+ * Group ids per user, memoised for one request.
+ *
+ * @var array
+ */
+ private array $groupCache = [];
+
+ /**
+ * Constructor.
+ *
+ * @param AdminSettingMapper $settingMapper Admin settings store.
+ * @param ProfileValueMapper $valueMapper Profile values store.
+ * @param IAccountManager $accountManager Standard profile fields.
+ * @param AdminTemplateService $adminTemplateService Group ids of a user.
+ * @param ILDAPProviderFactory $ldapProviderFactory LDAP attribute access.
+ * @param LoggerInterface $logger Logger.
+ *
+ * @spec openspec/specs/people-widget/spec.md
+ */
+ public function __construct(
+ private readonly AdminSettingMapper $settingMapper,
+ private readonly ProfileValueMapper $valueMapper,
+ private readonly IAccountManager $accountManager,
+ private readonly AdminTemplateService $adminTemplateService,
+ private readonly ILDAPProviderFactory $ldapProviderFactory,
+ private readonly LoggerInterface $logger,
+ ) {
+ }//end __construct()
+
+ /**
+ * The stored field definitions, cleaned.
+ *
+ * @return array>
+ *
+ * @spec openspec/specs/people-widget/spec.md
+ */
+ public function getDefinitions(): array {
+ $raw = $this->settingMapper->getValue(key: self::SETTING_KEY, default: []);
+ if (is_array(value: $raw) === false) {
+ return [];
+ }
+
+ try {
+ return $this->validateDefinitions(raw: $raw);
+ } catch (InvalidArgumentException) {
+ return [];
+ }
+ }//end getDefinitions()
+
+ /**
+ * Validate and store the field definitions (REQ-PEX-001).
+ *
+ * @param array $raw Definitions as sent by the admin screen.
+ *
+ * @return array> The stored definitions.
+ *
+ * @throws InvalidArgumentException When a definition is invalid.
+ *
+ * @spec openspec/specs/people-widget/spec.md
+ */
+ public function saveDefinitions(array $raw): array {
+ $definitions = $this->validateDefinitions(raw: $raw);
+ $this->settingMapper->setSetting(key: self::SETTING_KEY, value: $definitions);
+
+ return $definitions;
+ }//end saveDefinitions()
+
+ /**
+ * Check and normalise a list of definitions.
+ *
+ * @param array $raw Raw definitions.
+ *
+ * @return array>
+ *
+ * @throws InvalidArgumentException When a definition is invalid.
+ */
+ private function validateDefinitions(array $raw): array {
+ if (count(value: $raw) > self::MAX_FIELDS) {
+ throw new InvalidArgumentException(message: 'At most ' . self::MAX_FIELDS . ' profile fields');
+ }
+
+ $result = [];
+ $seen = [];
+ foreach (array_values(array: $raw) as $entry) {
+ $definition = $this->validateDefinition(entry: $entry);
+ if (isset($seen[$definition['key']]) === true) {
+ throw new InvalidArgumentException(message: 'Duplicate field key: ' . $definition['key']);
+ }
+
+ $seen[$definition['key']] = true;
+ $result[] = $definition;
+ }
+
+ return $result;
+ }//end validateDefinitions()
+
+ /**
+ * Check and normalise one definition.
+ *
+ * @param mixed $entry Raw definition.
+ *
+ * @return array
+ *
+ * @throws InvalidArgumentException When the definition is invalid.
+ */
+ private function validateDefinition(mixed $entry): array {
+ if (is_array(value: $entry) === false) {
+ throw new InvalidArgumentException(message: 'A profile field must be an object');
+ }
+
+ $key = (string)($entry['key'] ?? '');
+ if (preg_match(pattern: '/^[a-z][a-z0-9_]{0,31}$/', subject: $key) !== 1) {
+ throw new InvalidArgumentException(message: 'Field key must be lower case letters, digits or _: ' . $key);
+ }
+
+ $label = trim(string: (string)($entry['label'] ?? ''));
+ if ($label === '' || mb_strlen(string: $label) > 64) {
+ throw new InvalidArgumentException(message: 'Field label must be 1 to 64 characters: ' . $key);
+ }
+
+ $type = $this->oneOf(value: $entry['type'] ?? 'text', allowed: self::TYPES, name: 'type');
+ $source = $this->oneOf(value: $entry['source'] ?? 'self', allowed: self::SOURCES, name: 'source');
+ $visibility = $this->oneOf(value: $entry['visibility'] ?? 'everyone', allowed: self::VISIBILITIES, name: 'visibility');
+
+ $ldapAttribute = trim(string: (string)($entry['ldapAttribute'] ?? ''));
+ if ($source === 'ldap' && preg_match(pattern: '/^[A-Za-z][A-Za-z0-9-]{0,63}$/', subject: $ldapAttribute) !== 1) {
+ throw new InvalidArgumentException(message: 'An LDAP field needs an LDAP attribute name: ' . $key);
+ }
+
+ if ($source !== 'ldap') {
+ $ldapAttribute = '';
+ }
+
+ return [
+ 'key' => $key,
+ 'label' => $label,
+ 'type' => $type,
+ 'source' => $source,
+ 'ldapAttribute' => $ldapAttribute,
+ 'searchable' => ($entry['searchable'] ?? false) === true,
+ 'shownInWidget' => ($entry['shownInWidget'] ?? true) === true,
+ 'visibility' => $visibility,
+ ];
+ }//end validateDefinition()
+
+ /**
+ * Check that a value is one of the allowed strings.
+ *
+ * @param mixed $value The value.
+ * @param string[] $allowed Allowed values.
+ * @param string $name Property name for the message.
+ *
+ * @return string The value.
+ *
+ * @throws InvalidArgumentException When it is not allowed.
+ */
+ private function oneOf(mixed $value, array $allowed, string $name): string {
+ if (is_string(value: $value) === false || in_array(needle: $value, haystack: $allowed, strict: true) === false) {
+ throw new InvalidArgumentException(message: 'Field ' . $name . ' must be one of ' . implode(separator: ', ', array: $allowed));
+ }
+
+ return $value;
+ }//end oneOf()
+
+ /**
+ * A person's own fields with their values, for the personal settings (REQ-PEX-002).
+ *
+ * @param string $userId The person.
+ *
+ * @return array> Definitions with `values` and `readOnly`.
+ *
+ * @spec openspec/specs/people-widget/spec.md
+ */
+ public function getOwnFields(string $userId): array {
+ $byKey = $this->groupValues(rows: $this->valueMapper->findByUsers(userIds: [$userId]))[$userId] ?? [];
+
+ $result = [];
+ foreach ($this->getDefinitions() as $definition) {
+ $result[] = array_merge(
+ $definition,
+ [
+ 'values' => $byKey[$definition['key']] ?? [],
+ 'readOnly' => $definition['source'] !== 'self',
+ ]
+ );
+ }
+
+ return $result;
+ }//end getOwnFields()
+
+ /**
+ * Save a person's own values (REQ-PEX-002). LDAP fields are read-only
+ * and ignored; unknown keys are rejected.
+ *
+ * @param string $userId The person.
+ * @param array $values Map of field key to a string (text) or a list of strings (tags).
+ *
+ * @return array> The fields after saving.
+ *
+ * @throws InvalidArgumentException When a key or value is invalid.
+ *
+ * @spec openspec/specs/people-widget/spec.md
+ */
+ public function saveOwnValues(string $userId, array $values): array {
+ $definitions = [];
+ foreach ($this->getDefinitions() as $definition) {
+ $definitions[$definition['key']] = $definition;
+ }
+
+ $clean = [];
+ foreach ($values as $key => $value) {
+ $definition = $definitions[$key] ?? null;
+ if ($definition === null) {
+ throw new InvalidArgumentException(message: 'Unknown profile field: ' . $key);
+ }
+
+ if ($definition['source'] !== 'self') {
+ continue;
+ }
+
+ $clean[$key] = $this->cleanValues(definition: $definition, value: $value);
+ }
+
+ foreach ($clean as $key => $list) {
+ $this->valueMapper->replaceValues(userId: $userId, fieldKey: (string)$key, values: $list, source: 'self');
+ }
+
+ return $this->getOwnFields(userId: $userId);
+ }//end saveOwnValues()
+
+ /**
+ * Turn a submitted value into a clean list of strings.
+ *
+ * @param array $definition The field definition.
+ * @param mixed $value A string, or a list of strings for tags.
+ *
+ * @return string[]
+ *
+ * @throws InvalidArgumentException When the value does not fit the type.
+ */
+ private function cleanValues(array $definition, mixed $value): array {
+ $items = [$value];
+ if ($definition['type'] === 'tags') {
+ if (is_array(value: $value) === false) {
+ throw new InvalidArgumentException(message: 'Tags must be a list: ' . $definition['key']);
+ }
+
+ $items = $value;
+ }
+
+ $result = [];
+ foreach ($items as $item) {
+ if ($item === null) {
+ continue;
+ }
+
+ if (is_string(value: $item) === false) {
+ throw new InvalidArgumentException(message: 'A value must be text: ' . $definition['key']);
+ }
+
+ $item = trim(string: $item);
+ if ($item === '') {
+ continue;
+ }
+
+ if (mb_strlen(string: $item) > self::MAX_VALUE_LENGTH) {
+ throw new InvalidArgumentException(message: 'A value is longer than ' . self::MAX_VALUE_LENGTH . ' characters: ' . $definition['key']);
+ }
+
+ // The first spelling of a tag wins.
+ $result[mb_strtolower(string: $item)] ??= $item;
+ }
+
+ if (count(value: $result) > self::MAX_TAGS) {
+ throw new InvalidArgumentException(message: 'At most ' . self::MAX_TAGS . ' tags: ' . $definition['key']);
+ }
+
+ return array_values(array: $result);
+ }//end cleanValues()
+
+ /**
+ * Custom fields shown in the widget that the viewer may see (REQ-PEX-004).
+ *
+ * @param string|null $viewerId The viewer, or null for nobody.
+ * @param string[] $userIds The people on the page.
+ *
+ * @return array>> Per user id: `{key, label, type, values}`.
+ *
+ * @spec openspec/specs/people-widget/spec.md
+ */
+ public function visibleFieldsFor(?string $viewerId, array $userIds): array {
+ $definitions = array_filter(
+ array: $this->getDefinitions(),
+ callback: static fn (array $definition): bool => $definition['shownInWidget'] === true
+ );
+ if ($definitions === [] || $userIds === []) {
+ return [];
+ }
+
+ $grouped = $this->groupValues(rows: $this->valueMapper->findByUsers(userIds: $userIds));
+
+ $result = [];
+ foreach ($grouped as $userId => $byKey) {
+ foreach ($definitions as $definition) {
+ $values = $byKey[$definition['key']] ?? [];
+ if ($values === [] || $this->canSee(viewerId: $viewerId, ownerId: (string)$userId, definition: $definition) === false) {
+ continue;
+ }
+
+ $result[$userId][] = [
+ 'key' => $definition['key'],
+ 'label' => $definition['label'],
+ 'type' => $definition['type'],
+ 'values' => $values,
+ ];
+ }
+ }
+
+ return $result;
+ }//end visibleFieldsFor()
+
+ /**
+ * People whose searchable fields contain the query and the viewer may
+ * see that field (REQ-PEX-003, REQ-PEX-004).
+ *
+ * @param string|null $viewerId The viewer.
+ * @param string $query The search text.
+ *
+ * @return string[] Matching user ids.
+ *
+ * @spec openspec/specs/people-widget/spec.md
+ */
+ public function findMatchingUserIds(?string $viewerId, string $query): array {
+ $needle = mb_strtolower(string: trim(string: $query));
+ if ($needle === '') {
+ return [];
+ }
+
+ $definitions = [];
+ foreach ($this->getDefinitions() as $definition) {
+ if ($definition['searchable'] === true) {
+ $definitions[$definition['key']] = $definition;
+ }
+ }
+
+ $matches = [];
+ foreach ($this->valueMapper->search(needle: $needle) as $row) {
+ $owner = $row->getUserId();
+ if ($this->rowMatchesFor(viewerId: $viewerId, row: $row, definitions: $definitions) === true) {
+ $matches[$owner] = true;
+ }
+ }
+
+ return array_keys(array: $matches);
+ }//end findMatchingUserIds()
+
+ /**
+ * Whether a matching row may count for this viewer.
+ *
+ * @param string|null $viewerId The viewer.
+ * @param ProfileValue $row The matching row.
+ * @param array> $definitions Searchable definitions by key.
+ *
+ * @return bool
+ */
+ private function rowMatchesFor(?string $viewerId, ProfileValue $row, array $definitions): bool {
+ $key = $row->getFieldKey();
+ $owner = $row->getUserId();
+
+ if (isset(self::MIRRORED_STANDARD_FIELDS[$key]) === true) {
+ return self::scopeAllows(scope: $row->getScope(), viewerId: $viewerId, ownerId: $owner);
+ }
+
+ $definition = $definitions[$key] ?? null;
+ if ($definition === null) {
+ return false;
+ }
+
+ return $this->canSee(viewerId: $viewerId, ownerId: $owner, definition: $definition);
+ }//end rowMatchesFor()
+
+ /**
+ * Whether a standard field with this Nextcloud scope is visible to the viewer.
+ * A private field is visible to its owner only; the other scopes reach
+ * every signed-in user of this instance.
+ *
+ * @param string|null $scope The property scope.
+ * @param string|null $viewerId The viewer.
+ * @param string $ownerId The owner.
+ *
+ * @return bool
+ *
+ * @spec openspec/specs/people-widget/spec.md
+ */
+ public static function scopeAllows(?string $scope, ?string $viewerId, string $ownerId): bool {
+ if ($viewerId !== null && $viewerId === $ownerId) {
+ return true;
+ }
+
+ if ($viewerId === null) {
+ return false;
+ }
+
+ return $scope !== IAccountManager::SCOPE_PRIVATE;
+ }//end scopeAllows()
+
+ /**
+ * Whether the viewer may see a custom field of the owner.
+ *
+ * @param string|null $viewerId The viewer.
+ * @param string $ownerId The owner.
+ * @param array $definition The field definition.
+ *
+ * @return bool
+ *
+ * @spec openspec/specs/people-widget/spec.md
+ */
+ public function canSee(?string $viewerId, string $ownerId, array $definition): bool {
+ if ($viewerId === null) {
+ return false;
+ }
+
+ if ($viewerId === $ownerId || $definition['visibility'] === 'everyone') {
+ return true;
+ }
+
+ $shared = array_intersect(
+ $this->groupsOf(userId: $viewerId),
+ $this->groupsOf(userId: $ownerId)
+ );
+
+ return $shared !== [];
+ }//end canSee()
+
+ /**
+ * Group ids of a user, memoised.
+ *
+ * @param string $userId The user.
+ *
+ * @return string[]
+ */
+ private function groupsOf(string $userId): array {
+ if (isset($this->groupCache[$userId]) === false) {
+ $this->groupCache[$userId] = $this->adminTemplateService->getUserGroupIdsFor(userId: $userId);
+ }
+
+ return $this->groupCache[$userId];
+ }//end groupsOf()
+
+ /**
+ * Group rows by user and field key.
+ *
+ * @param ProfileValue[] $rows Rows.
+ *
+ * @return array>
+ */
+ private function groupValues(array $rows): array {
+ $result = [];
+ foreach ($rows as $row) {
+ $result[$row->getUserId()][$row->getFieldKey()][] = $row->getValue();
+ }
+
+ return $result;
+ }//end groupValues()
+
+ /**
+ * Mirror a person's searchable standard fields with their scope (REQ-PEX-004).
+ *
+ * @param IUser $user The person.
+ *
+ * @return void
+ *
+ * @spec openspec/specs/people-widget/spec.md
+ */
+ public function mirrorStandardFields(IUser $user): void {
+ try {
+ $account = $this->accountManager->getAccount(user: $user);
+ } catch (Exception $e) {
+ $this->logger->debug(message: 'Profile mirror: no account for ' . $user->getUID() . ': ' . $e->getMessage());
+ return;
+ }
+
+ foreach (self::MIRRORED_STANDARD_FIELDS as $key => $property) {
+ $value = '';
+ $scope = null;
+ try {
+ $prop = $account->getProperty(property: $property);
+ $value = trim(string: $prop->getValue());
+ $scope = $prop->getScope();
+ } catch (Exception) {
+ $value = '';
+ }
+
+ $values = [];
+ if ($value !== '') {
+ $values = [$value];
+ }
+
+ $this->valueMapper->replaceValues(
+ userId: $user->getUID(),
+ fieldKey: $key,
+ values: $values,
+ source: 'nextcloud',
+ scope: $scope
+ );
+ }//end foreach
+ }//end mirrorStandardFields()
+
+ /**
+ * Read a person's LDAP-sourced fields (REQ-PEX-001). Does nothing when
+ * `user_ldap` is not available or the account does not come from LDAP.
+ *
+ * @param IUser $user The person.
+ *
+ * @return int Number of fields synced.
+ *
+ * @spec openspec/specs/people-widget/spec.md
+ */
+ public function syncLdapFields(IUser $user): int {
+ $definitions = array_filter(
+ array: $this->getDefinitions(),
+ callback: static fn (array $definition): bool => $definition['source'] === 'ldap'
+ );
+ if ($definitions === [] || $user->getBackendClassName() !== 'LDAP') {
+ return 0;
+ }
+
+ if ($this->ldapProviderFactory->isAvailable() === false) {
+ return 0;
+ }
+
+ $synced = 0;
+ try {
+ $provider = $this->ldapProviderFactory->getLDAPProvider();
+ foreach ($definitions as $definition) {
+ $raw = $provider->getMultiValueUserAttribute(uid: $user->getUID(), attribute: $definition['ldapAttribute']);
+ $values = $this->cleanLdapValues(definition: $definition, raw: $raw);
+ $this->valueMapper->replaceValues(userId: $user->getUID(), fieldKey: $definition['key'], values: $values, source: 'ldap');
+ $synced++;
+ }
+ } catch (Exception $e) {
+ $this->logger->warning(message: 'Profile LDAP sync failed for ' . $user->getUID() . ': ' . $e->getMessage());
+ }
+
+ return $synced;
+ }//end syncLdapFields()
+
+ /**
+ * Clean the values read from LDAP: a text field keeps its first value.
+ *
+ * @param array $definition The field definition.
+ * @param array $raw Values from LDAP.
+ *
+ * @return string[]
+ */
+ private function cleanLdapValues(array $definition, array $raw): array {
+ $values = [];
+ foreach ($raw as $item) {
+ $item = mb_substr(string: trim(string: (string)$item), start: 0, length: self::MAX_VALUE_LENGTH);
+ if ($item !== '') {
+ $values[mb_strtolower(string: $item)] ??= $item;
+ }
+ }
+
+ $values = array_values(array: $values);
+ if ($definition['type'] === 'text') {
+ return array_slice(array: $values, offset: 0, length: 1);
+ }
+
+ return array_slice(array: $values, offset: 0, length: self::MAX_TAGS);
+ }//end cleanLdapValues()
+
+ /**
+ * Remove every value of a deleted person.
+ *
+ * @param string $userId The person.
+ *
+ * @return void
+ *
+ * @spec openspec/specs/people-widget/spec.md
+ */
+ public function deleteUser(string $userId): void {
+ $this->valueMapper->deleteByUser(userId: $userId);
+ }//end deleteUser()
+}//end class
diff --git a/lib/Settings/LaunchPadPersonal.php b/lib/Settings/LaunchPadPersonal.php
new file mode 100644
index 000000000..f702bafcb
--- /dev/null
+++ b/lib/Settings/LaunchPadPersonal.php
@@ -0,0 +1,90 @@
+
+ * @copyright 2026 Conduction b.v.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ * @version GIT:auto
+ * @link https://conduction.nl
+ *
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
+ * SPDX-License-Identifier: EUPL-1.2
+ */
+
+declare(strict_types=1);
+
+namespace OCA\LaunchPad\Settings;
+
+use OCA\LaunchPad\AppInfo\Application;
+use OCA\LaunchPad\Service\ProfileFieldService;
+use OCP\AppFramework\Http\TemplateResponse;
+use OCP\Settings\ISettings;
+use OCP\Util;
+
+/**
+ * Personal settings form for custom profile fields.
+ *
+ * @spec openspec/specs/people-widget/spec.md
+ */
+class LaunchPadPersonal implements ISettings {
+ /**
+ * Constructor.
+ *
+ * @param ProfileFieldService $profileFields Profile field service.
+ *
+ * @spec openspec/specs/people-widget/spec.md
+ */
+ public function __construct(
+ private readonly ProfileFieldService $profileFields,
+ ) {
+ }//end __construct()
+
+ /**
+ * The form: an empty mount point for the personal settings bundle.
+ *
+ * @return TemplateResponse
+ *
+ * @spec openspec/specs/people-widget/spec.md
+ */
+ public function getForm(): TemplateResponse {
+ Util::addScript(application: Application::APP_ID, file: 'launchpad-personal');
+
+ return new TemplateResponse(appName: Application::APP_ID, templateName: 'settings/personal');
+ }//end getForm()
+
+ /**
+ * The section: the personal information page, next to the standard profile.
+ * Null hides the form while no field is defined.
+ *
+ * @return string|null
+ *
+ * @spec openspec/specs/people-widget/spec.md
+ */
+ public function getSection(): ?string {
+ if ($this->profileFields->getDefinitions() === []) {
+ return null;
+ }
+
+ return 'personal-info';
+ }//end getSection()
+
+ /**
+ * Position within the section.
+ *
+ * @return int
+ *
+ * @spec openspec/specs/people-widget/spec.md
+ */
+ public function getPriority(): int {
+ return 90;
+ }//end getPriority()
+}//end class
diff --git a/lib/actions.seed.json b/lib/actions.seed.json
index 470ae2f32..f3c39cf91 100644
--- a/lib/actions.seed.json
+++ b/lib/actions.seed.json
@@ -60,6 +60,10 @@
"metadata-admin.update-field": ["admin"],
"metadata-admin.delete-field": ["admin"],
"people-widget.get-users": ["admin"],
+ "profile-fields.get-own": ["admin", "@all"],
+ "profile-fields.save-own": ["admin", "@all"],
+ "profile-fields.get-definitions": ["admin"],
+ "profile-fields.save-definitions": ["admin"],
"resource-serve.get-resource": ["admin", "@all"],
"resource-serve.list-resources": ["admin", "@all"],
"rule.get-rules": ["admin"],
diff --git a/templates/settings/personal.php b/templates/settings/personal.php
new file mode 100644
index 000000000..19965281a
--- /dev/null
+++ b/templates/settings/personal.php
@@ -0,0 +1,8 @@
+
+ * SPDX-License-Identifier: EUPL-1.2
+ */
+?>
+
+
diff --git a/tests/Unit/Controller/ProfileFieldsControllerTest.php b/tests/Unit/Controller/ProfileFieldsControllerTest.php
new file mode 100644
index 000000000..58134c901
--- /dev/null
+++ b/tests/Unit/Controller/ProfileFieldsControllerTest.php
@@ -0,0 +1,119 @@
+
+ * @copyright 2026 Conduction b.v.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
+ * SPDX-License-Identifier: EUPL-1.2
+ */
+
+declare(strict_types=1);
+
+namespace Unit\Controller;
+
+use OCA\LaunchPad\Controller\ProfileFieldsController;
+use OCA\LaunchPad\Db\AdminSetting;
+use OCA\LaunchPad\Db\AdminSettingMapper;
+use OCA\LaunchPad\Service\ActionAuthService;
+use OCA\LaunchPad\Service\AdminTemplateService;
+use OCA\LaunchPad\Service\ProfileFieldService;
+use OCP\Accounts\IAccountManager;
+use OCP\AppFramework\Http;
+use OCP\IGroupManager;
+use OCP\IRequest;
+use OCP\IUser;
+use OCP\IUserSession;
+use OCP\LDAP\ILDAPProviderFactory;
+use PHPUnit\Framework\TestCase;
+use Psr\Log\LoggerInterface;
+use Unit\Support\InMemoryProfileValues;
+
+class ProfileFieldsControllerTest extends TestCase {
+ /**
+ * Stored admin settings.
+ *
+ * @var array
+ */
+ private array $settings = [];
+
+ private InMemoryProfileValues $values;
+
+ private function controller(string $uid, bool $admin): ProfileFieldsController {
+ $settingMapper = $this->createMock(AdminSettingMapper::class);
+ $settingMapper->method('getValue')->willReturnCallback(
+ fn (string $key, mixed $default = null): mixed => $this->settings[$key] ?? $default
+ );
+ $settingMapper->method('setSetting')->willReturnCallback(
+ function (string $key, mixed $value): AdminSetting {
+ $this->settings[$key] = $value;
+ return new AdminSetting();
+ }
+ );
+ $this->values ??= new InMemoryProfileValues();
+
+ $service = new ProfileFieldService(
+ settingMapper: $settingMapper,
+ valueMapper: $this->values,
+ accountManager: $this->createMock(IAccountManager::class),
+ adminTemplateService: $this->createMock(AdminTemplateService::class),
+ ldapProviderFactory: $this->createMock(ILDAPProviderFactory::class),
+ logger: $this->createMock(LoggerInterface::class),
+ );
+
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn($uid);
+ $session = $this->createMock(IUserSession::class);
+ $session->method('getUser')->willReturn($user);
+ $groups = $this->createMock(IGroupManager::class);
+ $groups->method('isAdmin')->willReturn($admin);
+
+ return new ProfileFieldsController(
+ request: $this->createMock(IRequest::class),
+ profileFields: $service,
+ actionAuth: $this->createMock(ActionAuthService::class),
+ userSession: $session,
+ groupManager: $groups,
+ );
+ }//end controller()
+
+ public function testANonAdminCannotReadOrWriteDefinitions(): void {
+ $controller = $this->controller(uid: 'sanne', admin: false);
+
+ $this->assertSame(Http::STATUS_FORBIDDEN, $controller->getDefinitions()->getStatus());
+ $this->assertSame(Http::STATUS_FORBIDDEN, $controller->saveDefinitions(fields: [['key' => 'x', 'label' => 'X']])->getStatus());
+ $this->assertSame([], $this->settings, 'a refused write must not store anything');
+ }//end testANonAdminCannotReadOrWriteDefinitions()
+
+ public function testAnAdminDefinesAndAPersonSavesOnlyTheirOwnValues(): void {
+ $saved = $this->controller(uid: 'noor', admin: true)->saveDefinitions(
+ fields: [['key' => 'expertise', 'label' => 'Expertise', 'type' => 'tags', 'searchable' => true]]
+ );
+ $this->assertSame(Http::STATUS_OK, $saved->getStatus());
+
+ $pieter = $this->controller(uid: 'pieter', admin: false);
+ $response = $pieter->saveOwn(values: ['expertise' => ['subsidies']]);
+ $this->assertSame(Http::STATUS_OK, $response->getStatus());
+ $this->assertSame(['subsidies'], $response->getData()['fields'][0]['values']);
+ $this->assertSame('pieter', $this->values->rows[0]->getUserId(), 'the value must land on the caller');
+
+ $sanne = $this->controller(uid: 'sanne', admin: false);
+ $this->assertSame([], $sanne->getOwn()->getData()['fields'][0]['values'], 'Sanne must not read Pieter\'s values as her own');
+ }//end testAnAdminDefinesAndAPersonSavesOnlyTheirOwnValues()
+
+ public function testAnInvalidValueIsABadRequest(): void {
+ $this->controller(uid: 'noor', admin: true)->saveDefinitions(fields: [['key' => 'expertise', 'label' => 'Expertise', 'type' => 'tags']]);
+
+ $response = $this->controller(uid: 'pieter', admin: false)->saveOwn(values: ['expertise' => 'not a list']);
+ $this->assertSame(Http::STATUS_BAD_REQUEST, $response->getStatus());
+ }//end testAnInvalidValueIsABadRequest()
+}//end class
diff --git a/tests/Unit/Database/ProfileValuesDatabaseTest.php b/tests/Unit/Database/ProfileValuesDatabaseTest.php
new file mode 100644
index 000000000..b46cbf45a
--- /dev/null
+++ b/tests/Unit/Database/ProfileValuesDatabaseTest.php
@@ -0,0 +1,55 @@
+
+ * @copyright 2026 Conduction b.v.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
+ * SPDX-License-Identifier: EUPL-1.2
+ */
+
+declare(strict_types=1);
+
+namespace Unit\Database;
+
+use OCA\LaunchPad\Db\ProfileValueMapper;
+use OCP\Server;
+
+/**
+ * @SuppressWarnings(PHPMD.StaticAccess) Server::get() is the only way in from a test.
+ */
+class ProfileValuesDatabaseTest extends RealDatabaseTestCase {
+ /**
+ * A tag is stored per row and found on part of the word, case-insensitively.
+ *
+ * @return void
+ */
+ public function testATagIsStoredAndFoundOnPartOfTheWord(): void {
+ $owner = $this->makeUser(prefix: 'db-profile');
+ $this->cleanup(function () use ($owner): void {
+ $this->deleteRows('launchpad_profile_values', 'user_id', $owner);
+ });
+
+ $mapper = Server::get(ProfileValueMapper::class);
+ $mapper->replaceValues(userId: $owner, fieldKey: 'expertise', values: ['Subsidies', 'Omgevingswet'], source: 'self');
+
+ $rows = $this->storedRows('launchpad_profile_values', 'user_id', $owner);
+ $this->assertCount(2, $rows);
+ $this->assertColumnsFilled($rows[0], ['user_id', 'field_key', 'value', 'value_search', 'source', 'updated_at']);
+
+ $found = array_map(static fn ($row): string => $row->getUserId(), $mapper->search(needle: 'subsidie'));
+ $this->assertContains($owner, $found);
+
+ $mapper->replaceValues(userId: $owner, fieldKey: 'expertise', values: ['Omgevingswet'], source: 'self');
+ $this->assertCount(1, $this->storedRows('launchpad_profile_values', 'user_id', $owner), 'a save replaces the old tags');
+ }//end testATagIsStoredAndFoundOnPartOfTheWord()
+}//end class
diff --git a/tests/Unit/Service/ActionAuthBaselineTest.php b/tests/Unit/Service/ActionAuthBaselineTest.php
index 052d0e218..6dc033c92 100644
--- a/tests/Unit/Service/ActionAuthBaselineTest.php
+++ b/tests/Unit/Service/ActionAuthBaselineTest.php
@@ -84,6 +84,8 @@ class ActionAuthBaselineTest extends TestCase {
'metadata-admin.update-field',
'metadata-admin.delete-field',
'people-widget.get-users',
+ 'profile-fields.get-definitions',
+ 'profile-fields.save-definitions',
'rule.get-rules',
'rule.add-rule',
'rule.update-rule',
@@ -121,6 +123,9 @@ class ActionAuthBaselineTest extends TestCase {
// The `admin.` prefix is the controller's, not a privilege level:
// getMyRole() only ever answers for the caller.
'admin.get-my-role',
+ // REQ-PEX-002: every person fills their own profile fields.
+ 'profile-fields.get-own',
+ 'profile-fields.save-own',
];
/** @var IAppConfig&MockObject */
diff --git a/tests/Unit/Service/PeopleWidgetServiceTest.php b/tests/Unit/Service/PeopleWidgetServiceTest.php
index 18b559b8c..387e2adac 100644
--- a/tests/Unit/Service/PeopleWidgetServiceTest.php
+++ b/tests/Unit/Service/PeopleWidgetServiceTest.php
@@ -26,7 +26,10 @@
namespace Unit\Service;
use InvalidArgumentException;
+use OCA\LaunchPad\Db\AdminSetting;
+use OCA\LaunchPad\Db\AdminSettingMapper;
use OCA\LaunchPad\Service\AdminTemplateService;
+use OCA\LaunchPad\Service\ProfileFieldService;
use OCA\LaunchPad\Service\PeopleWidgetService;
use OCP\Accounts\IAccount;
use OCP\Accounts\IAccountManager;
@@ -36,8 +39,11 @@
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IUserManager;
+use OCP\LDAP\ILDAPProviderFactory;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
+use Psr\Log\LoggerInterface;
+use Unit\Support\InMemoryProfileValues;
/**
* Tests for the People widget backend service.
@@ -73,6 +79,17 @@ class PeopleWidgetServiceTest extends TestCase {
private PeopleWidgetService $service;
+ private ProfileFieldService $profileFields;
+
+ private InMemoryProfileValues $profileValues;
+
+ /**
+ * Stored admin settings.
+ *
+ * @var array
+ */
+ private array $settings = [];
+
/**
* @return void
*/
@@ -93,12 +110,34 @@ protected function setUp(): void {
// Default: any user has no groups. Tests can override per-call.
$this->adminTemplateService->method('getUserGroupIdsFor')->willReturn([]);
+ // The real profile field service, over in-memory values and settings.
+ $settingMapper = $this->createMock(originalClassName: AdminSettingMapper::class);
+ $settingMapper->method('getValue')->willReturnCallback(
+ callback: fn (string $key, mixed $default = null): mixed => $this->settings[$key] ?? $default
+ );
+ $settingMapper->method('setSetting')->willReturnCallback(
+ callback: function (string $key, mixed $value): AdminSetting {
+ $this->settings[$key] = $value;
+ return new AdminSetting();
+ }
+ );
+ $this->profileValues = new InMemoryProfileValues();
+ $this->profileFields = new ProfileFieldService(
+ settingMapper: $settingMapper,
+ valueMapper: $this->profileValues,
+ accountManager: $this->accountManager,
+ adminTemplateService: $this->adminTemplateService,
+ ldapProviderFactory: $this->createMock(originalClassName: ILDAPProviderFactory::class),
+ logger: $this->createMock(originalClassName: LoggerInterface::class),
+ );
+
$this->service = new PeopleWidgetService(
userManager: $this->userManager,
groupManager: $this->groupManager,
accountManager: $this->accountManager,
urlGenerator: $this->urlGenerator,
adminTemplateService: $this->adminTemplateService,
+ profileFields: $this->profileFields,
);
}//end setUp()
@@ -385,7 +424,7 @@ public function testBirthdateIsNormalisedToIso(): void {
);
$this->accountManager->method('getAccount')->willReturn($account);
- $result = $this->service->listUsers();
+ $result = $this->service->listUsers(viewerId: 'viewer');
$this->assertSame(
expected: '1990-06-10',
@@ -467,6 +506,96 @@ function (string $pattern, ?int $limit = null, ?int $offset = null) use (&$captu
// Helpers
// ---------------------------------------------------------------
+ // ---------------------------------------------------------------
+ // Search and custom fields (widgets-people-expertise-and-fields)
+ // ---------------------------------------------------------------
+
+ /**
+ * REQ-PEX-003 scenario "Sanne finds the subsidies expert": Pieter is not
+ * on the first page and his name does not contain the query; his tag does.
+ *
+ * @return void
+ */
+ public function testSearchFindsPieterByHisTagAcrossTheDirectory(): void {
+ $pieter = $this->makeUser(uid: 'pieter', display: 'Pieter de Vries');
+ $anna = $this->makeUser(uid: 'anna', display: 'Anna Subsidiemedewerker');
+ $this->userManager->expects($this->once())->method('search')
+ ->with('subsidie', PeopleWidgetService::SEARCH_CANDIDATE_CAP)
+ ->willReturn([$anna]);
+ $this->userManager->method('get')->willReturnMap([['pieter', $pieter]]);
+ $this->userManager->expects($this->never())->method('searchDisplayName');
+ $this->accountManager->method('getAccount')->willReturn($this->emptyAccount());
+
+ $this->profileFields->saveDefinitions(raw: [['key' => 'expertise', 'label' => 'Expertise', 'type' => 'tags', 'searchable' => true]]);
+ $this->profileFields->saveOwnValues(userId: 'pieter', values: ['expertise' => ['subsidies', 'Omgevingswet']]);
+
+ $result = $this->service->listUsers(query: 'subsidie', viewerId: 'sanne');
+
+ $uids = array_column(array: $result['users'], column_key: 'uid');
+ $this->assertSame(expected: ['anna', 'pieter'], actual: $uids);
+ $this->assertSame(expected: 2, actual: $result['total']);
+ $this->assertSame(
+ expected: [['key' => 'expertise', 'label' => 'Expertise', 'type' => 'tags', 'values' => ['subsidies', 'Omgevingswet']]],
+ actual: $result['users'][1]['customFields']
+ );
+ $this->assertSame(expected: [], actual: $result['users'][0]['customFields']);
+ }//end testSearchFindsPieterByHisTagAcrossTheDirectory()
+
+ public function testAOneCharacterQueryIsRefused(): void {
+ $this->expectException(exception: InvalidArgumentException::class);
+ $this->service->listUsers(query: 's', viewerId: 'sanne');
+ }//end testAOneCharacterQueryIsRefused()
+
+ public function testSearchStaysInsideTheWidgetsGroups(): void {
+ $pieter = $this->makeUser(uid: 'pieter', display: 'Pieter');
+ $karin = $this->makeUser(uid: 'karin', display: 'Karin');
+ $this->userManager->method('search')->willReturn([$pieter, $karin]);
+ $group = $this->createMock(originalClassName: IGroup::class);
+ $group->method('getUsers')->willReturn([$karin]);
+ $this->groupManager->method('get')->willReturn($group);
+ $this->accountManager->method('getAccount')->willReturn($this->emptyAccount());
+
+ $result = $this->service->listUsers(
+ filters: [['fieldName' => 'group', 'operator' => 'in', 'values' => ['kcc']]],
+ query: 'ie',
+ viewerId: 'sanne'
+ );
+
+ $this->assertSame(expected: ['karin'], actual: array_column(array: $result['users'], column_key: 'uid'));
+ }//end testSearchStaysInsideTheWidgetsGroups()
+
+ /**
+ * REQ-PEX-004 scenario "Private biography is not searchable": Karin does
+ * not match on her private biography, and her card does not show it.
+ *
+ * @return void
+ */
+ public function testAPrivateBiographyNeitherMatchesNorShows(): void {
+ $karin = $this->makeUser(uid: 'karin', display: 'Karin');
+ $account = $this->makeAccount(
+ properties: [
+ IAccountManager::PROPERTY_BIOGRAPHY => 'Ik weet alles van subsidies',
+ IAccountManager::PROPERTY_ROLE => 'Beleidsmedewerker',
+ ],
+ scopes: [IAccountManager::PROPERTY_BIOGRAPHY => IAccountManager::SCOPE_PRIVATE]
+ );
+ $this->accountManager->method('getAccount')->willReturn($account);
+ $this->profileFields->mirrorStandardFields(user: $karin);
+
+ $this->userManager->method('search')->willReturn([]);
+ $this->userManager->method('get')->willReturnMap([['karin', $karin]]);
+ $searched = $this->service->listUsers(query: 'subsidies', viewerId: 'sanne');
+ $this->assertSame(expected: [], actual: $searched['users'], message: 'Karin matched on her private biography');
+
+ $this->wireDirectory(orderedUsers: [$karin]);
+ $card = $this->service->listUsers(viewerId: 'sanne')['users'][0];
+ $this->assertArrayNotHasKey(key: 'biography', array: $card);
+ $this->assertSame(expected: 'Beleidsmedewerker', actual: $card['role'], message: 'CONTROL: a local field still shows');
+
+ $own = $this->service->listUsers(viewerId: 'karin')['users'][0];
+ $this->assertSame(expected: 'Ik weet alles van subsidies', actual: $own['biography'], message: 'CONTROL: Karin sees her own biography');
+ }//end testAPrivateBiographyNeitherMatchesNorShows()
+
/**
* Wire the user-directory backend for a no-group-filter,
* display-name-sorted listing: a bounded `searchDisplayName` that
@@ -535,14 +664,15 @@ private function emptyAccount(): IAccount {
*
* @return IAccount&MockObject
*/
- private function makeAccount(array $properties): IAccount {
+ private function makeAccount(array $properties, array $scopes = []): IAccount {
$account = $this->createMock(originalClassName: IAccount::class);
$account->method('getProperty')
->willReturnCallback(
- callback: function (string $name) use ($properties): IAccountProperty {
+ callback: function (string $name) use ($properties, $scopes): IAccountProperty {
$prop = $this->createMock(originalClassName: IAccountProperty::class);
$prop->method('getValue')->willReturn($properties[$name] ?? '');
$prop->method('getName')->willReturn($name);
+ $prop->method('getScope')->willReturn($scopes[$name] ?? IAccountManager::SCOPE_LOCAL);
return $prop;
}
);
diff --git a/tests/Unit/Service/ProfileFieldServiceTest.php b/tests/Unit/Service/ProfileFieldServiceTest.php
new file mode 100644
index 000000000..256dff610
--- /dev/null
+++ b/tests/Unit/Service/ProfileFieldServiceTest.php
@@ -0,0 +1,318 @@
+
+ * @copyright 2026 Conduction b.v.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
+ * SPDX-License-Identifier: EUPL-1.2
+ */
+
+declare(strict_types=1);
+
+namespace Unit\Service;
+
+use InvalidArgumentException;
+use OCA\LaunchPad\Db\AdminSettingMapper;
+use OCA\LaunchPad\Service\AdminTemplateService;
+use OCA\LaunchPad\Service\ProfileFieldService;
+use OCP\Accounts\IAccount;
+use OCP\Accounts\IAccountManager;
+use OCP\Accounts\IAccountProperty;
+use OCP\IUser;
+use OCP\LDAP\ILDAPProvider;
+use OCP\LDAP\ILDAPProviderFactory;
+use PHPUnit\Framework\TestCase;
+use Psr\Log\LoggerInterface;
+use Unit\Support\InMemoryProfileValues;
+
+/**
+ * @SuppressWarnings(PHPMD.TooManyPublicMethods) One test per scenario.
+ */
+class ProfileFieldServiceTest extends TestCase {
+ /**
+ * Stored admin settings.
+ *
+ * @var array
+ */
+ private array $settings = [];
+
+ private InMemoryProfileValues $values;
+
+ private $accountManager;
+
+ /**
+ * Accounts per user id.
+ *
+ * @var array
+ */
+ private array $accounts = [];
+
+ private $ldapFactory;
+
+ /**
+ * Group ids per user.
+ *
+ * @var array
+ */
+ private array $groups = [];
+
+ private ProfileFieldService $service;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $settingMapper = $this->createMock(AdminSettingMapper::class);
+ $settingMapper->method('getValue')->willReturnCallback(
+ fn (string $key, mixed $default = null): mixed => $this->settings[$key] ?? $default
+ );
+ $settingMapper->method('setSetting')->willReturnCallback(
+ function (string $key, mixed $value) {
+ $this->settings[$key] = $value;
+ return new \OCA\LaunchPad\Db\AdminSetting();
+ }
+ );
+
+ $templates = $this->createMock(AdminTemplateService::class);
+ $templates->method('getUserGroupIdsFor')->willReturnCallback(
+ fn (string $userId): array => $this->groups[$userId] ?? []
+ );
+
+ $this->values = new InMemoryProfileValues();
+ $this->accountManager = $this->createMock(IAccountManager::class);
+ $this->accountManager->method('getAccount')->willReturnCallback(
+ fn (IUser $user): IAccount => $this->accounts[$user->getUID()]
+ );
+ $this->ldapFactory = $this->createMock(ILDAPProviderFactory::class);
+
+ $this->service = new ProfileFieldService(
+ settingMapper: $settingMapper,
+ valueMapper: $this->values,
+ accountManager: $this->accountManager,
+ adminTemplateService: $templates,
+ ldapProviderFactory: $this->ldapFactory,
+ logger: $this->createMock(LoggerInterface::class),
+ );
+ }//end setUp()
+
+ /**
+ * The demo definitions: office (LDAP), cost centre (groups only), expertise (tags).
+ *
+ * @return void
+ */
+ private function defineFields(): void {
+ $this->service->saveDefinitions(
+ raw: [
+ ['key' => 'office', 'label' => 'Kantoorlocatie', 'type' => 'text', 'source' => 'ldap', 'ldapAttribute' => 'physicalDeliveryOfficeName', 'shownInWidget' => true],
+ ['key' => 'cost_centre', 'label' => 'Kostenplaats', 'type' => 'text', 'source' => 'self', 'searchable' => true, 'visibility' => 'groups'],
+ ['key' => 'expertise', 'label' => 'Expertise', 'type' => 'tags', 'source' => 'self', 'searchable' => true],
+ ]
+ );
+ }//end defineFields()
+
+ private function user(string $uid, string $backend = 'Database'): IUser {
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn($uid);
+ $user->method('getBackendClassName')->willReturn($backend);
+ return $user;
+ }//end user()
+
+ public function testDefinitionsAreValidatedAndNormalised(): void {
+ $this->defineFields();
+ $definitions = $this->service->getDefinitions();
+
+ $this->assertCount(3, $definitions);
+ $this->assertSame('everyone', $definitions[2]['visibility']);
+ $this->assertFalse($definitions[0]['searchable']);
+ $this->assertSame('', $definitions[1]['ldapAttribute']);
+ }//end testDefinitionsAreValidatedAndNormalised()
+
+ public function testMoreThanTenFieldsIsRefused(): void {
+ $raw = [];
+ for ($i = 0; $i < 11; $i++) {
+ $raw[] = ['key' => 'f' . $i, 'label' => 'Field ' . $i];
+ }
+
+ $this->expectException(InvalidArgumentException::class);
+ $this->service->saveDefinitions(raw: $raw);
+ }//end testMoreThanTenFieldsIsRefused()
+
+ public function testAnLdapFieldNeedsAnAttribute(): void {
+ $this->expectException(InvalidArgumentException::class);
+ $this->service->saveDefinitions(raw: [['key' => 'office', 'label' => 'Office', 'source' => 'ldap']]);
+ }//end testAnLdapFieldNeedsAnAttribute()
+
+ public function testDuplicateKeysAndBadTypesAreRefused(): void {
+ try {
+ $this->service->saveDefinitions(raw: [['key' => 'a', 'label' => 'A'], ['key' => 'a', 'label' => 'B']]);
+ $this->fail('duplicate key accepted');
+ } catch (InvalidArgumentException) {
+ $this->addToAssertionCount(1);
+ }
+
+ $this->expectException(InvalidArgumentException::class);
+ $this->service->saveDefinitions(raw: [['key' => 'a', 'label' => 'A', 'type' => 'number']]);
+ }//end testDuplicateKeysAndBadTypesAreRefused()
+
+ /**
+ * REQ-PEX-002 scenario "Pieter adds his subjects".
+ *
+ * @return void
+ */
+ public function testPieterAddsHisSubjectsAndLdapFieldsStayReadOnly(): void {
+ $this->defineFields();
+
+ $fields = $this->service->saveOwnValues(
+ userId: 'pieter',
+ values: [
+ 'expertise' => ['subsidies', ' Omgevingswet ', 'SUBSIDIES', ''],
+ 'office' => 'I typed this',
+ ]
+ );
+
+ $byKey = array_column($fields, null, 'key');
+ $this->assertSame(['subsidies', 'Omgevingswet'], $byKey['expertise']['values']);
+ $this->assertSame([], $byKey['office']['values'], 'an LDAP field must not take a self edit');
+ $this->assertTrue($byKey['office']['readOnly']);
+ $this->assertFalse($byKey['expertise']['readOnly']);
+ }//end testPieterAddsHisSubjectsAndLdapFieldsStayReadOnly()
+
+ public function testUnknownKeysAndWrongShapesAreRefused(): void {
+ $this->defineFields();
+
+ try {
+ $this->service->saveOwnValues(userId: 'pieter', values: ['nope' => 'x']);
+ $this->fail('unknown key accepted');
+ } catch (InvalidArgumentException) {
+ $this->addToAssertionCount(1);
+ }
+
+ $this->expectException(InvalidArgumentException::class);
+ $this->service->saveOwnValues(userId: 'pieter', values: ['expertise' => 'not a list']);
+ }//end testUnknownKeysAndWrongShapesAreRefused()
+
+ /**
+ * REQ-PEX-001 scenario "Office location from LDAP", with a stubbed provider.
+ *
+ * @return void
+ */
+ public function testLdapFieldsSyncForLdapAccountsOnly(): void {
+ $this->defineFields();
+ $provider = $this->createMock(ILDAPProvider::class);
+ $provider->method('getMultiValueUserAttribute')
+ ->with('pieter', 'physicalDeliveryOfficeName')
+ ->willReturn(['Stadhuis, 3e verdieping', 'ignored second value']);
+ $this->ldapFactory->method('isAvailable')->willReturn(true);
+ $this->ldapFactory->method('getLDAPProvider')->willReturn($provider);
+
+ $this->assertSame(0, $this->service->syncLdapFields(user: $this->user(uid: 'sanne')), 'a database account is not read from LDAP');
+ $this->assertSame(1, $this->service->syncLdapFields(user: $this->user(uid: 'pieter', backend: 'LDAP')));
+
+ $shown = $this->service->visibleFieldsFor(viewerId: 'sanne', userIds: ['pieter']);
+ $this->assertSame(
+ [['key' => 'office', 'label' => 'Kantoorlocatie', 'type' => 'text', 'values' => ['Stadhuis, 3e verdieping']]],
+ $shown['pieter']
+ );
+ }//end testLdapFieldsSyncForLdapAccountsOnly()
+
+ public function testLdapSyncDoesNothingWithoutUserLdap(): void {
+ $this->defineFields();
+ $this->ldapFactory->method('isAvailable')->willReturn(false);
+ $this->ldapFactory->expects($this->never())->method('getLDAPProvider');
+
+ $this->assertSame(0, $this->service->syncLdapFields(user: $this->user(uid: 'pieter', backend: 'LDAP')));
+ }//end testLdapSyncDoesNothingWithoutUserLdap()
+
+ /**
+ * REQ-PEX-003: a tag matches on a part of the word, for any signed-in viewer.
+ *
+ * @return void
+ */
+ public function testSearchFindsTheSubsidiesExpertByPartOfTheTag(): void {
+ $this->defineFields();
+ $this->service->saveOwnValues(userId: 'pieter', values: ['expertise' => ['subsidies']]);
+
+ $this->assertSame(['pieter'], $this->service->findMatchingUserIds(viewerId: 'sanne', query: 'Subsidie'));
+ $this->assertSame([], $this->service->findMatchingUserIds(viewerId: null, query: 'subsidie'));
+ }//end testSearchFindsTheSubsidiesExpertByPartOfTheTag()
+
+ /**
+ * REQ-PEX-004 scenario "Group-only field".
+ *
+ * @return void
+ */
+ public function testAGroupOnlyFieldNeitherShowsNorMatchesOutsideTheGroup(): void {
+ $this->defineFields();
+ $this->service->saveDefinitions(
+ raw: array_map(
+ static fn (array $definition): array => array_merge($definition, ['shownInWidget' => true]),
+ $this->service->getDefinitions()
+ )
+ );
+ $this->service->saveOwnValues(userId: 'pieter', values: ['cost_centre' => 'KP-4711']);
+ $this->groups = ['pieter' => ['finance'], 'sanne' => ['kcc'], 'karin' => ['finance']];
+
+ $this->assertSame([], $this->service->findMatchingUserIds(viewerId: 'sanne', query: 'kp-47'));
+ $this->assertArrayNotHasKey('pieter', $this->service->visibleFieldsFor(viewerId: 'sanne', userIds: ['pieter']));
+
+ $this->assertSame(['pieter'], $this->service->findMatchingUserIds(viewerId: 'karin', query: 'kp-47'), 'CONTROL: a colleague in the group does match');
+ $this->assertArrayHasKey('pieter', $this->service->visibleFieldsFor(viewerId: 'karin', userIds: ['pieter']));
+ }//end testAGroupOnlyFieldNeitherShowsNorMatchesOutsideTheGroup()
+
+ public function testANotSearchableFieldDoesNotMatch(): void {
+ $this->service->saveDefinitions(raw: [['key' => 'room', 'label' => 'Room', 'searchable' => false]]);
+ $this->service->saveOwnValues(userId: 'pieter', values: ['room' => 'B-204']);
+
+ $this->assertSame([], $this->service->findMatchingUserIds(viewerId: 'sanne', query: 'b-204'));
+ }//end testANotSearchableFieldDoesNotMatch()
+
+ /**
+ * REQ-PEX-004 scenario "Private biography is not searchable".
+ *
+ * @return void
+ */
+ public function testAPrivateBiographyIsMirroredButNeverMatchesForOthers(): void {
+ $this->mirrorBiography(uid: 'karin', text: 'Ik weet alles van subsidies', scope: IAccountManager::SCOPE_PRIVATE);
+ $this->mirrorBiography(uid: 'pieter', text: 'Subsidies en fondsen', scope: IAccountManager::SCOPE_LOCAL);
+
+ $this->assertSame(['pieter'], $this->service->findMatchingUserIds(viewerId: 'sanne', query: 'subsidies'));
+ $this->assertContains('karin', $this->service->findMatchingUserIds(viewerId: 'karin', query: 'subsidies'), 'CONTROL: the owner matches her own private field');
+ }//end testAPrivateBiographyIsMirroredButNeverMatchesForOthers()
+
+ public function testDeletingAUserRemovesTheirValues(): void {
+ $this->defineFields();
+ $this->service->saveOwnValues(userId: 'pieter', values: ['expertise' => ['subsidies']]);
+ $this->service->deleteUser(userId: 'pieter');
+
+ $this->assertSame([], $this->values->rows);
+ }//end testDeletingAUserRemovesTheirValues()
+
+ private function mirrorBiography(string $uid, string $text, string $scope): void {
+ $account = $this->createMock(IAccount::class);
+ $account->method('getProperty')->willReturnCallback(
+ function (string $property) use ($text, $scope) {
+ $prop = $this->createMock(IAccountProperty::class);
+ $value = '';
+ if ($property === IAccountManager::PROPERTY_BIOGRAPHY) {
+ $value = $text;
+ }
+
+ $prop->method('getValue')->willReturn($value);
+ $prop->method('getScope')->willReturn($scope);
+ return $prop;
+ }
+ );
+ $this->accounts[$uid] = $account;
+
+ $this->service->mirrorStandardFields(user: $this->user(uid: $uid));
+ }//end mirrorBiography()
+}//end class
diff --git a/tests/Unit/Support/InMemoryProfileValues.php b/tests/Unit/Support/InMemoryProfileValues.php
new file mode 100644
index 000000000..7972be1a2
--- /dev/null
+++ b/tests/Unit/Support/InMemoryProfileValues.php
@@ -0,0 +1,137 @@
+
+ * @copyright 2026 Conduction b.v.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
+ * SPDX-License-Identifier: EUPL-1.2
+ */
+
+declare(strict_types=1);
+
+namespace Unit\Support;
+
+use OCA\LaunchPad\Db\ProfileValue;
+use OCA\LaunchPad\Db\ProfileValueMapper;
+
+/**
+ * In-memory profile values.
+ */
+class InMemoryProfileValues extends ProfileValueMapper {
+ /**
+ * Stored rows.
+ *
+ * @var ProfileValue[]
+ */
+ public array $rows = [];
+
+ /**
+ * No database.
+ */
+ public function __construct() {
+ }//end __construct()
+
+ /**
+ * Rows of the given users.
+ *
+ * @param string[] $userIds User ids.
+ *
+ * @return ProfileValue[]
+ */
+ public function findByUsers(array $userIds): array {
+ return array_values(
+ array_filter(
+ $this->rows,
+ static fn (ProfileValue $row): bool => in_array($row->getUserId(), $userIds, true)
+ )
+ );
+ }//end findByUsers()
+
+ /**
+ * Rows whose lower-cased value contains the needle.
+ *
+ * @param string $needle Lower-cased search text.
+ *
+ * @return ProfileValue[]
+ */
+ public function search(string $needle): array {
+ return array_values(
+ array_filter(
+ $this->rows,
+ static fn (ProfileValue $row): bool => str_contains($row->getValueSearch(), $needle)
+ )
+ );
+ }//end search()
+
+ /**
+ * Replace one person's values of one field.
+ *
+ * @param string $userId Owner.
+ * @param string $fieldKey Field key.
+ * @param string[] $values Values.
+ * @param string $source Source.
+ * @param string|null $scope Scope.
+ *
+ * @return void
+ */
+ public function replaceValues(string $userId, string $fieldKey, array $values, string $source, ?string $scope = null): void {
+ $this->deleteField($userId, $fieldKey);
+ foreach ($values as $value) {
+ $row = new ProfileValue();
+ $row->setUserId($userId);
+ $row->setFieldKey($fieldKey);
+ $row->setValue($value);
+ $row->setValueSearch(mb_strtolower($value));
+ $row->setSource($source);
+ $row->setScope($scope);
+ $this->rows[] = $row;
+ }
+ }//end replaceValues()
+
+ /**
+ * Delete one person's values of one field.
+ *
+ * @param string $userId Owner.
+ * @param string $fieldKey Field key.
+ *
+ * @return int
+ */
+ public function deleteField(string $userId, string $fieldKey): int {
+ $before = count($this->rows);
+ $this->rows = array_values(
+ array_filter(
+ $this->rows,
+ static fn (ProfileValue $row): bool => ($row->getUserId() === $userId && $row->getFieldKey() === $fieldKey) === false
+ )
+ );
+
+ return $before - count($this->rows);
+ }//end deleteField()
+
+ /**
+ * Delete every value of a person.
+ *
+ * @param string $userId Owner.
+ *
+ * @return int
+ */
+ public function deleteByUser(string $userId): int {
+ $before = count($this->rows);
+ $this->rows = array_values(
+ array_filter($this->rows, static fn (ProfileValue $row): bool => $row->getUserId() !== $userId)
+ );
+
+ return $before - count($this->rows);
+ }//end deleteByUser()
+}//end class
From 96ff1766d21511d77ddf4fe6325f566500eee561 Mon Sep 17 00:00:00 2001
From: Ruben van der Linde
Date: Wed, 30 Sep 2026 07:46:21 +0200
Subject: [PATCH 02/60] feat(people): search the whole directory from the
people widget, show profile fields and tags, and let people fill their own
fields
A query of two or more characters asks the server; a tag is a button that searches
for it and a matching tag is highlighted. A personal settings form edits the
person's own fields (LDAP fields read-only) and an admin tab defines them.
REQ-PEX-001..003
---
l10n/be.js | 30 +-
l10n/be.json | 30 +-
l10n/bg.js | 30 +-
l10n/bg.json | 30 +-
l10n/bs.js | 30 +-
l10n/bs.json | 30 +-
l10n/ca.js | 30 +-
l10n/ca.json | 30 +-
l10n/cs.js | 30 +-
l10n/cs.json | 30 +-
l10n/da.js | 30 +-
l10n/da.json | 30 +-
l10n/de.js | 30 +-
l10n/de.json | 30 +-
l10n/el.js | 30 +-
l10n/el.json | 30 +-
l10n/en.js | 30 +-
l10n/en.json | 30 +-
l10n/es.js | 30 +-
l10n/es.json | 30 +-
l10n/et.js | 30 +-
l10n/et.json | 30 +-
l10n/fi.js | 30 +-
l10n/fi.json | 30 +-
l10n/fr.js | 30 +-
l10n/fr.json | 30 +-
l10n/ga.js | 30 +-
l10n/ga.json | 30 +-
l10n/hr.js | 30 +-
l10n/hr.json | 30 +-
l10n/hu.js | 30 +-
l10n/hu.json | 30 +-
l10n/is.js | 30 +-
l10n/is.json | 30 +-
l10n/it.js | 30 +-
l10n/it.json | 30 +-
l10n/lb.js | 30 +-
l10n/lb.json | 30 +-
l10n/lt.js | 30 +-
l10n/lt.json | 30 +-
l10n/lv.js | 30 +-
l10n/lv.json | 30 +-
l10n/mk.js | 30 +-
l10n/mk.json | 30 +-
l10n/mt.js | 30 +-
l10n/mt.json | 30 +-
l10n/nb.js | 30 +-
l10n/nb.json | 30 +-
l10n/nl.js | 30 +-
l10n/nl.json | 30 +-
l10n/pl.js | 30 +-
l10n/pl.json | 30 +-
l10n/pt.js | 30 +-
l10n/pt.json | 30 +-
l10n/rm.js | 30 +-
l10n/rm.json | 30 +-
l10n/ro.js | 30 +-
l10n/ro.json | 30 +-
l10n/ru.js | 30 +-
l10n/ru.json | 30 +-
l10n/sk.js | 30 +-
l10n/sk.json | 30 +-
l10n/sl.js | 30 +-
l10n/sl.json | 30 +-
l10n/sq.js | 30 +-
l10n/sq.json | 30 +-
l10n/sr.js | 30 +-
l10n/sr.json | 30 +-
l10n/sv.js | 30 +-
l10n/sv.json | 30 +-
l10n/tr.js | 30 +-
l10n/tr.json | 30 +-
l10n/uk.js | 30 +-
l10n/uk.json | 30 +-
.../Widgets/Renderers/PeopleWidget.vue | 360 ++++++++++++++----
.../Renderers/__tests__/PeopleWidget.spec.js | 152 ++++++++
src/components/admin/AdminSettings.vue | 9 +
.../admin/tabs/ProfileFieldsTab.vue | 243 ++++++++++++
.../tabs/__tests__/ProfileFieldsTab.spec.js | 121 ++++++
src/personal.js | 26 ++
src/views/settings/ProfileFieldsSettings.vue | 205 ++++++++++
.../__tests__/ProfileFieldsSettings.spec.js | 126 ++++++
webpack.config.js | 2 +
83 files changed, 3321 insertions(+), 143 deletions(-)
create mode 100644 src/components/Widgets/Renderers/__tests__/PeopleWidget.spec.js
create mode 100644 src/components/admin/tabs/ProfileFieldsTab.vue
create mode 100644 src/components/admin/tabs/__tests__/ProfileFieldsTab.spec.js
create mode 100644 src/personal.js
create mode 100644 src/views/settings/ProfileFieldsSettings.vue
create mode 100644 src/views/settings/__tests__/ProfileFieldsSettings.spec.js
diff --git a/l10n/be.js b/l10n/be.js
index 59ae76f16..4fc3bafb1 100644
--- a/l10n/be.js
+++ b/l10n/be.js
@@ -1284,6 +1284,34 @@ OC.L10N.register(
"Read receipts at the top of the dashboard show who has confirmed." : "Квітанцыі аб прачытанні ўверсе панэлі паказваюць, хто пацвердзіў.",
"{emoji}: {count} reactions" : "{emoji}: рэакцый {count}",
"React with {emoji}" : "Адрэагаваць {emoji}",
- "{names} and {count} more" : "{names} і яшчэ {count}"
+ "{names} and {count} more" : "{names} і яшчэ {count}",
+ "Search people" : "Пошук людзей",
+ "Search by name, email or expertise…" : "Пошук па імені, электроннай пошце або экспертызе…",
+ "Find everyone tagged {tag}" : "Знайсці ўсіх з тэгам {tag}",
+ "LaunchPad profile" : "Профіль LaunchPad",
+ "Colleagues see these fields on your card in the people widget and can find you by them." : "Калегі бачаць гэтыя палі на вашай картцы ў віджэце людзей і могуць знайсці вас па іх.",
+ "Your profile fields could not be loaded." : "Не ўдалося загрузіць палі вашага профілю.",
+ "Not filled in" : "Не запоўнена",
+ "Filled from your organisation’s directory. You cannot change it here." : "Запоўнена з каталога вашай арганізацыі. Тут змяніць нельга.",
+ "Type a subject and press Enter" : "Увядзіце тэму і націсніце Enter",
+ "Your profile fields are saved." : "Палі вашага профілю захаваны.",
+ "Your profile fields could not be saved." : "Не ўдалося захаваць палі вашага профілю.",
+ "Profile fields" : "Палі профілю",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute." : "Дадатковыя палі на картках калег у віджэце людзей, напрыклад месца працы або экспертыза. Людзі запаўняюць іх у асабістых наладах, або яны бяруцца з атрыбута LDAP.",
+ "New field" : "Новае поле",
+ "Tags" : "Тэгі",
+ "Filled by" : "Хто запаўняе",
+ "The person" : "Сам чалавек",
+ "LDAP attribute" : "Атрыбут LDAP",
+ "Who may see it" : "Хто можа бачыць",
+ "Everyone" : "Усе",
+ "People in the same group" : "Людзі з той жа групы",
+ "Searchable in the people widget" : "Можна шукаць у віджэце людзей",
+ "Shown on the card" : "Паказваецца на картцы",
+ "Remove field" : "Выдаліць поле",
+ "Profile fields saved." : "Палі профілю захаваны.",
+ "Save profile fields" : "Захаваць палі профілю",
+ "The profile fields could not be loaded." : "Не ўдалося загрузіць палі профілю.",
+ "The profile fields could not be saved." : "Не ўдалося захаваць палі профілю."
},
"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);");
diff --git a/l10n/be.json b/l10n/be.json
index 857fabbf9..f1b3999ce 100644
--- a/l10n/be.json
+++ b/l10n/be.json
@@ -1261,6 +1261,34 @@
"Read receipts at the top of the dashboard show who has confirmed.": "Квітанцыі аб прачытанні ўверсе панэлі паказваюць, хто пацвердзіў.",
"{emoji}: {count} reactions": "{emoji}: рэакцый {count}",
"React with {emoji}": "Адрэагаваць {emoji}",
- "{names} and {count} more": "{names} і яшчэ {count}"
+ "{names} and {count} more": "{names} і яшчэ {count}",
+ "Search people": "Пошук людзей",
+ "Search by name, email or expertise…": "Пошук па імені, электроннай пошце або экспертызе…",
+ "Find everyone tagged {tag}": "Знайсці ўсіх з тэгам {tag}",
+ "LaunchPad profile": "Профіль LaunchPad",
+ "Colleagues see these fields on your card in the people widget and can find you by them.": "Калегі бачаць гэтыя палі на вашай картцы ў віджэце людзей і могуць знайсці вас па іх.",
+ "Your profile fields could not be loaded.": "Не ўдалося загрузіць палі вашага профілю.",
+ "Not filled in": "Не запоўнена",
+ "Filled from your organisation’s directory. You cannot change it here.": "Запоўнена з каталога вашай арганізацыі. Тут змяніць нельга.",
+ "Type a subject and press Enter": "Увядзіце тэму і націсніце Enter",
+ "Your profile fields are saved.": "Палі вашага профілю захаваны.",
+ "Your profile fields could not be saved.": "Не ўдалося захаваць палі вашага профілю.",
+ "Profile fields": "Палі профілю",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute.": "Дадатковыя палі на картках калег у віджэце людзей, напрыклад месца працы або экспертыза. Людзі запаўняюць іх у асабістых наладах, або яны бяруцца з атрыбута LDAP.",
+ "New field": "Новае поле",
+ "Tags": "Тэгі",
+ "Filled by": "Хто запаўняе",
+ "The person": "Сам чалавек",
+ "LDAP attribute": "Атрыбут LDAP",
+ "Who may see it": "Хто можа бачыць",
+ "Everyone": "Усе",
+ "People in the same group": "Людзі з той жа групы",
+ "Searchable in the people widget": "Можна шукаць у віджэце людзей",
+ "Shown on the card": "Паказваецца на картцы",
+ "Remove field": "Выдаліць поле",
+ "Profile fields saved.": "Палі профілю захаваны.",
+ "Save profile fields": "Захаваць палі профілю",
+ "The profile fields could not be loaded.": "Не ўдалося загрузіць палі профілю.",
+ "The profile fields could not be saved.": "Не ўдалося захаваць палі профілю."
}
}
diff --git a/l10n/bg.js b/l10n/bg.js
index 30c5ce64f..94a897089 100644
--- a/l10n/bg.js
+++ b/l10n/bg.js
@@ -1293,7 +1293,35 @@ OC.L10N.register(
"Read receipts at the top of the dashboard show who has confirmed." : "Разписките за прочитане в горната част на таблото показват кой е потвърдил.",
"{emoji}: {count} reactions" : "{emoji}: {count} реакции",
"React with {emoji}" : "Реагиране с {emoji}",
- "{names} and {count} more" : "{names} и още {count}"
+ "{names} and {count} more" : "{names} и още {count}",
+ "Search people" : "Търсене на хора",
+ "Search by name, email or expertise…" : "Търсене по име, имейл или експертиза…",
+ "Find everyone tagged {tag}" : "Намери всички с етикет {tag}",
+ "LaunchPad profile" : "Профил в LaunchPad",
+ "Colleagues see these fields on your card in the people widget and can find you by them." : "Колегите виждат тези полета на вашата карта в приспособлението за хора и могат да ви намерят по тях.",
+ "Your profile fields could not be loaded." : "Полетата на профила ви не можаха да се заредят.",
+ "Not filled in" : "Не е попълнено",
+ "Filled from your organisation’s directory. You cannot change it here." : "Попълва се от директорията на вашата организация. Не можете да го промените тук.",
+ "Type a subject and press Enter" : "Въведете тема и натиснете Enter",
+ "Your profile fields are saved." : "Полетата на профила ви са запазени.",
+ "Your profile fields could not be saved." : "Полетата на профила ви не можаха да се запазят.",
+ "Profile fields" : "Полета на профила",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute." : "Допълнителни полета в картите на колегите в приспособлението за хора, като местоположение на офиса или експертиза. Хората ги попълват в личните си настройки или те идват от атрибут в LDAP.",
+ "New field" : "Ново поле",
+ "Tags" : "Етикети",
+ "Filled by" : "Попълва се от",
+ "The person" : "Самият човек",
+ "LDAP attribute" : "Атрибут в LDAP",
+ "Who may see it" : "Кой може да го вижда",
+ "Everyone" : "Всички",
+ "People in the same group" : "Хората в същата група",
+ "Searchable in the people widget" : "Може да се търси в приспособлението за хора",
+ "Shown on the card" : "Показва се на картата",
+ "Remove field" : "Премахване на полето",
+ "Profile fields saved." : "Полетата на профила са запазени.",
+ "Save profile fields" : "Запазване на полетата на профила",
+ "The profile fields could not be loaded." : "Полетата на профила не можаха да се заредят.",
+ "The profile fields could not be saved." : "Полетата на профила не можаха да се запазят."
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/bg.json b/l10n/bg.json
index 779669dc6..8f5c8ea7b 100644
--- a/l10n/bg.json
+++ b/l10n/bg.json
@@ -1260,6 +1260,34 @@
"Read receipts at the top of the dashboard show who has confirmed.": "Разписките за прочитане в горната част на таблото показват кой е потвърдил.",
"{emoji}: {count} reactions": "{emoji}: {count} реакции",
"React with {emoji}": "Реагиране с {emoji}",
- "{names} and {count} more": "{names} и още {count}"
+ "{names} and {count} more": "{names} и още {count}",
+ "Search people": "Търсене на хора",
+ "Search by name, email or expertise…": "Търсене по име, имейл или експертиза…",
+ "Find everyone tagged {tag}": "Намери всички с етикет {tag}",
+ "LaunchPad profile": "Профил в LaunchPad",
+ "Colleagues see these fields on your card in the people widget and can find you by them.": "Колегите виждат тези полета на вашата карта в приспособлението за хора и могат да ви намерят по тях.",
+ "Your profile fields could not be loaded.": "Полетата на профила ви не можаха да се заредят.",
+ "Not filled in": "Не е попълнено",
+ "Filled from your organisation’s directory. You cannot change it here.": "Попълва се от директорията на вашата организация. Не можете да го промените тук.",
+ "Type a subject and press Enter": "Въведете тема и натиснете Enter",
+ "Your profile fields are saved.": "Полетата на профила ви са запазени.",
+ "Your profile fields could not be saved.": "Полетата на профила ви не можаха да се запазят.",
+ "Profile fields": "Полета на профила",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute.": "Допълнителни полета в картите на колегите в приспособлението за хора, като местоположение на офиса или експертиза. Хората ги попълват в личните си настройки или те идват от атрибут в LDAP.",
+ "New field": "Ново поле",
+ "Tags": "Етикети",
+ "Filled by": "Попълва се от",
+ "The person": "Самият човек",
+ "LDAP attribute": "Атрибут в LDAP",
+ "Who may see it": "Кой може да го вижда",
+ "Everyone": "Всички",
+ "People in the same group": "Хората в същата група",
+ "Searchable in the people widget": "Може да се търси в приспособлението за хора",
+ "Shown on the card": "Показва се на картата",
+ "Remove field": "Премахване на полето",
+ "Profile fields saved.": "Полетата на профила са запазени.",
+ "Save profile fields": "Запазване на полетата на профила",
+ "The profile fields could not be loaded.": "Полетата на профила не можаха да се заредят.",
+ "The profile fields could not be saved.": "Полетата на профила не можаха да се запазят."
}
}
diff --git a/l10n/bs.js b/l10n/bs.js
index 75355048f..361af2eed 100644
--- a/l10n/bs.js
+++ b/l10n/bs.js
@@ -1293,7 +1293,35 @@ OC.L10N.register(
"Read receipts at the top of the dashboard show who has confirmed." : "Potvrde čitanja na vrhu kontrolne ploče pokazuju ko je potvrdio.",
"{emoji}: {count} reactions" : "{emoji}: {count} reakcija",
"React with {emoji}" : "Reaguj sa {emoji}",
- "{names} and {count} more" : "{names} i još {count}"
+ "{names} and {count} more" : "{names} i još {count}",
+ "Search people" : "Pretraži osobe",
+ "Search by name, email or expertise…" : "Pretraži po imenu, e-pošti ili stručnosti…",
+ "Find everyone tagged {tag}" : "Pronađi sve s oznakom {tag}",
+ "LaunchPad profile" : "LaunchPad profil",
+ "Colleagues see these fields on your card in the people widget and can find you by them." : "Kolege vide ova polja na vašoj kartici u widgetu osoba i mogu vas pronaći po njima.",
+ "Your profile fields could not be loaded." : "Polja vašeg profila nisu mogla biti učitana.",
+ "Not filled in" : "Nije popunjeno",
+ "Filled from your organisation’s directory. You cannot change it here." : "Popunjeno iz imenika vaše organizacije. Ovdje to ne možete promijeniti.",
+ "Type a subject and press Enter" : "Upišite temu i pritisnite Enter",
+ "Your profile fields are saved." : "Polja vašeg profila su spremljena.",
+ "Your profile fields could not be saved." : "Polja vašeg profila nisu mogla biti spremljena.",
+ "Profile fields" : "Polja profila",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute." : "Dodatna polja na karticama kolega u widgetu osoba, poput lokacije ureda ili stručnosti. Osobe ih popunjavaju u svojim osobnim postavkama ili dolaze iz LDAP atributa.",
+ "New field" : "Novo polje",
+ "Tags" : "Oznake",
+ "Filled by" : "Popunjava",
+ "The person" : "Sama osoba",
+ "LDAP attribute" : "LDAP atribut",
+ "Who may see it" : "Tko ga smije vidjeti",
+ "Everyone" : "Svi",
+ "People in the same group" : "Osobe u istoj grupi",
+ "Searchable in the people widget" : "Pretraživo u widgetu osoba",
+ "Shown on the card" : "Prikazano na kartici",
+ "Remove field" : "Ukloni polje",
+ "Profile fields saved." : "Polja profila su spremljena.",
+ "Save profile fields" : "Spremi polja profila",
+ "The profile fields could not be loaded." : "Polja profila nisu mogla biti učitana.",
+ "The profile fields could not be saved." : "Polja profila nisu mogla biti spremljena."
},
"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"
);
diff --git a/l10n/bs.json b/l10n/bs.json
index dad14796e..3afcedf8a 100644
--- a/l10n/bs.json
+++ b/l10n/bs.json
@@ -1261,6 +1261,34 @@
"Read receipts at the top of the dashboard show who has confirmed.": "Potvrde čitanja na vrhu kontrolne ploče pokazuju ko je potvrdio.",
"{emoji}: {count} reactions": "{emoji}: {count} reakcija",
"React with {emoji}": "Reaguj sa {emoji}",
- "{names} and {count} more": "{names} i još {count}"
+ "{names} and {count} more": "{names} i još {count}",
+ "Search people": "Pretraži osobe",
+ "Search by name, email or expertise…": "Pretraži po imenu, e-pošti ili stručnosti…",
+ "Find everyone tagged {tag}": "Pronađi sve s oznakom {tag}",
+ "LaunchPad profile": "LaunchPad profil",
+ "Colleagues see these fields on your card in the people widget and can find you by them.": "Kolege vide ova polja na vašoj kartici u widgetu osoba i mogu vas pronaći po njima.",
+ "Your profile fields could not be loaded.": "Polja vašeg profila nisu mogla biti učitana.",
+ "Not filled in": "Nije popunjeno",
+ "Filled from your organisation’s directory. You cannot change it here.": "Popunjeno iz imenika vaše organizacije. Ovdje to ne možete promijeniti.",
+ "Type a subject and press Enter": "Upišite temu i pritisnite Enter",
+ "Your profile fields are saved.": "Polja vašeg profila su spremljena.",
+ "Your profile fields could not be saved.": "Polja vašeg profila nisu mogla biti spremljena.",
+ "Profile fields": "Polja profila",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute.": "Dodatna polja na karticama kolega u widgetu osoba, poput lokacije ureda ili stručnosti. Osobe ih popunjavaju u svojim osobnim postavkama ili dolaze iz LDAP atributa.",
+ "New field": "Novo polje",
+ "Tags": "Oznake",
+ "Filled by": "Popunjava",
+ "The person": "Sama osoba",
+ "LDAP attribute": "LDAP atribut",
+ "Who may see it": "Tko ga smije vidjeti",
+ "Everyone": "Svi",
+ "People in the same group": "Osobe u istoj grupi",
+ "Searchable in the people widget": "Pretraživo u widgetu osoba",
+ "Shown on the card": "Prikazano na kartici",
+ "Remove field": "Ukloni polje",
+ "Profile fields saved.": "Polja profila su spremljena.",
+ "Save profile fields": "Spremi polja profila",
+ "The profile fields could not be loaded.": "Polja profila nisu mogla biti učitana.",
+ "The profile fields could not be saved.": "Polja profila nisu mogla biti spremljena."
}
}
diff --git a/l10n/ca.js b/l10n/ca.js
index a6dae35af..bf45de116 100644
--- a/l10n/ca.js
+++ b/l10n/ca.js
@@ -1293,7 +1293,35 @@ OC.L10N.register(
"Read receipts at the top of the dashboard show who has confirmed." : "Els justificants de lectura a dalt del tauler mostren qui ha confirmat.",
"{emoji}: {count} reactions" : "{emoji}: {count} reaccions",
"React with {emoji}" : "Reacciona amb {emoji}",
- "{names} and {count} more" : "{names} i {count} més"
+ "{names} and {count} more" : "{names} i {count} més",
+ "Search people" : "Cerca persones",
+ "Search by name, email or expertise…" : "Cerca per nom, correu o especialitat…",
+ "Find everyone tagged {tag}" : "Troba tothom amb l'etiqueta {tag}",
+ "LaunchPad profile" : "Perfil de LaunchPad",
+ "Colleagues see these fields on your card in the people widget and can find you by them." : "Els companys veuen aquests camps a la vostra targeta del giny de persones i us poden trobar per ells.",
+ "Your profile fields could not be loaded." : "No s'han pogut carregar els camps del vostre perfil.",
+ "Not filled in" : "No emplenat",
+ "Filled from your organisation’s directory. You cannot change it here." : "S'omple des del directori de la vostra organització. No ho podeu canviar aquí.",
+ "Type a subject and press Enter" : "Escriviu un tema i premeu Retorn",
+ "Your profile fields are saved." : "S'han desat els camps del vostre perfil.",
+ "Your profile fields could not be saved." : "No s'han pogut desar els camps del vostre perfil.",
+ "Profile fields" : "Camps del perfil",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute." : "Camps addicionals a les targetes dels companys al giny de persones, com ara la ubicació de l'oficina o l'especialitat. Les persones els emplenen a la seva configuració personal, o provenen d'un atribut LDAP.",
+ "New field" : "Camp nou",
+ "Tags" : "Etiquetes",
+ "Filled by" : "Ho emplena",
+ "The person" : "La persona",
+ "LDAP attribute" : "Atribut LDAP",
+ "Who may see it" : "Qui el pot veure",
+ "Everyone" : "Tothom",
+ "People in the same group" : "Persones del mateix grup",
+ "Searchable in the people widget" : "Es pot cercar al giny de persones",
+ "Shown on the card" : "Es mostra a la targeta",
+ "Remove field" : "Suprimeix el camp",
+ "Profile fields saved." : "S'han desat els camps del perfil.",
+ "Save profile fields" : "Desa els camps del perfil",
+ "The profile fields could not be loaded." : "No s'han pogut carregar els camps del perfil.",
+ "The profile fields could not be saved." : "No s'han pogut desar els camps del perfil."
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/ca.json b/l10n/ca.json
index 8ec3ce4a7..ed8885f1a 100644
--- a/l10n/ca.json
+++ b/l10n/ca.json
@@ -1260,6 +1260,34 @@
"Read receipts at the top of the dashboard show who has confirmed.": "Els justificants de lectura a dalt del tauler mostren qui ha confirmat.",
"{emoji}: {count} reactions": "{emoji}: {count} reaccions",
"React with {emoji}": "Reacciona amb {emoji}",
- "{names} and {count} more": "{names} i {count} més"
+ "{names} and {count} more": "{names} i {count} més",
+ "Search people": "Cerca persones",
+ "Search by name, email or expertise…": "Cerca per nom, correu o especialitat…",
+ "Find everyone tagged {tag}": "Troba tothom amb l'etiqueta {tag}",
+ "LaunchPad profile": "Perfil de LaunchPad",
+ "Colleagues see these fields on your card in the people widget and can find you by them.": "Els companys veuen aquests camps a la vostra targeta del giny de persones i us poden trobar per ells.",
+ "Your profile fields could not be loaded.": "No s'han pogut carregar els camps del vostre perfil.",
+ "Not filled in": "No emplenat",
+ "Filled from your organisation’s directory. You cannot change it here.": "S'omple des del directori de la vostra organització. No ho podeu canviar aquí.",
+ "Type a subject and press Enter": "Escriviu un tema i premeu Retorn",
+ "Your profile fields are saved.": "S'han desat els camps del vostre perfil.",
+ "Your profile fields could not be saved.": "No s'han pogut desar els camps del vostre perfil.",
+ "Profile fields": "Camps del perfil",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute.": "Camps addicionals a les targetes dels companys al giny de persones, com ara la ubicació de l'oficina o l'especialitat. Les persones els emplenen a la seva configuració personal, o provenen d'un atribut LDAP.",
+ "New field": "Camp nou",
+ "Tags": "Etiquetes",
+ "Filled by": "Ho emplena",
+ "The person": "La persona",
+ "LDAP attribute": "Atribut LDAP",
+ "Who may see it": "Qui el pot veure",
+ "Everyone": "Tothom",
+ "People in the same group": "Persones del mateix grup",
+ "Searchable in the people widget": "Es pot cercar al giny de persones",
+ "Shown on the card": "Es mostra a la targeta",
+ "Remove field": "Suprimeix el camp",
+ "Profile fields saved.": "S'han desat els camps del perfil.",
+ "Save profile fields": "Desa els camps del perfil",
+ "The profile fields could not be loaded.": "No s'han pogut carregar els camps del perfil.",
+ "The profile fields could not be saved.": "No s'han pogut desar els camps del perfil."
}
}
diff --git a/l10n/cs.js b/l10n/cs.js
index 4f62f348a..d11ca0b25 100644
--- a/l10n/cs.js
+++ b/l10n/cs.js
@@ -1293,7 +1293,35 @@ OC.L10N.register(
"Read receipts at the top of the dashboard show who has confirmed." : "Potvrzení o přečtení v horní části panelu ukazují, kdo potvrdil.",
"{emoji}: {count} reactions" : "{emoji}: {count} reakcí",
"React with {emoji}" : "Reagovat pomocí {emoji}",
- "{names} and {count} more" : "{names} a další ({count})"
+ "{names} and {count} more" : "{names} a další ({count})",
+ "Search people" : "Hledat osoby",
+ "Search by name, email or expertise…" : "Hledat podle jména, e-mailu nebo odbornosti…",
+ "Find everyone tagged {tag}" : "Najít všechny se štítkem {tag}",
+ "LaunchPad profile" : "Profil v LaunchPad",
+ "Colleagues see these fields on your card in the people widget and can find you by them." : "Kolegové vidí tato pole na vaší kartě ve widgetu osob a mohou vás podle nich najít.",
+ "Your profile fields could not be loaded." : "Pole vašeho profilu se nepodařilo načíst.",
+ "Not filled in" : "Nevyplněno",
+ "Filled from your organisation’s directory. You cannot change it here." : "Vyplněno z adresáře vaší organizace. Zde to nelze změnit.",
+ "Type a subject and press Enter" : "Napište téma a stiskněte Enter",
+ "Your profile fields are saved." : "Pole vašeho profilu jsou uložena.",
+ "Your profile fields could not be saved." : "Pole vašeho profilu se nepodařilo uložit.",
+ "Profile fields" : "Pole profilu",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute." : "Další pole na kartách kolegů ve widgetu osob, například umístění kanceláře nebo odbornost. Lidé je vyplňují v osobním nastavení, nebo pocházejí z atributu LDAP.",
+ "New field" : "Nové pole",
+ "Tags" : "Štítky",
+ "Filled by" : "Vyplňuje",
+ "The person" : "Sama osoba",
+ "LDAP attribute" : "Atribut LDAP",
+ "Who may see it" : "Kdo ho smí vidět",
+ "Everyone" : "Všichni",
+ "People in the same group" : "Lidé ve stejné skupině",
+ "Searchable in the people widget" : "Vyhledatelné ve widgetu osob",
+ "Shown on the card" : "Zobrazeno na kartě",
+ "Remove field" : "Odebrat pole",
+ "Profile fields saved." : "Pole profilu jsou uložena.",
+ "Save profile fields" : "Uložit pole profilu",
+ "The profile fields could not be loaded." : "Pole profilu se nepodařilo načíst.",
+ "The profile fields could not be saved." : "Pole profilu se nepodařilo uložit."
},
"nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;"
);
diff --git a/l10n/cs.json b/l10n/cs.json
index 93ba6ec8c..3f64c588c 100644
--- a/l10n/cs.json
+++ b/l10n/cs.json
@@ -1261,6 +1261,34 @@
"Read receipts at the top of the dashboard show who has confirmed.": "Potvrzení o přečtení v horní části panelu ukazují, kdo potvrdil.",
"{emoji}: {count} reactions": "{emoji}: {count} reakcí",
"React with {emoji}": "Reagovat pomocí {emoji}",
- "{names} and {count} more": "{names} a další ({count})"
+ "{names} and {count} more": "{names} a další ({count})",
+ "Search people": "Hledat osoby",
+ "Search by name, email or expertise…": "Hledat podle jména, e-mailu nebo odbornosti…",
+ "Find everyone tagged {tag}": "Najít všechny se štítkem {tag}",
+ "LaunchPad profile": "Profil v LaunchPad",
+ "Colleagues see these fields on your card in the people widget and can find you by them.": "Kolegové vidí tato pole na vaší kartě ve widgetu osob a mohou vás podle nich najít.",
+ "Your profile fields could not be loaded.": "Pole vašeho profilu se nepodařilo načíst.",
+ "Not filled in": "Nevyplněno",
+ "Filled from your organisation’s directory. You cannot change it here.": "Vyplněno z adresáře vaší organizace. Zde to nelze změnit.",
+ "Type a subject and press Enter": "Napište téma a stiskněte Enter",
+ "Your profile fields are saved.": "Pole vašeho profilu jsou uložena.",
+ "Your profile fields could not be saved.": "Pole vašeho profilu se nepodařilo uložit.",
+ "Profile fields": "Pole profilu",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute.": "Další pole na kartách kolegů ve widgetu osob, například umístění kanceláře nebo odbornost. Lidé je vyplňují v osobním nastavení, nebo pocházejí z atributu LDAP.",
+ "New field": "Nové pole",
+ "Tags": "Štítky",
+ "Filled by": "Vyplňuje",
+ "The person": "Sama osoba",
+ "LDAP attribute": "Atribut LDAP",
+ "Who may see it": "Kdo ho smí vidět",
+ "Everyone": "Všichni",
+ "People in the same group": "Lidé ve stejné skupině",
+ "Searchable in the people widget": "Vyhledatelné ve widgetu osob",
+ "Shown on the card": "Zobrazeno na kartě",
+ "Remove field": "Odebrat pole",
+ "Profile fields saved.": "Pole profilu jsou uložena.",
+ "Save profile fields": "Uložit pole profilu",
+ "The profile fields could not be loaded.": "Pole profilu se nepodařilo načíst.",
+ "The profile fields could not be saved.": "Pole profilu se nepodařilo uložit."
}
}
diff --git a/l10n/da.js b/l10n/da.js
index cc7305977..ff77c1aaa 100644
--- a/l10n/da.js
+++ b/l10n/da.js
@@ -1293,7 +1293,35 @@ OC.L10N.register(
"Read receipts at the top of the dashboard show who has confirmed." : "Læsekvitteringer øverst på dashboardet viser, hvem der har bekræftet.",
"{emoji}: {count} reactions" : "{emoji}: {count} reaktioner",
"React with {emoji}" : "Reagér med {emoji}",
- "{names} and {count} more" : "{names} og {count} til"
+ "{names} and {count} more" : "{names} og {count} til",
+ "Search people" : "Søg efter personer",
+ "Search by name, email or expertise…" : "Søg efter navn, e-mail eller ekspertise…",
+ "Find everyone tagged {tag}" : "Find alle med mærket {tag}",
+ "LaunchPad profile" : "LaunchPad-profil",
+ "Colleagues see these fields on your card in the people widget and can find you by them." : "Kolleger ser disse felter på dit kort i personwidgetten og kan finde dig ud fra dem.",
+ "Your profile fields could not be loaded." : "Dine profilfelter kunne ikke indlæses.",
+ "Not filled in" : "Ikke udfyldt",
+ "Filled from your organisation’s directory. You cannot change it here." : "Udfyldt fra din organisations katalog. Du kan ikke ændre det her.",
+ "Type a subject and press Enter" : "Skriv et emne, og tryk på Enter",
+ "Your profile fields are saved." : "Dine profilfelter er gemt.",
+ "Your profile fields could not be saved." : "Dine profilfelter kunne ikke gemmes.",
+ "Profile fields" : "Profilfelter",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute." : "Ekstra felter på kollegers kort i personwidgetten, fx kontorplacering eller ekspertise. Personerne udfylder dem i deres personlige indstillinger, eller de kommer fra en LDAP-attribut.",
+ "New field" : "Nyt felt",
+ "Tags" : "Mærker",
+ "Filled by" : "Udfyldes af",
+ "The person" : "Personen selv",
+ "LDAP attribute" : "LDAP-attribut",
+ "Who may see it" : "Hvem må se det",
+ "Everyone" : "Alle",
+ "People in the same group" : "Personer i samme gruppe",
+ "Searchable in the people widget" : "Søgbar i personwidgetten",
+ "Shown on the card" : "Vises på kortet",
+ "Remove field" : "Fjern felt",
+ "Profile fields saved." : "Profilfelterne er gemt.",
+ "Save profile fields" : "Gem profilfelter",
+ "The profile fields could not be loaded." : "Profilfelterne kunne ikke indlæses.",
+ "The profile fields could not be saved." : "Profilfelterne kunne ikke gemmes."
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/da.json b/l10n/da.json
index c95a84279..78fd8908a 100644
--- a/l10n/da.json
+++ b/l10n/da.json
@@ -1260,6 +1260,34 @@
"Read receipts at the top of the dashboard show who has confirmed.": "Læsekvitteringer øverst på dashboardet viser, hvem der har bekræftet.",
"{emoji}: {count} reactions": "{emoji}: {count} reaktioner",
"React with {emoji}": "Reagér med {emoji}",
- "{names} and {count} more": "{names} og {count} til"
+ "{names} and {count} more": "{names} og {count} til",
+ "Search people": "Søg efter personer",
+ "Search by name, email or expertise…": "Søg efter navn, e-mail eller ekspertise…",
+ "Find everyone tagged {tag}": "Find alle med mærket {tag}",
+ "LaunchPad profile": "LaunchPad-profil",
+ "Colleagues see these fields on your card in the people widget and can find you by them.": "Kolleger ser disse felter på dit kort i personwidgetten og kan finde dig ud fra dem.",
+ "Your profile fields could not be loaded.": "Dine profilfelter kunne ikke indlæses.",
+ "Not filled in": "Ikke udfyldt",
+ "Filled from your organisation’s directory. You cannot change it here.": "Udfyldt fra din organisations katalog. Du kan ikke ændre det her.",
+ "Type a subject and press Enter": "Skriv et emne, og tryk på Enter",
+ "Your profile fields are saved.": "Dine profilfelter er gemt.",
+ "Your profile fields could not be saved.": "Dine profilfelter kunne ikke gemmes.",
+ "Profile fields": "Profilfelter",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute.": "Ekstra felter på kollegers kort i personwidgetten, fx kontorplacering eller ekspertise. Personerne udfylder dem i deres personlige indstillinger, eller de kommer fra en LDAP-attribut.",
+ "New field": "Nyt felt",
+ "Tags": "Mærker",
+ "Filled by": "Udfyldes af",
+ "The person": "Personen selv",
+ "LDAP attribute": "LDAP-attribut",
+ "Who may see it": "Hvem må se det",
+ "Everyone": "Alle",
+ "People in the same group": "Personer i samme gruppe",
+ "Searchable in the people widget": "Søgbar i personwidgetten",
+ "Shown on the card": "Vises på kortet",
+ "Remove field": "Fjern felt",
+ "Profile fields saved.": "Profilfelterne er gemt.",
+ "Save profile fields": "Gem profilfelter",
+ "The profile fields could not be loaded.": "Profilfelterne kunne ikke indlæses.",
+ "The profile fields could not be saved.": "Profilfelterne kunne ikke gemmes."
}
}
diff --git a/l10n/de.js b/l10n/de.js
index f288c5f85..6063757b5 100644
--- a/l10n/de.js
+++ b/l10n/de.js
@@ -1293,7 +1293,35 @@ OC.L10N.register(
"Read receipts at the top of the dashboard show who has confirmed." : "Lesebestätigungen oben im Dashboard zeigen, wer bestätigt hat.",
"{emoji}: {count} reactions" : "{emoji}: {count} Reaktionen",
"React with {emoji}" : "Mit {emoji} reagieren",
- "{names} and {count} more" : "{names} und {count} weitere"
+ "{names} and {count} more" : "{names} und {count} weitere",
+ "Search people" : "Personen suchen",
+ "Search by name, email or expertise…" : "Nach Name, E-Mail oder Fachgebiet suchen…",
+ "Find everyone tagged {tag}" : "Alle mit dem Schlagwort {tag} finden",
+ "LaunchPad profile" : "LaunchPad-Profil",
+ "Colleagues see these fields on your card in the people widget and can find you by them." : "Kolleginnen und Kollegen sehen diese Felder auf deiner Karte im Personen-Widget und können dich darüber finden.",
+ "Your profile fields could not be loaded." : "Deine Profilfelder konnten nicht geladen werden.",
+ "Not filled in" : "Nicht ausgefüllt",
+ "Filled from your organisation’s directory. You cannot change it here." : "Wird aus dem Verzeichnis deiner Organisation übernommen. Du kannst es hier nicht ändern.",
+ "Type a subject and press Enter" : "Thema eingeben und Enter drücken",
+ "Your profile fields are saved." : "Deine Profilfelder sind gespeichert.",
+ "Your profile fields could not be saved." : "Deine Profilfelder konnten nicht gespeichert werden.",
+ "Profile fields" : "Profilfelder",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute." : "Zusätzliche Felder auf den Karten von Kolleginnen und Kollegen im Personen-Widget, etwa Bürostandort oder Fachgebiet. Personen füllen sie in ihren persönlichen Einstellungen aus, oder sie stammen aus einem LDAP-Attribut.",
+ "New field" : "Neues Feld",
+ "Tags" : "Schlagwörter",
+ "Filled by" : "Ausgefüllt von",
+ "The person" : "Die Person selbst",
+ "LDAP attribute" : "LDAP-Attribut",
+ "Who may see it" : "Wer es sehen darf",
+ "Everyone" : "Alle",
+ "People in the same group" : "Personen in derselben Gruppe",
+ "Searchable in the people widget" : "Im Personen-Widget durchsuchbar",
+ "Shown on the card" : "Auf der Karte angezeigt",
+ "Remove field" : "Feld entfernen",
+ "Profile fields saved." : "Profilfelder gespeichert.",
+ "Save profile fields" : "Profilfelder speichern",
+ "The profile fields could not be loaded." : "Die Profilfelder konnten nicht geladen werden.",
+ "The profile fields could not be saved." : "Die Profilfelder konnten nicht gespeichert werden."
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/de.json b/l10n/de.json
index 2d10ea448..5468a77de 100644
--- a/l10n/de.json
+++ b/l10n/de.json
@@ -1260,6 +1260,34 @@
"Read receipts at the top of the dashboard show who has confirmed.": "Lesebestätigungen oben im Dashboard zeigen, wer bestätigt hat.",
"{emoji}: {count} reactions": "{emoji}: {count} Reaktionen",
"React with {emoji}": "Mit {emoji} reagieren",
- "{names} and {count} more": "{names} und {count} weitere"
+ "{names} and {count} more": "{names} und {count} weitere",
+ "Search people": "Personen suchen",
+ "Search by name, email or expertise…": "Nach Name, E-Mail oder Fachgebiet suchen…",
+ "Find everyone tagged {tag}": "Alle mit dem Schlagwort {tag} finden",
+ "LaunchPad profile": "LaunchPad-Profil",
+ "Colleagues see these fields on your card in the people widget and can find you by them.": "Kolleginnen und Kollegen sehen diese Felder auf deiner Karte im Personen-Widget und können dich darüber finden.",
+ "Your profile fields could not be loaded.": "Deine Profilfelder konnten nicht geladen werden.",
+ "Not filled in": "Nicht ausgefüllt",
+ "Filled from your organisation’s directory. You cannot change it here.": "Wird aus dem Verzeichnis deiner Organisation übernommen. Du kannst es hier nicht ändern.",
+ "Type a subject and press Enter": "Thema eingeben und Enter drücken",
+ "Your profile fields are saved.": "Deine Profilfelder sind gespeichert.",
+ "Your profile fields could not be saved.": "Deine Profilfelder konnten nicht gespeichert werden.",
+ "Profile fields": "Profilfelder",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute.": "Zusätzliche Felder auf den Karten von Kolleginnen und Kollegen im Personen-Widget, etwa Bürostandort oder Fachgebiet. Personen füllen sie in ihren persönlichen Einstellungen aus, oder sie stammen aus einem LDAP-Attribut.",
+ "New field": "Neues Feld",
+ "Tags": "Schlagwörter",
+ "Filled by": "Ausgefüllt von",
+ "The person": "Die Person selbst",
+ "LDAP attribute": "LDAP-Attribut",
+ "Who may see it": "Wer es sehen darf",
+ "Everyone": "Alle",
+ "People in the same group": "Personen in derselben Gruppe",
+ "Searchable in the people widget": "Im Personen-Widget durchsuchbar",
+ "Shown on the card": "Auf der Karte angezeigt",
+ "Remove field": "Feld entfernen",
+ "Profile fields saved.": "Profilfelder gespeichert.",
+ "Save profile fields": "Profilfelder speichern",
+ "The profile fields could not be loaded.": "Die Profilfelder konnten nicht geladen werden.",
+ "The profile fields could not be saved.": "Die Profilfelder konnten nicht gespeichert werden."
}
}
diff --git a/l10n/el.js b/l10n/el.js
index 7f0b2c758..4fe15f6bc 100644
--- a/l10n/el.js
+++ b/l10n/el.js
@@ -1293,7 +1293,35 @@ OC.L10N.register(
"Read receipts at the top of the dashboard show who has confirmed." : "Οι αποδείξεις ανάγνωσης στο πάνω μέρος του πίνακα δείχνουν ποιος επιβεβαίωσε.",
"{emoji}: {count} reactions" : "{emoji}: {count} αντιδράσεις",
"React with {emoji}" : "Αντίδραση με {emoji}",
- "{names} and {count} more" : "{names} και {count} ακόμη"
+ "{names} and {count} more" : "{names} και {count} ακόμη",
+ "Search people" : "Αναζήτηση ατόμων",
+ "Search by name, email or expertise…" : "Αναζήτηση με όνομα, email ή ειδίκευση…",
+ "Find everyone tagged {tag}" : "Βρες όλους με την ετικέτα {tag}",
+ "LaunchPad profile" : "Προφίλ LaunchPad",
+ "Colleagues see these fields on your card in the people widget and can find you by them." : "Οι συνάδελφοι βλέπουν αυτά τα πεδία στην κάρτα σας στο γραφικό στοιχείο ατόμων και μπορούν να σας βρουν μέσω αυτών.",
+ "Your profile fields could not be loaded." : "Δεν ήταν δυνατή η φόρτωση των πεδίων του προφίλ σας.",
+ "Not filled in" : "Δεν έχει συμπληρωθεί",
+ "Filled from your organisation’s directory. You cannot change it here." : "Συμπληρώνεται από τον κατάλογο του οργανισμού σας. Δεν μπορείτε να το αλλάξετε εδώ.",
+ "Type a subject and press Enter" : "Πληκτρολογήστε ένα θέμα και πατήστε Enter",
+ "Your profile fields are saved." : "Τα πεδία του προφίλ σας αποθηκεύτηκαν.",
+ "Your profile fields could not be saved." : "Δεν ήταν δυνατή η αποθήκευση των πεδίων του προφίλ σας.",
+ "Profile fields" : "Πεδία προφίλ",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute." : "Επιπλέον πεδία στις κάρτες συναδέλφων στο γραφικό στοιχείο ατόμων, όπως τοποθεσία γραφείου ή ειδίκευση. Τα άτομα τα συμπληρώνουν στις προσωπικές τους ρυθμίσεις ή προέρχονται από ένα χαρακτηριστικό LDAP.",
+ "New field" : "Νέο πεδίο",
+ "Tags" : "Ετικέτες",
+ "Filled by" : "Συμπληρώνεται από",
+ "The person" : "Το ίδιο το άτομο",
+ "LDAP attribute" : "Χαρακτηριστικό LDAP",
+ "Who may see it" : "Ποιος μπορεί να το δει",
+ "Everyone" : "Όλοι",
+ "People in the same group" : "Άτομα στην ίδια ομάδα",
+ "Searchable in the people widget" : "Με δυνατότητα αναζήτησης στο γραφικό στοιχείο ατόμων",
+ "Shown on the card" : "Εμφανίζεται στην κάρτα",
+ "Remove field" : "Αφαίρεση πεδίου",
+ "Profile fields saved." : "Τα πεδία προφίλ αποθηκεύτηκαν.",
+ "Save profile fields" : "Αποθήκευση πεδίων προφίλ",
+ "The profile fields could not be loaded." : "Δεν ήταν δυνατή η φόρτωση των πεδίων προφίλ.",
+ "The profile fields could not be saved." : "Δεν ήταν δυνατή η αποθήκευση των πεδίων προφίλ."
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/el.json b/l10n/el.json
index b61edabfc..c4aaf0219 100644
--- a/l10n/el.json
+++ b/l10n/el.json
@@ -1260,6 +1260,34 @@
"Read receipts at the top of the dashboard show who has confirmed.": "Οι αποδείξεις ανάγνωσης στο πάνω μέρος του πίνακα δείχνουν ποιος επιβεβαίωσε.",
"{emoji}: {count} reactions": "{emoji}: {count} αντιδράσεις",
"React with {emoji}": "Αντίδραση με {emoji}",
- "{names} and {count} more": "{names} και {count} ακόμη"
+ "{names} and {count} more": "{names} και {count} ακόμη",
+ "Search people": "Αναζήτηση ατόμων",
+ "Search by name, email or expertise…": "Αναζήτηση με όνομα, email ή ειδίκευση…",
+ "Find everyone tagged {tag}": "Βρες όλους με την ετικέτα {tag}",
+ "LaunchPad profile": "Προφίλ LaunchPad",
+ "Colleagues see these fields on your card in the people widget and can find you by them.": "Οι συνάδελφοι βλέπουν αυτά τα πεδία στην κάρτα σας στο γραφικό στοιχείο ατόμων και μπορούν να σας βρουν μέσω αυτών.",
+ "Your profile fields could not be loaded.": "Δεν ήταν δυνατή η φόρτωση των πεδίων του προφίλ σας.",
+ "Not filled in": "Δεν έχει συμπληρωθεί",
+ "Filled from your organisation’s directory. You cannot change it here.": "Συμπληρώνεται από τον κατάλογο του οργανισμού σας. Δεν μπορείτε να το αλλάξετε εδώ.",
+ "Type a subject and press Enter": "Πληκτρολογήστε ένα θέμα και πατήστε Enter",
+ "Your profile fields are saved.": "Τα πεδία του προφίλ σας αποθηκεύτηκαν.",
+ "Your profile fields could not be saved.": "Δεν ήταν δυνατή η αποθήκευση των πεδίων του προφίλ σας.",
+ "Profile fields": "Πεδία προφίλ",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute.": "Επιπλέον πεδία στις κάρτες συναδέλφων στο γραφικό στοιχείο ατόμων, όπως τοποθεσία γραφείου ή ειδίκευση. Τα άτομα τα συμπληρώνουν στις προσωπικές τους ρυθμίσεις ή προέρχονται από ένα χαρακτηριστικό LDAP.",
+ "New field": "Νέο πεδίο",
+ "Tags": "Ετικέτες",
+ "Filled by": "Συμπληρώνεται από",
+ "The person": "Το ίδιο το άτομο",
+ "LDAP attribute": "Χαρακτηριστικό LDAP",
+ "Who may see it": "Ποιος μπορεί να το δει",
+ "Everyone": "Όλοι",
+ "People in the same group": "Άτομα στην ίδια ομάδα",
+ "Searchable in the people widget": "Με δυνατότητα αναζήτησης στο γραφικό στοιχείο ατόμων",
+ "Shown on the card": "Εμφανίζεται στην κάρτα",
+ "Remove field": "Αφαίρεση πεδίου",
+ "Profile fields saved.": "Τα πεδία προφίλ αποθηκεύτηκαν.",
+ "Save profile fields": "Αποθήκευση πεδίων προφίλ",
+ "The profile fields could not be loaded.": "Δεν ήταν δυνατή η φόρτωση των πεδίων προφίλ.",
+ "The profile fields could not be saved.": "Δεν ήταν δυνατή η αποθήκευση των πεδίων προφίλ."
}
}
diff --git a/l10n/en.js b/l10n/en.js
index 37ec679c0..c86a4c71c 100644
--- a/l10n/en.js
+++ b/l10n/en.js
@@ -1273,7 +1273,35 @@ OC.L10N.register(
"Read receipts at the top of the dashboard show who has confirmed." : "Read receipts at the top of the dashboard show who has confirmed.",
"{emoji}: {count} reactions" : "{emoji}: {count} reactions",
"React with {emoji}" : "React with {emoji}",
- "{names} and {count} more" : "{names} and {count} more"
+ "{names} and {count} more" : "{names} and {count} more",
+ "Search people" : "Search people",
+ "Search by name, email or expertise…" : "Search by name, email or expertise…",
+ "Find everyone tagged {tag}" : "Find everyone tagged {tag}",
+ "LaunchPad profile" : "LaunchPad profile",
+ "Colleagues see these fields on your card in the people widget and can find you by them." : "Colleagues see these fields on your card in the people widget and can find you by them.",
+ "Your profile fields could not be loaded." : "Your profile fields could not be loaded.",
+ "Not filled in" : "Not filled in",
+ "Filled from your organisation’s directory. You cannot change it here." : "Filled from your organisation’s directory. You cannot change it here.",
+ "Type a subject and press Enter" : "Type a subject and press Enter",
+ "Your profile fields are saved." : "Your profile fields are saved.",
+ "Your profile fields could not be saved." : "Your profile fields could not be saved.",
+ "Profile fields" : "Profile fields",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute." : "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute.",
+ "New field" : "New field",
+ "Tags" : "Tags",
+ "Filled by" : "Filled by",
+ "The person" : "The person",
+ "LDAP attribute" : "LDAP attribute",
+ "Who may see it" : "Who may see it",
+ "Everyone" : "Everyone",
+ "People in the same group" : "People in the same group",
+ "Searchable in the people widget" : "Searchable in the people widget",
+ "Shown on the card" : "Shown on the card",
+ "Remove field" : "Remove field",
+ "Profile fields saved." : "Profile fields saved.",
+ "Save profile fields" : "Save profile fields",
+ "The profile fields could not be loaded." : "The profile fields could not be loaded.",
+ "The profile fields could not be saved." : "The profile fields could not be saved."
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/en.json b/l10n/en.json
index 98283e8f2..50a16c214 100644
--- a/l10n/en.json
+++ b/l10n/en.json
@@ -1238,6 +1238,34 @@
"Read receipts at the top of the dashboard show who has confirmed.": "Read receipts at the top of the dashboard show who has confirmed.",
"{emoji}: {count} reactions": "{emoji}: {count} reactions",
"React with {emoji}": "React with {emoji}",
- "{names} and {count} more": "{names} and {count} more"
+ "{names} and {count} more": "{names} and {count} more",
+ "Search people": "Search people",
+ "Search by name, email or expertise…": "Search by name, email or expertise…",
+ "Find everyone tagged {tag}": "Find everyone tagged {tag}",
+ "LaunchPad profile": "LaunchPad profile",
+ "Colleagues see these fields on your card in the people widget and can find you by them.": "Colleagues see these fields on your card in the people widget and can find you by them.",
+ "Your profile fields could not be loaded.": "Your profile fields could not be loaded.",
+ "Not filled in": "Not filled in",
+ "Filled from your organisation’s directory. You cannot change it here.": "Filled from your organisation’s directory. You cannot change it here.",
+ "Type a subject and press Enter": "Type a subject and press Enter",
+ "Your profile fields are saved.": "Your profile fields are saved.",
+ "Your profile fields could not be saved.": "Your profile fields could not be saved.",
+ "Profile fields": "Profile fields",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute.": "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute.",
+ "New field": "New field",
+ "Tags": "Tags",
+ "Filled by": "Filled by",
+ "The person": "The person",
+ "LDAP attribute": "LDAP attribute",
+ "Who may see it": "Who may see it",
+ "Everyone": "Everyone",
+ "People in the same group": "People in the same group",
+ "Searchable in the people widget": "Searchable in the people widget",
+ "Shown on the card": "Shown on the card",
+ "Remove field": "Remove field",
+ "Profile fields saved.": "Profile fields saved.",
+ "Save profile fields": "Save profile fields",
+ "The profile fields could not be loaded.": "The profile fields could not be loaded.",
+ "The profile fields could not be saved.": "The profile fields could not be saved."
}
}
diff --git a/l10n/es.js b/l10n/es.js
index 9cf89896c..3c4155627 100644
--- a/l10n/es.js
+++ b/l10n/es.js
@@ -1293,7 +1293,35 @@ OC.L10N.register(
"Read receipts at the top of the dashboard show who has confirmed." : "Los acuses de lectura en la parte superior del panel muestran quién ha confirmado.",
"{emoji}: {count} reactions" : "{emoji}: {count} reacciones",
"React with {emoji}" : "Reaccionar con {emoji}",
- "{names} and {count} more" : "{names} y {count} más"
+ "{names} and {count} more" : "{names} y {count} más",
+ "Search people" : "Buscar personas",
+ "Search by name, email or expertise…" : "Buscar por nombre, correo o especialidad…",
+ "Find everyone tagged {tag}" : "Buscar a todos con la etiqueta {tag}",
+ "LaunchPad profile" : "Perfil de LaunchPad",
+ "Colleagues see these fields on your card in the people widget and can find you by them." : "Tus compañeros ven estos campos en tu tarjeta del widget de personas y pueden encontrarte por ellos.",
+ "Your profile fields could not be loaded." : "No se pudieron cargar los campos de tu perfil.",
+ "Not filled in" : "Sin rellenar",
+ "Filled from your organisation’s directory. You cannot change it here." : "Se rellena desde el directorio de tu organización. No puedes cambiarlo aquí.",
+ "Type a subject and press Enter" : "Escribe un tema y pulsa Intro",
+ "Your profile fields are saved." : "Los campos de tu perfil se han guardado.",
+ "Your profile fields could not be saved." : "No se pudieron guardar los campos de tu perfil.",
+ "Profile fields" : "Campos del perfil",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute." : "Campos adicionales en las tarjetas de los compañeros del widget de personas, como la ubicación de la oficina o la especialidad. Las personas los rellenan en su configuración personal o proceden de un atributo LDAP.",
+ "New field" : "Campo nuevo",
+ "Tags" : "Etiquetas",
+ "Filled by" : "Lo rellena",
+ "The person" : "La propia persona",
+ "LDAP attribute" : "Atributo LDAP",
+ "Who may see it" : "Quién puede verlo",
+ "Everyone" : "Todos",
+ "People in the same group" : "Personas del mismo grupo",
+ "Searchable in the people widget" : "Se puede buscar en el widget de personas",
+ "Shown on the card" : "Se muestra en la tarjeta",
+ "Remove field" : "Quitar campo",
+ "Profile fields saved." : "Campos del perfil guardados.",
+ "Save profile fields" : "Guardar campos del perfil",
+ "The profile fields could not be loaded." : "No se pudieron cargar los campos del perfil.",
+ "The profile fields could not be saved." : "No se pudieron guardar los campos del perfil."
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/es.json b/l10n/es.json
index 1e79f347d..b5bb51d96 100644
--- a/l10n/es.json
+++ b/l10n/es.json
@@ -1260,6 +1260,34 @@
"Read receipts at the top of the dashboard show who has confirmed.": "Los acuses de lectura en la parte superior del panel muestran quién ha confirmado.",
"{emoji}: {count} reactions": "{emoji}: {count} reacciones",
"React with {emoji}": "Reaccionar con {emoji}",
- "{names} and {count} more": "{names} y {count} más"
+ "{names} and {count} more": "{names} y {count} más",
+ "Search people": "Buscar personas",
+ "Search by name, email or expertise…": "Buscar por nombre, correo o especialidad…",
+ "Find everyone tagged {tag}": "Buscar a todos con la etiqueta {tag}",
+ "LaunchPad profile": "Perfil de LaunchPad",
+ "Colleagues see these fields on your card in the people widget and can find you by them.": "Tus compañeros ven estos campos en tu tarjeta del widget de personas y pueden encontrarte por ellos.",
+ "Your profile fields could not be loaded.": "No se pudieron cargar los campos de tu perfil.",
+ "Not filled in": "Sin rellenar",
+ "Filled from your organisation’s directory. You cannot change it here.": "Se rellena desde el directorio de tu organización. No puedes cambiarlo aquí.",
+ "Type a subject and press Enter": "Escribe un tema y pulsa Intro",
+ "Your profile fields are saved.": "Los campos de tu perfil se han guardado.",
+ "Your profile fields could not be saved.": "No se pudieron guardar los campos de tu perfil.",
+ "Profile fields": "Campos del perfil",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute.": "Campos adicionales en las tarjetas de los compañeros del widget de personas, como la ubicación de la oficina o la especialidad. Las personas los rellenan en su configuración personal o proceden de un atributo LDAP.",
+ "New field": "Campo nuevo",
+ "Tags": "Etiquetas",
+ "Filled by": "Lo rellena",
+ "The person": "La propia persona",
+ "LDAP attribute": "Atributo LDAP",
+ "Who may see it": "Quién puede verlo",
+ "Everyone": "Todos",
+ "People in the same group": "Personas del mismo grupo",
+ "Searchable in the people widget": "Se puede buscar en el widget de personas",
+ "Shown on the card": "Se muestra en la tarjeta",
+ "Remove field": "Quitar campo",
+ "Profile fields saved.": "Campos del perfil guardados.",
+ "Save profile fields": "Guardar campos del perfil",
+ "The profile fields could not be loaded.": "No se pudieron cargar los campos del perfil.",
+ "The profile fields could not be saved.": "No se pudieron guardar los campos del perfil."
}
}
diff --git a/l10n/et.js b/l10n/et.js
index c0a83f748..176dbb655 100644
--- a/l10n/et.js
+++ b/l10n/et.js
@@ -1293,7 +1293,35 @@ OC.L10N.register(
"Read receipts at the top of the dashboard show who has confirmed." : "Töölaua ülaosas olevad lugemiskinnitused näitavad, kes on kinnitanud.",
"{emoji}: {count} reactions" : "{emoji}: {count} reaktsiooni",
"React with {emoji}" : "Reageeri: {emoji}",
- "{names} and {count} more" : "{names} ja veel {count}"
+ "{names} and {count} more" : "{names} ja veel {count}",
+ "Search people" : "Otsi inimesi",
+ "Search by name, email or expertise…" : "Otsi nime, e-posti või asjatundlikkuse järgi…",
+ "Find everyone tagged {tag}" : "Leia kõik sildiga {tag}",
+ "LaunchPad profile" : "LaunchPadi profiil",
+ "Colleagues see these fields on your card in the people widget and can find you by them." : "Kolleegid näevad neid välju sinu kaardil inimeste vidinas ja saavad sind nende järgi leida.",
+ "Your profile fields could not be loaded." : "Sinu profiili välju ei õnnestunud laadida.",
+ "Not filled in" : "Täitmata",
+ "Filled from your organisation’s directory. You cannot change it here." : "Täidetud sinu organisatsiooni kataloogist. Siin seda muuta ei saa.",
+ "Type a subject and press Enter" : "Sisesta teema ja vajuta Enter",
+ "Your profile fields are saved." : "Sinu profiili väljad on salvestatud.",
+ "Your profile fields could not be saved." : "Sinu profiili välju ei õnnestunud salvestada.",
+ "Profile fields" : "Profiili väljad",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute." : "Lisaväljad kolleegide kaartidel inimeste vidinas, näiteks kontori asukoht või asjatundlikkus. Inimesed täidavad need oma isiklikes seadetes või tulevad need LDAP-atribuudist.",
+ "New field" : "Uus väli",
+ "Tags" : "Sildid",
+ "Filled by" : "Täidab",
+ "The person" : "Inimene ise",
+ "LDAP attribute" : "LDAP-atribuut",
+ "Who may see it" : "Kes võib seda näha",
+ "Everyone" : "Kõik",
+ "People in the same group" : "Samas grupis olevad inimesed",
+ "Searchable in the people widget" : "Otsitav inimeste vidinas",
+ "Shown on the card" : "Näidatakse kaardil",
+ "Remove field" : "Eemalda väli",
+ "Profile fields saved." : "Profiili väljad salvestatud.",
+ "Save profile fields" : "Salvesta profiili väljad",
+ "The profile fields could not be loaded." : "Profiili välju ei õnnestunud laadida.",
+ "The profile fields could not be saved." : "Profiili välju ei õnnestunud salvestada."
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/et.json b/l10n/et.json
index 11f3fba3b..856cf2a6a 100644
--- a/l10n/et.json
+++ b/l10n/et.json
@@ -1260,6 +1260,34 @@
"Read receipts at the top of the dashboard show who has confirmed.": "Töölaua ülaosas olevad lugemiskinnitused näitavad, kes on kinnitanud.",
"{emoji}: {count} reactions": "{emoji}: {count} reaktsiooni",
"React with {emoji}": "Reageeri: {emoji}",
- "{names} and {count} more": "{names} ja veel {count}"
+ "{names} and {count} more": "{names} ja veel {count}",
+ "Search people": "Otsi inimesi",
+ "Search by name, email or expertise…": "Otsi nime, e-posti või asjatundlikkuse järgi…",
+ "Find everyone tagged {tag}": "Leia kõik sildiga {tag}",
+ "LaunchPad profile": "LaunchPadi profiil",
+ "Colleagues see these fields on your card in the people widget and can find you by them.": "Kolleegid näevad neid välju sinu kaardil inimeste vidinas ja saavad sind nende järgi leida.",
+ "Your profile fields could not be loaded.": "Sinu profiili välju ei õnnestunud laadida.",
+ "Not filled in": "Täitmata",
+ "Filled from your organisation’s directory. You cannot change it here.": "Täidetud sinu organisatsiooni kataloogist. Siin seda muuta ei saa.",
+ "Type a subject and press Enter": "Sisesta teema ja vajuta Enter",
+ "Your profile fields are saved.": "Sinu profiili väljad on salvestatud.",
+ "Your profile fields could not be saved.": "Sinu profiili välju ei õnnestunud salvestada.",
+ "Profile fields": "Profiili väljad",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute.": "Lisaväljad kolleegide kaartidel inimeste vidinas, näiteks kontori asukoht või asjatundlikkus. Inimesed täidavad need oma isiklikes seadetes või tulevad need LDAP-atribuudist.",
+ "New field": "Uus väli",
+ "Tags": "Sildid",
+ "Filled by": "Täidab",
+ "The person": "Inimene ise",
+ "LDAP attribute": "LDAP-atribuut",
+ "Who may see it": "Kes võib seda näha",
+ "Everyone": "Kõik",
+ "People in the same group": "Samas grupis olevad inimesed",
+ "Searchable in the people widget": "Otsitav inimeste vidinas",
+ "Shown on the card": "Näidatakse kaardil",
+ "Remove field": "Eemalda väli",
+ "Profile fields saved.": "Profiili väljad salvestatud.",
+ "Save profile fields": "Salvesta profiili väljad",
+ "The profile fields could not be loaded.": "Profiili välju ei õnnestunud laadida.",
+ "The profile fields could not be saved.": "Profiili välju ei õnnestunud salvestada."
}
}
diff --git a/l10n/fi.js b/l10n/fi.js
index 9c99b20e5..cddec8a91 100644
--- a/l10n/fi.js
+++ b/l10n/fi.js
@@ -1293,7 +1293,35 @@ OC.L10N.register(
"Read receipts at the top of the dashboard show who has confirmed." : "Koontinäytön yläosan lukukuittaukset näyttävät, kuka on vahvistanut.",
"{emoji}: {count} reactions" : "{emoji}: {count} reaktiota",
"React with {emoji}" : "Reagoi: {emoji}",
- "{names} and {count} more" : "{names} ja {count} muuta"
+ "{names} and {count} more" : "{names} ja {count} muuta",
+ "Search people" : "Etsi henkilöitä",
+ "Search by name, email or expertise…" : "Hae nimellä, sähköpostilla tai osaamisella…",
+ "Find everyone tagged {tag}" : "Etsi kaikki, joilla on tunniste {tag}",
+ "LaunchPad profile" : "LaunchPad-profiili",
+ "Colleagues see these fields on your card in the people widget and can find you by them." : "Kollegat näkevät nämä kentät kortillasi henkilöt-pienoisohjelmassa ja voivat löytää sinut niiden avulla.",
+ "Your profile fields could not be loaded." : "Profiilikenttiäsi ei voitu ladata.",
+ "Not filled in" : "Ei täytetty",
+ "Filled from your organisation’s directory. You cannot change it here." : "Täytetty organisaatiosi hakemistosta. Et voi muuttaa sitä täällä.",
+ "Type a subject and press Enter" : "Kirjoita aihe ja paina Enter",
+ "Your profile fields are saved." : "Profiilikenttäsi on tallennettu.",
+ "Your profile fields could not be saved." : "Profiilikenttiäsi ei voitu tallentaa.",
+ "Profile fields" : "Profiilikentät",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute." : "Lisäkentät kollegoiden korteissa henkilöt-pienoisohjelmassa, kuten toimiston sijainti tai osaaminen. Henkilöt täyttävät ne omissa asetuksissaan, tai ne tulevat LDAP-attribuutista.",
+ "New field" : "Uusi kenttä",
+ "Tags" : "Tunnisteet",
+ "Filled by" : "Täyttäjä",
+ "The person" : "Henkilö itse",
+ "LDAP attribute" : "LDAP-attribuutti",
+ "Who may see it" : "Kuka saa nähdä sen",
+ "Everyone" : "Kaikki",
+ "People in the same group" : "Saman ryhmän henkilöt",
+ "Searchable in the people widget" : "Haettavissa henkilöt-pienoisohjelmassa",
+ "Shown on the card" : "Näytetään kortilla",
+ "Remove field" : "Poista kenttä",
+ "Profile fields saved." : "Profiilikentät tallennettu.",
+ "Save profile fields" : "Tallenna profiilikentät",
+ "The profile fields could not be loaded." : "Profiilikenttiä ei voitu ladata.",
+ "The profile fields could not be saved." : "Profiilikenttiä ei voitu tallentaa."
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/fi.json b/l10n/fi.json
index 399376115..943c32a29 100644
--- a/l10n/fi.json
+++ b/l10n/fi.json
@@ -1260,6 +1260,34 @@
"Read receipts at the top of the dashboard show who has confirmed.": "Koontinäytön yläosan lukukuittaukset näyttävät, kuka on vahvistanut.",
"{emoji}: {count} reactions": "{emoji}: {count} reaktiota",
"React with {emoji}": "Reagoi: {emoji}",
- "{names} and {count} more": "{names} ja {count} muuta"
+ "{names} and {count} more": "{names} ja {count} muuta",
+ "Search people": "Etsi henkilöitä",
+ "Search by name, email or expertise…": "Hae nimellä, sähköpostilla tai osaamisella…",
+ "Find everyone tagged {tag}": "Etsi kaikki, joilla on tunniste {tag}",
+ "LaunchPad profile": "LaunchPad-profiili",
+ "Colleagues see these fields on your card in the people widget and can find you by them.": "Kollegat näkevät nämä kentät kortillasi henkilöt-pienoisohjelmassa ja voivat löytää sinut niiden avulla.",
+ "Your profile fields could not be loaded.": "Profiilikenttiäsi ei voitu ladata.",
+ "Not filled in": "Ei täytetty",
+ "Filled from your organisation’s directory. You cannot change it here.": "Täytetty organisaatiosi hakemistosta. Et voi muuttaa sitä täällä.",
+ "Type a subject and press Enter": "Kirjoita aihe ja paina Enter",
+ "Your profile fields are saved.": "Profiilikenttäsi on tallennettu.",
+ "Your profile fields could not be saved.": "Profiilikenttiäsi ei voitu tallentaa.",
+ "Profile fields": "Profiilikentät",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute.": "Lisäkentät kollegoiden korteissa henkilöt-pienoisohjelmassa, kuten toimiston sijainti tai osaaminen. Henkilöt täyttävät ne omissa asetuksissaan, tai ne tulevat LDAP-attribuutista.",
+ "New field": "Uusi kenttä",
+ "Tags": "Tunnisteet",
+ "Filled by": "Täyttäjä",
+ "The person": "Henkilö itse",
+ "LDAP attribute": "LDAP-attribuutti",
+ "Who may see it": "Kuka saa nähdä sen",
+ "Everyone": "Kaikki",
+ "People in the same group": "Saman ryhmän henkilöt",
+ "Searchable in the people widget": "Haettavissa henkilöt-pienoisohjelmassa",
+ "Shown on the card": "Näytetään kortilla",
+ "Remove field": "Poista kenttä",
+ "Profile fields saved.": "Profiilikentät tallennettu.",
+ "Save profile fields": "Tallenna profiilikentät",
+ "The profile fields could not be loaded.": "Profiilikenttiä ei voitu ladata.",
+ "The profile fields could not be saved.": "Profiilikenttiä ei voitu tallentaa."
}
}
diff --git a/l10n/fr.js b/l10n/fr.js
index e18e7f107..307678377 100644
--- a/l10n/fr.js
+++ b/l10n/fr.js
@@ -1293,7 +1293,35 @@ OC.L10N.register(
"Read receipts at the top of the dashboard show who has confirmed." : "Les accusés de lecture en haut du tableau de bord montrent qui a confirmé.",
"{emoji}: {count} reactions" : "{emoji} : {count} réactions",
"React with {emoji}" : "Réagir avec {emoji}",
- "{names} and {count} more" : "{names} et {count} autres"
+ "{names} and {count} more" : "{names} et {count} autres",
+ "Search people" : "Rechercher des personnes",
+ "Search by name, email or expertise…" : "Rechercher par nom, e-mail ou expertise…",
+ "Find everyone tagged {tag}" : "Trouver toutes les personnes avec l'étiquette {tag}",
+ "LaunchPad profile" : "Profil LaunchPad",
+ "Colleagues see these fields on your card in the people widget and can find you by them." : "Vos collègues voient ces champs sur votre carte dans le widget Personnes et peuvent vous trouver grâce à eux.",
+ "Your profile fields could not be loaded." : "Les champs de votre profil n'ont pas pu être chargés.",
+ "Not filled in" : "Non renseigné",
+ "Filled from your organisation’s directory. You cannot change it here." : "Rempli depuis l'annuaire de votre organisation. Vous ne pouvez pas le modifier ici.",
+ "Type a subject and press Enter" : "Saisissez un sujet et appuyez sur Entrée",
+ "Your profile fields are saved." : "Les champs de votre profil sont enregistrés.",
+ "Your profile fields could not be saved." : "Les champs de votre profil n'ont pas pu être enregistrés.",
+ "Profile fields" : "Champs du profil",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute." : "Champs supplémentaires sur les cartes des collègues dans le widget Personnes, comme le lieu de travail ou l'expertise. Les personnes les remplissent dans leurs paramètres personnels, ou ils proviennent d'un attribut LDAP.",
+ "New field" : "Nouveau champ",
+ "Tags" : "Étiquettes",
+ "Filled by" : "Rempli par",
+ "The person" : "La personne",
+ "LDAP attribute" : "Attribut LDAP",
+ "Who may see it" : "Qui peut le voir",
+ "Everyone" : "Tout le monde",
+ "People in the same group" : "Les personnes du même groupe",
+ "Searchable in the people widget" : "Recherchable dans le widget Personnes",
+ "Shown on the card" : "Affiché sur la carte",
+ "Remove field" : "Supprimer le champ",
+ "Profile fields saved." : "Champs du profil enregistrés.",
+ "Save profile fields" : "Enregistrer les champs du profil",
+ "The profile fields could not be loaded." : "Les champs du profil n'ont pas pu être chargés.",
+ "The profile fields could not be saved." : "Les champs du profil n'ont pas pu être enregistrés."
},
"nplurals=2; plural=(n > 1);"
);
diff --git a/l10n/fr.json b/l10n/fr.json
index 31c837f66..3c6046a8f 100644
--- a/l10n/fr.json
+++ b/l10n/fr.json
@@ -1260,6 +1260,34 @@
"Read receipts at the top of the dashboard show who has confirmed.": "Les accusés de lecture en haut du tableau de bord montrent qui a confirmé.",
"{emoji}: {count} reactions": "{emoji} : {count} réactions",
"React with {emoji}": "Réagir avec {emoji}",
- "{names} and {count} more": "{names} et {count} autres"
+ "{names} and {count} more": "{names} et {count} autres",
+ "Search people": "Rechercher des personnes",
+ "Search by name, email or expertise…": "Rechercher par nom, e-mail ou expertise…",
+ "Find everyone tagged {tag}": "Trouver toutes les personnes avec l'étiquette {tag}",
+ "LaunchPad profile": "Profil LaunchPad",
+ "Colleagues see these fields on your card in the people widget and can find you by them.": "Vos collègues voient ces champs sur votre carte dans le widget Personnes et peuvent vous trouver grâce à eux.",
+ "Your profile fields could not be loaded.": "Les champs de votre profil n'ont pas pu être chargés.",
+ "Not filled in": "Non renseigné",
+ "Filled from your organisation’s directory. You cannot change it here.": "Rempli depuis l'annuaire de votre organisation. Vous ne pouvez pas le modifier ici.",
+ "Type a subject and press Enter": "Saisissez un sujet et appuyez sur Entrée",
+ "Your profile fields are saved.": "Les champs de votre profil sont enregistrés.",
+ "Your profile fields could not be saved.": "Les champs de votre profil n'ont pas pu être enregistrés.",
+ "Profile fields": "Champs du profil",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute.": "Champs supplémentaires sur les cartes des collègues dans le widget Personnes, comme le lieu de travail ou l'expertise. Les personnes les remplissent dans leurs paramètres personnels, ou ils proviennent d'un attribut LDAP.",
+ "New field": "Nouveau champ",
+ "Tags": "Étiquettes",
+ "Filled by": "Rempli par",
+ "The person": "La personne",
+ "LDAP attribute": "Attribut LDAP",
+ "Who may see it": "Qui peut le voir",
+ "Everyone": "Tout le monde",
+ "People in the same group": "Les personnes du même groupe",
+ "Searchable in the people widget": "Recherchable dans le widget Personnes",
+ "Shown on the card": "Affiché sur la carte",
+ "Remove field": "Supprimer le champ",
+ "Profile fields saved.": "Champs du profil enregistrés.",
+ "Save profile fields": "Enregistrer les champs du profil",
+ "The profile fields could not be loaded.": "Les champs du profil n'ont pas pu être chargés.",
+ "The profile fields could not be saved.": "Les champs du profil n'ont pas pu être enregistrés."
}
}
diff --git a/l10n/ga.js b/l10n/ga.js
index 8f9f55d31..8ba1d1216 100644
--- a/l10n/ga.js
+++ b/l10n/ga.js
@@ -1293,7 +1293,35 @@ OC.L10N.register(
"Read receipts at the top of the dashboard show who has confirmed." : "Taispeánann admhálacha léite ag barr an deais cé a dheimhnigh.",
"{emoji}: {count} reactions" : "{emoji}: {count} imoibriú",
"React with {emoji}" : "Freagair le {emoji}",
- "{names} and {count} more" : "{names} agus {count} eile"
+ "{names} and {count} more" : "{names} agus {count} eile",
+ "Search people" : "Cuardaigh daoine",
+ "Search by name, email or expertise…" : "Cuardaigh de réir ainm, ríomhphost nó saineolas…",
+ "Find everyone tagged {tag}" : "Aimsigh gach duine leis an gclib {tag}",
+ "LaunchPad profile" : "Próifíl LaunchPad",
+ "Colleagues see these fields on your card in the people widget and can find you by them." : "Feiceann comhghleacaithe na réimsí seo ar do chárta sa ghiuirléid daoine agus is féidir leo tú a aimsiú dá réir.",
+ "Your profile fields could not be loaded." : "Níorbh fhéidir réimsí do phróifíle a lódáil.",
+ "Not filled in" : "Gan líonadh",
+ "Filled from your organisation’s directory. You cannot change it here." : "Líonta ó eolaire d'eagraíochta. Ní féidir leat é a athrú anseo.",
+ "Type a subject and press Enter" : "Clóscríobh ábhar agus brúigh Enter",
+ "Your profile fields are saved." : "Tá réimsí do phróifíle sábháilte.",
+ "Your profile fields could not be saved." : "Níorbh fhéidir réimsí do phróifíle a shábháil.",
+ "Profile fields" : "Réimsí próifíle",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute." : "Réimsí breise ar chártaí comhghleacaithe sa ghiuirléid daoine, mar shampla suíomh oifige nó saineolas. Líonann daoine iad ina socruithe pearsanta, nó tagann siad ó aitreabúid LDAP.",
+ "New field" : "Réimse nua",
+ "Tags" : "Clibeanna",
+ "Filled by" : "Líonta ag",
+ "The person" : "An duine féin",
+ "LDAP attribute" : "Aitreabúid LDAP",
+ "Who may see it" : "Cé a fhéadfaidh é a fheiceáil",
+ "Everyone" : "Gach duine",
+ "People in the same group" : "Daoine sa ghrúpa céanna",
+ "Searchable in the people widget" : "Inchuardaithe sa ghiuirléid daoine",
+ "Shown on the card" : "Taispeántar ar an gcárta",
+ "Remove field" : "Bain an réimse",
+ "Profile fields saved." : "Réimsí próifíle sábháilte.",
+ "Save profile fields" : "Sábháil réimsí próifíle",
+ "The profile fields could not be loaded." : "Níorbh fhéidir na réimsí próifíle a lódáil.",
+ "The profile fields could not be saved." : "Níorbh fhéidir na réimsí próifíle a shábháil."
},
"nplurals=5; plural=(n==1 ? 0 : n==2 ? 1 : n<7 ? 2 : n<11 ? 3 : 4);"
);
diff --git a/l10n/ga.json b/l10n/ga.json
index 36856aec8..408ceb33b 100644
--- a/l10n/ga.json
+++ b/l10n/ga.json
@@ -1263,6 +1263,34 @@
"Read receipts at the top of the dashboard show who has confirmed.": "Taispeánann admhálacha léite ag barr an deais cé a dheimhnigh.",
"{emoji}: {count} reactions": "{emoji}: {count} imoibriú",
"React with {emoji}": "Freagair le {emoji}",
- "{names} and {count} more": "{names} agus {count} eile"
+ "{names} and {count} more": "{names} agus {count} eile",
+ "Search people": "Cuardaigh daoine",
+ "Search by name, email or expertise…": "Cuardaigh de réir ainm, ríomhphost nó saineolas…",
+ "Find everyone tagged {tag}": "Aimsigh gach duine leis an gclib {tag}",
+ "LaunchPad profile": "Próifíl LaunchPad",
+ "Colleagues see these fields on your card in the people widget and can find you by them.": "Feiceann comhghleacaithe na réimsí seo ar do chárta sa ghiuirléid daoine agus is féidir leo tú a aimsiú dá réir.",
+ "Your profile fields could not be loaded.": "Níorbh fhéidir réimsí do phróifíle a lódáil.",
+ "Not filled in": "Gan líonadh",
+ "Filled from your organisation’s directory. You cannot change it here.": "Líonta ó eolaire d'eagraíochta. Ní féidir leat é a athrú anseo.",
+ "Type a subject and press Enter": "Clóscríobh ábhar agus brúigh Enter",
+ "Your profile fields are saved.": "Tá réimsí do phróifíle sábháilte.",
+ "Your profile fields could not be saved.": "Níorbh fhéidir réimsí do phróifíle a shábháil.",
+ "Profile fields": "Réimsí próifíle",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute.": "Réimsí breise ar chártaí comhghleacaithe sa ghiuirléid daoine, mar shampla suíomh oifige nó saineolas. Líonann daoine iad ina socruithe pearsanta, nó tagann siad ó aitreabúid LDAP.",
+ "New field": "Réimse nua",
+ "Tags": "Clibeanna",
+ "Filled by": "Líonta ag",
+ "The person": "An duine féin",
+ "LDAP attribute": "Aitreabúid LDAP",
+ "Who may see it": "Cé a fhéadfaidh é a fheiceáil",
+ "Everyone": "Gach duine",
+ "People in the same group": "Daoine sa ghrúpa céanna",
+ "Searchable in the people widget": "Inchuardaithe sa ghiuirléid daoine",
+ "Shown on the card": "Taispeántar ar an gcárta",
+ "Remove field": "Bain an réimse",
+ "Profile fields saved.": "Réimsí próifíle sábháilte.",
+ "Save profile fields": "Sábháil réimsí próifíle",
+ "The profile fields could not be loaded.": "Níorbh fhéidir na réimsí próifíle a lódáil.",
+ "The profile fields could not be saved.": "Níorbh fhéidir na réimsí próifíle a shábháil."
}
}
diff --git a/l10n/hr.js b/l10n/hr.js
index 7deacf70f..c4b220d61 100644
--- a/l10n/hr.js
+++ b/l10n/hr.js
@@ -1293,7 +1293,35 @@ OC.L10N.register(
"Read receipts at the top of the dashboard show who has confirmed." : "Potvrde čitanja na vrhu nadzorne ploče pokazuju tko je potvrdio.",
"{emoji}: {count} reactions" : "{emoji}: {count} reakcija",
"React with {emoji}" : "Reagiraj s {emoji}",
- "{names} and {count} more" : "{names} i još {count}"
+ "{names} and {count} more" : "{names} i još {count}",
+ "Search people" : "Pretraži osobe",
+ "Search by name, email or expertise…" : "Pretraži po imenu, e-pošti ili stručnosti…",
+ "Find everyone tagged {tag}" : "Pronađi sve s oznakom {tag}",
+ "LaunchPad profile" : "LaunchPad profil",
+ "Colleagues see these fields on your card in the people widget and can find you by them." : "Kolege vide ova polja na vašoj kartici u widgetu osoba i mogu vas pronaći po njima.",
+ "Your profile fields could not be loaded." : "Polja vašeg profila nije bilo moguće učitati.",
+ "Not filled in" : "Nije popunjeno",
+ "Filled from your organisation’s directory. You cannot change it here." : "Popunjeno iz imenika vaše organizacije. Ovdje ga ne možete promijeniti.",
+ "Type a subject and press Enter" : "Upišite temu i pritisnite Enter",
+ "Your profile fields are saved." : "Polja vašeg profila su spremljena.",
+ "Your profile fields could not be saved." : "Polja vašeg profila nije bilo moguće spremiti.",
+ "Profile fields" : "Polja profila",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute." : "Dodatna polja na karticama kolega u widgetu osoba, poput lokacije ureda ili stručnosti. Osobe ih ispunjavaju u osobnim postavkama ili dolaze iz LDAP atributa.",
+ "New field" : "Novo polje",
+ "Tags" : "Oznake",
+ "Filled by" : "Popunjava",
+ "The person" : "Sama osoba",
+ "LDAP attribute" : "LDAP atribut",
+ "Who may see it" : "Ko ga smije vidjeti",
+ "Everyone" : "Svi",
+ "People in the same group" : "Osobe u istoj grupi",
+ "Searchable in the people widget" : "Pretraživo u widgetu osoba",
+ "Shown on the card" : "Prikazano na kartici",
+ "Remove field" : "Ukloni polje",
+ "Profile fields saved." : "Polja profila su spremljena.",
+ "Save profile fields" : "Spremi polja profila",
+ "The profile fields could not be loaded." : "Polja profila nije bilo moguće učitati.",
+ "The profile fields could not be saved." : "Polja profila nije bilo moguće spremiti."
},
"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"
);
diff --git a/l10n/hr.json b/l10n/hr.json
index 315f901d9..2c565a477 100644
--- a/l10n/hr.json
+++ b/l10n/hr.json
@@ -1261,6 +1261,34 @@
"Read receipts at the top of the dashboard show who has confirmed.": "Potvrde čitanja na vrhu nadzorne ploče pokazuju tko je potvrdio.",
"{emoji}: {count} reactions": "{emoji}: {count} reakcija",
"React with {emoji}": "Reagiraj s {emoji}",
- "{names} and {count} more": "{names} i još {count}"
+ "{names} and {count} more": "{names} i još {count}",
+ "Search people": "Pretraži osobe",
+ "Search by name, email or expertise…": "Pretraži po imenu, e-pošti ili stručnosti…",
+ "Find everyone tagged {tag}": "Pronađi sve s oznakom {tag}",
+ "LaunchPad profile": "LaunchPad profil",
+ "Colleagues see these fields on your card in the people widget and can find you by them.": "Kolege vide ova polja na vašoj kartici u widgetu osoba i mogu vas pronaći po njima.",
+ "Your profile fields could not be loaded.": "Polja vašeg profila nije bilo moguće učitati.",
+ "Not filled in": "Nije popunjeno",
+ "Filled from your organisation’s directory. You cannot change it here.": "Popunjeno iz imenika vaše organizacije. Ovdje ga ne možete promijeniti.",
+ "Type a subject and press Enter": "Upišite temu i pritisnite Enter",
+ "Your profile fields are saved.": "Polja vašeg profila su spremljena.",
+ "Your profile fields could not be saved.": "Polja vašeg profila nije bilo moguće spremiti.",
+ "Profile fields": "Polja profila",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute.": "Dodatna polja na karticama kolega u widgetu osoba, poput lokacije ureda ili stručnosti. Osobe ih ispunjavaju u osobnim postavkama ili dolaze iz LDAP atributa.",
+ "New field": "Novo polje",
+ "Tags": "Oznake",
+ "Filled by": "Popunjava",
+ "The person": "Sama osoba",
+ "LDAP attribute": "LDAP atribut",
+ "Who may see it": "Ko ga smije vidjeti",
+ "Everyone": "Svi",
+ "People in the same group": "Osobe u istoj grupi",
+ "Searchable in the people widget": "Pretraživo u widgetu osoba",
+ "Shown on the card": "Prikazano na kartici",
+ "Remove field": "Ukloni polje",
+ "Profile fields saved.": "Polja profila su spremljena.",
+ "Save profile fields": "Spremi polja profila",
+ "The profile fields could not be loaded.": "Polja profila nije bilo moguće učitati.",
+ "The profile fields could not be saved.": "Polja profila nije bilo moguće spremiti."
}
}
diff --git a/l10n/hu.js b/l10n/hu.js
index 0e3e1cb1d..fc626aa13 100644
--- a/l10n/hu.js
+++ b/l10n/hu.js
@@ -1293,7 +1293,35 @@ OC.L10N.register(
"Read receipts at the top of the dashboard show who has confirmed." : "Az irányítópult tetején lévő olvasási nyugták mutatják, ki igazolta.",
"{emoji}: {count} reactions" : "{emoji}: {count} reakció",
"React with {emoji}" : "Reagálás ezzel: {emoji}",
- "{names} and {count} more" : "{names} és még {count}"
+ "{names} and {count} more" : "{names} és még {count}",
+ "Search people" : "Személyek keresése",
+ "Search by name, email or expertise…" : "Keresés név, e-mail vagy szakterület szerint…",
+ "Find everyone tagged {tag}" : "Mindenki keresése {tag} címkével",
+ "LaunchPad profile" : "LaunchPad-profil",
+ "Colleagues see these fields on your card in the people widget and can find you by them." : "A kollégák látják ezeket a mezőket a kártyádon a személyek widgetben, és ezek alapján megtalálhatnak.",
+ "Your profile fields could not be loaded." : "A profilmezőidet nem sikerült betölteni.",
+ "Not filled in" : "Nincs kitöltve",
+ "Filled from your organisation’s directory. You cannot change it here." : "A szervezeted címtárából töltődik ki. Itt nem módosíthatod.",
+ "Type a subject and press Enter" : "Írj be egy témát, és nyomd meg az Entert",
+ "Your profile fields are saved." : "A profilmezőid mentve.",
+ "Your profile fields could not be saved." : "A profilmezőidet nem sikerült menteni.",
+ "Profile fields" : "Profilmezők",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute." : "További mezők a kollégák kártyáin a személyek widgetben, például irodai helyszín vagy szakterület. Az emberek a személyes beállításaikban töltik ki őket, vagy egy LDAP-attribútumból származnak.",
+ "New field" : "Új mező",
+ "Tags" : "Címkék",
+ "Filled by" : "Kitölti",
+ "The person" : "Maga a személy",
+ "LDAP attribute" : "LDAP-attribútum",
+ "Who may see it" : "Ki láthatja",
+ "Everyone" : "Mindenki",
+ "People in the same group" : "Az azonos csoport tagjai",
+ "Searchable in the people widget" : "Kereshető a személyek widgetben",
+ "Shown on the card" : "Megjelenik a kártyán",
+ "Remove field" : "Mező eltávolítása",
+ "Profile fields saved." : "Profilmezők mentve.",
+ "Save profile fields" : "Profilmezők mentése",
+ "The profile fields could not be loaded." : "A profilmezőket nem sikerült betölteni.",
+ "The profile fields could not be saved." : "A profilmezőket nem sikerült menteni."
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/hu.json b/l10n/hu.json
index 65657518d..4ca822358 100644
--- a/l10n/hu.json
+++ b/l10n/hu.json
@@ -1260,6 +1260,34 @@
"Read receipts at the top of the dashboard show who has confirmed.": "Az irányítópult tetején lévő olvasási nyugták mutatják, ki igazolta.",
"{emoji}: {count} reactions": "{emoji}: {count} reakció",
"React with {emoji}": "Reagálás ezzel: {emoji}",
- "{names} and {count} more": "{names} és még {count}"
+ "{names} and {count} more": "{names} és még {count}",
+ "Search people": "Személyek keresése",
+ "Search by name, email or expertise…": "Keresés név, e-mail vagy szakterület szerint…",
+ "Find everyone tagged {tag}": "Mindenki keresése {tag} címkével",
+ "LaunchPad profile": "LaunchPad-profil",
+ "Colleagues see these fields on your card in the people widget and can find you by them.": "A kollégák látják ezeket a mezőket a kártyádon a személyek widgetben, és ezek alapján megtalálhatnak.",
+ "Your profile fields could not be loaded.": "A profilmezőidet nem sikerült betölteni.",
+ "Not filled in": "Nincs kitöltve",
+ "Filled from your organisation’s directory. You cannot change it here.": "A szervezeted címtárából töltődik ki. Itt nem módosíthatod.",
+ "Type a subject and press Enter": "Írj be egy témát, és nyomd meg az Entert",
+ "Your profile fields are saved.": "A profilmezőid mentve.",
+ "Your profile fields could not be saved.": "A profilmezőidet nem sikerült menteni.",
+ "Profile fields": "Profilmezők",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute.": "További mezők a kollégák kártyáin a személyek widgetben, például irodai helyszín vagy szakterület. Az emberek a személyes beállításaikban töltik ki őket, vagy egy LDAP-attribútumból származnak.",
+ "New field": "Új mező",
+ "Tags": "Címkék",
+ "Filled by": "Kitölti",
+ "The person": "Maga a személy",
+ "LDAP attribute": "LDAP-attribútum",
+ "Who may see it": "Ki láthatja",
+ "Everyone": "Mindenki",
+ "People in the same group": "Az azonos csoport tagjai",
+ "Searchable in the people widget": "Kereshető a személyek widgetben",
+ "Shown on the card": "Megjelenik a kártyán",
+ "Remove field": "Mező eltávolítása",
+ "Profile fields saved.": "Profilmezők mentve.",
+ "Save profile fields": "Profilmezők mentése",
+ "The profile fields could not be loaded.": "A profilmezőket nem sikerült betölteni.",
+ "The profile fields could not be saved.": "A profilmezőket nem sikerült menteni."
}
}
diff --git a/l10n/is.js b/l10n/is.js
index 21f97d669..64029bd3c 100644
--- a/l10n/is.js
+++ b/l10n/is.js
@@ -1293,7 +1293,35 @@ OC.L10N.register(
"Read receipts at the top of the dashboard show who has confirmed." : "Leskvittanir efst á stjórnborðinu sýna hver hefur staðfest.",
"{emoji}: {count} reactions" : "{emoji}: {count} viðbrögð",
"React with {emoji}" : "Bregðast við með {emoji}",
- "{names} and {count} more" : "{names} og {count} til viðbótar"
+ "{names} and {count} more" : "{names} og {count} til viðbótar",
+ "Search people" : "Leita að fólki",
+ "Search by name, email or expertise…" : "Leita eftir nafni, netfangi eða sérþekkingu…",
+ "Find everyone tagged {tag}" : "Finna alla með merkið {tag}",
+ "LaunchPad profile" : "LaunchPad-prófíll",
+ "Colleagues see these fields on your card in the people widget and can find you by them." : "Samstarfsfólk sér þessa reiti á spjaldinu þínu í fólksviðbótinni og getur fundið þig eftir þeim.",
+ "Your profile fields could not be loaded." : "Ekki tókst að hlaða inn prófílreitunum þínum.",
+ "Not filled in" : "Ekki útfyllt",
+ "Filled from your organisation’s directory. You cannot change it here." : "Fyllt út úr skrá fyrirtækisins. Þú getur ekki breytt því hér.",
+ "Type a subject and press Enter" : "Sláðu inn efni og ýttu á Enter",
+ "Your profile fields are saved." : "Prófílreitirnir þínir eru vistaðir.",
+ "Your profile fields could not be saved." : "Ekki tókst að vista prófílreitina þína.",
+ "Profile fields" : "Prófílreitir",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute." : "Viðbótarreitir á spjöldum samstarfsfólks í fólksviðbótinni, svo sem staðsetning skrifstofu eða sérþekking. Fólk fyllir þá út í eigin stillingum, eða þeir koma úr LDAP-eigind.",
+ "New field" : "Nýr reitur",
+ "Tags" : "Merki",
+ "Filled by" : "Útfyllt af",
+ "The person" : "Viðkomandi sjálfur",
+ "LDAP attribute" : "LDAP-eigind",
+ "Who may see it" : "Hver má sjá það",
+ "Everyone" : "Allir",
+ "People in the same group" : "Fólk í sama hópi",
+ "Searchable in the people widget" : "Leitanlegt í fólksviðbótinni",
+ "Shown on the card" : "Birtist á spjaldinu",
+ "Remove field" : "Fjarlægja reit",
+ "Profile fields saved." : "Prófílreitir vistaðir.",
+ "Save profile fields" : "Vista prófílreiti",
+ "The profile fields could not be loaded." : "Ekki tókst að hlaða inn prófílreitunum.",
+ "The profile fields could not be saved." : "Ekki tókst að vista prófílreitina."
},
"nplurals=2; plural=(n%10!=1 || n%100==11);"
);
diff --git a/l10n/is.json b/l10n/is.json
index b3c5075f6..de529d623 100644
--- a/l10n/is.json
+++ b/l10n/is.json
@@ -1260,6 +1260,34 @@
"Read receipts at the top of the dashboard show who has confirmed.": "Leskvittanir efst á stjórnborðinu sýna hver hefur staðfest.",
"{emoji}: {count} reactions": "{emoji}: {count} viðbrögð",
"React with {emoji}": "Bregðast við með {emoji}",
- "{names} and {count} more": "{names} og {count} til viðbótar"
+ "{names} and {count} more": "{names} og {count} til viðbótar",
+ "Search people": "Leita að fólki",
+ "Search by name, email or expertise…": "Leita eftir nafni, netfangi eða sérþekkingu…",
+ "Find everyone tagged {tag}": "Finna alla með merkið {tag}",
+ "LaunchPad profile": "LaunchPad-prófíll",
+ "Colleagues see these fields on your card in the people widget and can find you by them.": "Samstarfsfólk sér þessa reiti á spjaldinu þínu í fólksviðbótinni og getur fundið þig eftir þeim.",
+ "Your profile fields could not be loaded.": "Ekki tókst að hlaða inn prófílreitunum þínum.",
+ "Not filled in": "Ekki útfyllt",
+ "Filled from your organisation’s directory. You cannot change it here.": "Fyllt út úr skrá fyrirtækisins. Þú getur ekki breytt því hér.",
+ "Type a subject and press Enter": "Sláðu inn efni og ýttu á Enter",
+ "Your profile fields are saved.": "Prófílreitirnir þínir eru vistaðir.",
+ "Your profile fields could not be saved.": "Ekki tókst að vista prófílreitina þína.",
+ "Profile fields": "Prófílreitir",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute.": "Viðbótarreitir á spjöldum samstarfsfólks í fólksviðbótinni, svo sem staðsetning skrifstofu eða sérþekking. Fólk fyllir þá út í eigin stillingum, eða þeir koma úr LDAP-eigind.",
+ "New field": "Nýr reitur",
+ "Tags": "Merki",
+ "Filled by": "Útfyllt af",
+ "The person": "Viðkomandi sjálfur",
+ "LDAP attribute": "LDAP-eigind",
+ "Who may see it": "Hver má sjá það",
+ "Everyone": "Allir",
+ "People in the same group": "Fólk í sama hópi",
+ "Searchable in the people widget": "Leitanlegt í fólksviðbótinni",
+ "Shown on the card": "Birtist á spjaldinu",
+ "Remove field": "Fjarlægja reit",
+ "Profile fields saved.": "Prófílreitir vistaðir.",
+ "Save profile fields": "Vista prófílreiti",
+ "The profile fields could not be loaded.": "Ekki tókst að hlaða inn prófílreitunum.",
+ "The profile fields could not be saved.": "Ekki tókst að vista prófílreitina."
}
}
diff --git a/l10n/it.js b/l10n/it.js
index 754591227..ab0446c0d 100644
--- a/l10n/it.js
+++ b/l10n/it.js
@@ -1293,7 +1293,35 @@ OC.L10N.register(
"Read receipts at the top of the dashboard show who has confirmed." : "Le conferme di lettura in cima alla dashboard mostrano chi ha confermato.",
"{emoji}: {count} reactions" : "{emoji}: {count} reazioni",
"React with {emoji}" : "Reagisci con {emoji}",
- "{names} and {count} more" : "{names} e altri {count}"
+ "{names} and {count} more" : "{names} e altri {count}",
+ "Search people" : "Cerca persone",
+ "Search by name, email or expertise…" : "Cerca per nome, e-mail o competenza…",
+ "Find everyone tagged {tag}" : "Trova tutti con il tag {tag}",
+ "LaunchPad profile" : "Profilo LaunchPad",
+ "Colleagues see these fields on your card in the people widget and can find you by them." : "I colleghi vedono questi campi sulla tua scheda nel widget persone e possono trovarti tramite essi.",
+ "Your profile fields could not be loaded." : "Non è stato possibile caricare i campi del tuo profilo.",
+ "Not filled in" : "Non compilato",
+ "Filled from your organisation’s directory. You cannot change it here." : "Compilato dalla directory della tua organizzazione. Non puoi modificarlo qui.",
+ "Type a subject and press Enter" : "Scrivi un argomento e premi Invio",
+ "Your profile fields are saved." : "I campi del tuo profilo sono salvati.",
+ "Your profile fields could not be saved." : "Non è stato possibile salvare i campi del tuo profilo.",
+ "Profile fields" : "Campi del profilo",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute." : "Campi aggiuntivi sulle schede dei colleghi nel widget persone, come la sede dell'ufficio o la competenza. Le persone li compilano nelle impostazioni personali, oppure provengono da un attributo LDAP.",
+ "New field" : "Nuovo campo",
+ "Tags" : "Tag",
+ "Filled by" : "Compilato da",
+ "The person" : "La persona stessa",
+ "LDAP attribute" : "Attributo LDAP",
+ "Who may see it" : "Chi può vederlo",
+ "Everyone" : "Tutti",
+ "People in the same group" : "Persone dello stesso gruppo",
+ "Searchable in the people widget" : "Ricercabile nel widget persone",
+ "Shown on the card" : "Mostrato sulla scheda",
+ "Remove field" : "Rimuovi campo",
+ "Profile fields saved." : "Campi del profilo salvati.",
+ "Save profile fields" : "Salva campi del profilo",
+ "The profile fields could not be loaded." : "Non è stato possibile caricare i campi del profilo.",
+ "The profile fields could not be saved." : "Non è stato possibile salvare i campi del profilo."
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/it.json b/l10n/it.json
index 51683e4fb..fa3dd24fa 100644
--- a/l10n/it.json
+++ b/l10n/it.json
@@ -1260,6 +1260,34 @@
"Read receipts at the top of the dashboard show who has confirmed.": "Le conferme di lettura in cima alla dashboard mostrano chi ha confermato.",
"{emoji}: {count} reactions": "{emoji}: {count} reazioni",
"React with {emoji}": "Reagisci con {emoji}",
- "{names} and {count} more": "{names} e altri {count}"
+ "{names} and {count} more": "{names} e altri {count}",
+ "Search people": "Cerca persone",
+ "Search by name, email or expertise…": "Cerca per nome, e-mail o competenza…",
+ "Find everyone tagged {tag}": "Trova tutti con il tag {tag}",
+ "LaunchPad profile": "Profilo LaunchPad",
+ "Colleagues see these fields on your card in the people widget and can find you by them.": "I colleghi vedono questi campi sulla tua scheda nel widget persone e possono trovarti tramite essi.",
+ "Your profile fields could not be loaded.": "Non è stato possibile caricare i campi del tuo profilo.",
+ "Not filled in": "Non compilato",
+ "Filled from your organisation’s directory. You cannot change it here.": "Compilato dalla directory della tua organizzazione. Non puoi modificarlo qui.",
+ "Type a subject and press Enter": "Scrivi un argomento e premi Invio",
+ "Your profile fields are saved.": "I campi del tuo profilo sono salvati.",
+ "Your profile fields could not be saved.": "Non è stato possibile salvare i campi del tuo profilo.",
+ "Profile fields": "Campi del profilo",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute.": "Campi aggiuntivi sulle schede dei colleghi nel widget persone, come la sede dell'ufficio o la competenza. Le persone li compilano nelle impostazioni personali, oppure provengono da un attributo LDAP.",
+ "New field": "Nuovo campo",
+ "Tags": "Tag",
+ "Filled by": "Compilato da",
+ "The person": "La persona stessa",
+ "LDAP attribute": "Attributo LDAP",
+ "Who may see it": "Chi può vederlo",
+ "Everyone": "Tutti",
+ "People in the same group": "Persone dello stesso gruppo",
+ "Searchable in the people widget": "Ricercabile nel widget persone",
+ "Shown on the card": "Mostrato sulla scheda",
+ "Remove field": "Rimuovi campo",
+ "Profile fields saved.": "Campi del profilo salvati.",
+ "Save profile fields": "Salva campi del profilo",
+ "The profile fields could not be loaded.": "Non è stato possibile caricare i campi del profilo.",
+ "The profile fields could not be saved.": "Non è stato possibile salvare i campi del profilo."
}
}
diff --git a/l10n/lb.js b/l10n/lb.js
index 6cfc4cc3b..84faec3ed 100644
--- a/l10n/lb.js
+++ b/l10n/lb.js
@@ -1284,7 +1284,35 @@ OC.L10N.register(
"Read receipts at the top of the dashboard show who has confirmed." : "Liesquittungen uewen um Dashboard weisen, wie bestätegt huet.",
"{emoji}: {count} reactions" : "{emoji}: {count} Reaktiounen",
"React with {emoji}" : "Mat {emoji} reagéieren",
- "{names} and {count} more" : "{names} an nach {count}"
+ "{names} and {count} more" : "{names} an nach {count}",
+ "Search people" : "Leit sichen",
+ "Search by name, email or expertise…" : "No Numm, E-Mail oder Expertis sichen…",
+ "Find everyone tagged {tag}" : "All Leit mam Tag {tag} fannen",
+ "LaunchPad profile" : "LaunchPad-Profil",
+ "Colleagues see these fields on your card in the people widget and can find you by them." : "Kolleegen gesinn dës Felder op denger Kaart am Leit-Widget a kënnen dech doriwwer fannen.",
+ "Your profile fields could not be loaded." : "Deng Profilfelder konnte net geluede ginn.",
+ "Not filled in" : "Net ausgefëllt",
+ "Filled from your organisation’s directory. You cannot change it here." : "Aus dem Verzeechnes vun denger Organisatioun. Du kanns et hei net änneren.",
+ "Type a subject and press Enter" : "Thema aginn an Enter drécken",
+ "Your profile fields are saved." : "Deng Profilfelder sinn gespäichert.",
+ "Your profile fields could not be saved." : "Deng Profilfelder konnte net gespäichert ginn.",
+ "Profile fields" : "Profilfelder",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute." : "Zousätzlech Felder op de Kaarte vu Kolleegen am Leit-Widget, wéi Büro oder Expertis. D'Leit fëllen se an hire perséinlechen Astellungen aus, oder se kommen aus engem LDAP-Attribut.",
+ "New field" : "Neit Feld",
+ "Tags" : "Tags",
+ "Filled by" : "Ausgefëllt vun",
+ "The person" : "D'Persoun selwer",
+ "LDAP attribute" : "LDAP-Attribut",
+ "Who may see it" : "Wien et gesi dierf",
+ "Everyone" : "Jiddereen",
+ "People in the same group" : "Leit an der selwechter Grupp",
+ "Searchable in the people widget" : "Am Leit-Widget sichbar",
+ "Shown on the card" : "Op der Kaart gewisen",
+ "Remove field" : "Feld ewechhuelen",
+ "Profile fields saved." : "Profilfelder gespäichert.",
+ "Save profile fields" : "Profilfelder späicheren",
+ "The profile fields could not be loaded." : "D'Profilfelder konnten net geluede ginn.",
+ "The profile fields could not be saved." : "D'Profilfelder konnten net gespäichert ginn."
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/lb.json b/l10n/lb.json
index 81d6fd118..e6fc3747f 100644
--- a/l10n/lb.json
+++ b/l10n/lb.json
@@ -1260,6 +1260,34 @@
"Read receipts at the top of the dashboard show who has confirmed.": "Liesquittungen uewen um Dashboard weisen, wie bestätegt huet.",
"{emoji}: {count} reactions": "{emoji}: {count} Reaktiounen",
"React with {emoji}": "Mat {emoji} reagéieren",
- "{names} and {count} more": "{names} an nach {count}"
+ "{names} and {count} more": "{names} an nach {count}",
+ "Search people": "Leit sichen",
+ "Search by name, email or expertise…": "No Numm, E-Mail oder Expertis sichen…",
+ "Find everyone tagged {tag}": "All Leit mam Tag {tag} fannen",
+ "LaunchPad profile": "LaunchPad-Profil",
+ "Colleagues see these fields on your card in the people widget and can find you by them.": "Kolleegen gesinn dës Felder op denger Kaart am Leit-Widget a kënnen dech doriwwer fannen.",
+ "Your profile fields could not be loaded.": "Deng Profilfelder konnte net geluede ginn.",
+ "Not filled in": "Net ausgefëllt",
+ "Filled from your organisation’s directory. You cannot change it here.": "Aus dem Verzeechnes vun denger Organisatioun. Du kanns et hei net änneren.",
+ "Type a subject and press Enter": "Thema aginn an Enter drécken",
+ "Your profile fields are saved.": "Deng Profilfelder sinn gespäichert.",
+ "Your profile fields could not be saved.": "Deng Profilfelder konnte net gespäichert ginn.",
+ "Profile fields": "Profilfelder",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute.": "Zousätzlech Felder op de Kaarte vu Kolleegen am Leit-Widget, wéi Büro oder Expertis. D'Leit fëllen se an hire perséinlechen Astellungen aus, oder se kommen aus engem LDAP-Attribut.",
+ "New field": "Neit Feld",
+ "Tags": "Tags",
+ "Filled by": "Ausgefëllt vun",
+ "The person": "D'Persoun selwer",
+ "LDAP attribute": "LDAP-Attribut",
+ "Who may see it": "Wien et gesi dierf",
+ "Everyone": "Jiddereen",
+ "People in the same group": "Leit an der selwechter Grupp",
+ "Searchable in the people widget": "Am Leit-Widget sichbar",
+ "Shown on the card": "Op der Kaart gewisen",
+ "Remove field": "Feld ewechhuelen",
+ "Profile fields saved.": "Profilfelder gespäichert.",
+ "Save profile fields": "Profilfelder späicheren",
+ "The profile fields could not be loaded.": "D'Profilfelder konnten net geluede ginn.",
+ "The profile fields could not be saved.": "D'Profilfelder konnten net gespäichert ginn."
}
}
diff --git a/l10n/lt.js b/l10n/lt.js
index fe964afa9..8017d3ed4 100644
--- a/l10n/lt.js
+++ b/l10n/lt.js
@@ -1293,7 +1293,35 @@ OC.L10N.register(
"Read receipts at the top of the dashboard show who has confirmed." : "Perskaitymo kvitai suvestinės viršuje rodo, kas patvirtino.",
"{emoji}: {count} reactions" : "{emoji}: {count} reakcijos",
"React with {emoji}" : "Reaguoti {emoji}",
- "{names} and {count} more" : "{names} ir dar {count}"
+ "{names} and {count} more" : "{names} ir dar {count}",
+ "Search people" : "Ieškoti žmonių",
+ "Search by name, email or expertise…" : "Ieškoti pagal vardą, el. paštą ar kompetenciją…",
+ "Find everyone tagged {tag}" : "Rasti visus su žyma {tag}",
+ "LaunchPad profile" : "LaunchPad profilis",
+ "Colleagues see these fields on your card in the people widget and can find you by them." : "Kolegos mato šiuos laukus jūsų kortelėje žmonių valdiklyje ir gali jus pagal juos rasti.",
+ "Your profile fields could not be loaded." : "Nepavyko įkelti jūsų profilio laukų.",
+ "Not filled in" : "Neužpildyta",
+ "Filled from your organisation’s directory. You cannot change it here." : "Užpildyta iš jūsų organizacijos katalogo. Čia to pakeisti negalite.",
+ "Type a subject and press Enter" : "Įveskite temą ir paspauskite Enter",
+ "Your profile fields are saved." : "Jūsų profilio laukai išsaugoti.",
+ "Your profile fields could not be saved." : "Nepavyko išsaugoti jūsų profilio laukų.",
+ "Profile fields" : "Profilio laukai",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute." : "Papildomi laukai kolegų kortelėse žmonių valdiklyje, pavyzdžiui, biuro vieta ar kompetencija. Žmonės juos užpildo savo asmeniniuose nustatymuose arba jie gaunami iš LDAP atributo.",
+ "New field" : "Naujas laukas",
+ "Tags" : "Žymos",
+ "Filled by" : "Užpildo",
+ "The person" : "Pats asmuo",
+ "LDAP attribute" : "LDAP atributas",
+ "Who may see it" : "Kas gali matyti",
+ "Everyone" : "Visi",
+ "People in the same group" : "To paties grupės žmonės",
+ "Searchable in the people widget" : "Ieškoma žmonių valdiklyje",
+ "Shown on the card" : "Rodoma kortelėje",
+ "Remove field" : "Pašalinti lauką",
+ "Profile fields saved." : "Profilio laukai išsaugoti.",
+ "Save profile fields" : "Išsaugoti profilio laukus",
+ "The profile fields could not be loaded." : "Nepavyko įkelti profilio laukų.",
+ "The profile fields could not be saved." : "Nepavyko išsaugoti profilio laukų."
},
"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2);"
);
diff --git a/l10n/lt.json b/l10n/lt.json
index 1949dc682..b0bd59a86 100644
--- a/l10n/lt.json
+++ b/l10n/lt.json
@@ -1261,6 +1261,34 @@
"Read receipts at the top of the dashboard show who has confirmed.": "Perskaitymo kvitai suvestinės viršuje rodo, kas patvirtino.",
"{emoji}: {count} reactions": "{emoji}: {count} reakcijos",
"React with {emoji}": "Reaguoti {emoji}",
- "{names} and {count} more": "{names} ir dar {count}"
+ "{names} and {count} more": "{names} ir dar {count}",
+ "Search people": "Ieškoti žmonių",
+ "Search by name, email or expertise…": "Ieškoti pagal vardą, el. paštą ar kompetenciją…",
+ "Find everyone tagged {tag}": "Rasti visus su žyma {tag}",
+ "LaunchPad profile": "LaunchPad profilis",
+ "Colleagues see these fields on your card in the people widget and can find you by them.": "Kolegos mato šiuos laukus jūsų kortelėje žmonių valdiklyje ir gali jus pagal juos rasti.",
+ "Your profile fields could not be loaded.": "Nepavyko įkelti jūsų profilio laukų.",
+ "Not filled in": "Neužpildyta",
+ "Filled from your organisation’s directory. You cannot change it here.": "Užpildyta iš jūsų organizacijos katalogo. Čia to pakeisti negalite.",
+ "Type a subject and press Enter": "Įveskite temą ir paspauskite Enter",
+ "Your profile fields are saved.": "Jūsų profilio laukai išsaugoti.",
+ "Your profile fields could not be saved.": "Nepavyko išsaugoti jūsų profilio laukų.",
+ "Profile fields": "Profilio laukai",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute.": "Papildomi laukai kolegų kortelėse žmonių valdiklyje, pavyzdžiui, biuro vieta ar kompetencija. Žmonės juos užpildo savo asmeniniuose nustatymuose arba jie gaunami iš LDAP atributo.",
+ "New field": "Naujas laukas",
+ "Tags": "Žymos",
+ "Filled by": "Užpildo",
+ "The person": "Pats asmuo",
+ "LDAP attribute": "LDAP atributas",
+ "Who may see it": "Kas gali matyti",
+ "Everyone": "Visi",
+ "People in the same group": "To paties grupės žmonės",
+ "Searchable in the people widget": "Ieškoma žmonių valdiklyje",
+ "Shown on the card": "Rodoma kortelėje",
+ "Remove field": "Pašalinti lauką",
+ "Profile fields saved.": "Profilio laukai išsaugoti.",
+ "Save profile fields": "Išsaugoti profilio laukus",
+ "The profile fields could not be loaded.": "Nepavyko įkelti profilio laukų.",
+ "The profile fields could not be saved.": "Nepavyko išsaugoti profilio laukų."
}
}
diff --git a/l10n/lv.js b/l10n/lv.js
index c630fb4c2..967b9beb5 100644
--- a/l10n/lv.js
+++ b/l10n/lv.js
@@ -1293,7 +1293,35 @@ OC.L10N.register(
"Read receipts at the top of the dashboard show who has confirmed." : "Izlasīšanas apliecinājumi paneļa augšā rāda, kurš ir apstiprinājis.",
"{emoji}: {count} reactions" : "{emoji}: {count} reakcijas",
"React with {emoji}" : "Reaģēt ar {emoji}",
- "{names} and {count} more" : "{names} un vēl {count}"
+ "{names} and {count} more" : "{names} un vēl {count}",
+ "Search people" : "Meklēt cilvēkus",
+ "Search by name, email or expertise…" : "Meklēt pēc vārda, e-pasta vai kompetences…",
+ "Find everyone tagged {tag}" : "Atrast visus ar birku {tag}",
+ "LaunchPad profile" : "LaunchPad profils",
+ "Colleagues see these fields on your card in the people widget and can find you by them." : "Kolēģi redz šos laukus jūsu kartītē cilvēku logrīkā un var jūs pēc tiem atrast.",
+ "Your profile fields could not be loaded." : "Neizdevās ielādēt jūsu profila laukus.",
+ "Not filled in" : "Nav aizpildīts",
+ "Filled from your organisation’s directory. You cannot change it here." : "Aizpildīts no jūsu organizācijas direktorija. Šeit to mainīt nevar.",
+ "Type a subject and press Enter" : "Ierakstiet tēmu un nospiediet Enter",
+ "Your profile fields are saved." : "Jūsu profila lauki ir saglabāti.",
+ "Your profile fields could not be saved." : "Neizdevās saglabāt jūsu profila laukus.",
+ "Profile fields" : "Profila lauki",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute." : "Papildu lauki kolēģu kartītēs cilvēku logrīkā, piemēram, biroja atrašanās vieta vai kompetence. Cilvēki tos aizpilda savos personīgajos iestatījumos, vai tie nāk no LDAP atribūta.",
+ "New field" : "Jauns lauks",
+ "Tags" : "Birkas",
+ "Filled by" : "Aizpilda",
+ "The person" : "Pati persona",
+ "LDAP attribute" : "LDAP atribūts",
+ "Who may see it" : "Kas drīkst to redzēt",
+ "Everyone" : "Visi",
+ "People in the same group" : "Tās pašas grupas cilvēki",
+ "Searchable in the people widget" : "Meklējams cilvēku logrīkā",
+ "Shown on the card" : "Rādīts kartītē",
+ "Remove field" : "Noņemt lauku",
+ "Profile fields saved." : "Profila lauki saglabāti.",
+ "Save profile fields" : "Saglabāt profila laukus",
+ "The profile fields could not be loaded." : "Neizdevās ielādēt profila laukus.",
+ "The profile fields could not be saved." : "Neizdevās saglabāt profila laukus."
},
"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2);"
);
diff --git a/l10n/lv.json b/l10n/lv.json
index b6bfc65d8..e1a6d207b 100644
--- a/l10n/lv.json
+++ b/l10n/lv.json
@@ -1261,6 +1261,34 @@
"Read receipts at the top of the dashboard show who has confirmed.": "Izlasīšanas apliecinājumi paneļa augšā rāda, kurš ir apstiprinājis.",
"{emoji}: {count} reactions": "{emoji}: {count} reakcijas",
"React with {emoji}": "Reaģēt ar {emoji}",
- "{names} and {count} more": "{names} un vēl {count}"
+ "{names} and {count} more": "{names} un vēl {count}",
+ "Search people": "Meklēt cilvēkus",
+ "Search by name, email or expertise…": "Meklēt pēc vārda, e-pasta vai kompetences…",
+ "Find everyone tagged {tag}": "Atrast visus ar birku {tag}",
+ "LaunchPad profile": "LaunchPad profils",
+ "Colleagues see these fields on your card in the people widget and can find you by them.": "Kolēģi redz šos laukus jūsu kartītē cilvēku logrīkā un var jūs pēc tiem atrast.",
+ "Your profile fields could not be loaded.": "Neizdevās ielādēt jūsu profila laukus.",
+ "Not filled in": "Nav aizpildīts",
+ "Filled from your organisation’s directory. You cannot change it here.": "Aizpildīts no jūsu organizācijas direktorija. Šeit to mainīt nevar.",
+ "Type a subject and press Enter": "Ierakstiet tēmu un nospiediet Enter",
+ "Your profile fields are saved.": "Jūsu profila lauki ir saglabāti.",
+ "Your profile fields could not be saved.": "Neizdevās saglabāt jūsu profila laukus.",
+ "Profile fields": "Profila lauki",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute.": "Papildu lauki kolēģu kartītēs cilvēku logrīkā, piemēram, biroja atrašanās vieta vai kompetence. Cilvēki tos aizpilda savos personīgajos iestatījumos, vai tie nāk no LDAP atribūta.",
+ "New field": "Jauns lauks",
+ "Tags": "Birkas",
+ "Filled by": "Aizpilda",
+ "The person": "Pati persona",
+ "LDAP attribute": "LDAP atribūts",
+ "Who may see it": "Kas drīkst to redzēt",
+ "Everyone": "Visi",
+ "People in the same group": "Tās pašas grupas cilvēki",
+ "Searchable in the people widget": "Meklējams cilvēku logrīkā",
+ "Shown on the card": "Rādīts kartītē",
+ "Remove field": "Noņemt lauku",
+ "Profile fields saved.": "Profila lauki saglabāti.",
+ "Save profile fields": "Saglabāt profila laukus",
+ "The profile fields could not be loaded.": "Neizdevās ielādēt profila laukus.",
+ "The profile fields could not be saved.": "Neizdevās saglabāt profila laukus."
}
}
diff --git a/l10n/mk.js b/l10n/mk.js
index 91ae7490c..e7f077905 100644
--- a/l10n/mk.js
+++ b/l10n/mk.js
@@ -1284,7 +1284,35 @@ OC.L10N.register(
"Read receipts at the top of the dashboard show who has confirmed." : "Потврдите за прочитано на врвот од таблата покажуваат кој потврдил.",
"{emoji}: {count} reactions" : "{emoji}: {count} реакции",
"React with {emoji}" : "Реагирај со {emoji}",
- "{names} and {count} more" : "{names} и уште {count}"
+ "{names} and {count} more" : "{names} и уште {count}",
+ "Search people" : "Пребарај луѓе",
+ "Search by name, email or expertise…" : "Пребарај по име, е-пошта или експертиза…",
+ "Find everyone tagged {tag}" : "Најди ги сите со ознаката {tag}",
+ "LaunchPad profile" : "LaunchPad профил",
+ "Colleagues see these fields on your card in the people widget and can find you by them." : "Колегите ги гледаат овие полиња на вашата картичка во виџетот за луѓе и можат да ве најдат преку нив.",
+ "Your profile fields could not be loaded." : "Полињата на вашиот профил не можеа да се вчитаат.",
+ "Not filled in" : "Не е пополнето",
+ "Filled from your organisation’s directory. You cannot change it here." : "Пополнето од именикот на вашата организација. Не можете да го промените тука.",
+ "Type a subject and press Enter" : "Внесете тема и притиснете Enter",
+ "Your profile fields are saved." : "Полињата на вашиот профил се зачувани.",
+ "Your profile fields could not be saved." : "Полињата на вашиот профил не можеа да се зачуваат.",
+ "Profile fields" : "Полиња на профилот",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute." : "Дополнителни полиња на картичките на колегите во виџетот за луѓе, како локација на канцеларија или експертиза. Луѓето ги пополнуваат во личните поставки или доаѓаат од LDAP атрибут.",
+ "New field" : "Ново поле",
+ "Tags" : "Ознаки",
+ "Filled by" : "Пополнува",
+ "The person" : "Самата личност",
+ "LDAP attribute" : "LDAP атрибут",
+ "Who may see it" : "Кој може да го гледа",
+ "Everyone" : "Сите",
+ "People in the same group" : "Луѓе во истата група",
+ "Searchable in the people widget" : "Пребарливо во виџетот за луѓе",
+ "Shown on the card" : "Прикажано на картичката",
+ "Remove field" : "Отстрани поле",
+ "Profile fields saved." : "Полињата на профилот се зачувани.",
+ "Save profile fields" : "Зачувај полиња на профилот",
+ "The profile fields could not be loaded." : "Полињата на профилот не можеа да се вчитаат.",
+ "The profile fields could not be saved." : "Полињата на профилот не можеа да се зачуваат."
},
"nplurals=2; plural=(n%10==1 && n%100!=11 ? 0 : 1);"
);
diff --git a/l10n/mk.json b/l10n/mk.json
index 974f3963c..967e1c28d 100644
--- a/l10n/mk.json
+++ b/l10n/mk.json
@@ -1260,6 +1260,34 @@
"Read receipts at the top of the dashboard show who has confirmed.": "Потврдите за прочитано на врвот од таблата покажуваат кој потврдил.",
"{emoji}: {count} reactions": "{emoji}: {count} реакции",
"React with {emoji}": "Реагирај со {emoji}",
- "{names} and {count} more": "{names} и уште {count}"
+ "{names} and {count} more": "{names} и уште {count}",
+ "Search people": "Пребарај луѓе",
+ "Search by name, email or expertise…": "Пребарај по име, е-пошта или експертиза…",
+ "Find everyone tagged {tag}": "Најди ги сите со ознаката {tag}",
+ "LaunchPad profile": "LaunchPad профил",
+ "Colleagues see these fields on your card in the people widget and can find you by them.": "Колегите ги гледаат овие полиња на вашата картичка во виџетот за луѓе и можат да ве најдат преку нив.",
+ "Your profile fields could not be loaded.": "Полињата на вашиот профил не можеа да се вчитаат.",
+ "Not filled in": "Не е пополнето",
+ "Filled from your organisation’s directory. You cannot change it here.": "Пополнето од именикот на вашата организација. Не можете да го промените тука.",
+ "Type a subject and press Enter": "Внесете тема и притиснете Enter",
+ "Your profile fields are saved.": "Полињата на вашиот профил се зачувани.",
+ "Your profile fields could not be saved.": "Полињата на вашиот профил не можеа да се зачуваат.",
+ "Profile fields": "Полиња на профилот",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute.": "Дополнителни полиња на картичките на колегите во виџетот за луѓе, како локација на канцеларија или експертиза. Луѓето ги пополнуваат во личните поставки или доаѓаат од LDAP атрибут.",
+ "New field": "Ново поле",
+ "Tags": "Ознаки",
+ "Filled by": "Пополнува",
+ "The person": "Самата личност",
+ "LDAP attribute": "LDAP атрибут",
+ "Who may see it": "Кој може да го гледа",
+ "Everyone": "Сите",
+ "People in the same group": "Луѓе во истата група",
+ "Searchable in the people widget": "Пребарливо во виџетот за луѓе",
+ "Shown on the card": "Прикажано на картичката",
+ "Remove field": "Отстрани поле",
+ "Profile fields saved.": "Полињата на профилот се зачувани.",
+ "Save profile fields": "Зачувај полиња на профилот",
+ "The profile fields could not be loaded.": "Полињата на профилот не можеа да се вчитаат.",
+ "The profile fields could not be saved.": "Полињата на профилот не можеа да се зачуваат."
}
}
diff --git a/l10n/mt.js b/l10n/mt.js
index 24a661b6d..21ab65e16 100644
--- a/l10n/mt.js
+++ b/l10n/mt.js
@@ -1293,7 +1293,35 @@ OC.L10N.register(
"Read receipts at the top of the dashboard show who has confirmed." : "Ir-rċevuti tal-qari fuq nett tad-dashboard juru min ikkonferma.",
"{emoji}: {count} reactions" : "{emoji}: {count} reazzjonijiet",
"React with {emoji}" : "Irreaġixxi b'{emoji}",
- "{names} and {count} more" : "{names} u {count} oħra"
+ "{names} and {count} more" : "{names} u {count} oħra",
+ "Search people" : "Fittex persuni",
+ "Search by name, email or expertise…" : "Fittex bl-isem, l-email jew il-kompetenza…",
+ "Find everyone tagged {tag}" : "Sib lil kulħadd bit-tikketta {tag}",
+ "LaunchPad profile" : "Profil ta' LaunchPad",
+ "Colleagues see these fields on your card in the people widget and can find you by them." : "Il-kollegi jaraw dawn il-oqsma fuq il-karta tiegħek fil-widget tan-nies u jistgħu jsibuk permezz tagħhom.",
+ "Your profile fields could not be loaded." : "Il-oqsma tal-profil tiegħek ma setgħux jitgħabbew.",
+ "Not filled in" : "Mhux mimli",
+ "Filled from your organisation’s directory. You cannot change it here." : "Mimli mid-direttorju tal-organizzazzjoni tiegħek. Ma tistax tibdlu hawn.",
+ "Type a subject and press Enter" : "Ikteb suġġett u agħfas Enter",
+ "Your profile fields are saved." : "Il-oqsma tal-profil tiegħek huma salvati.",
+ "Your profile fields could not be saved." : "Il-oqsma tal-profil tiegħek ma setgħux jiġu salvati.",
+ "Profile fields" : "Oqsma tal-profil",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute." : "Oqsma żejda fuq il-karti tal-kollegi fil-widget tan-nies, bħall-post tal-uffiċċju jew il-kompetenza. In-nies jimlewhom fl-issettjar personali tagħhom, jew jiġu minn attribut LDAP.",
+ "New field" : "Qasam ġdid",
+ "Tags" : "Tikketti",
+ "Filled by" : "Jimlieh",
+ "The person" : "Il-persuna nfisha",
+ "LDAP attribute" : "Attribut LDAP",
+ "Who may see it" : "Min jista' jarah",
+ "Everyone" : "Kulħadd",
+ "People in the same group" : "Nies fl-istess grupp",
+ "Searchable in the people widget" : "Jista' jiġi mfittex fil-widget tan-nies",
+ "Shown on the card" : "Jintwera fuq il-karta",
+ "Remove field" : "Neħħi l-qasam",
+ "Profile fields saved." : "Il-oqsma tal-profil ġew salvati.",
+ "Save profile fields" : "Issejvja l-oqsma tal-profil",
+ "The profile fields could not be loaded." : "Il-oqsma tal-profil ma setgħux jitgħabbew.",
+ "The profile fields could not be saved." : "Il-oqsma tal-profil ma setgħux jiġu salvati."
},
"nplurals=4; plural=(n==1 ? 0 : n==0 || (n%100>1 && n%100<11) ? 1 : (n%100>10 && n%100<20) ? 2 : 3);"
);
diff --git a/l10n/mt.json b/l10n/mt.json
index 3ea26482a..ff0697add 100644
--- a/l10n/mt.json
+++ b/l10n/mt.json
@@ -1262,6 +1262,34 @@
"Read receipts at the top of the dashboard show who has confirmed.": "Ir-rċevuti tal-qari fuq nett tad-dashboard juru min ikkonferma.",
"{emoji}: {count} reactions": "{emoji}: {count} reazzjonijiet",
"React with {emoji}": "Irreaġixxi b'{emoji}",
- "{names} and {count} more": "{names} u {count} oħra"
+ "{names} and {count} more": "{names} u {count} oħra",
+ "Search people": "Fittex persuni",
+ "Search by name, email or expertise…": "Fittex bl-isem, l-email jew il-kompetenza…",
+ "Find everyone tagged {tag}": "Sib lil kulħadd bit-tikketta {tag}",
+ "LaunchPad profile": "Profil ta' LaunchPad",
+ "Colleagues see these fields on your card in the people widget and can find you by them.": "Il-kollegi jaraw dawn il-oqsma fuq il-karta tiegħek fil-widget tan-nies u jistgħu jsibuk permezz tagħhom.",
+ "Your profile fields could not be loaded.": "Il-oqsma tal-profil tiegħek ma setgħux jitgħabbew.",
+ "Not filled in": "Mhux mimli",
+ "Filled from your organisation’s directory. You cannot change it here.": "Mimli mid-direttorju tal-organizzazzjoni tiegħek. Ma tistax tibdlu hawn.",
+ "Type a subject and press Enter": "Ikteb suġġett u agħfas Enter",
+ "Your profile fields are saved.": "Il-oqsma tal-profil tiegħek huma salvati.",
+ "Your profile fields could not be saved.": "Il-oqsma tal-profil tiegħek ma setgħux jiġu salvati.",
+ "Profile fields": "Oqsma tal-profil",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute.": "Oqsma żejda fuq il-karti tal-kollegi fil-widget tan-nies, bħall-post tal-uffiċċju jew il-kompetenza. In-nies jimlewhom fl-issettjar personali tagħhom, jew jiġu minn attribut LDAP.",
+ "New field": "Qasam ġdid",
+ "Tags": "Tikketti",
+ "Filled by": "Jimlieh",
+ "The person": "Il-persuna nfisha",
+ "LDAP attribute": "Attribut LDAP",
+ "Who may see it": "Min jista' jarah",
+ "Everyone": "Kulħadd",
+ "People in the same group": "Nies fl-istess grupp",
+ "Searchable in the people widget": "Jista' jiġi mfittex fil-widget tan-nies",
+ "Shown on the card": "Jintwera fuq il-karta",
+ "Remove field": "Neħħi l-qasam",
+ "Profile fields saved.": "Il-oqsma tal-profil ġew salvati.",
+ "Save profile fields": "Issejvja l-oqsma tal-profil",
+ "The profile fields could not be loaded.": "Il-oqsma tal-profil ma setgħux jitgħabbew.",
+ "The profile fields could not be saved.": "Il-oqsma tal-profil ma setgħux jiġu salvati."
}
}
diff --git a/l10n/nb.js b/l10n/nb.js
index 143fe5d35..226a19ce8 100644
--- a/l10n/nb.js
+++ b/l10n/nb.js
@@ -1293,7 +1293,35 @@ OC.L10N.register(
"Read receipts at the top of the dashboard show who has confirmed." : "Lesekvitteringer øverst på dashbordet viser hvem som har bekreftet.",
"{emoji}: {count} reactions" : "{emoji}: {count} reaksjoner",
"React with {emoji}" : "Reager med {emoji}",
- "{names} and {count} more" : "{names} og {count} til"
+ "{names} and {count} more" : "{names} og {count} til",
+ "Search people" : "Søk etter personer",
+ "Search by name, email or expertise…" : "Søk etter navn, e-post eller kompetanse…",
+ "Find everyone tagged {tag}" : "Finn alle med merket {tag}",
+ "LaunchPad profile" : "LaunchPad-profil",
+ "Colleagues see these fields on your card in the people widget and can find you by them." : "Kolleger ser disse feltene på kortet ditt i personmodulen og kan finne deg ut fra dem.",
+ "Your profile fields could not be loaded." : "Profilfeltene dine kunne ikke lastes inn.",
+ "Not filled in" : "Ikke utfylt",
+ "Filled from your organisation’s directory. You cannot change it here." : "Fylt ut fra organisasjonens katalog. Du kan ikke endre det her.",
+ "Type a subject and press Enter" : "Skriv et emne og trykk Enter",
+ "Your profile fields are saved." : "Profilfeltene dine er lagret.",
+ "Your profile fields could not be saved." : "Profilfeltene dine kunne ikke lagres.",
+ "Profile fields" : "Profilfelt",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute." : "Ekstra felt på kollegers kort i personmodulen, for eksempel kontorsted eller kompetanse. Personene fyller dem ut i sine personlige innstillinger, eller de kommer fra et LDAP-attributt.",
+ "New field" : "Nytt felt",
+ "Tags" : "Merker",
+ "Filled by" : "Fylles ut av",
+ "The person" : "Personen selv",
+ "LDAP attribute" : "LDAP-attributt",
+ "Who may see it" : "Hvem kan se det",
+ "Everyone" : "Alle",
+ "People in the same group" : "Personer i samme gruppe",
+ "Searchable in the people widget" : "Søkbart i personmodulen",
+ "Shown on the card" : "Vises på kortet",
+ "Remove field" : "Fjern felt",
+ "Profile fields saved." : "Profilfeltene er lagret.",
+ "Save profile fields" : "Lagre profilfelt",
+ "The profile fields could not be loaded." : "Profilfeltene kunne ikke lastes inn.",
+ "The profile fields could not be saved." : "Profilfeltene kunne ikke lagres."
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/nb.json b/l10n/nb.json
index d46b23e21..60cb3f614 100644
--- a/l10n/nb.json
+++ b/l10n/nb.json
@@ -1260,6 +1260,34 @@
"Read receipts at the top of the dashboard show who has confirmed.": "Lesekvitteringer øverst på dashbordet viser hvem som har bekreftet.",
"{emoji}: {count} reactions": "{emoji}: {count} reaksjoner",
"React with {emoji}": "Reager med {emoji}",
- "{names} and {count} more": "{names} og {count} til"
+ "{names} and {count} more": "{names} og {count} til",
+ "Search people": "Søk etter personer",
+ "Search by name, email or expertise…": "Søk etter navn, e-post eller kompetanse…",
+ "Find everyone tagged {tag}": "Finn alle med merket {tag}",
+ "LaunchPad profile": "LaunchPad-profil",
+ "Colleagues see these fields on your card in the people widget and can find you by them.": "Kolleger ser disse feltene på kortet ditt i personmodulen og kan finne deg ut fra dem.",
+ "Your profile fields could not be loaded.": "Profilfeltene dine kunne ikke lastes inn.",
+ "Not filled in": "Ikke utfylt",
+ "Filled from your organisation’s directory. You cannot change it here.": "Fylt ut fra organisasjonens katalog. Du kan ikke endre det her.",
+ "Type a subject and press Enter": "Skriv et emne og trykk Enter",
+ "Your profile fields are saved.": "Profilfeltene dine er lagret.",
+ "Your profile fields could not be saved.": "Profilfeltene dine kunne ikke lagres.",
+ "Profile fields": "Profilfelt",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute.": "Ekstra felt på kollegers kort i personmodulen, for eksempel kontorsted eller kompetanse. Personene fyller dem ut i sine personlige innstillinger, eller de kommer fra et LDAP-attributt.",
+ "New field": "Nytt felt",
+ "Tags": "Merker",
+ "Filled by": "Fylles ut av",
+ "The person": "Personen selv",
+ "LDAP attribute": "LDAP-attributt",
+ "Who may see it": "Hvem kan se det",
+ "Everyone": "Alle",
+ "People in the same group": "Personer i samme gruppe",
+ "Searchable in the people widget": "Søkbart i personmodulen",
+ "Shown on the card": "Vises på kortet",
+ "Remove field": "Fjern felt",
+ "Profile fields saved.": "Profilfeltene er lagret.",
+ "Save profile fields": "Lagre profilfelt",
+ "The profile fields could not be loaded.": "Profilfeltene kunne ikke lastes inn.",
+ "The profile fields could not be saved.": "Profilfeltene kunne ikke lagres."
}
}
diff --git a/l10n/nl.js b/l10n/nl.js
index aa9bd9b1b..59f9b10fa 100644
--- a/l10n/nl.js
+++ b/l10n/nl.js
@@ -1343,7 +1343,35 @@ OC.L10N.register(
"Read receipts at the top of the dashboard show who has confirmed." : "Leesbevestigingen bovenaan het dashboard tonen wie heeft bevestigd.",
"{emoji}: {count} reactions" : "{emoji}: {count} reacties",
"React with {emoji}" : "Reageren met {emoji}",
- "{names} and {count} more" : "{names} en {count} anderen"
+ "{names} and {count} more" : "{names} en {count} anderen",
+ "Search people" : "Personen zoeken",
+ "Search by name, email or expertise…" : "Zoeken op naam, e-mail of expertise…",
+ "Find everyone tagged {tag}" : "Iedereen met de tag {tag} zoeken",
+ "LaunchPad profile" : "LaunchPad-profiel",
+ "Colleagues see these fields on your card in the people widget and can find you by them." : "Collega's zien deze velden op je kaart in de personenwidget en kunnen je erop vinden.",
+ "Your profile fields could not be loaded." : "Je profielvelden konden niet worden geladen.",
+ "Not filled in" : "Niet ingevuld",
+ "Filled from your organisation’s directory. You cannot change it here." : "Ingevuld vanuit de gebruikerslijst van je organisatie. Je kunt het hier niet wijzigen.",
+ "Type a subject and press Enter" : "Typ een onderwerp en druk op Enter",
+ "Your profile fields are saved." : "Je profielvelden zijn opgeslagen.",
+ "Your profile fields could not be saved." : "Je profielvelden konden niet worden opgeslagen.",
+ "Profile fields" : "Profielvelden",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute." : "Extra velden op de kaarten van collega's in de personenwidget, zoals kantoorlocatie of expertise. Mensen vullen ze in bij hun persoonlijke instellingen, of ze komen uit een LDAP-attribuut.",
+ "New field" : "Nieuw veld",
+ "Tags" : "Tags",
+ "Filled by" : "Ingevuld door",
+ "The person" : "De persoon zelf",
+ "LDAP attribute" : "LDAP-attribuut",
+ "Who may see it" : "Wie het mag zien",
+ "Everyone" : "Iedereen",
+ "People in the same group" : "Mensen in dezelfde groep",
+ "Searchable in the people widget" : "Doorzoekbaar in de personenwidget",
+ "Shown on the card" : "Getoond op de kaart",
+ "Remove field" : "Veld verwijderen",
+ "Profile fields saved." : "Profielvelden opgeslagen.",
+ "Save profile fields" : "Profielvelden opslaan",
+ "The profile fields could not be loaded." : "De profielvelden konden niet worden geladen.",
+ "The profile fields could not be saved." : "De profielvelden konden niet worden opgeslagen."
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/nl.json b/l10n/nl.json
index 4fbb565ba..c1070d47a 100644
--- a/l10n/nl.json
+++ b/l10n/nl.json
@@ -1320,6 +1320,34 @@
"Read receipts at the top of the dashboard show who has confirmed.": "Leesbevestigingen bovenaan het dashboard tonen wie heeft bevestigd.",
"{emoji}: {count} reactions": "{emoji}: {count} reacties",
"React with {emoji}": "Reageren met {emoji}",
- "{names} and {count} more": "{names} en {count} anderen"
+ "{names} and {count} more": "{names} en {count} anderen",
+ "Search people": "Personen zoeken",
+ "Search by name, email or expertise…": "Zoeken op naam, e-mail of expertise…",
+ "Find everyone tagged {tag}": "Iedereen met de tag {tag} zoeken",
+ "LaunchPad profile": "LaunchPad-profiel",
+ "Colleagues see these fields on your card in the people widget and can find you by them.": "Collega's zien deze velden op je kaart in de personenwidget en kunnen je erop vinden.",
+ "Your profile fields could not be loaded.": "Je profielvelden konden niet worden geladen.",
+ "Not filled in": "Niet ingevuld",
+ "Filled from your organisation’s directory. You cannot change it here.": "Ingevuld vanuit de gebruikerslijst van je organisatie. Je kunt het hier niet wijzigen.",
+ "Type a subject and press Enter": "Typ een onderwerp en druk op Enter",
+ "Your profile fields are saved.": "Je profielvelden zijn opgeslagen.",
+ "Your profile fields could not be saved.": "Je profielvelden konden niet worden opgeslagen.",
+ "Profile fields": "Profielvelden",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute.": "Extra velden op de kaarten van collega's in de personenwidget, zoals kantoorlocatie of expertise. Mensen vullen ze in bij hun persoonlijke instellingen, of ze komen uit een LDAP-attribuut.",
+ "New field": "Nieuw veld",
+ "Tags": "Tags",
+ "Filled by": "Ingevuld door",
+ "The person": "De persoon zelf",
+ "LDAP attribute": "LDAP-attribuut",
+ "Who may see it": "Wie het mag zien",
+ "Everyone": "Iedereen",
+ "People in the same group": "Mensen in dezelfde groep",
+ "Searchable in the people widget": "Doorzoekbaar in de personenwidget",
+ "Shown on the card": "Getoond op de kaart",
+ "Remove field": "Veld verwijderen",
+ "Profile fields saved.": "Profielvelden opgeslagen.",
+ "Save profile fields": "Profielvelden opslaan",
+ "The profile fields could not be loaded.": "De profielvelden konden niet worden geladen.",
+ "The profile fields could not be saved.": "De profielvelden konden niet worden opgeslagen."
}
}
diff --git a/l10n/pl.js b/l10n/pl.js
index c6f5d07d4..7754a5c2a 100644
--- a/l10n/pl.js
+++ b/l10n/pl.js
@@ -1293,7 +1293,35 @@ OC.L10N.register(
"Read receipts at the top of the dashboard show who has confirmed." : "Potwierdzenia odczytu u góry pulpitu pokazują, kto potwierdził.",
"{emoji}: {count} reactions" : "{emoji}: reakcje: {count}",
"React with {emoji}" : "Zareaguj {emoji}",
- "{names} and {count} more" : "{names} i jeszcze {count}"
+ "{names} and {count} more" : "{names} i jeszcze {count}",
+ "Search people" : "Szukaj osób",
+ "Search by name, email or expertise…" : "Szukaj według imienia, e-maila lub specjalizacji…",
+ "Find everyone tagged {tag}" : "Znajdź wszystkich z tagiem {tag}",
+ "LaunchPad profile" : "Profil LaunchPad",
+ "Colleagues see these fields on your card in the people widget and can find you by them." : "Współpracownicy widzą te pola na Twojej karcie w widżecie osób i mogą Cię po nich znaleźć.",
+ "Your profile fields could not be loaded." : "Nie udało się wczytać pól Twojego profilu.",
+ "Not filled in" : "Nie wypełniono",
+ "Filled from your organisation’s directory. You cannot change it here." : "Wypełniane z katalogu Twojej organizacji. Nie możesz tego tutaj zmienić.",
+ "Type a subject and press Enter" : "Wpisz temat i naciśnij Enter",
+ "Your profile fields are saved." : "Pola Twojego profilu zostały zapisane.",
+ "Your profile fields could not be saved." : "Nie udało się zapisać pól Twojego profilu.",
+ "Profile fields" : "Pola profilu",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute." : "Dodatkowe pola na kartach współpracowników w widżecie osób, np. lokalizacja biura lub specjalizacja. Osoby wypełniają je w ustawieniach osobistych albo pochodzą one z atrybutu LDAP.",
+ "New field" : "Nowe pole",
+ "Tags" : "Tagi",
+ "Filled by" : "Wypełnia",
+ "The person" : "Sama osoba",
+ "LDAP attribute" : "Atrybut LDAP",
+ "Who may see it" : "Kto może to widzieć",
+ "Everyone" : "Wszyscy",
+ "People in the same group" : "Osoby z tej samej grupy",
+ "Searchable in the people widget" : "Można wyszukiwać w widżecie osób",
+ "Shown on the card" : "Widoczne na karcie",
+ "Remove field" : "Usuń pole",
+ "Profile fields saved." : "Pola profilu zapisane.",
+ "Save profile fields" : "Zapisz pola profilu",
+ "The profile fields could not be loaded." : "Nie udało się wczytać pól profilu.",
+ "The profile fields could not be saved." : "Nie udało się zapisać pól profilu."
},
"nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"
);
diff --git a/l10n/pl.json b/l10n/pl.json
index 8cf07b857..60814ee56 100644
--- a/l10n/pl.json
+++ b/l10n/pl.json
@@ -1261,6 +1261,34 @@
"Read receipts at the top of the dashboard show who has confirmed.": "Potwierdzenia odczytu u góry pulpitu pokazują, kto potwierdził.",
"{emoji}: {count} reactions": "{emoji}: reakcje: {count}",
"React with {emoji}": "Zareaguj {emoji}",
- "{names} and {count} more": "{names} i jeszcze {count}"
+ "{names} and {count} more": "{names} i jeszcze {count}",
+ "Search people": "Szukaj osób",
+ "Search by name, email or expertise…": "Szukaj według imienia, e-maila lub specjalizacji…",
+ "Find everyone tagged {tag}": "Znajdź wszystkich z tagiem {tag}",
+ "LaunchPad profile": "Profil LaunchPad",
+ "Colleagues see these fields on your card in the people widget and can find you by them.": "Współpracownicy widzą te pola na Twojej karcie w widżecie osób i mogą Cię po nich znaleźć.",
+ "Your profile fields could not be loaded.": "Nie udało się wczytać pól Twojego profilu.",
+ "Not filled in": "Nie wypełniono",
+ "Filled from your organisation’s directory. You cannot change it here.": "Wypełniane z katalogu Twojej organizacji. Nie możesz tego tutaj zmienić.",
+ "Type a subject and press Enter": "Wpisz temat i naciśnij Enter",
+ "Your profile fields are saved.": "Pola Twojego profilu zostały zapisane.",
+ "Your profile fields could not be saved.": "Nie udało się zapisać pól Twojego profilu.",
+ "Profile fields": "Pola profilu",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute.": "Dodatkowe pola na kartach współpracowników w widżecie osób, np. lokalizacja biura lub specjalizacja. Osoby wypełniają je w ustawieniach osobistych albo pochodzą one z atrybutu LDAP.",
+ "New field": "Nowe pole",
+ "Tags": "Tagi",
+ "Filled by": "Wypełnia",
+ "The person": "Sama osoba",
+ "LDAP attribute": "Atrybut LDAP",
+ "Who may see it": "Kto może to widzieć",
+ "Everyone": "Wszyscy",
+ "People in the same group": "Osoby z tej samej grupy",
+ "Searchable in the people widget": "Można wyszukiwać w widżecie osób",
+ "Shown on the card": "Widoczne na karcie",
+ "Remove field": "Usuń pole",
+ "Profile fields saved.": "Pola profilu zapisane.",
+ "Save profile fields": "Zapisz pola profilu",
+ "The profile fields could not be loaded.": "Nie udało się wczytać pól profilu.",
+ "The profile fields could not be saved.": "Nie udało się zapisać pól profilu."
}
}
diff --git a/l10n/pt.js b/l10n/pt.js
index a25bc6a2e..4cdf7ed03 100644
--- a/l10n/pt.js
+++ b/l10n/pt.js
@@ -1293,7 +1293,35 @@ OC.L10N.register(
"Read receipts at the top of the dashboard show who has confirmed." : "Os recibos de leitura no topo do painel mostram quem confirmou.",
"{emoji}: {count} reactions" : "{emoji}: {count} reações",
"React with {emoji}" : "Reagir com {emoji}",
- "{names} and {count} more" : "{names} e mais {count}"
+ "{names} and {count} more" : "{names} e mais {count}",
+ "Search people" : "Pesquisar pessoas",
+ "Search by name, email or expertise…" : "Pesquisar por nome, e-mail ou especialidade…",
+ "Find everyone tagged {tag}" : "Encontrar todos com a etiqueta {tag}",
+ "LaunchPad profile" : "Perfil do LaunchPad",
+ "Colleagues see these fields on your card in the people widget and can find you by them." : "Os colegas veem estes campos no seu cartão no widget de pessoas e podem encontrá-lo por eles.",
+ "Your profile fields could not be loaded." : "Não foi possível carregar os campos do seu perfil.",
+ "Not filled in" : "Não preenchido",
+ "Filled from your organisation’s directory. You cannot change it here." : "Preenchido a partir do diretório da sua organização. Não pode alterá-lo aqui.",
+ "Type a subject and press Enter" : "Escreva um assunto e prima Enter",
+ "Your profile fields are saved." : "Os campos do seu perfil foram guardados.",
+ "Your profile fields could not be saved." : "Não foi possível guardar os campos do seu perfil.",
+ "Profile fields" : "Campos do perfil",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute." : "Campos extra nos cartões dos colegas no widget de pessoas, como a localização do escritório ou a especialidade. As pessoas preenchem-nos nas definições pessoais, ou vêm de um atributo LDAP.",
+ "New field" : "Novo campo",
+ "Tags" : "Etiquetas",
+ "Filled by" : "Preenchido por",
+ "The person" : "A própria pessoa",
+ "LDAP attribute" : "Atributo LDAP",
+ "Who may see it" : "Quem o pode ver",
+ "Everyone" : "Todos",
+ "People in the same group" : "Pessoas do mesmo grupo",
+ "Searchable in the people widget" : "Pesquisável no widget de pessoas",
+ "Shown on the card" : "Mostrado no cartão",
+ "Remove field" : "Remover campo",
+ "Profile fields saved." : "Campos do perfil guardados.",
+ "Save profile fields" : "Guardar campos do perfil",
+ "The profile fields could not be loaded." : "Não foi possível carregar os campos do perfil.",
+ "The profile fields could not be saved." : "Não foi possível guardar os campos do perfil."
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/pt.json b/l10n/pt.json
index c6347a128..41b8a8f8d 100644
--- a/l10n/pt.json
+++ b/l10n/pt.json
@@ -1260,6 +1260,34 @@
"Read receipts at the top of the dashboard show who has confirmed.": "Os recibos de leitura no topo do painel mostram quem confirmou.",
"{emoji}: {count} reactions": "{emoji}: {count} reações",
"React with {emoji}": "Reagir com {emoji}",
- "{names} and {count} more": "{names} e mais {count}"
+ "{names} and {count} more": "{names} e mais {count}",
+ "Search people": "Pesquisar pessoas",
+ "Search by name, email or expertise…": "Pesquisar por nome, e-mail ou especialidade…",
+ "Find everyone tagged {tag}": "Encontrar todos com a etiqueta {tag}",
+ "LaunchPad profile": "Perfil do LaunchPad",
+ "Colleagues see these fields on your card in the people widget and can find you by them.": "Os colegas veem estes campos no seu cartão no widget de pessoas e podem encontrá-lo por eles.",
+ "Your profile fields could not be loaded.": "Não foi possível carregar os campos do seu perfil.",
+ "Not filled in": "Não preenchido",
+ "Filled from your organisation’s directory. You cannot change it here.": "Preenchido a partir do diretório da sua organização. Não pode alterá-lo aqui.",
+ "Type a subject and press Enter": "Escreva um assunto e prima Enter",
+ "Your profile fields are saved.": "Os campos do seu perfil foram guardados.",
+ "Your profile fields could not be saved.": "Não foi possível guardar os campos do seu perfil.",
+ "Profile fields": "Campos do perfil",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute.": "Campos extra nos cartões dos colegas no widget de pessoas, como a localização do escritório ou a especialidade. As pessoas preenchem-nos nas definições pessoais, ou vêm de um atributo LDAP.",
+ "New field": "Novo campo",
+ "Tags": "Etiquetas",
+ "Filled by": "Preenchido por",
+ "The person": "A própria pessoa",
+ "LDAP attribute": "Atributo LDAP",
+ "Who may see it": "Quem o pode ver",
+ "Everyone": "Todos",
+ "People in the same group": "Pessoas do mesmo grupo",
+ "Searchable in the people widget": "Pesquisável no widget de pessoas",
+ "Shown on the card": "Mostrado no cartão",
+ "Remove field": "Remover campo",
+ "Profile fields saved.": "Campos do perfil guardados.",
+ "Save profile fields": "Guardar campos do perfil",
+ "The profile fields could not be loaded.": "Não foi possível carregar os campos do perfil.",
+ "The profile fields could not be saved.": "Não foi possível guardar os campos do perfil."
}
}
diff --git a/l10n/rm.js b/l10n/rm.js
index f319bafc5..457c0a42f 100644
--- a/l10n/rm.js
+++ b/l10n/rm.js
@@ -1293,7 +1293,35 @@ OC.L10N.register(
"Read receipts at the top of the dashboard show who has confirmed." : "Las quittanzas da lectura sisum il dashboard mussan tgi ch'ha confermà.",
"{emoji}: {count} reactions" : "{emoji}: {count} reacziuns",
"React with {emoji}" : "Reagir cun {emoji}",
- "{names} and {count} more" : "{names} ed anc {count}"
+ "{names} and {count} more" : "{names} ed anc {count}",
+ "Search people" : "Tschertgar persunas",
+ "Search by name, email or expertise…" : "Tschertgar tenor num, e-mail u expertisa…",
+ "Find everyone tagged {tag}" : "Chattar tuts cun la marca {tag}",
+ "LaunchPad profile" : "Profil LaunchPad",
+ "Colleagues see these fields on your card in the people widget and can find you by them." : "Collegas vesan quests champs sin tia carta en il widget da persunas e pon ta chattar tras quels.",
+ "Your profile fields could not be loaded." : "I n'è betg reussì da chargiar ils champs da tes profil.",
+ "Not filled in" : "Betg emplenì",
+ "Filled from your organisation’s directory. You cannot change it here." : "Emplenì dal register da tia organisaziun. Ti na pos betg midar quai qua.",
+ "Type a subject and press Enter" : "Endatescha in tema e smatga Enter",
+ "Your profile fields are saved." : "Ils champs da tes profil èn memorisads.",
+ "Your profile fields could not be saved." : "I n'è betg reussì da memorisar ils champs da tes profil.",
+ "Profile fields" : "Champs dal profil",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute." : "Champs supplementars sin las cartas da collegas en il widget da persunas, sco il lieu da biro u l'expertisa. Las persunas als emplenan en lur parameters persunals, u els derivan d'in attribut LDAP.",
+ "New field" : "Nov champ",
+ "Tags" : "Marcas",
+ "Filled by" : "Emplenì da",
+ "The person" : "La persuna sezza",
+ "LDAP attribute" : "Attribut LDAP",
+ "Who may see it" : "Tgi che po al vesair",
+ "Everyone" : "Tuts",
+ "People in the same group" : "Persunas en la medema gruppa",
+ "Searchable in the people widget" : "Tschertgabel en il widget da persunas",
+ "Shown on the card" : "Mussà sin la carta",
+ "Remove field" : "Allontanar il champ",
+ "Profile fields saved." : "Champs dal profil memorisads.",
+ "Save profile fields" : "Memorisar ils champs dal profil",
+ "The profile fields could not be loaded." : "I n'è betg reussì da chargiar ils champs dal profil.",
+ "The profile fields could not be saved." : "I n'è betg reussì da memorisar ils champs dal profil."
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/rm.json b/l10n/rm.json
index d63967a86..56794d514 100644
--- a/l10n/rm.json
+++ b/l10n/rm.json
@@ -1260,6 +1260,34 @@
"Read receipts at the top of the dashboard show who has confirmed.": "Las quittanzas da lectura sisum il dashboard mussan tgi ch'ha confermà.",
"{emoji}: {count} reactions": "{emoji}: {count} reacziuns",
"React with {emoji}": "Reagir cun {emoji}",
- "{names} and {count} more": "{names} ed anc {count}"
+ "{names} and {count} more": "{names} ed anc {count}",
+ "Search people": "Tschertgar persunas",
+ "Search by name, email or expertise…": "Tschertgar tenor num, e-mail u expertisa…",
+ "Find everyone tagged {tag}": "Chattar tuts cun la marca {tag}",
+ "LaunchPad profile": "Profil LaunchPad",
+ "Colleagues see these fields on your card in the people widget and can find you by them.": "Collegas vesan quests champs sin tia carta en il widget da persunas e pon ta chattar tras quels.",
+ "Your profile fields could not be loaded.": "I n'è betg reussì da chargiar ils champs da tes profil.",
+ "Not filled in": "Betg emplenì",
+ "Filled from your organisation’s directory. You cannot change it here.": "Emplenì dal register da tia organisaziun. Ti na pos betg midar quai qua.",
+ "Type a subject and press Enter": "Endatescha in tema e smatga Enter",
+ "Your profile fields are saved.": "Ils champs da tes profil èn memorisads.",
+ "Your profile fields could not be saved.": "I n'è betg reussì da memorisar ils champs da tes profil.",
+ "Profile fields": "Champs dal profil",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute.": "Champs supplementars sin las cartas da collegas en il widget da persunas, sco il lieu da biro u l'expertisa. Las persunas als emplenan en lur parameters persunals, u els derivan d'in attribut LDAP.",
+ "New field": "Nov champ",
+ "Tags": "Marcas",
+ "Filled by": "Emplenì da",
+ "The person": "La persuna sezza",
+ "LDAP attribute": "Attribut LDAP",
+ "Who may see it": "Tgi che po al vesair",
+ "Everyone": "Tuts",
+ "People in the same group": "Persunas en la medema gruppa",
+ "Searchable in the people widget": "Tschertgabel en il widget da persunas",
+ "Shown on the card": "Mussà sin la carta",
+ "Remove field": "Allontanar il champ",
+ "Profile fields saved.": "Champs dal profil memorisads.",
+ "Save profile fields": "Memorisar ils champs dal profil",
+ "The profile fields could not be loaded.": "I n'è betg reussì da chargiar ils champs dal profil.",
+ "The profile fields could not be saved.": "I n'è betg reussì da memorisar ils champs dal profil."
}
}
diff --git a/l10n/ro.js b/l10n/ro.js
index fdfbc2f1f..350c650fa 100644
--- a/l10n/ro.js
+++ b/l10n/ro.js
@@ -1293,7 +1293,35 @@ OC.L10N.register(
"Read receipts at the top of the dashboard show who has confirmed." : "Confirmările de citire din partea de sus arată cine a confirmat.",
"{emoji}: {count} reactions" : "{emoji}: {count} reacții",
"React with {emoji}" : "Reacționează cu {emoji}",
- "{names} and {count} more" : "{names} și încă {count}"
+ "{names} and {count} more" : "{names} și încă {count}",
+ "Search people" : "Caută persoane",
+ "Search by name, email or expertise…" : "Caută după nume, e-mail sau expertiză…",
+ "Find everyone tagged {tag}" : "Găsește pe toți cu eticheta {tag}",
+ "LaunchPad profile" : "Profil LaunchPad",
+ "Colleagues see these fields on your card in the people widget and can find you by them." : "Colegii văd aceste câmpuri pe cardul tău din widgetul de persoane și te pot găsi după ele.",
+ "Your profile fields could not be loaded." : "Câmpurile profilului tău nu au putut fi încărcate.",
+ "Not filled in" : "Necompletat",
+ "Filled from your organisation’s directory. You cannot change it here." : "Completat din directorul organizației tale. Nu îl poți schimba aici.",
+ "Type a subject and press Enter" : "Scrie un subiect și apasă Enter",
+ "Your profile fields are saved." : "Câmpurile profilului tău au fost salvate.",
+ "Your profile fields could not be saved." : "Câmpurile profilului tău nu au putut fi salvate.",
+ "Profile fields" : "Câmpuri de profil",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute." : "Câmpuri suplimentare pe cardurile colegilor din widgetul de persoane, cum ar fi locația biroului sau expertiza. Persoanele le completează în setările personale sau provin dintr-un atribut LDAP.",
+ "New field" : "Câmp nou",
+ "Tags" : "Etichete",
+ "Filled by" : "Completat de",
+ "The person" : "Persoana însăși",
+ "LDAP attribute" : "Atribut LDAP",
+ "Who may see it" : "Cine îl poate vedea",
+ "Everyone" : "Toată lumea",
+ "People in the same group" : "Persoane din același grup",
+ "Searchable in the people widget" : "Căutabil în widgetul de persoane",
+ "Shown on the card" : "Afișat pe card",
+ "Remove field" : "Elimină câmpul",
+ "Profile fields saved." : "Câmpurile de profil au fost salvate.",
+ "Save profile fields" : "Salvează câmpurile de profil",
+ "The profile fields could not be loaded." : "Câmpurile de profil nu au putut fi încărcate.",
+ "The profile fields could not be saved." : "Câmpurile de profil nu au putut fi salvate."
},
"nplurals=3; plural=(n==1 ? 0 : (n==0 || (n%100>0 && n%100<20)) ? 1 : 2);"
);
diff --git a/l10n/ro.json b/l10n/ro.json
index 915fb9665..bf0d96182 100644
--- a/l10n/ro.json
+++ b/l10n/ro.json
@@ -1261,6 +1261,34 @@
"Read receipts at the top of the dashboard show who has confirmed.": "Confirmările de citire din partea de sus arată cine a confirmat.",
"{emoji}: {count} reactions": "{emoji}: {count} reacții",
"React with {emoji}": "Reacționează cu {emoji}",
- "{names} and {count} more": "{names} și încă {count}"
+ "{names} and {count} more": "{names} și încă {count}",
+ "Search people": "Caută persoane",
+ "Search by name, email or expertise…": "Caută după nume, e-mail sau expertiză…",
+ "Find everyone tagged {tag}": "Găsește pe toți cu eticheta {tag}",
+ "LaunchPad profile": "Profil LaunchPad",
+ "Colleagues see these fields on your card in the people widget and can find you by them.": "Colegii văd aceste câmpuri pe cardul tău din widgetul de persoane și te pot găsi după ele.",
+ "Your profile fields could not be loaded.": "Câmpurile profilului tău nu au putut fi încărcate.",
+ "Not filled in": "Necompletat",
+ "Filled from your organisation’s directory. You cannot change it here.": "Completat din directorul organizației tale. Nu îl poți schimba aici.",
+ "Type a subject and press Enter": "Scrie un subiect și apasă Enter",
+ "Your profile fields are saved.": "Câmpurile profilului tău au fost salvate.",
+ "Your profile fields could not be saved.": "Câmpurile profilului tău nu au putut fi salvate.",
+ "Profile fields": "Câmpuri de profil",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute.": "Câmpuri suplimentare pe cardurile colegilor din widgetul de persoane, cum ar fi locația biroului sau expertiza. Persoanele le completează în setările personale sau provin dintr-un atribut LDAP.",
+ "New field": "Câmp nou",
+ "Tags": "Etichete",
+ "Filled by": "Completat de",
+ "The person": "Persoana însăși",
+ "LDAP attribute": "Atribut LDAP",
+ "Who may see it": "Cine îl poate vedea",
+ "Everyone": "Toată lumea",
+ "People in the same group": "Persoane din același grup",
+ "Searchable in the people widget": "Căutabil în widgetul de persoane",
+ "Shown on the card": "Afișat pe card",
+ "Remove field": "Elimină câmpul",
+ "Profile fields saved.": "Câmpurile de profil au fost salvate.",
+ "Save profile fields": "Salvează câmpurile de profil",
+ "The profile fields could not be loaded.": "Câmpurile de profil nu au putut fi încărcate.",
+ "The profile fields could not be saved.": "Câmpurile de profil nu au putut fi salvate."
}
}
diff --git a/l10n/ru.js b/l10n/ru.js
index 22206c75a..6eb71e4a6 100644
--- a/l10n/ru.js
+++ b/l10n/ru.js
@@ -1293,7 +1293,35 @@ OC.L10N.register(
"Read receipts at the top of the dashboard show who has confirmed." : "Квитанции о прочтении вверху панели показывают, кто подтвердил.",
"{emoji}: {count} reactions" : "{emoji}: реакций {count}",
"React with {emoji}" : "Отреагировать {emoji}",
- "{names} and {count} more" : "{names} и ещё {count}"
+ "{names} and {count} more" : "{names} и ещё {count}",
+ "Search people" : "Поиск людей",
+ "Search by name, email or expertise…" : "Поиск по имени, электронной почте или экспертизе…",
+ "Find everyone tagged {tag}" : "Найти всех с тегом {tag}",
+ "LaunchPad profile" : "Профиль LaunchPad",
+ "Colleagues see these fields on your card in the people widget and can find you by them." : "Коллеги видят эти поля на вашей карточке в виджете людей и могут найти вас по ним.",
+ "Your profile fields could not be loaded." : "Не удалось загрузить поля вашего профиля.",
+ "Not filled in" : "Не заполнено",
+ "Filled from your organisation’s directory. You cannot change it here." : "Заполняется из каталога вашей организации. Здесь это изменить нельзя.",
+ "Type a subject and press Enter" : "Введите тему и нажмите Enter",
+ "Your profile fields are saved." : "Поля вашего профиля сохранены.",
+ "Your profile fields could not be saved." : "Не удалось сохранить поля вашего профиля.",
+ "Profile fields" : "Поля профиля",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute." : "Дополнительные поля на карточках коллег в виджете людей, например расположение офиса или экспертиза. Люди заполняют их в личных настройках, или они берутся из атрибута LDAP.",
+ "New field" : "Новое поле",
+ "Tags" : "Теги",
+ "Filled by" : "Кто заполняет",
+ "The person" : "Сам человек",
+ "LDAP attribute" : "Атрибут LDAP",
+ "Who may see it" : "Кто может видеть",
+ "Everyone" : "Все",
+ "People in the same group" : "Люди из той же группы",
+ "Searchable in the people widget" : "Доступно для поиска в виджете людей",
+ "Shown on the card" : "Показывается на карточке",
+ "Remove field" : "Удалить поле",
+ "Profile fields saved." : "Поля профиля сохранены.",
+ "Save profile fields" : "Сохранить поля профиля",
+ "The profile fields could not be loaded." : "Не удалось загрузить поля профиля.",
+ "The profile fields could not be saved." : "Не удалось сохранить поля профиля."
},
"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"
);
diff --git a/l10n/ru.json b/l10n/ru.json
index 696d4278d..9958f3d25 100644
--- a/l10n/ru.json
+++ b/l10n/ru.json
@@ -1261,6 +1261,34 @@
"Read receipts at the top of the dashboard show who has confirmed.": "Квитанции о прочтении вверху панели показывают, кто подтвердил.",
"{emoji}: {count} reactions": "{emoji}: реакций {count}",
"React with {emoji}": "Отреагировать {emoji}",
- "{names} and {count} more": "{names} и ещё {count}"
+ "{names} and {count} more": "{names} и ещё {count}",
+ "Search people": "Поиск людей",
+ "Search by name, email or expertise…": "Поиск по имени, электронной почте или экспертизе…",
+ "Find everyone tagged {tag}": "Найти всех с тегом {tag}",
+ "LaunchPad profile": "Профиль LaunchPad",
+ "Colleagues see these fields on your card in the people widget and can find you by them.": "Коллеги видят эти поля на вашей карточке в виджете людей и могут найти вас по ним.",
+ "Your profile fields could not be loaded.": "Не удалось загрузить поля вашего профиля.",
+ "Not filled in": "Не заполнено",
+ "Filled from your organisation’s directory. You cannot change it here.": "Заполняется из каталога вашей организации. Здесь это изменить нельзя.",
+ "Type a subject and press Enter": "Введите тему и нажмите Enter",
+ "Your profile fields are saved.": "Поля вашего профиля сохранены.",
+ "Your profile fields could not be saved.": "Не удалось сохранить поля вашего профиля.",
+ "Profile fields": "Поля профиля",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute.": "Дополнительные поля на карточках коллег в виджете людей, например расположение офиса или экспертиза. Люди заполняют их в личных настройках, или они берутся из атрибута LDAP.",
+ "New field": "Новое поле",
+ "Tags": "Теги",
+ "Filled by": "Кто заполняет",
+ "The person": "Сам человек",
+ "LDAP attribute": "Атрибут LDAP",
+ "Who may see it": "Кто может видеть",
+ "Everyone": "Все",
+ "People in the same group": "Люди из той же группы",
+ "Searchable in the people widget": "Доступно для поиска в виджете людей",
+ "Shown on the card": "Показывается на карточке",
+ "Remove field": "Удалить поле",
+ "Profile fields saved.": "Поля профиля сохранены.",
+ "Save profile fields": "Сохранить поля профиля",
+ "The profile fields could not be loaded.": "Не удалось загрузить поля профиля.",
+ "The profile fields could not be saved.": "Не удалось сохранить поля профиля."
}
}
diff --git a/l10n/sk.js b/l10n/sk.js
index c3bb57879..972c0e9cc 100644
--- a/l10n/sk.js
+++ b/l10n/sk.js
@@ -1293,7 +1293,35 @@ OC.L10N.register(
"Read receipts at the top of the dashboard show who has confirmed." : "Potvrdenia o prečítaní v hornej časti panela ukazujú, kto potvrdil.",
"{emoji}: {count} reactions" : "{emoji}: {count} reakcií",
"React with {emoji}" : "Reagovať pomocou {emoji}",
- "{names} and {count} more" : "{names} a ďalší ({count})"
+ "{names} and {count} more" : "{names} a ďalší ({count})",
+ "Search people" : "Hľadať osoby",
+ "Search by name, email or expertise…" : "Hľadať podľa mena, e-mailu alebo odbornosti…",
+ "Find everyone tagged {tag}" : "Nájsť všetkých so štítkom {tag}",
+ "LaunchPad profile" : "Profil v LaunchPad",
+ "Colleagues see these fields on your card in the people widget and can find you by them." : "Kolegovia vidia tieto polia na vašej karte vo widgete osôb a môžu vás podľa nich nájsť.",
+ "Your profile fields could not be loaded." : "Polia vášho profilu sa nepodarilo načítať.",
+ "Not filled in" : "Nevyplnené",
+ "Filled from your organisation’s directory. You cannot change it here." : "Vyplnené z adresára vašej organizácie. Tu to nemôžete zmeniť.",
+ "Type a subject and press Enter" : "Napíšte tému a stlačte Enter",
+ "Your profile fields are saved." : "Polia vášho profilu sú uložené.",
+ "Your profile fields could not be saved." : "Polia vášho profilu sa nepodarilo uložiť.",
+ "Profile fields" : "Polia profilu",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute." : "Ďalšie polia na kartách kolegov vo widgete osôb, napríklad umiestnenie kancelárie alebo odbornosť. Ľudia ich vypĺňajú v osobných nastaveniach, alebo pochádzajú z atribútu LDAP.",
+ "New field" : "Nové pole",
+ "Tags" : "Štítky",
+ "Filled by" : "Vypĺňa",
+ "The person" : "Samotná osoba",
+ "LDAP attribute" : "Atribút LDAP",
+ "Who may see it" : "Kto to smie vidieť",
+ "Everyone" : "Všetci",
+ "People in the same group" : "Ľudia v tej istej skupine",
+ "Searchable in the people widget" : "Vyhľadateľné vo widgete osôb",
+ "Shown on the card" : "Zobrazené na karte",
+ "Remove field" : "Odstrániť pole",
+ "Profile fields saved." : "Polia profilu sú uložené.",
+ "Save profile fields" : "Uložiť polia profilu",
+ "The profile fields could not be loaded." : "Polia profilu sa nepodarilo načítať.",
+ "The profile fields could not be saved." : "Polia profilu sa nepodarilo uložiť."
},
"nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;"
);
diff --git a/l10n/sk.json b/l10n/sk.json
index 140f63854..84474ab28 100644
--- a/l10n/sk.json
+++ b/l10n/sk.json
@@ -1261,6 +1261,34 @@
"Read receipts at the top of the dashboard show who has confirmed.": "Potvrdenia o prečítaní v hornej časti panela ukazujú, kto potvrdil.",
"{emoji}: {count} reactions": "{emoji}: {count} reakcií",
"React with {emoji}": "Reagovať pomocou {emoji}",
- "{names} and {count} more": "{names} a ďalší ({count})"
+ "{names} and {count} more": "{names} a ďalší ({count})",
+ "Search people": "Hľadať osoby",
+ "Search by name, email or expertise…": "Hľadať podľa mena, e-mailu alebo odbornosti…",
+ "Find everyone tagged {tag}": "Nájsť všetkých so štítkom {tag}",
+ "LaunchPad profile": "Profil v LaunchPad",
+ "Colleagues see these fields on your card in the people widget and can find you by them.": "Kolegovia vidia tieto polia na vašej karte vo widgete osôb a môžu vás podľa nich nájsť.",
+ "Your profile fields could not be loaded.": "Polia vášho profilu sa nepodarilo načítať.",
+ "Not filled in": "Nevyplnené",
+ "Filled from your organisation’s directory. You cannot change it here.": "Vyplnené z adresára vašej organizácie. Tu to nemôžete zmeniť.",
+ "Type a subject and press Enter": "Napíšte tému a stlačte Enter",
+ "Your profile fields are saved.": "Polia vášho profilu sú uložené.",
+ "Your profile fields could not be saved.": "Polia vášho profilu sa nepodarilo uložiť.",
+ "Profile fields": "Polia profilu",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute.": "Ďalšie polia na kartách kolegov vo widgete osôb, napríklad umiestnenie kancelárie alebo odbornosť. Ľudia ich vypĺňajú v osobných nastaveniach, alebo pochádzajú z atribútu LDAP.",
+ "New field": "Nové pole",
+ "Tags": "Štítky",
+ "Filled by": "Vypĺňa",
+ "The person": "Samotná osoba",
+ "LDAP attribute": "Atribút LDAP",
+ "Who may see it": "Kto to smie vidieť",
+ "Everyone": "Všetci",
+ "People in the same group": "Ľudia v tej istej skupine",
+ "Searchable in the people widget": "Vyhľadateľné vo widgete osôb",
+ "Shown on the card": "Zobrazené na karte",
+ "Remove field": "Odstrániť pole",
+ "Profile fields saved.": "Polia profilu sú uložené.",
+ "Save profile fields": "Uložiť polia profilu",
+ "The profile fields could not be loaded.": "Polia profilu sa nepodarilo načítať.",
+ "The profile fields could not be saved.": "Polia profilu sa nepodarilo uložiť."
}
}
diff --git a/l10n/sl.js b/l10n/sl.js
index 71736f3b0..368c7922b 100644
--- a/l10n/sl.js
+++ b/l10n/sl.js
@@ -1293,7 +1293,35 @@ OC.L10N.register(
"Read receipts at the top of the dashboard show who has confirmed." : "Potrdila o branju na vrhu nadzorne plošče kažejo, kdo je potrdil.",
"{emoji}: {count} reactions" : "{emoji}: {count} odzivov",
"React with {emoji}" : "Odzovi se z {emoji}",
- "{names} and {count} more" : "{names} in še {count}"
+ "{names} and {count} more" : "{names} in še {count}",
+ "Search people" : "Išči osebe",
+ "Search by name, email or expertise…" : "Išči po imenu, e-pošti ali strokovnem znanju…",
+ "Find everyone tagged {tag}" : "Najdi vse z oznako {tag}",
+ "LaunchPad profile" : "Profil LaunchPad",
+ "Colleagues see these fields on your card in the people widget and can find you by them." : "Sodelavci vidijo ta polja na vaši kartici v gradniku oseb in vas lahko po njih najdejo.",
+ "Your profile fields could not be loaded." : "Polj vašega profila ni bilo mogoče naložiti.",
+ "Not filled in" : "Ni izpolnjeno",
+ "Filled from your organisation’s directory. You cannot change it here." : "Izpolnjeno iz imenika vaše organizacije. Tukaj tega ne morete spremeniti.",
+ "Type a subject and press Enter" : "Vnesite temo in pritisnite Enter",
+ "Your profile fields are saved." : "Polja vašega profila so shranjena.",
+ "Your profile fields could not be saved." : "Polj vašega profila ni bilo mogoče shraniti.",
+ "Profile fields" : "Polja profila",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute." : "Dodatna polja na karticah sodelavcev v gradniku oseb, na primer lokacija pisarne ali strokovno znanje. Osebe jih izpolnijo v osebnih nastavitvah ali pa izhajajo iz atributa LDAP.",
+ "New field" : "Novo polje",
+ "Tags" : "Oznake",
+ "Filled by" : "Izpolni",
+ "The person" : "Oseba sama",
+ "LDAP attribute" : "Atribut LDAP",
+ "Who may see it" : "Kdo ga lahko vidi",
+ "Everyone" : "Vsi",
+ "People in the same group" : "Osebe v isti skupini",
+ "Searchable in the people widget" : "Iskalno v gradniku oseb",
+ "Shown on the card" : "Prikazano na kartici",
+ "Remove field" : "Odstrani polje",
+ "Profile fields saved." : "Polja profila so shranjena.",
+ "Save profile fields" : "Shrani polja profila",
+ "The profile fields could not be loaded." : "Polj profila ni bilo mogoče naložiti.",
+ "The profile fields could not be saved." : "Polj profila ni bilo mogoče shraniti."
},
"nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);"
);
diff --git a/l10n/sl.json b/l10n/sl.json
index 3c01dd9e0..4fadf33d5 100644
--- a/l10n/sl.json
+++ b/l10n/sl.json
@@ -1262,6 +1262,34 @@
"Read receipts at the top of the dashboard show who has confirmed.": "Potrdila o branju na vrhu nadzorne plošče kažejo, kdo je potrdil.",
"{emoji}: {count} reactions": "{emoji}: {count} odzivov",
"React with {emoji}": "Odzovi se z {emoji}",
- "{names} and {count} more": "{names} in še {count}"
+ "{names} and {count} more": "{names} in še {count}",
+ "Search people": "Išči osebe",
+ "Search by name, email or expertise…": "Išči po imenu, e-pošti ali strokovnem znanju…",
+ "Find everyone tagged {tag}": "Najdi vse z oznako {tag}",
+ "LaunchPad profile": "Profil LaunchPad",
+ "Colleagues see these fields on your card in the people widget and can find you by them.": "Sodelavci vidijo ta polja na vaši kartici v gradniku oseb in vas lahko po njih najdejo.",
+ "Your profile fields could not be loaded.": "Polj vašega profila ni bilo mogoče naložiti.",
+ "Not filled in": "Ni izpolnjeno",
+ "Filled from your organisation’s directory. You cannot change it here.": "Izpolnjeno iz imenika vaše organizacije. Tukaj tega ne morete spremeniti.",
+ "Type a subject and press Enter": "Vnesite temo in pritisnite Enter",
+ "Your profile fields are saved.": "Polja vašega profila so shranjena.",
+ "Your profile fields could not be saved.": "Polj vašega profila ni bilo mogoče shraniti.",
+ "Profile fields": "Polja profila",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute.": "Dodatna polja na karticah sodelavcev v gradniku oseb, na primer lokacija pisarne ali strokovno znanje. Osebe jih izpolnijo v osebnih nastavitvah ali pa izhajajo iz atributa LDAP.",
+ "New field": "Novo polje",
+ "Tags": "Oznake",
+ "Filled by": "Izpolni",
+ "The person": "Oseba sama",
+ "LDAP attribute": "Atribut LDAP",
+ "Who may see it": "Kdo ga lahko vidi",
+ "Everyone": "Vsi",
+ "People in the same group": "Osebe v isti skupini",
+ "Searchable in the people widget": "Iskalno v gradniku oseb",
+ "Shown on the card": "Prikazano na kartici",
+ "Remove field": "Odstrani polje",
+ "Profile fields saved.": "Polja profila so shranjena.",
+ "Save profile fields": "Shrani polja profila",
+ "The profile fields could not be loaded.": "Polj profila ni bilo mogoče naložiti.",
+ "The profile fields could not be saved.": "Polj profila ni bilo mogoče shraniti."
}
}
diff --git a/l10n/sq.js b/l10n/sq.js
index f06671db8..cdcf747d1 100644
--- a/l10n/sq.js
+++ b/l10n/sq.js
@@ -1293,7 +1293,35 @@ OC.L10N.register(
"Read receipts at the top of the dashboard show who has confirmed." : "Dëftesat e leximit në krye të panelit tregojnë kush ka konfirmuar.",
"{emoji}: {count} reactions" : "{emoji}: {count} reagime",
"React with {emoji}" : "Reago me {emoji}",
- "{names} and {count} more" : "{names} dhe {count} të tjerë"
+ "{names} and {count} more" : "{names} dhe {count} të tjerë",
+ "Search people" : "Kërko persona",
+ "Search by name, email or expertise…" : "Kërko sipas emrit, email-it ose ekspertizës…",
+ "Find everyone tagged {tag}" : "Gjej të gjithë me etiketën {tag}",
+ "LaunchPad profile" : "Profili i LaunchPad",
+ "Colleagues see these fields on your card in the people widget and can find you by them." : "Kolegët i shohin këto fusha në kartën tënde te widget-i i personave dhe mund të të gjejnë përmes tyre.",
+ "Your profile fields could not be loaded." : "Fushat e profilit tënd nuk u ngarkuan.",
+ "Not filled in" : "Pa plotësuar",
+ "Filled from your organisation’s directory. You cannot change it here." : "Plotësohet nga direktoria e organizatës sate. Nuk mund ta ndryshosh këtu.",
+ "Type a subject and press Enter" : "Shkruaj një temë dhe shtyp Enter",
+ "Your profile fields are saved." : "Fushat e profilit tënd u ruajtën.",
+ "Your profile fields could not be saved." : "Fushat e profilit tënd nuk u ruajtën.",
+ "Profile fields" : "Fushat e profilit",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute." : "Fusha shtesë në kartat e kolegëve te widget-i i personave, si vendndodhja e zyrës ose ekspertiza. Personat i plotësojnë në cilësimet e tyre personale, ose vijnë nga një atribut LDAP.",
+ "New field" : "Fushë e re",
+ "Tags" : "Etiketa",
+ "Filled by" : "Plotësohet nga",
+ "The person" : "Vetë personi",
+ "LDAP attribute" : "Atribut LDAP",
+ "Who may see it" : "Kush mund ta shohë",
+ "Everyone" : "Të gjithë",
+ "People in the same group" : "Personat në të njëjtin grup",
+ "Searchable in the people widget" : "I kërkueshëm te widget-i i personave",
+ "Shown on the card" : "Shfaqet në kartë",
+ "Remove field" : "Hiq fushën",
+ "Profile fields saved." : "Fushat e profilit u ruajtën.",
+ "Save profile fields" : "Ruaj fushat e profilit",
+ "The profile fields could not be loaded." : "Fushat e profilit nuk u ngarkuan.",
+ "The profile fields could not be saved." : "Fushat e profilit nuk u ruajtën."
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/sq.json b/l10n/sq.json
index a7e099047..1fb6fecd0 100644
--- a/l10n/sq.json
+++ b/l10n/sq.json
@@ -1260,6 +1260,34 @@
"Read receipts at the top of the dashboard show who has confirmed.": "Dëftesat e leximit në krye të panelit tregojnë kush ka konfirmuar.",
"{emoji}: {count} reactions": "{emoji}: {count} reagime",
"React with {emoji}": "Reago me {emoji}",
- "{names} and {count} more": "{names} dhe {count} të tjerë"
+ "{names} and {count} more": "{names} dhe {count} të tjerë",
+ "Search people": "Kërko persona",
+ "Search by name, email or expertise…": "Kërko sipas emrit, email-it ose ekspertizës…",
+ "Find everyone tagged {tag}": "Gjej të gjithë me etiketën {tag}",
+ "LaunchPad profile": "Profili i LaunchPad",
+ "Colleagues see these fields on your card in the people widget and can find you by them.": "Kolegët i shohin këto fusha në kartën tënde te widget-i i personave dhe mund të të gjejnë përmes tyre.",
+ "Your profile fields could not be loaded.": "Fushat e profilit tënd nuk u ngarkuan.",
+ "Not filled in": "Pa plotësuar",
+ "Filled from your organisation’s directory. You cannot change it here.": "Plotësohet nga direktoria e organizatës sate. Nuk mund ta ndryshosh këtu.",
+ "Type a subject and press Enter": "Shkruaj një temë dhe shtyp Enter",
+ "Your profile fields are saved.": "Fushat e profilit tënd u ruajtën.",
+ "Your profile fields could not be saved.": "Fushat e profilit tënd nuk u ruajtën.",
+ "Profile fields": "Fushat e profilit",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute.": "Fusha shtesë në kartat e kolegëve te widget-i i personave, si vendndodhja e zyrës ose ekspertiza. Personat i plotësojnë në cilësimet e tyre personale, ose vijnë nga një atribut LDAP.",
+ "New field": "Fushë e re",
+ "Tags": "Etiketa",
+ "Filled by": "Plotësohet nga",
+ "The person": "Vetë personi",
+ "LDAP attribute": "Atribut LDAP",
+ "Who may see it": "Kush mund ta shohë",
+ "Everyone": "Të gjithë",
+ "People in the same group": "Personat në të njëjtin grup",
+ "Searchable in the people widget": "I kërkueshëm te widget-i i personave",
+ "Shown on the card": "Shfaqet në kartë",
+ "Remove field": "Hiq fushën",
+ "Profile fields saved.": "Fushat e profilit u ruajtën.",
+ "Save profile fields": "Ruaj fushat e profilit",
+ "The profile fields could not be loaded.": "Fushat e profilit nuk u ngarkuan.",
+ "The profile fields could not be saved.": "Fushat e profilit nuk u ruajtën."
}
}
diff --git a/l10n/sr.js b/l10n/sr.js
index 0d60501bc..612b24d10 100644
--- a/l10n/sr.js
+++ b/l10n/sr.js
@@ -1284,6 +1284,34 @@ OC.L10N.register(
"Read receipts at the top of the dashboard show who has confirmed." : "Потврде читања на врху контролне табле показују ко је потврдио.",
"{emoji}: {count} reactions" : "{emoji}: {count} реакција",
"React with {emoji}" : "Реагуј са {emoji}",
- "{names} and {count} more" : "{names} и још {count}"
+ "{names} and {count} more" : "{names} и још {count}",
+ "Search people" : "Претражи особе",
+ "Search by name, email or expertise…" : "Претражи по имену, е-пошти или стручности…",
+ "Find everyone tagged {tag}" : "Пронађи све са ознаком {tag}",
+ "LaunchPad profile" : "LaunchPad профил",
+ "Colleagues see these fields on your card in the people widget and can find you by them." : "Колеге виде ова поља на вашој картици у виџету особа и могу да вас пронађу по њима.",
+ "Your profile fields could not be loaded." : "Поља вашег профила нису могла да се учитају.",
+ "Not filled in" : "Није попуњено",
+ "Filled from your organisation’s directory. You cannot change it here." : "Попуњено из именика ваше организације. Овде то не можете да промените.",
+ "Type a subject and press Enter" : "Унесите тему и притисните Enter",
+ "Your profile fields are saved." : "Поља вашег профила су сачувана.",
+ "Your profile fields could not be saved." : "Поља вашег профила нису могла да се сачувају.",
+ "Profile fields" : "Поља профила",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute." : "Додатна поља на картицама колега у виџету особа, као што су локација канцеларије или стручност. Особе их попуњавају у личним подешавањима или долазе из LDAP атрибута.",
+ "New field" : "Ново поље",
+ "Tags" : "Ознаке",
+ "Filled by" : "Попуњава",
+ "The person" : "Сама особа",
+ "LDAP attribute" : "LDAP атрибут",
+ "Who may see it" : "Ко сме да га види",
+ "Everyone" : "Сви",
+ "People in the same group" : "Особе у истој групи",
+ "Searchable in the people widget" : "Претраживо у виџету особа",
+ "Shown on the card" : "Приказано на картици",
+ "Remove field" : "Уклони поље",
+ "Profile fields saved." : "Поља профила су сачувана.",
+ "Save profile fields" : "Сачувај поља профила",
+ "The profile fields could not be loaded." : "Поља профила нису могла да се учитају.",
+ "The profile fields could not be saved." : "Поља профила нису могла да се сачувају."
},
"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);");
diff --git a/l10n/sr.json b/l10n/sr.json
index 96e2004a5..fa807ae69 100644
--- a/l10n/sr.json
+++ b/l10n/sr.json
@@ -1261,6 +1261,34 @@
"Read receipts at the top of the dashboard show who has confirmed.": "Потврде читања на врху контролне табле показују ко је потврдио.",
"{emoji}: {count} reactions": "{emoji}: {count} реакција",
"React with {emoji}": "Реагуј са {emoji}",
- "{names} and {count} more": "{names} и још {count}"
+ "{names} and {count} more": "{names} и још {count}",
+ "Search people": "Претражи особе",
+ "Search by name, email or expertise…": "Претражи по имену, е-пошти или стручности…",
+ "Find everyone tagged {tag}": "Пронађи све са ознаком {tag}",
+ "LaunchPad profile": "LaunchPad профил",
+ "Colleagues see these fields on your card in the people widget and can find you by them.": "Колеге виде ова поља на вашој картици у виџету особа и могу да вас пронађу по њима.",
+ "Your profile fields could not be loaded.": "Поља вашег профила нису могла да се учитају.",
+ "Not filled in": "Није попуњено",
+ "Filled from your organisation’s directory. You cannot change it here.": "Попуњено из именика ваше организације. Овде то не можете да промените.",
+ "Type a subject and press Enter": "Унесите тему и притисните Enter",
+ "Your profile fields are saved.": "Поља вашег профила су сачувана.",
+ "Your profile fields could not be saved.": "Поља вашег профила нису могла да се сачувају.",
+ "Profile fields": "Поља профила",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute.": "Додатна поља на картицама колега у виџету особа, као што су локација канцеларије или стручност. Особе их попуњавају у личним подешавањима или долазе из LDAP атрибута.",
+ "New field": "Ново поље",
+ "Tags": "Ознаке",
+ "Filled by": "Попуњава",
+ "The person": "Сама особа",
+ "LDAP attribute": "LDAP атрибут",
+ "Who may see it": "Ко сме да га види",
+ "Everyone": "Сви",
+ "People in the same group": "Особе у истој групи",
+ "Searchable in the people widget": "Претраживо у виџету особа",
+ "Shown on the card": "Приказано на картици",
+ "Remove field": "Уклони поље",
+ "Profile fields saved.": "Поља профила су сачувана.",
+ "Save profile fields": "Сачувај поља профила",
+ "The profile fields could not be loaded.": "Поља профила нису могла да се учитају.",
+ "The profile fields could not be saved.": "Поља профила нису могла да се сачувају."
}
}
diff --git a/l10n/sv.js b/l10n/sv.js
index 96a8c9acd..b00492b8a 100644
--- a/l10n/sv.js
+++ b/l10n/sv.js
@@ -1293,7 +1293,35 @@ OC.L10N.register(
"Read receipts at the top of the dashboard show who has confirmed." : "Läskvitton högst upp på instrumentpanelen visar vem som har bekräftat.",
"{emoji}: {count} reactions" : "{emoji}: {count} reaktioner",
"React with {emoji}" : "Reagera med {emoji}",
- "{names} and {count} more" : "{names} och {count} till"
+ "{names} and {count} more" : "{names} och {count} till",
+ "Search people" : "Sök personer",
+ "Search by name, email or expertise…" : "Sök på namn, e-post eller expertis…",
+ "Find everyone tagged {tag}" : "Hitta alla med taggen {tag}",
+ "LaunchPad profile" : "LaunchPad-profil",
+ "Colleagues see these fields on your card in the people widget and can find you by them." : "Kollegor ser de här fälten på ditt kort i personwidgeten och kan hitta dig med dem.",
+ "Your profile fields could not be loaded." : "Dina profilfält kunde inte läsas in.",
+ "Not filled in" : "Inte ifyllt",
+ "Filled from your organisation’s directory. You cannot change it here." : "Hämtas från din organisations katalog. Du kan inte ändra det här.",
+ "Type a subject and press Enter" : "Skriv ett ämne och tryck på Enter",
+ "Your profile fields are saved." : "Dina profilfält har sparats.",
+ "Your profile fields could not be saved." : "Dina profilfält kunde inte sparas.",
+ "Profile fields" : "Profilfält",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute." : "Extra fält på kollegors kort i personwidgeten, till exempel kontorsplats eller expertis. Personerna fyller i dem i sina personliga inställningar, eller så kommer de från ett LDAP-attribut.",
+ "New field" : "Nytt fält",
+ "Tags" : "Taggar",
+ "Filled by" : "Fylls i av",
+ "The person" : "Personen själv",
+ "LDAP attribute" : "LDAP-attribut",
+ "Who may see it" : "Vem får se det",
+ "Everyone" : "Alla",
+ "People in the same group" : "Personer i samma grupp",
+ "Searchable in the people widget" : "Sökbart i personwidgeten",
+ "Shown on the card" : "Visas på kortet",
+ "Remove field" : "Ta bort fält",
+ "Profile fields saved." : "Profilfälten har sparats.",
+ "Save profile fields" : "Spara profilfält",
+ "The profile fields could not be loaded." : "Profilfälten kunde inte läsas in.",
+ "The profile fields could not be saved." : "Profilfälten kunde inte sparas."
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/sv.json b/l10n/sv.json
index 8b7b314d9..01609597c 100644
--- a/l10n/sv.json
+++ b/l10n/sv.json
@@ -1260,6 +1260,34 @@
"Read receipts at the top of the dashboard show who has confirmed.": "Läskvitton högst upp på instrumentpanelen visar vem som har bekräftat.",
"{emoji}: {count} reactions": "{emoji}: {count} reaktioner",
"React with {emoji}": "Reagera med {emoji}",
- "{names} and {count} more": "{names} och {count} till"
+ "{names} and {count} more": "{names} och {count} till",
+ "Search people": "Sök personer",
+ "Search by name, email or expertise…": "Sök på namn, e-post eller expertis…",
+ "Find everyone tagged {tag}": "Hitta alla med taggen {tag}",
+ "LaunchPad profile": "LaunchPad-profil",
+ "Colleagues see these fields on your card in the people widget and can find you by them.": "Kollegor ser de här fälten på ditt kort i personwidgeten och kan hitta dig med dem.",
+ "Your profile fields could not be loaded.": "Dina profilfält kunde inte läsas in.",
+ "Not filled in": "Inte ifyllt",
+ "Filled from your organisation’s directory. You cannot change it here.": "Hämtas från din organisations katalog. Du kan inte ändra det här.",
+ "Type a subject and press Enter": "Skriv ett ämne och tryck på Enter",
+ "Your profile fields are saved.": "Dina profilfält har sparats.",
+ "Your profile fields could not be saved.": "Dina profilfält kunde inte sparas.",
+ "Profile fields": "Profilfält",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute.": "Extra fält på kollegors kort i personwidgeten, till exempel kontorsplats eller expertis. Personerna fyller i dem i sina personliga inställningar, eller så kommer de från ett LDAP-attribut.",
+ "New field": "Nytt fält",
+ "Tags": "Taggar",
+ "Filled by": "Fylls i av",
+ "The person": "Personen själv",
+ "LDAP attribute": "LDAP-attribut",
+ "Who may see it": "Vem får se det",
+ "Everyone": "Alla",
+ "People in the same group": "Personer i samma grupp",
+ "Searchable in the people widget": "Sökbart i personwidgeten",
+ "Shown on the card": "Visas på kortet",
+ "Remove field": "Ta bort fält",
+ "Profile fields saved.": "Profilfälten har sparats.",
+ "Save profile fields": "Spara profilfält",
+ "The profile fields could not be loaded.": "Profilfälten kunde inte läsas in.",
+ "The profile fields could not be saved.": "Profilfälten kunde inte sparas."
}
}
diff --git a/l10n/tr.js b/l10n/tr.js
index af83b5161..c6aedbaa1 100644
--- a/l10n/tr.js
+++ b/l10n/tr.js
@@ -1293,7 +1293,35 @@ OC.L10N.register(
"Read receipts at the top of the dashboard show who has confirmed." : "Panonun üstündeki okundu bilgileri kimin onayladığını gösterir.",
"{emoji}: {count} reactions" : "{emoji}: {count} tepki",
"React with {emoji}" : "{emoji} ile tepki ver",
- "{names} and {count} more" : "{names} ve {count} kişi daha"
+ "{names} and {count} more" : "{names} ve {count} kişi daha",
+ "Search people" : "Kişi ara",
+ "Search by name, email or expertise…" : "Ada, e-posta veya uzmanlığa göre ara…",
+ "Find everyone tagged {tag}" : "{tag} etiketli herkesi bul",
+ "LaunchPad profile" : "LaunchPad profili",
+ "Colleagues see these fields on your card in the people widget and can find you by them." : "Meslektaşların bu alanları kişiler bileşenindeki kartında görür ve seni bunlarla bulabilir.",
+ "Your profile fields could not be loaded." : "Profil alanların yüklenemedi.",
+ "Not filled in" : "Doldurulmadı",
+ "Filled from your organisation’s directory. You cannot change it here." : "Kuruluşunun dizininden doldurulur. Burada değiştiremezsin.",
+ "Type a subject and press Enter" : "Bir konu yaz ve Enter'a bas",
+ "Your profile fields are saved." : "Profil alanların kaydedildi.",
+ "Your profile fields could not be saved." : "Profil alanların kaydedilemedi.",
+ "Profile fields" : "Profil alanları",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute." : "Kişiler bileşenindeki meslektaş kartlarında ofis konumu veya uzmanlık gibi ek alanlar. Kişiler bunları kişisel ayarlarında doldurur ya da bir LDAP özniteliğinden gelir.",
+ "New field" : "Yeni alan",
+ "Tags" : "Etiketler",
+ "Filled by" : "Dolduran",
+ "The person" : "Kişinin kendisi",
+ "LDAP attribute" : "LDAP özniteliği",
+ "Who may see it" : "Kimler görebilir",
+ "Everyone" : "Herkes",
+ "People in the same group" : "Aynı gruptaki kişiler",
+ "Searchable in the people widget" : "Kişiler bileşeninde aranabilir",
+ "Shown on the card" : "Kartta gösterilir",
+ "Remove field" : "Alanı kaldır",
+ "Profile fields saved." : "Profil alanları kaydedildi.",
+ "Save profile fields" : "Profil alanlarını kaydet",
+ "The profile fields could not be loaded." : "Profil alanları yüklenemedi.",
+ "The profile fields could not be saved." : "Profil alanları kaydedilemedi."
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/tr.json b/l10n/tr.json
index 400ce5b09..377f35188 100644
--- a/l10n/tr.json
+++ b/l10n/tr.json
@@ -1260,6 +1260,34 @@
"Read receipts at the top of the dashboard show who has confirmed.": "Panonun üstündeki okundu bilgileri kimin onayladığını gösterir.",
"{emoji}: {count} reactions": "{emoji}: {count} tepki",
"React with {emoji}": "{emoji} ile tepki ver",
- "{names} and {count} more": "{names} ve {count} kişi daha"
+ "{names} and {count} more": "{names} ve {count} kişi daha",
+ "Search people": "Kişi ara",
+ "Search by name, email or expertise…": "Ada, e-posta veya uzmanlığa göre ara…",
+ "Find everyone tagged {tag}": "{tag} etiketli herkesi bul",
+ "LaunchPad profile": "LaunchPad profili",
+ "Colleagues see these fields on your card in the people widget and can find you by them.": "Meslektaşların bu alanları kişiler bileşenindeki kartında görür ve seni bunlarla bulabilir.",
+ "Your profile fields could not be loaded.": "Profil alanların yüklenemedi.",
+ "Not filled in": "Doldurulmadı",
+ "Filled from your organisation’s directory. You cannot change it here.": "Kuruluşunun dizininden doldurulur. Burada değiştiremezsin.",
+ "Type a subject and press Enter": "Bir konu yaz ve Enter'a bas",
+ "Your profile fields are saved.": "Profil alanların kaydedildi.",
+ "Your profile fields could not be saved.": "Profil alanların kaydedilemedi.",
+ "Profile fields": "Profil alanları",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute.": "Kişiler bileşenindeki meslektaş kartlarında ofis konumu veya uzmanlık gibi ek alanlar. Kişiler bunları kişisel ayarlarında doldurur ya da bir LDAP özniteliğinden gelir.",
+ "New field": "Yeni alan",
+ "Tags": "Etiketler",
+ "Filled by": "Dolduran",
+ "The person": "Kişinin kendisi",
+ "LDAP attribute": "LDAP özniteliği",
+ "Who may see it": "Kimler görebilir",
+ "Everyone": "Herkes",
+ "People in the same group": "Aynı gruptaki kişiler",
+ "Searchable in the people widget": "Kişiler bileşeninde aranabilir",
+ "Shown on the card": "Kartta gösterilir",
+ "Remove field": "Alanı kaldır",
+ "Profile fields saved.": "Profil alanları kaydedildi.",
+ "Save profile fields": "Profil alanlarını kaydet",
+ "The profile fields could not be loaded.": "Profil alanları yüklenemedi.",
+ "The profile fields could not be saved.": "Profil alanları kaydedilemedi."
}
}
diff --git a/l10n/uk.js b/l10n/uk.js
index 34458a71c..eee0990e0 100644
--- a/l10n/uk.js
+++ b/l10n/uk.js
@@ -1293,7 +1293,35 @@ OC.L10N.register(
"Read receipts at the top of the dashboard show who has confirmed." : "Квитанції про прочитання вгорі панелі показують, хто підтвердив.",
"{emoji}: {count} reactions" : "{emoji}: реакцій {count}",
"React with {emoji}" : "Відреагувати {emoji}",
- "{names} and {count} more" : "{names} та ще {count}"
+ "{names} and {count} more" : "{names} та ще {count}",
+ "Search people" : "Пошук людей",
+ "Search by name, email or expertise…" : "Пошук за ім'ям, електронною поштою або експертизою…",
+ "Find everyone tagged {tag}" : "Знайти всіх з тегом {tag}",
+ "LaunchPad profile" : "Профіль LaunchPad",
+ "Colleagues see these fields on your card in the people widget and can find you by them." : "Колеги бачать ці поля на вашій картці у віджеті людей і можуть знайти вас за ними.",
+ "Your profile fields could not be loaded." : "Не вдалося завантажити поля вашого профілю.",
+ "Not filled in" : "Не заповнено",
+ "Filled from your organisation’s directory. You cannot change it here." : "Заповнюється з каталогу вашої організації. Тут це змінити не можна.",
+ "Type a subject and press Enter" : "Введіть тему й натисніть Enter",
+ "Your profile fields are saved." : "Поля вашого профілю збережено.",
+ "Your profile fields could not be saved." : "Не вдалося зберегти поля вашого профілю.",
+ "Profile fields" : "Поля профілю",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute." : "Додаткові поля на картках колег у віджеті людей, наприклад розташування офісу або експертиза. Люди заповнюють їх в особистих налаштуваннях, або вони беруться з атрибута LDAP.",
+ "New field" : "Нове поле",
+ "Tags" : "Теги",
+ "Filled by" : "Хто заповнює",
+ "The person" : "Сама людина",
+ "LDAP attribute" : "Атрибут LDAP",
+ "Who may see it" : "Хто може бачити",
+ "Everyone" : "Усі",
+ "People in the same group" : "Люди з тієї ж групи",
+ "Searchable in the people widget" : "Доступно для пошуку у віджеті людей",
+ "Shown on the card" : "Показується на картці",
+ "Remove field" : "Вилучити поле",
+ "Profile fields saved." : "Поля профілю збережено.",
+ "Save profile fields" : "Зберегти поля профілю",
+ "The profile fields could not be loaded." : "Не вдалося завантажити поля профілю.",
+ "The profile fields could not be saved." : "Не вдалося зберегти поля профілю."
},
"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"
);
diff --git a/l10n/uk.json b/l10n/uk.json
index 3d9ae2751..50e244f2e 100644
--- a/l10n/uk.json
+++ b/l10n/uk.json
@@ -1261,6 +1261,34 @@
"Read receipts at the top of the dashboard show who has confirmed.": "Квитанції про прочитання вгорі панелі показують, хто підтвердив.",
"{emoji}: {count} reactions": "{emoji}: реакцій {count}",
"React with {emoji}": "Відреагувати {emoji}",
- "{names} and {count} more": "{names} та ще {count}"
+ "{names} and {count} more": "{names} та ще {count}",
+ "Search people": "Пошук людей",
+ "Search by name, email or expertise…": "Пошук за ім'ям, електронною поштою або експертизою…",
+ "Find everyone tagged {tag}": "Знайти всіх з тегом {tag}",
+ "LaunchPad profile": "Профіль LaunchPad",
+ "Colleagues see these fields on your card in the people widget and can find you by them.": "Колеги бачать ці поля на вашій картці у віджеті людей і можуть знайти вас за ними.",
+ "Your profile fields could not be loaded.": "Не вдалося завантажити поля вашого профілю.",
+ "Not filled in": "Не заповнено",
+ "Filled from your organisation’s directory. You cannot change it here.": "Заповнюється з каталогу вашої організації. Тут це змінити не можна.",
+ "Type a subject and press Enter": "Введіть тему й натисніть Enter",
+ "Your profile fields are saved.": "Поля вашого профілю збережено.",
+ "Your profile fields could not be saved.": "Не вдалося зберегти поля вашого профілю.",
+ "Profile fields": "Поля профілю",
+ "Extra fields on colleague cards in the people widget, such as office location or expertise. People fill them in on their personal settings, or they come from an LDAP attribute.": "Додаткові поля на картках колег у віджеті людей, наприклад розташування офісу або експертиза. Люди заповнюють їх в особистих налаштуваннях, або вони беруться з атрибута LDAP.",
+ "New field": "Нове поле",
+ "Tags": "Теги",
+ "Filled by": "Хто заповнює",
+ "The person": "Сама людина",
+ "LDAP attribute": "Атрибут LDAP",
+ "Who may see it": "Хто може бачити",
+ "Everyone": "Усі",
+ "People in the same group": "Люди з тієї ж групи",
+ "Searchable in the people widget": "Доступно для пошуку у віджеті людей",
+ "Shown on the card": "Показується на картці",
+ "Remove field": "Вилучити поле",
+ "Profile fields saved.": "Поля профілю збережено.",
+ "Save profile fields": "Зберегти поля профілю",
+ "The profile fields could not be loaded.": "Не вдалося завантажити поля профілю.",
+ "The profile fields could not be saved.": "Не вдалося зберегти поля профілю."
}
}
diff --git a/src/components/Widgets/Renderers/PeopleWidget.vue b/src/components/Widgets/Renderers/PeopleWidget.vue
index c775028d8..c0302f331 100644
--- a/src/components/Widgets/Renderers/PeopleWidget.vue
+++ b/src/components/Widgets/Renderers/PeopleWidget.vue
@@ -11,7 +11,9 @@
type="search"
class="people-widget__search"
:aria-label="t('launchpad', 'Search people')"
- :placeholder="t('launchpad', 'Search by name or email…')" />
+ :placeholder="
+ t('launchpad', 'Search by name, email or expertise…')
+ " />
+
+
{{ t('launchpad', 'Ready-made templates') }}
+
+ {{
+ t(
+ 'launchpad',
+ 'Add a template that comes with LaunchPad. Then edit it to choose who gets it.',
+ )
+ }}
+
+
+
+ {{ shipped.name }}
+ {{
+ shipped.description
+ }}
+
+ {{
+ t(
+ 'launchpad',
+ 'No app here provides these widgets, so they will show empty: {widgets}',
+ { widgets: shipped.missingWidgets.join(', ') },
+ )
+ }}
+
+
@@ -57,6 +121,12 @@
@click="openResyncModal(template)">
{{ t('launchpad', 'Re-sync to existing copies') }}
+
+ {{ t('launchpad', 'Download') }}
+
{{ t('launchpad', 'Edit') }}
@@ -117,6 +187,9 @@ export default {
data() {
return {
templates: [],
+ shippedTemplates: [],
+ installingId: null,
+ errorMessage: '',
isEditorOpen: false,
editingTemplate: null,
resyncingTemplate: null,
@@ -126,6 +199,7 @@ export default {
/** @spec openspec/specs/admin-templates/spec.md */
created() {
this.loadTemplates()
+ this.loadShippedTemplates()
},
methods: {
@@ -141,6 +215,84 @@ export default {
}
},
+ /**
+ * Load the templates LaunchPad ships with. A failure hides the
+ * section and says so, the list of templates below still works.
+ *
+ * @spec openspec/specs/admin-templates/spec.md#req-tmpl-018
+ */
+ async loadShippedTemplates() {
+ try {
+ const { data } = await api.getShippedTemplates()
+ this.shippedTemplates = Array.isArray(data) ? data : []
+ } catch (error) {
+ logger.error('Failed to load shipped templates:', error)
+ this.errorMessage = t(
+ 'launchpad',
+ 'The ready-made templates could not be loaded.',
+ )
+ }
+ },
+
+ /**
+ * Add a shipped template, then show it in the list.
+ *
+ * @param {object} shipped The shipped template to add.
+ * @spec openspec/specs/admin-templates/spec.md#req-tmpl-018
+ */
+ async installShipped(shipped) {
+ this.installingId = shipped.id
+ this.errorMessage = ''
+ try {
+ await api.installShippedTemplate(shipped.id)
+ await Promise.all([
+ this.loadTemplates(),
+ this.loadShippedTemplates(),
+ ])
+ } catch (error) {
+ logger.error('Failed to add shipped template:', error)
+ this.errorMessage = t(
+ 'launchpad',
+ 'The template "{name}" could not be added.',
+ { name: shipped.name },
+ )
+ } finally {
+ this.installingId = null
+ }
+ },
+
+ /**
+ * Download one template as an archive that another LaunchPad can
+ * import (REQ-EXIM-012).
+ *
+ * @param {object} template The template to download.
+ * @spec openspec/specs/dashboard-export-import/spec.md#req-exim-012
+ */
+ async downloadTemplate(template) {
+ this.errorMessage = ''
+ try {
+ const response = await api.exportDashboards({
+ scope: 'dashboard',
+ dashboardUuid: template.uuid,
+ })
+ const url = window.URL.createObjectURL(response.data)
+ const link = document.createElement('a')
+ link.href = url
+ link.download = `launchpad-template-${template.slug || template.uuid}.zip`
+ document.body.appendChild(link)
+ link.click()
+ document.body.removeChild(link)
+ window.URL.revokeObjectURL(url)
+ } catch (error) {
+ logger.error('Failed to download template:', error)
+ this.errorMessage = t(
+ 'launchpad',
+ 'The template "{name}" could not be downloaded.',
+ { name: template.name },
+ )
+ }
+ },
+
/** @spec openspec/specs/admin-templates/spec.md */
createTemplate() {
this.editingTemplate = null
@@ -290,4 +442,17 @@ export default {
.launchpad-admin__empty {
padding: 48px 0;
}
+
+.launchpad-admin__shipped {
+ margin-bottom: 24px;
+}
+
+.launchpad-admin__shipped h4 {
+ margin: 0 0 4px;
+}
+
+.launchpad-admin__error {
+ color: var(--color-error);
+ margin-bottom: 16px;
+}
diff --git a/src/components/admin/tabs/__tests__/TemplatesPage.shipped.spec.js b/src/components/admin/tabs/__tests__/TemplatesPage.shipped.spec.js
new file mode 100644
index 000000000..84e1721f4
--- /dev/null
+++ b/src/components/admin/tabs/__tests__/TemplatesPage.shipped.spec.js
@@ -0,0 +1,245 @@
+/**
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
+ * SPDX-License-Identifier: EUPL-1.2
+ *
+ * TemplatesPage: the ready-made templates section and the per-template
+ * download (admin-templates REQ-TMPL-018, dashboard-export-import
+ * REQ-EXIM-012). Payloads are the shapes ShippedTemplateService::listTemplates
+ * and Dashboard::jsonSerialize return.
+ */
+
+import { flushPromises, mount } from '@vue/test-utils'
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+
+vi.mock('@nextcloud/l10n', () => ({
+ t: (_app, text, vars = {}) =>
+ text.replace(/\{(\w+)\}/g, (_m, k) => vars[k] ?? `{${k}}`),
+}))
+vi.mock('@conduction/nextcloud-vue', () => ({
+ NcButton: {
+ name: 'NcButton',
+ props: ['disabled'],
+ emits: ['click'],
+ template:
+ '',
+ },
+ NcEmptyContent: { name: 'NcEmptyContent', template: '' },
+ CnDashboardIcon: { name: 'CnDashboardIcon', template: '' },
+}))
+vi.mock('../../../../utils/logger.js', () => ({ logger: { error: vi.fn() } }))
+
+const { api } = vi.hoisted(() => ({
+ api: {
+ getAdminTemplates: vi.fn(),
+ getShippedTemplates: vi.fn(),
+ installShippedTemplate: vi.fn(),
+ exportDashboards: vi.fn(),
+ },
+}))
+vi.mock('../../../../services/api.js', () => ({ api }))
+// The two modals are the page's own children and not under test here.
+vi.mock('../../../../modals/TemplateEditorModal.vue', () => ({
+ default: { name: 'TemplateEditorModal', template: '' },
+}))
+vi.mock('../../../../modals/TemplateResyncModal.vue', () => ({
+ default: { name: 'TemplateResyncModal', template: '' },
+}))
+
+import TemplatesPage from '../TemplatesPage.vue'
+
+function shipped(overrides = {}) {
+ return {
+ id: 'mijn-werkdag',
+ name: 'Mijn werkdag',
+ description: 'Startpagina voor medewerkers',
+ language: 'nl',
+ version: 1,
+ widgetCount: 8,
+ isInstalled: false,
+ installedUuid: '',
+ installedVersion: null,
+ missingWidgets: [],
+ ...overrides,
+ }
+}
+
+const installedTemplate = {
+ id: 4,
+ uuid: 'uuid-4',
+ name: 'Mijn werkdag',
+ icon: null,
+ slug: null,
+ isDefault: 0,
+ targetGroups: [],
+}
+
+function mountPage() {
+ return mount(TemplatesPage, {
+ global: {
+ stubs: {
+ TemplateResyncModal: true,
+ TemplateEditorModal: true,
+ Plus: true,
+ ViewDashboard: true,
+ },
+ },
+ })
+}
+
+beforeEach(() => {
+ vi.clearAllMocks()
+ api.getAdminTemplates.mockResolvedValue({ data: [] })
+ api.getShippedTemplates.mockResolvedValue({ data: [shipped()] })
+})
+
+describe('TemplatesPage ready-made templates', () => {
+ it('lists a shipped template with an add button', async () => {
+ const wrapper = mountPage()
+ await flushPromises()
+
+ const row = wrapper.find(
+ '[data-testid="admin-shipped-template-mijn-werkdag"]',
+ )
+ expect(row.exists()).toBe(true)
+ expect(row.text()).toContain('Mijn werkdag')
+ expect(row.text()).toContain('Startpagina voor medewerkers')
+ expect(row.find('[data-testid="admin-shipped-template-add"]').exists()).toBe(
+ true,
+ )
+ expect(
+ row.find('[data-testid="admin-shipped-template-added"]').exists(),
+ ).toBe(false)
+ expect(
+ row.find('[data-testid="admin-shipped-template-missing"]').exists(),
+ ).toBe(false)
+ })
+
+ it('adds the template, then shows it in the list and marks it added', async () => {
+ api.installShippedTemplate.mockResolvedValue({
+ data: { uuid: 'uuid-4', alreadyInstalled: false },
+ })
+ const wrapper = mountPage()
+ await flushPromises()
+ api.getAdminTemplates.mockResolvedValue({ data: [installedTemplate] })
+ api.getShippedTemplates.mockResolvedValue({
+ data: [shipped({ isInstalled: true, installedUuid: 'uuid-4' })],
+ })
+
+ await wrapper
+ .find('[data-testid="admin-shipped-template-add"]')
+ .trigger('click')
+ await flushPromises()
+
+ expect(api.installShippedTemplate).toHaveBeenCalledTimes(1)
+ expect(api.installShippedTemplate).toHaveBeenCalledWith('mijn-werkdag')
+ expect(
+ wrapper.find('[data-testid="admin-shipped-template-added"]').exists(),
+ ).toBe(true)
+ expect(
+ wrapper.find('[data-testid="admin-shipped-template-add"]').exists(),
+ ).toBe(false)
+ expect(wrapper.find('.launchpad-admin__templates').text()).toContain(
+ 'Mijn werkdag',
+ )
+ expect(wrapper.find('[data-testid="admin-templates-error"]').exists()).toBe(
+ false,
+ )
+ })
+
+ it('says so when adding fails, and leaves the button', async () => {
+ api.installShippedTemplate.mockRejectedValue(new Error('500'))
+ const wrapper = mountPage()
+ await flushPromises()
+
+ await wrapper
+ .find('[data-testid="admin-shipped-template-add"]')
+ .trigger('click')
+ await flushPromises()
+
+ expect(wrapper.find('[data-testid="admin-templates-error"]').text()).toBe(
+ 'The template "Mijn werkdag" could not be added.',
+ )
+ expect(
+ wrapper
+ .find('[data-testid="admin-shipped-template-add"]')
+ .attributes('disabled'),
+ ).toBeUndefined()
+ })
+
+ it('names the widgets no app provides', async () => {
+ api.getShippedTemplates.mockResolvedValue({
+ data: [shipped({ missingWidgets: ['decidesk', 'activity'] })],
+ })
+ const wrapper = mountPage()
+ await flushPromises()
+
+ expect(
+ wrapper.find('[data-testid="admin-shipped-template-missing"]').text(),
+ ).toBe(
+ 'No app here provides these widgets, so they will show empty: decidesk, activity',
+ )
+ })
+
+ it('says so when the shipped list fails, and still lists the templates', async () => {
+ api.getShippedTemplates.mockRejectedValue(new Error('500'))
+ api.getAdminTemplates.mockResolvedValue({ data: [installedTemplate] })
+ const wrapper = mountPage()
+ await flushPromises()
+
+ expect(
+ wrapper.find('[data-testid="admin-shipped-templates"]').exists(),
+ ).toBe(false)
+ expect(wrapper.find('[data-testid="admin-templates-error"]').text()).toBe(
+ 'The ready-made templates could not be loaded.',
+ )
+ expect(wrapper.find('.launchpad-admin__templates').text()).toContain(
+ 'Mijn werkdag',
+ )
+ })
+})
+
+describe('TemplatesPage download', () => {
+ it('asks for a one-dashboard export of that template', async () => {
+ api.getAdminTemplates.mockResolvedValue({ data: [installedTemplate] })
+ api.exportDashboards.mockResolvedValue({ data: new Blob(['zip']) })
+ window.URL.createObjectURL = vi.fn(() => 'blob:x')
+ window.URL.revokeObjectURL = vi.fn()
+ const click = vi
+ .spyOn(HTMLAnchorElement.prototype, 'click')
+ .mockImplementation(() => {})
+ const wrapper = mountPage()
+ await flushPromises()
+
+ await wrapper
+ .find('[data-testid="admin-download-template"]')
+ .trigger('click')
+ await flushPromises()
+
+ expect(api.exportDashboards).toHaveBeenCalledWith({
+ scope: 'dashboard',
+ dashboardUuid: 'uuid-4',
+ })
+ expect(click).toHaveBeenCalledTimes(1)
+ expect(click.mock.instances[0].download).toBe(
+ 'launchpad-template-uuid-4.zip',
+ )
+ expect(window.URL.revokeObjectURL).toHaveBeenCalledWith('blob:x')
+ click.mockRestore()
+ })
+
+ it('says so when the download fails', async () => {
+ api.getAdminTemplates.mockResolvedValue({ data: [installedTemplate] })
+ api.exportDashboards.mockRejectedValue(new Error('404'))
+ const wrapper = mountPage()
+ await flushPromises()
+
+ await wrapper
+ .find('[data-testid="admin-download-template"]')
+ .trigger('click')
+ await flushPromises()
+
+ expect(wrapper.find('[data-testid="admin-templates-error"]').text()).toBe(
+ 'The template "Mijn werkdag" could not be downloaded.',
+ )
+ })
+})
diff --git a/src/services/api.js b/src/services/api.js
index bb793cdc1..a91ff6b35 100644
--- a/src/services/api.js
+++ b/src/services/api.js
@@ -769,6 +769,27 @@ export const api = {
return axios.get(`${baseUrl}/api/admin/templates`)
},
+ /**
+ * List the templates LaunchPad ships with, and whether each is installed.
+ *
+ * @return {Promise} Axios response resolving to the shipped templates.
+ * @spec openspec/specs/admin-templates/spec.md#req-tmpl-018
+ */
+ getShippedTemplates() {
+ return axios.get(`${baseUrl}/api/admin/templates/shipped`)
+ },
+
+ /**
+ * Add a shipped template as an admin template.
+ *
+ * @param {string} id Shipped template id, for example `mijn-werkdag`.
+ * @return {Promise} Axios response resolving to the installed template.
+ * @spec openspec/specs/admin-templates/spec.md#req-tmpl-018
+ */
+ installShippedTemplate(id) {
+ return axios.post(`${baseUrl}/api/admin/templates/shipped/${id}/install`)
+ },
+
/**
* Create an admin dashboard template.
*
diff --git a/tests/Unit/Command/TemplateInstallCommandTest.php b/tests/Unit/Command/TemplateInstallCommandTest.php
new file mode 100644
index 000000000..d503913fa
--- /dev/null
+++ b/tests/Unit/Command/TemplateInstallCommandTest.php
@@ -0,0 +1,123 @@
+
+ * SPDX-License-Identifier: EUPL-1.2
+ */
+
+declare(strict_types=1);
+
+namespace OCA\LaunchPad\Tests\Unit\Command;
+
+use InvalidArgumentException;
+use OCA\LaunchPad\Command\TemplateInstallCommand;
+use OCA\LaunchPad\Service\AdminSettingsService;
+use OCA\LaunchPad\Service\ShippedTemplateService;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+use RuntimeException;
+use Symfony\Component\Console\Tester\CommandTester;
+
+/**
+ * `launchpad:template:install` (REQ-CLI-012): what it passes on, what it
+ * prints and how it exits. The install itself is ShippedTemplateServiceTest.
+ */
+class TemplateInstallCommandTest extends TestCase {
+ /** @var ShippedTemplateService&MockObject */
+ private $templates;
+
+ protected function setUp(): void {
+ parent::setUp();
+ $this->templates = $this->createMock(ShippedTemplateService::class);
+ }
+
+ /**
+ * @param array $overrides Result fields to override.
+ *
+ * @return array
+ */
+ private static function installResult(array $overrides = []): array {
+ return $overrides + [
+ 'templateId' => 'mijn-werkdag',
+ 'uuid' => 'uuid-1',
+ 'id' => 3,
+ 'version' => 1,
+ 'alreadyInstalled' => false,
+ 'targetGroups' => [],
+ 'isDefault' => false,
+ 'missingWidgets' => [],
+ ];
+ }
+
+ private function settings(bool $personalDashboards): AdminSettingsService {
+ $settings = $this->createMock(AdminSettingsService::class);
+ $settings->method('getSettings')->willReturn(['allowUserDashboards' => $personalDashboards]);
+ return $settings;
+ }
+
+ public function testInstallsForAGroup(): void {
+ $this->templates->expects(self::once())->method('install')
+ ->with('mijn-werkdag', ['medewerkers', 'bestuur'], true, false)
+ ->willReturn(self::installResult(['targetGroups' => ['medewerkers', 'bestuur'], 'isDefault' => true]));
+
+ $tester = new CommandTester(new TemplateInstallCommand($this->templates, $this->settings(true)));
+ $exit = $tester->execute([
+ 'id' => 'mijn-werkdag',
+ '--group' => ['medewerkers', 'bestuur'],
+ '--default' => true,
+ ]);
+
+ self::assertSame(0, $exit);
+ $display = $tester->getDisplay();
+ self::assertStringContainsString('Installed template mijn-werkdag version 1 (UUID: uuid-1).', $display);
+ self::assertStringContainsString('Groups: medewerkers, bestuur. Default for everyone: yes.', $display);
+ self::assertStringNotContainsString('registers', $display);
+ self::assertStringNotContainsString('Personal dashboards are off', $display);
+ }
+
+ public function testASecondRunSaysSoAndPassesForce(): void {
+ $this->templates->expects(self::once())->method('install')
+ ->with('mijn-werkdag', [], false, true)
+ ->willReturn(self::installResult(['alreadyInstalled' => true, 'missingWidgets' => ['decidesk']]));
+
+ $tester = new CommandTester(new TemplateInstallCommand($this->templates, $this->settings(false)));
+ $exit = $tester->execute(['id' => 'mijn-werkdag', '--force' => true]);
+
+ self::assertSame(0, $exit);
+ $display = $tester->getDisplay();
+ self::assertStringContainsString('Already installed: template mijn-werkdag', $display);
+ self::assertStringContainsString('Groups: (none). Default for everyone: no.', $display);
+ self::assertStringContainsString('decidesk', $display);
+ self::assertStringContainsString('Personal dashboards are off. Members will see this template read-only', $display);
+ }
+
+ public function testAnUnknownGroupExitsOne(): void {
+ $this->templates->method('install')
+ ->willThrowException(new InvalidArgumentException('Unknown group: medewerkerz'));
+
+ $tester = new CommandTester(new TemplateInstallCommand($this->templates, $this->settings(true)));
+
+ self::assertSame(1, $tester->execute(['id' => 'mijn-werkdag', '--group' => ['medewerkerz']]));
+ self::assertStringContainsString('Unknown group: medewerkerz', $tester->getDisplay());
+ }
+
+ public function testAnUnknownIdExitsOne(): void {
+ $this->templates->method('install')->willThrowException(
+ new InvalidArgumentException('Unknown template: nope. Shipped templates: mijn-werkdag')
+ );
+
+ $tester = new CommandTester(new TemplateInstallCommand($this->templates, $this->settings(true)));
+
+ self::assertSame(1, $tester->execute(['id' => 'nope']));
+ self::assertStringContainsString('Shipped templates: mijn-werkdag', $tester->getDisplay());
+ }
+
+ public function testAFailedInstallExitsOne(): void {
+ $this->templates->method('install')->willThrowException(new RuntimeException('disk full'));
+
+ $tester = new CommandTester(new TemplateInstallCommand($this->templates, $this->settings(true)));
+
+ self::assertSame(1, $tester->execute(['id' => 'mijn-werkdag']));
+ self::assertStringContainsString('Installation failed: disk full', $tester->getDisplay());
+ }
+}
diff --git a/tests/Unit/Controller/AdminShippedTemplateControllerTest.php b/tests/Unit/Controller/AdminShippedTemplateControllerTest.php
new file mode 100644
index 000000000..42d1b4135
--- /dev/null
+++ b/tests/Unit/Controller/AdminShippedTemplateControllerTest.php
@@ -0,0 +1,123 @@
+
+ * SPDX-License-Identifier: EUPL-1.2
+ */
+
+declare(strict_types=1);
+
+namespace Unit\Controller;
+
+use InvalidArgumentException;
+use OCA\LaunchPad\Controller\AdminShippedTemplateController;
+use OCA\LaunchPad\Service\ShippedTemplateService;
+use OCP\AppFramework\Http;
+use OCP\IGroupManager;
+use OCP\IRequest;
+use OCP\IUser;
+use OCP\IUserSession;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+use Psr\Log\NullLogger;
+use RuntimeException;
+
+/**
+ * The shipped template endpoints (REQ-TMPL-018): who may call them and what
+ * each outcome answers.
+ */
+class AdminShippedTemplateControllerTest extends TestCase {
+ /** @var ShippedTemplateService&MockObject */
+ private $service;
+
+ /** @var IGroupManager&MockObject */
+ private $groupManager;
+
+ /** @var IUserSession&MockObject */
+ private $userSession;
+
+ private AdminShippedTemplateController $controller;
+
+ protected function setUp(): void {
+ $this->service = $this->createMock(ShippedTemplateService::class);
+ $this->groupManager = $this->createMock(IGroupManager::class);
+ $this->userSession = $this->createMock(IUserSession::class);
+
+ $this->controller = new AdminShippedTemplateController(
+ request: $this->createMock(IRequest::class),
+ templates: $this->service,
+ userSession: $this->userSession,
+ groupManager: $this->groupManager,
+ logger: new NullLogger(),
+ );
+ }
+
+ private function loginAs(string $uid, bool $admin): void {
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn($uid);
+ $this->userSession->method('getUser')->willReturn($user);
+ $this->groupManager->method('isAdmin')->with($uid)->willReturn($admin);
+ }
+
+ public function testNobodySignedInGets401AndNothingIsInstalled(): void {
+ $this->userSession->method('getUser')->willReturn(null);
+ $this->service->expects(self::never())->method('install');
+ $this->service->expects(self::never())->method('listTemplates');
+
+ self::assertSame(Http::STATUS_UNAUTHORIZED, $this->controller->index()->getStatus());
+ self::assertSame(Http::STATUS_UNAUTHORIZED, $this->controller->install(id: 'mijn-werkdag')->getStatus());
+ }
+
+ public function testANonAdminGets403AndNothingIsInstalled(): void {
+ $this->loginAs('bob', false);
+ $this->service->expects(self::never())->method('install');
+ $this->service->expects(self::never())->method('listTemplates');
+
+ self::assertSame(Http::STATUS_FORBIDDEN, $this->controller->index()->getStatus());
+ self::assertSame(Http::STATUS_FORBIDDEN, $this->controller->install(id: 'mijn-werkdag')->getStatus());
+ }
+
+ public function testAnAdminGetsTheListing(): void {
+ $this->loginAs('alice', true);
+ $this->service->method('listTemplates')->willReturn([['id' => 'mijn-werkdag']]);
+
+ $response = $this->controller->index();
+
+ self::assertSame(Http::STATUS_OK, $response->getStatus());
+ self::assertSame([['id' => 'mijn-werkdag']], $response->getData());
+ }
+
+ public function testInstallAnswers201ThenThe200OfASecondRun(): void {
+ $this->loginAs('alice', true);
+ $this->service->expects(self::exactly(2))->method('install')
+ ->with('mijn-werkdag', [], false, false, 'alice')
+ ->willReturnOnConsecutiveCalls(
+ ['uuid' => 'u1', 'alreadyInstalled' => false],
+ ['uuid' => 'u1', 'alreadyInstalled' => true],
+ );
+
+ $first = $this->controller->install(id: 'mijn-werkdag');
+ $second = $this->controller->install(id: 'mijn-werkdag');
+
+ self::assertSame(Http::STATUS_CREATED, $first->getStatus());
+ self::assertSame('u1', $first->getData()['uuid']);
+ self::assertSame(Http::STATUS_OK, $second->getStatus());
+ }
+
+ public function testAnUnknownTemplateIs404AndAFailureIs500WithoutTheReason(): void {
+ $this->loginAs('alice', true);
+ $this->service->method('install')->willReturnCallback(
+ static function (string $templateId): array {
+ if ($templateId === 'nope') {
+ throw new InvalidArgumentException('Unknown template: nope');
+ }
+ throw new RuntimeException('SQLSTATE secret detail');
+ }
+ );
+
+ self::assertSame(Http::STATUS_NOT_FOUND, $this->controller->install(id: 'nope')->getStatus());
+ $failed = $this->controller->install(id: 'mijn-werkdag');
+ self::assertSame(Http::STATUS_INTERNAL_SERVER_ERROR, $failed->getStatus());
+ self::assertSame(['error' => 'Template installation failed'], $failed->getData());
+ }
+}
diff --git a/tests/Unit/Service/DashboardServiceTemplateRungTest.php b/tests/Unit/Service/DashboardServiceTemplateRungTest.php
new file mode 100644
index 000000000..7a94c5a75
--- /dev/null
+++ b/tests/Unit/Service/DashboardServiceTemplateRungTest.php
@@ -0,0 +1,349 @@
+
+ * SPDX-License-Identifier: EUPL-1.2
+ */
+
+declare(strict_types=1);
+
+namespace Unit\Service;
+
+use OCA\LaunchPad\Db\AdminSettingMapper;
+use OCA\LaunchPad\Db\Dashboard;
+use OCA\LaunchPad\Db\DashboardMapper;
+use OCA\LaunchPad\Db\WidgetPlacementMapper;
+use OCA\LaunchPad\Service\AdminTemplateService;
+use OCA\LaunchPad\Service\DashboardFactory;
+use OCA\LaunchPad\Service\DashboardResolver;
+use OCA\LaunchPad\Service\DashboardService;
+use OCA\LaunchPad\Service\DashboardTreeService;
+use OCA\LaunchPad\Service\FooterService;
+use OCA\LaunchPad\Service\PersonalLayerService;
+use OCA\LaunchPad\Service\TemplateService;
+use OCP\IConfig;
+use OCP\IDBConnection;
+use OCP\IGroupManager;
+use OCP\L10N\IFactory;
+use PHPUnit\Framework\TestCase;
+use Psr\Log\LoggerInterface;
+
+/**
+ * REQ-TMPL-019: a member who owns no dashboard gets the admin template meant
+ * for them, from BOTH chains: the page shell (`resolveActiveDashboard`, what
+ * decides whether the page shows a dashboard at all) and `GET /api/dashboard`
+ * (`getEffectiveDashboard`, what the grid then loads).
+ *
+ * Why both, in one file: the feature shipped working in the second chain only.
+ * Its tests were green and no member ever saw a template, because the page
+ * asks the first chain and that one did not know templates exist. Every case
+ * here asks both and holds the answers against each other.
+ *
+ * The resolver is the real DashboardResolver, so a wrong order between "has a
+ * personal dashboard" and "gets a template" shows up here.
+ */
+class DashboardServiceTemplateRungTest extends TestCase {
+ private const USER = 'sanne';
+
+ /** @var array Personal dashboards the user owns. */
+ private array $owned = [];
+
+ /** @var array Group dashboards the user can see. */
+ private array $groupDashboards = [];
+
+ private ?Dashboard $template = null;
+
+ private bool $allowPersonal = true;
+
+ private string $primaryGroup = 'behandelaars';
+
+ /** @var array The user's preferences. */
+ private array $prefs = [];
+
+ private int $copiesMade = 0;
+
+ private DashboardService $service;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $dashboardMapper = $this->createMock(DashboardMapper::class);
+ $dashboardMapper->method('findByUserId')->willReturnCallback(fn (): array => $this->owned);
+ $dashboardMapper->method('find')->willReturnCallback(
+ function (int $id): Dashboard {
+ if ($this->template !== null && $this->template->getId() === $id) {
+ return $this->template;
+ }
+ throw new \OCP\AppFramework\Db\DoesNotExistException('none');
+ }
+ );
+ $dashboardMapper->method('findVisibleToUser')->willReturnCallback(
+ fn (): array => array_merge(
+ array_map(
+ static fn (Dashboard $d): array => ['dashboard' => $d, 'source' => Dashboard::SOURCE_USER],
+ $this->owned
+ ),
+ $this->groupDashboards
+ )
+ );
+ $dashboardMapper->method('findActiveByUserId')->willReturnCallback(
+ function (): Dashboard {
+ foreach ($this->owned as $dashboard) {
+ if ($dashboard->getIsActive() === 1) {
+ return $dashboard;
+ }
+ }
+ throw new \OCP\AppFramework\Db\DoesNotExistException('none');
+ }
+ );
+
+ $placementMapper = $this->createMock(WidgetPlacementMapper::class);
+ $placementMapper->method('findByDashboardId')->willReturn([]);
+
+ $settingMapper = $this->createMock(AdminSettingMapper::class);
+ $settingMapper->method('getValue')->willReturnCallback(
+ fn (string $key, mixed $default = null): mixed => $key === 'allow_user_dashboards'
+ ? $this->allowPersonal
+ : $default
+ );
+
+ $templateService = $this->createMock(TemplateService::class);
+ $templateService->method('getApplicableTemplate')->willReturnCallback(fn (): ?Dashboard => $this->template);
+ $templateService->method('createDashboardFromTemplate')->willReturnCallback(
+ function (string $userId, Dashboard $template): Dashboard {
+ $this->copiesMade++;
+ $copy = new Dashboard();
+ $copy->setId(100 + $this->copiesMade);
+ $copy->setUuid('copy-' . $this->copiesMade);
+ $copy->setType(Dashboard::TYPE_USER);
+ $copy->setUserId($userId);
+ $copy->setBasedOnTemplate($template->getId());
+ $copy->setPermissionLevel($template->getPermissionLevel());
+ $copy->setIsActive(1);
+ $this->owned[] = $copy;
+ return $copy;
+ }
+ );
+
+ $adminTemplateService = $this->createMock(AdminTemplateService::class);
+ $adminTemplateService->method('getUserGroupIdsFor')->willReturnCallback(fn (): array => [$this->primaryGroup]);
+ $adminTemplateService->method('resolvePrimaryGroup')->willReturnCallback(fn (): string => $this->primaryGroup);
+
+ $config = $this->createMock(IConfig::class);
+ $config->method('getUserValue')->willReturnCallback(
+ fn ($uid, $app, $key, $default = ''): string => $this->prefs[$key] ?? (string)$default
+ );
+ $config->method('setUserValue')->willReturnCallback(
+ function ($uid, $app, $key, $value): void {
+ $this->prefs[$key] = (string)$value;
+ }
+ );
+
+ $this->service = new DashboardService(
+ dashboardMapper: $dashboardMapper,
+ placementMapper: $placementMapper,
+ settingMapper: $settingMapper,
+ templateService: $templateService,
+ dashboardFactory: new DashboardFactory(),
+ dashResolver: new DashboardResolver(
+ dashboardMapper: $dashboardMapper,
+ placementMapper: $placementMapper,
+ templateService: $templateService,
+ ),
+ treeService: $this->createMock(DashboardTreeService::class),
+ groupManager: $this->createMock(IGroupManager::class),
+ adminTemplateService: $adminTemplateService,
+ db: $this->createMock(IDBConnection::class),
+ config: $config,
+ l10nFactory: $this->createMock(IFactory::class),
+ logger: $this->createMock(LoggerInterface::class),
+ personalLayers: $this->createMock(PersonalLayerService::class),
+ footerService: $this->createMock(FooterService::class),
+ );
+ }
+
+ private function template(array $targetGroups): Dashboard {
+ $template = new Dashboard();
+ $template->setId(2);
+ $template->setUuid('template-uuid');
+ $template->setType(Dashboard::TYPE_ADMIN_TEMPLATE);
+ $template->setPermissionLevel(Dashboard::PERMISSION_ADD_ONLY);
+ $template->setTargetGroupsArray($targetGroups);
+ if ($targetGroups === []) {
+ $template->setIsDefault(1);
+ }
+ return $template;
+ }
+
+ private function groupDashboard(string $uuid, string $groupId, int $isDefault): array {
+ $dashboard = new Dashboard();
+ $dashboard->setId(crc32($uuid) % 1000 + 500);
+ $dashboard->setUuid($uuid);
+ $dashboard->setType(Dashboard::TYPE_GROUP_SHARED);
+ $dashboard->setGroupId($groupId);
+ $dashboard->setIsDefault($isDefault);
+ $source = Dashboard::SOURCE_GROUP;
+ if ($groupId === Dashboard::DEFAULT_GROUP_ID) {
+ $source = Dashboard::SOURCE_DEFAULT;
+ }
+ return ['dashboard' => $dashboard, 'source' => $source];
+ }
+
+ /** The dashboard everyone starts on, as SeedDefaultDashboard leaves it. */
+ private function seedEveryoneDashboard(): void {
+ $this->groupDashboards[] = $this->groupDashboard('seeded-everyone', Dashboard::DEFAULT_GROUP_ID, 1);
+ }
+
+ /**
+ * Ask the page shell, then the API, as one page load does.
+ *
+ * @return array{page: ?string, api: ?string, apiPermission: ?string}
+ */
+ private function openLaunchpad(): array {
+ $page = $this->service->resolveActiveDashboard(userId: self::USER, primaryGroupId: $this->primaryGroup);
+ $api = $this->service->getEffectiveDashboard(userId: self::USER);
+
+ return [
+ 'page' => $page === null ? null : (string)$page['dashboard']->getUuid(),
+ 'pageSource' => $page['source'] ?? null,
+ 'api' => $api === null ? null : (string)$api['dashboard']->getUuid(),
+ 'apiPermission' => $api['permissionLevel'] ?? null,
+ ];
+ }
+
+ /**
+ * The coordinator's live failure 1: the seeded dashboard for everyone won.
+ */
+ public function testAGroupTemplateBeatsTheSeededDashboardForEveryone(): void {
+ $this->seedEveryoneDashboard();
+ $this->template = $this->template(['behandelaars']);
+
+ $first = $this->openLaunchpad();
+
+ self::assertSame('copy-1', $first['page'], 'the page shell must hand out the copy, not the seeded dashboard');
+ self::assertSame(Dashboard::SOURCE_USER, $first['pageSource']);
+ self::assertSame('copy-1', $first['api'], 'the grid must load the dashboard the page chose');
+ self::assertSame(Dashboard::PERMISSION_ADD_ONLY, $first['apiPermission']);
+ self::assertSame(1, $this->copiesMade);
+ self::assertSame('copy-1', $this->prefs[DashboardService::ACTIVE_DASHBOARD_UUID_PREF_KEY]);
+ }
+
+ /**
+ * Live failure 2: with no group dashboard at all the page said
+ * "No dashboards available", because the page chain answered null.
+ */
+ public function testWithNoOtherDashboardThePageStillShowsTheTemplate(): void {
+ $this->template = $this->template(['behandelaars']);
+
+ $first = $this->openLaunchpad();
+
+ self::assertSame('copy-1', $first['page']);
+ self::assertSame('copy-1', $first['api']);
+ }
+
+ public function testASecondVisitMakesNoSecondCopy(): void {
+ $this->seedEveryoneDashboard();
+ $this->template = $this->template(['behandelaars']);
+ $this->openLaunchpad();
+
+ $second = $this->openLaunchpad();
+
+ self::assertSame(1, $this->copiesMade);
+ self::assertSame('copy-1', $second['page']);
+ self::assertSame('copy-1', $second['api']);
+ }
+
+ /**
+ * Personal dashboards off (the factory default): no copy may be made,
+ * the template itself is shown, view only, by both chains.
+ */
+ public function testWithPersonalDashboardsOffTheTemplateItselfIsShownViewOnly(): void {
+ $this->allowPersonal = false;
+ $this->seedEveryoneDashboard();
+ $this->template = $this->template(['behandelaars']);
+
+ $first = $this->openLaunchpad();
+
+ self::assertSame('template-uuid', $first['page']);
+ self::assertSame(Dashboard::SOURCE_GROUP, $first['pageSource']);
+ self::assertSame('template-uuid', $first['api']);
+ self::assertSame(Dashboard::PERMISSION_VIEW_ONLY, $first['apiPermission']);
+ self::assertSame(0, $this->copiesMade);
+ self::assertArrayNotHasKey(DashboardService::ACTIVE_DASHBOARD_UUID_PREF_KEY, $this->prefs);
+ }
+
+ public function testTheDefaultTemplateReplacesTheSeededDashboard(): void {
+ $this->seedEveryoneDashboard();
+ $this->template = $this->template([]);
+
+ $first = $this->openLaunchpad();
+
+ self::assertSame('copy-1', $first['page']);
+ self::assertSame('copy-1', $first['api']);
+ }
+
+ /**
+ * The default template is for everyone; a default dashboard of the
+ * user's own group is more specific and keeps its place.
+ */
+ public function testTheDefaultTemplateStepsAsideForTheUsersOwnGroupDefault(): void {
+ $this->seedEveryoneDashboard();
+ $this->groupDashboards[] = $this->groupDashboard('team-default', 'behandelaars', 1);
+ $this->template = $this->template([]);
+
+ $first = $this->openLaunchpad();
+
+ self::assertSame('team-default', $first['page']);
+ self::assertSame(0, $this->copiesMade);
+ }
+
+ public function testAGroupTemplateBeatsEvenTheUsersOwnGroupDefault(): void {
+ $this->groupDashboards[] = $this->groupDashboard('team-default', 'behandelaars', 1);
+ $this->template = $this->template(['behandelaars']);
+
+ self::assertSame('copy-1', $this->openLaunchpad()['page']);
+ }
+
+ /**
+ * What does NOT change for people already using LaunchPad.
+ */
+ public function testAUserWhoOwnsADashboardGetsNoTemplate(): void {
+ $own = new Dashboard();
+ $own->setId(7);
+ $own->setUuid('my-own');
+ $own->setType(Dashboard::TYPE_USER);
+ $own->setUserId(self::USER);
+ $own->setIsActive(1);
+ $this->owned[] = $own;
+ $this->seedEveryoneDashboard();
+ $this->template = $this->template(['behandelaars']);
+
+ $visit = $this->openLaunchpad();
+
+ self::assertSame(0, $this->copiesMade);
+ self::assertSame('seeded-everyone', $visit['page'], 'unchanged: the page shell ranks group dashboards first');
+ self::assertSame('my-own', $visit['api'], 'unchanged: the API returns the active personal dashboard');
+ }
+
+ public function testASavedChoiceIsKept(): void {
+ $this->seedEveryoneDashboard();
+ $this->prefs[DashboardService::ACTIVE_DASHBOARD_UUID_PREF_KEY] = 'seeded-everyone';
+ $this->template = $this->template(['behandelaars']);
+
+ $visit = $this->openLaunchpad();
+
+ self::assertSame('seeded-everyone', $visit['page']);
+ self::assertSame('seeded-everyone', $visit['api']);
+ self::assertSame(0, $this->copiesMade);
+ }
+
+ public function testWithoutAnApplicableTemplateNothingChanges(): void {
+ $this->seedEveryoneDashboard();
+
+ $visit = $this->openLaunchpad();
+
+ self::assertSame('seeded-everyone', $visit['page']);
+ self::assertSame('seeded-everyone', $visit['api']);
+ self::assertSame(0, $this->copiesMade);
+ }
+}
diff --git a/tests/Unit/Service/ImportServiceTest.php b/tests/Unit/Service/ImportServiceTest.php
index 60d08b0bc..b247da8c2 100644
--- a/tests/Unit/Service/ImportServiceTest.php
+++ b/tests/Unit/Service/ImportServiceTest.php
@@ -703,6 +703,75 @@ private function configuredTile(): WidgetPlacement {
return $tile;
}//end configuredTile()
+ /**
+ * REQ-EXIM-012: `isCompulsory` crosses for an admin template and for
+ * nothing else, and a template arrives without an owner.
+ *
+ * @return void
+ */
+ public function testCompulsoryIsCarriedForATemplateOnly(): void {
+ $seen = [];
+ foreach ([Dashboard::TYPE_ADMIN_TEMPLATE, Dashboard::TYPE_USER, Dashboard::TYPE_GROUP_SHARED] as $type) {
+ $dashboardMapper = $this->createMock(originalClassName: DashboardMapper::class);
+ $placementMapper = $this->createMock(originalClassName: WidgetPlacementMapper::class);
+ $dashboardMapper->method('findByUuid')
+ ->willThrowException(exception: new DoesNotExistException(msg: 'no'));
+ $owner = 'unset';
+ $dashboardMapper->method('insert')->willReturnCallback(
+ static function (Dashboard $dashboard) use (&$owner): Dashboard {
+ $owner = $dashboard->getUserId();
+ // phpcs:ignore CustomSniffs.Functions.NamedParameters.RequireNamedParameters
+ $dashboard->setId(7);
+ return $dashboard;
+ }
+ );
+ $compulsory = null;
+ $placementMapper->method('insert')->willReturnCallback(
+ static function (WidgetPlacement $placement) use (&$compulsory): WidgetPlacement {
+ $compulsory = $placement->getIsCompulsory();
+ return $placement;
+ }
+ );
+ $service = new ImportService(
+ dashboardMapper: $dashboardMapper,
+ placementMapper: $placementMapper,
+ db: $this->db,
+ logger: new NullLogger(),
+ );
+
+ $uuid = 'a0000000-0000-4000-8000-00000000000' . (string)count($seen);
+ $zipPath = $this->makeZip(entries: [
+ 'manifest.json' => (string)json_encode(value: ['schemaVersion' => 1, 'scope' => 'dashboard']),
+ 'dashboards/' . $uuid . '.json' => (string)json_encode(value: [
+ 'uuid' => $uuid,
+ 'name' => 'Pinned',
+ 'type' => $type,
+ 'userId' => null,
+ 'widgets' => [['widgetId' => 'text', 'isCompulsory' => 1]],
+ ]),
+ ]);
+ try {
+ $result = $service->import(zipPath: $zipPath, preserveUuids: false, currentUserId: 'admin');
+ } finally {
+ @unlink(filename: $zipPath);
+ }
+
+ self::assertSame(1, $result['importedDashboardCount']);
+ self::assertSame($uuid, $result['dashboards'][0]['sourceUuid']);
+ self::assertSame(7, $result['dashboards'][0]['id']);
+ $seen[$type] = ['compulsory' => $compulsory, 'owner' => $owner];
+ }//end foreach
+
+ self::assertSame(
+ [
+ Dashboard::TYPE_ADMIN_TEMPLATE => ['compulsory' => 1, 'owner' => null],
+ Dashboard::TYPE_USER => ['compulsory' => 0, 'owner' => 'admin'],
+ Dashboard::TYPE_GROUP_SHARED => ['compulsory' => 0, 'owner' => 'admin'],
+ ],
+ $seen
+ );
+ }
+
/**
* Build a temporary ZIP archive containing the provided entries.
*
diff --git a/tests/Unit/Service/ShippedTemplateServiceTest.php b/tests/Unit/Service/ShippedTemplateServiceTest.php
new file mode 100644
index 000000000..30fae9aa7
--- /dev/null
+++ b/tests/Unit/Service/ShippedTemplateServiceTest.php
@@ -0,0 +1,560 @@
+
+ * SPDX-License-Identifier: EUPL-1.2
+ */
+
+declare(strict_types=1);
+
+namespace OCA\LaunchPad\Tests\Unit\Service;
+
+use InvalidArgumentException;
+use OCA\LaunchPad\Command\ExportCommand;
+use OCA\LaunchPad\Command\ImportCommand;
+use OCA\LaunchPad\Db\Dashboard;
+use OCA\LaunchPad\Db\DashboardMapper;
+use OCA\LaunchPad\Db\WidgetPlacement;
+use OCA\LaunchPad\Db\WidgetPlacementMapper;
+use OCA\LaunchPad\Service\AdminSettingsService;
+use OCA\LaunchPad\Service\AdminTemplateService;
+use OCA\LaunchPad\Service\ExportService;
+use OCA\LaunchPad\Service\ImportService;
+use OCA\LaunchPad\Service\ShippedTemplateService;
+use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\Dashboard\IManager;
+use OCP\Dashboard\IWidget;
+use OCP\IAppConfig;
+use OCP\IDBConnection;
+use OCP\IGroupManager;
+use OCP\IUserManager;
+use PHPUnit\Framework\TestCase;
+use Psr\Log\NullLogger;
+use RuntimeException;
+use Symfony\Component\Console\Tester\CommandTester;
+
+/**
+ * The shipped templates: REQ-TMPL-018, and the round trip REQ-EXIM-012.
+ *
+ * Nothing between the definition file and the stored rows is mocked. The real
+ * importer builds the entities and the real exporter serialises them; only the
+ * two mappers are replaced, by an in-memory store, because there is no
+ * database in a unit run.
+ *
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Wires the real services.
+ */
+class ShippedTemplateServiceTest extends TestCase {
+ /** @var array App config, in memory. */
+ private array $config = [];
+
+ /** @var array Groups that exist. */
+ private array $groups = ['medewerkers'];
+
+ /** @var array Nextcloud widget ids registered "here". */
+ private array $registeredWidgets = [];
+
+ /**
+ * One instance: an in-memory store behind real services.
+ *
+ * @return array{dashboards: \ArrayObject, placements: \ArrayObject, dashboardMapper: DashboardMapper,
+ * placementMapper: WidgetPlacementMapper, import: ImportService, export: ExportService}
+ */
+ private function makeInstance(): array {
+ $dashboards = new \ArrayObject();
+ $placements = new \ArrayObject();
+
+ $dashboardMapper = $this->createMock(DashboardMapper::class);
+ $dashboardMapper->method('insert')->willReturnCallback(
+ static function (Dashboard $dashboard) use ($dashboards): Dashboard {
+ $dashboard->setId(count($dashboards) + 1);
+ $dashboards[] = $dashboard;
+ return $dashboard;
+ }
+ );
+ $dashboardMapper->method('update')->willReturnArgument(0);
+ $dashboardMapper->method('findByUuid')->willReturnCallback(
+ static function (string $uuid) use ($dashboards): Dashboard {
+ foreach ($dashboards as $dashboard) {
+ if ($dashboard->getUuid() === $uuid) {
+ return $dashboard;
+ }
+ }
+ throw new DoesNotExistException('no dashboard ' . $uuid);
+ }
+ );
+ $dashboardMapper->method('find')->willReturnCallback(
+ static function (int $id) use ($dashboards): Dashboard {
+ foreach ($dashboards as $dashboard) {
+ if ($dashboard->getId() === $id) {
+ return $dashboard;
+ }
+ }
+ throw new DoesNotExistException('no dashboard ' . $id);
+ }
+ );
+ $dashboardMapper->method('clearDefaultTemplates')->willReturnCallback(
+ static function () use ($dashboards): void {
+ foreach ($dashboards as $dashboard) {
+ $dashboard->setIsDefault(0);
+ }
+ }
+ );
+
+ $placementMapper = $this->createMock(WidgetPlacementMapper::class);
+ $placementMapper->method('insert')->willReturnCallback(
+ static function (WidgetPlacement $placement) use ($placements): WidgetPlacement {
+ $placement->setId(count($placements) + 1);
+ $placements[] = $placement;
+ return $placement;
+ }
+ );
+ $placementMapper->method('findByDashboardId')->willReturnCallback(
+ static fn (int $dashboardId): array => array_values(array_filter(
+ $placements->getArrayCopy(),
+ static fn (WidgetPlacement $p): bool => $p->getDashboardId() === $dashboardId
+ ))
+ );
+
+ return [
+ 'dashboards' => $dashboards,
+ 'placements' => $placements,
+ 'dashboardMapper' => $dashboardMapper,
+ 'placementMapper' => $placementMapper,
+ 'import' => new ImportService(
+ dashboardMapper: $dashboardMapper,
+ placementMapper: $placementMapper,
+ db: $this->createMock(IDBConnection::class),
+ logger: new NullLogger(),
+ ),
+ 'export' => new ExportService(
+ dashboardMapper: $dashboardMapper,
+ placementMapper: $placementMapper,
+ groupManager: $this->createMock(IGroupManager::class),
+ logger: new NullLogger(),
+ ),
+ ];
+ }
+
+ /**
+ * The service under test on one instance.
+ *
+ * @param array $instance From makeInstance().
+ * @param string|null $dataDir Definitions directory override.
+ *
+ * @return ShippedTemplateService
+ */
+ private function makeService(array $instance, ?string $dataDir = null): ShippedTemplateService {
+ $appConfig = $this->createMock(IAppConfig::class);
+ $appConfig->method('getValueString')->willReturnCallback(
+ fn (string $app, string $key, string $default = ''): string => (string)($this->config[$key] ?? $default)
+ );
+ $appConfig->method('getValueInt')->willReturnCallback(
+ fn (string $app, string $key, int $default = 0): int => (int)($this->config[$key] ?? $default)
+ );
+ $appConfig->method('setValueString')->willReturnCallback(
+ function (string $app, string $key, string $value): bool {
+ $this->config[$key] = $value;
+ return true;
+ }
+ );
+ $appConfig->method('setValueInt')->willReturnCallback(
+ function (string $app, string $key, int $value): bool {
+ $this->config[$key] = $value;
+ return true;
+ }
+ );
+
+ $groupManager = $this->createMock(IGroupManager::class);
+ $groupManager->method('groupExists')->willReturnCallback(
+ fn (string $gid): bool => in_array($gid, $this->groups, true)
+ );
+
+ $dashboardManager = $this->createMock(IManager::class);
+ $dashboardManager->method('getWidgets')->willReturnCallback(
+ function (): array {
+ $widgets = [];
+ foreach ($this->registeredWidgets as $id) {
+ $widget = $this->createMock(IWidget::class);
+ $widget->method('getId')->willReturn($id);
+ $widgets[$id] = $widget;
+ }
+ return $widgets;
+ }
+ );
+
+ $service = new ShippedTemplateService(
+ importService: $instance['import'],
+ templateService: new AdminTemplateService(
+ dashboardMapper: $instance['dashboardMapper'],
+ placementMapper: $instance['placementMapper'],
+ settingsService: $this->createMock(AdminSettingsService::class),
+ groupManager: $groupManager,
+ userManager: $this->createMock(IUserManager::class),
+ ),
+ dashboardMapper: $instance['dashboardMapper'],
+ appConfig: $appConfig,
+ dashboardMgr: $dashboardManager,
+ groupManager: $groupManager,
+ );
+ if ($dataDir !== null) {
+ $service->setDataDirForTesting(path: $dataDir);
+ }
+
+ return $service;
+ }
+
+ /**
+ * What a template IS, as REQ-EXIM-012 lists it: the fields that must be
+ * equal on both sides of a round trip.
+ *
+ * @param array $dashboard A dashboard payload.
+ *
+ * @return array
+ */
+ private static function definitionOf(array $dashboard): array {
+ $widgets = [];
+ foreach ($dashboard['widgets'] as $widget) {
+ $widgets[] = [
+ 'widgetId' => $widget['widgetId'],
+ 'gridX' => $widget['gridX'],
+ 'gridY' => $widget['gridY'],
+ 'gridWidth' => $widget['gridWidth'],
+ 'gridHeight' => $widget['gridHeight'],
+ 'isCompulsory' => $widget['isCompulsory'],
+ 'isVisible' => $widget['isVisible'],
+ 'showTitle' => $widget['showTitle'],
+ 'sortOrder' => $widget['sortOrder'],
+ 'customTitle' => $widget['customTitle'],
+ 'customIcon' => $widget['customIcon'],
+ // An empty blob is `{}` on one side and `[]` on the other.
+ 'styleConfig' => (array)$widget['styleConfig'],
+ 'content' => (array)$widget['content'],
+ ];
+ }
+
+ usort($widgets, static fn (array $a, array $b): int => $a['sortOrder'] <=> $b['sortOrder']);
+
+ return [
+ 'name' => $dashboard['name'],
+ 'description' => $dashboard['description'],
+ 'icon' => $dashboard['icon'],
+ 'type' => $dashboard['type'],
+ 'userId' => $dashboard['userId'],
+ 'gridColumns' => $dashboard['gridColumns'],
+ 'permissionLevel' => $dashboard['permissionLevel'],
+ 'targetGroups' => $dashboard['targetGroups'],
+ 'publicationStatus' => $dashboard['publicationStatus'],
+ 'templateCategory' => $dashboard['templateCategory'],
+ 'templateDescription' => $dashboard['templateDescription'],
+ 'widgets' => $widgets,
+ ];
+ }
+
+ /**
+ * REQ-TMPL-018: the install lands the definition, compulsory flags
+ * included, as an ownerless admin template that targets nobody.
+ */
+ public function testInstallAddsTheTemplateThroughTheImporter(): void {
+ $instance = $this->makeInstance();
+ $service = $this->makeService($instance);
+ $shipped = $service->readDefinition(templateId: 'mijn-werkdag');
+
+ $result = $service->install(templateId: 'mijn-werkdag');
+
+ self::assertFalse($result['alreadyInstalled']);
+ self::assertSame($shipped['templateVersion'], $result['version']);
+ self::assertCount(1, $instance['dashboards']);
+
+ $template = $instance['dashboards'][0];
+ self::assertSame(Dashboard::TYPE_ADMIN_TEMPLATE, $template->getType());
+ self::assertNull($template->getUserId());
+ self::assertSame(0, $template->getIsDefault());
+ self::assertSame([], $template->getTargetGroupsArray());
+ // A fresh UUID: the shipped one is the file's, not the instance's.
+ self::assertNotSame($shipped['dashboard']['uuid'], $template->getUuid());
+ self::assertSame($template->getUuid(), $result['uuid']);
+ self::assertSame($template->getUuid(), $this->config['shipped_template_mijn-werkdag']);
+ self::assertSame($shipped['templateVersion'], $this->config['shipped_template_version_mijn-werkdag']);
+
+ self::assertSame(
+ self::definitionOf($shipped['dashboard']),
+ self::definitionOf($instance['export']->serializeDashboard(dashboard: $template))
+ );
+ $compulsory = array_filter(
+ $instance['placements']->getArrayCopy(),
+ static fn (WidgetPlacement $p): bool => $p->getIsCompulsory() === 1
+ );
+ self::assertCount(2, $compulsory, 'the header and the list of cases past their deadline are compulsory');
+ }
+
+ /**
+ * REQ-EXIM-012: shipped definition -> instance A -> `launchpad:export`
+ * -> archive -> `launchpad:import` on instance B reproduces the template.
+ */
+ public function testTheShippedTemplateSurvivesExportAndImport(): void {
+ $a = $this->makeInstance();
+ $service = $this->makeService($a);
+ $installed = $service->install(templateId: 'mijn-werkdag', targetGroups: ['medewerkers']);
+ $onA = $a['export']->serializeDashboard(dashboard: $a['dashboards'][0]);
+ self::assertSame(['medewerkers'], $onA['targetGroups']);
+
+ $archive = (string)tempnam(sys_get_temp_dir(), 'launchpad-roundtrip-') . '.zip';
+ try {
+ $export = new CommandTester(new ExportCommand($a['export'], $a['dashboardMapper']));
+ $exit = $export->execute([
+ '--scope' => 'dashboard',
+ '--dashboard-uuid' => $installed['uuid'],
+ '--output' => $archive,
+ ]);
+ self::assertSame(0, $exit, $export->getDisplay());
+
+ $b = $this->makeInstance();
+ $import = new CommandTester(new ImportCommand($b['import']));
+ $exit = $import->execute(['--file' => $archive]);
+ self::assertSame(0, $exit, $import->getDisplay());
+ self::assertStringContainsString('Imported 1 dashboards, skipped 0, errors: 0', $import->getDisplay());
+ } finally {
+ @unlink($archive);
+ @unlink(substr($archive, 0, -4));
+ }
+
+ self::assertCount(1, $b['dashboards']);
+ $onB = $b['export']->serializeDashboard(dashboard: $b['dashboards'][0]);
+
+ self::assertSame(self::definitionOf($onA), self::definitionOf($onB));
+ // A against B alone would pass if the install had already lost a field
+ // on A, so B is also held against the shipped file.
+ $shipped = $service->readDefinition(templateId: 'mijn-werkdag')['dashboard'];
+ $shipped['targetGroups'] = ['medewerkers'];
+ self::assertSame(self::definitionOf($shipped), self::definitionOf($onB));
+ self::assertNotSame($onA['uuid'], $onB['uuid']);
+ self::assertSame(0, $onB['isDefault']);
+ self::assertNull($onB['userId'], 'a template has no owner, the importer included');
+ // The control: the comparison would also pass if both sides were
+ // empty, so pin that the compulsory flags really crossed.
+ self::assertSame(
+ [1, 1, 0, 0],
+ array_column(self::definitionOf($onB)['widgets'], 'isCompulsory')
+ );
+ }
+
+ /**
+ * REQ-TMPL-018: a second install adds nothing and still applies options.
+ */
+ public function testASecondInstallAddsNothing(): void {
+ $instance = $this->makeInstance();
+ $service = $this->makeService($instance);
+ $first = $service->install(templateId: 'mijn-werkdag');
+
+ $second = $service->install(templateId: 'mijn-werkdag', targetGroups: ['medewerkers'], makeDefault: true);
+
+ self::assertTrue($second['alreadyInstalled']);
+ self::assertSame($first['uuid'], $second['uuid']);
+ self::assertCount(1, $instance['dashboards']);
+ self::assertSame(['medewerkers'], $second['targetGroups']);
+ self::assertTrue($second['isDefault']);
+ }
+
+ /**
+ * REQ-TMPL-018: forcing adds a fresh copy and keeps the earlier one.
+ */
+ public function testForceAddsAFreshCopyAndKeepsTheEarlierOne(): void {
+ $instance = $this->makeInstance();
+ $service = $this->makeService($instance);
+ $first = $service->install(templateId: 'mijn-werkdag');
+
+ $second = $service->install(templateId: 'mijn-werkdag', force: true);
+
+ self::assertFalse($second['alreadyInstalled']);
+ self::assertNotSame($first['uuid'], $second['uuid']);
+ self::assertCount(2, $instance['dashboards']);
+ self::assertSame($second['uuid'], $this->config['shipped_template_mijn-werkdag']);
+ }
+
+ /**
+ * REQ-TMPL-018: a recorded install whose template is gone is no install.
+ */
+ public function testADeletedTemplateCountsAsNotInstalled(): void {
+ $instance = $this->makeInstance();
+ $service = $this->makeService($instance);
+ $this->config['shipped_template_mijn-werkdag'] = 'gone-uuid';
+
+ self::assertFalse($service->listTemplates()[0]['isInstalled']);
+ $result = $service->install(templateId: 'mijn-werkdag');
+
+ self::assertFalse($result['alreadyInstalled']);
+ self::assertCount(1, $instance['dashboards']);
+ }
+
+ /**
+ * REQ-CLI-012: a mistyped group fails before anything is written.
+ */
+ public function testAnUnknownGroupStopsTheInstall(): void {
+ $instance = $this->makeInstance();
+ $service = $this->makeService($instance);
+
+ try {
+ $service->install(templateId: 'mijn-werkdag', targetGroups: ['medewerkerz']);
+ self::fail('an unknown group must throw');
+ } catch (InvalidArgumentException $e) {
+ self::assertStringContainsString('medewerkerz', $e->getMessage());
+ }
+
+ self::assertCount(0, $instance['dashboards']);
+ self::assertSame([], $this->config);
+ }
+
+ /**
+ * REQ-TMPL-018: an unknown id names the shipped ones.
+ */
+ public function testAnUnknownTemplateNamesTheShippedOnes(): void {
+ $service = $this->makeService($this->makeInstance());
+
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('mijn-werkdag');
+ $service->install(templateId: 'does-not-exist');
+ }
+
+ /**
+ * REQ-TMPL-018: a definition with no widgets is an error, not an empty template.
+ */
+ public function testAMalformedDefinitionFails(): void {
+ $dir = sys_get_temp_dir() . '/launchpad-templates-' . bin2hex(random_bytes(4));
+ mkdir($dir);
+ file_put_contents(
+ $dir . '/mijn-werkdag.json',
+ json_encode(['templateVersion' => 1, 'dashboard' => ['uuid' => 'x', 'name' => 'x', 'widgets' => []]])
+ );
+ $instance = $this->makeInstance();
+ $service = $this->makeService($instance, $dir);
+
+ try {
+ $service->install(templateId: 'mijn-werkdag');
+ self::fail('a definition without widgets must throw');
+ } catch (RuntimeException $e) {
+ self::assertStringContainsString('mijn-werkdag.json', $e->getMessage());
+ } finally {
+ unlink($dir . '/mijn-werkdag.json');
+ rmdir($dir);
+ }
+
+ self::assertCount(0, $instance['dashboards']);
+ }
+
+ /**
+ * REQ-TMPL-018: the listing names proxied widgets nothing registers.
+ */
+ public function testTheListingNamesWidgetsNothingRegisters(): void {
+ // Nothing registers `activity` here, the one Nextcloud widget the template proxies.
+ $this->registeredWidgets = ['files-favorites'];
+ $service = $this->makeService($this->makeInstance());
+
+ $listing = $service->listTemplates();
+
+ self::assertCount(1, $listing);
+ self::assertSame('mijn-werkdag', $listing[0]['id']);
+ self::assertSame('Mijn werkdag', $listing[0]['name']);
+ self::assertSame('nl', $listing[0]['language']);
+ self::assertSame(4, $listing[0]['widgetCount']);
+ self::assertFalse($listing[0]['isInstalled']);
+ self::assertNull($listing[0]['installedVersion']);
+ self::assertSame(['activity'], $listing[0]['missingWidgets']);
+ }
+
+ /**
+ * REQ-TMPL-018: a shipped template renders as installed.
+ *
+ * Two live defects, both invisible to every other test here:
+ * - four widgets proxied Nextcloud dashboard widgets that only paint
+ * through their own script (`IWidget` without an items API). LaunchPad
+ * loads those scripts only when the legacy widget bridge is switched
+ * on, so as installed they showed one sentence and their raw id;
+ * - a list sorted on `plannedEndDate` and showed it as a column, while a
+ * dossiq case carries `deadline`. The column was empty for every case.
+ *
+ * So: a proxied Nextcloud widget must be on the short list of widgets
+ * known to answer the items API, and every field a list names on a known
+ * register must exist in that register's schema.
+ */
+ public function testAShippedTemplateRendersAsInstalled(): void {
+ $service = $this->makeService($this->makeInstance());
+ // Nextcloud widgets whose provider implements IAPIWidget(V2), measured
+ // on Nextcloud 35 via /ocs/v2.php/apps/dashboard/api/v1/widgets
+ // (`item_api_versions` not empty). Add one only after measuring it.
+ $itemsApiWidgets = ['activity', 'recommendations', 'files-favorites', 'user_status'];
+ $schemas = [];
+ foreach (glob(dirname(__DIR__, 2) . '/fixtures/registers/*.json') ?: [] as $file) {
+ $fixture = json_decode((string)file_get_contents($file), true);
+ $schemas[$fixture['register'] . '/' . $fixture['schema']] = $fixture['properties'];
+ }
+ self::assertArrayHasKey('dossiq/case', $schemas);
+
+ $listsChecked = 0;
+ foreach (ShippedTemplateService::SHIPPED_IDS as $id) {
+ foreach ($service->readDefinition(templateId: $id)['dashboard']['widgets'] as $widget) {
+ $content = $widget['content'];
+ if ($widget['widgetId'] === 'nc-widget') {
+ self::assertContains($content['widgetId'], $itemsApiWidgets, 'this widget cannot paint without the legacy bridge');
+ }
+ if ($widget['widgetId'] !== 'object-list') {
+ continue;
+ }
+
+ $key = $content['register'] . '/' . $content['schema'];
+ self::assertArrayHasKey($key, $schemas, 'no field list for ' . $key . ': add tests/fixtures/registers');
+ $named = array_column($content['columns'], 'key');
+ $named[] = $content['sort']['field'];
+ foreach (array_keys($content['filter']) as $filterKey) {
+ // `deadline[lt]` names the field `deadline`.
+ $named[] = preg_replace('/\[.*$/', '', (string)$filterKey);
+ }
+ foreach ($named as $field) {
+ self::assertContains($field, $schemas[$key], $widget['customTitle'] . ' names a field ' . $key . ' does not have');
+ }
+ foreach ($content['filter'] as $value) {
+ self::assertIsNotArray($value, 'an operator goes in the key, as in "deadline[lt]"');
+ }
+ $listsChecked++;
+ }
+ }
+
+ self::assertGreaterThanOrEqual(2, $listsChecked);
+ }
+
+ /**
+ * REQ-TMPL-018: the shipped data itself. Every widget is a type LaunchPad
+ * renders, sits inside the grid and overlaps no other widget.
+ */
+ public function testTheShippedDefinitionIsWellFormed(): void {
+ $service = $this->makeService($this->makeInstance());
+ $types = json_decode(
+ (string)file_get_contents(dirname(__DIR__, 3) . '/lib/widget-types.json'),
+ true
+ )['types'];
+
+ foreach (ShippedTemplateService::SHIPPED_IDS as $id) {
+ $definition = $service->readDefinition(templateId: $id);
+ $dashboard = $definition['dashboard'];
+ self::assertSame($id, $definition['templateId']);
+ self::assertSame(Dashboard::TYPE_ADMIN_TEMPLATE, $dashboard['type']);
+ self::assertSame([], $dashboard['targetGroups'], 'a shipped template targets nobody');
+ self::assertArrayNotHasKey('isDefault', $dashboard);
+ self::assertStringNotContainsString("\u{2014}", json_encode($dashboard, JSON_UNESCAPED_UNICODE));
+
+ $cells = [];
+ foreach ($dashboard['widgets'] as $widget) {
+ self::assertContains($widget['widgetId'], $types);
+ self::assertLessThanOrEqual($dashboard['gridColumns'], $widget['gridX'] + $widget['gridWidth']);
+ if ($widget['widgetId'] === 'nc-widget') {
+ self::assertNotSame('', $widget['content']['widgetId'] ?? '');
+ }
+ for ($x = $widget['gridX']; $x < $widget['gridX'] + $widget['gridWidth']; $x++) {
+ for ($y = $widget['gridY']; $y < $widget['gridY'] + $widget['gridHeight']; $y++) {
+ self::assertArrayNotHasKey($x . ':' . $y, $cells, 'two widgets share cell ' . $x . ':' . $y);
+ $cells[$x . ':' . $y] = true;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/tests/e2e/template-reaches-member.spec.ts b/tests/e2e/template-reaches-member.spec.ts
new file mode 100644
index 000000000..6f044b0ea
--- /dev/null
+++ b/tests/e2e/template-reaches-member.spec.ts
@@ -0,0 +1,211 @@
+/*
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
+ * SPDX-License-Identifier: EUPL-1.2
+ *
+ * A member of a group an admin template targets SEES that template when they
+ * open LaunchPad for the first time (admin-templates REQ-TMPL-019, REQ-TMPL-018).
+ *
+ * WHY THIS IS A BROWSER TEST. The feature had unit tests on every part and did
+ * not work: the page decides what to show from one resolution chain
+ * (`resolveActiveDashboard`), and templates were only known to the other
+ * (`GET /api/dashboard`), which the page never reaches when the first says
+ * "nothing" and never needs when the first says "the dashboard for everyone".
+ * Only a real first visit joins the two. Found on 5 October 2026 by a live
+ * check, with everything green.
+ *
+ * It drives the documented flow: an administrator adds the shipped template
+ * and gives it to a group; a brand-new member opens LaunchPad. It does not
+ * assume the instance is empty: the seeded dashboard for everyone may be
+ * there, and the template must still win.
+ *
+ * @e2e openspec/specs/admin-templates/spec.md#req-tmpl-019
+ */
+
+import type { APIRequestContext } from '@playwright/test'
+
+import { expect, request as pwRequest, test } from '@playwright/test'
+import {
+ deprovisionUser,
+ loginAs,
+ provisionThrowawayUser,
+} from './fixtures/secondary-user.ts'
+import { BASE_URL as BASE } from './support/baseUrl.ts'
+
+const ADMIN = {
+ user: process.env.NC_ADMIN_USER ?? 'admin',
+ pass: process.env.NC_ADMIN_PASS ?? 'admin',
+}
+const API = '/index.php/apps/launchpad/api'
+
+let api: APIRequestContext
+let group = ''
+let member: { username: string; password: string } | null = null
+let outsider: { username: string; password: string } | null = null
+let templateId: number | null = null
+let allowBefore: boolean | null = null
+
+test.describe.configure({ mode: 'serial' })
+
+// A brand-new account's first login builds its home folder, and that alone can
+// take most of the default 60 s on a loaded instance. Measured on the first run
+// of this spec: the page was showing the template when the default ran out.
+test.setTimeout(180_000)
+
+test.beforeAll(async () => {
+ api = await pwRequest.newContext({
+ baseURL: BASE,
+ httpCredentials: { username: ADMIN.user, password: ADMIN.pass },
+ extraHTTPHeaders: { 'OCS-APIRequest': 'true' },
+ })
+
+ const settings = await api.get(`${API}/admin/settings`)
+ expect(settings.ok(), 'admin settings must be readable').toBe(true)
+ allowBefore = (await settings.json()).allowUserDashboards === true
+ // A member can only be handed a copy, with its compulsory widgets, when
+ // personal dashboards are on. The view-only case is the PHPUnit's.
+ const put = await api.put(`${API}/admin/settings`, {
+ data: { allowUserDashboards: true },
+ })
+ expect(put.ok(), 'personal dashboards must switch on').toBe(true)
+
+ group = `e2e-werkdag-${Date.now()}`
+ const madeGroup = await api.post('/ocs/v1.php/cloud/groups', {
+ form: { groupid: group },
+ })
+ expect(madeGroup.ok(), 'the group must be created').toBe(true)
+
+ member = await provisionThrowawayUser('e2e-werkdag-lid')
+ outsider = await provisionThrowawayUser('e2e-werkdag-buiten')
+ const joined = await api.post(
+ `/ocs/v1.php/cloud/users/${encodeURIComponent(member.username)}/groups`,
+ { form: { groupid: group } },
+ )
+ expect(joined.ok(), 'the member must join the group').toBe(true)
+
+ // The documented flow: add the shipped template (a fresh copy, so this
+ // spec owns it), then give it to the group as the Templates page does.
+ const installed = await api.post(
+ `${API}/admin/templates/shipped/mijn-werkdag/install`,
+ {
+ data: { force: true },
+ },
+ )
+ expect(installed.ok(), `install answered ${installed.status()}`).toBe(true)
+ templateId = (await installed.json()).id
+ const targeted = await api.put(`${API}/admin/templates/${templateId}`, {
+ data: { targetGroups: [group] },
+ })
+ expect(targeted.ok(), 'the template must take the group').toBe(true)
+})
+
+test.afterAll(async () => {
+ if (templateId !== null) {
+ await api.delete(`${API}/admin/templates/${templateId}`)
+ }
+ for (const user of [member, outsider]) {
+ if (user !== null) {
+ await deprovisionUser(user.username)
+ }
+ }
+ if (group !== '') {
+ await api.delete(`/ocs/v1.php/cloud/groups/${encodeURIComponent(group)}`)
+ }
+ if (allowBefore !== null) {
+ await api.put(`${API}/admin/settings`, {
+ data: { allowUserDashboards: allowBefore },
+ })
+ }
+ await api.dispose()
+})
+
+// @e2e admin-templates::a-new-member-sees-the-template-although-a-dashboard-for-everyone-exists
+test('a new member of the group sees the template on their first visit', async ({
+ browser,
+}) => {
+ const { context, page } = await loginAs(
+ browser,
+ member!.username,
+ member!.password,
+ )
+ try {
+ await page.goto('/index.php/apps/launchpad/')
+
+ // The header widget of the template, and the page is not the empty state.
+ await expect(
+ page.getByText('Mijn werkdag', { exact: true }).first(),
+ ).toBeVisible({ timeout: 30_000 })
+ await expect(page.getByText('No dashboards available')).toHaveCount(0)
+
+ // What the server holds for this member: one personal copy made from
+ // THIS template, with the template's compulsory widgets.
+ const mine = await context.request.get(`${API}/dashboard`, {
+ headers: { 'OCS-APIRequest': 'true' },
+ })
+ expect(mine.ok()).toBe(true)
+ const body = await mine.json()
+ expect(body.dashboard.type).toBe('user')
+ expect(body.dashboard.userId).toBe(member!.username)
+ expect(body.dashboard.basedOnTemplate).toBe(templateId)
+ expect(body.permissionLevel).toBe('add_only')
+ const compulsory = body.placements.filter(
+ (p: { isCompulsory: number }) => p.isCompulsory === 1,
+ )
+ expect(compulsory.length).toBeGreaterThanOrEqual(2)
+ expect(compulsory.map((p: { widgetId: string }) => p.widgetId)).toContain(
+ 'header',
+ )
+ } finally {
+ await context.close()
+ }
+})
+
+test('a second visit shows the same copy and makes no second one', async ({
+ browser,
+}) => {
+ const { context, page } = await loginAs(
+ browser,
+ member!.username,
+ member!.password,
+ )
+ try {
+ await page.goto('/index.php/apps/launchpad/')
+ await expect(
+ page.getByText('Mijn werkdag', { exact: true }).first(),
+ ).toBeVisible({ timeout: 30_000 })
+
+ const visible = await context.request.get(`${API}/dashboards/visible`, {
+ headers: { 'OCS-APIRequest': 'true' },
+ })
+ const items = (await visible.json()).items as Array<{
+ basedOnTemplate: number | null
+ }>
+ expect(items.filter((d) => d.basedOnTemplate === templateId)).toHaveLength(1)
+ } finally {
+ await context.close()
+ }
+})
+
+test('someone outside the group does not get the template', async ({ browser }) => {
+ const { context, page } = await loginAs(
+ browser,
+ outsider!.username,
+ outsider!.password,
+ )
+ try {
+ await page.goto('/index.php/apps/launchpad/')
+ await page.waitForSelector(
+ '.workspace-shell, .launchpad-floating-controls',
+ { timeout: 30_000 },
+ )
+
+ const visible = await context.request.get(`${API}/dashboards/visible`, {
+ headers: { 'OCS-APIRequest': 'true' },
+ })
+ const items = (await visible.json()).items as Array<{
+ basedOnTemplate: number | null
+ }>
+ expect(items.filter((d) => d.basedOnTemplate === templateId)).toHaveLength(0)
+ } finally {
+ await context.close()
+ }
+})
diff --git a/tests/fixtures/registers/dossiq-case.json b/tests/fixtures/registers/dossiq-case.json
new file mode 100644
index 000000000..58be63b75
--- /dev/null
+++ b/tests/fixtures/registers/dossiq-case.json
@@ -0,0 +1,196 @@
+{
+ "_comment": [
+ "Property names of dossiq's `case` schema (main register plus register.d fragments),",
+ "copied from ConductionNL/dossiq at 0a363453f on 2026-10-05. ShippedTemplateServiceTest holds every",
+ "field a shipped template names on register `dossiq`, schema `case`, against this list.",
+ "It is a COPY: it catches a field that never existed (the template once sorted on `plannedEndDate`",
+ "while cases carry `deadline`), it does not catch dossiq renaming a field later. Refresh it when the template changes."
+ ],
+ "register": "dossiq",
+ "schema": "case",
+ "properties": [
+ "aanvulling",
+ "aanvullingRound",
+ "acknowledgementDuty",
+ "actionFacet",
+ "actionResult",
+ "activity",
+ "admissibility",
+ "approvalRefs",
+ "archiveActionDate",
+ "archiveNomination",
+ "archiveStatus",
+ "areaAddressId",
+ "areaFallbackUsed",
+ "areaResolvedAt",
+ "areaSource",
+ "assignedGroup",
+ "assignedGroupPublicName",
+ "assignee",
+ "associatedCases",
+ "attentionFlag",
+ "attentionFlagHistory",
+ "attentionMarkers",
+ "besluitDocument",
+ "besluitdatum",
+ "blockingCases",
+ "casePlanState",
+ "caseType",
+ "chasesSent",
+ "classification",
+ "collaborationRequests",
+ "commissieBesluit",
+ "communicationChannel",
+ "competentAuthority",
+ "confidentiality",
+ "contract",
+ "contributingCases",
+ "conversations",
+ "currentStatusDwellDays",
+ "currentStatusEnteredAt",
+ "dataSubject",
+ "dataSubjectRequestType",
+ "dataSubjectType",
+ "deadline",
+ "deadlineDate",
+ "decisions",
+ "description",
+ "district",
+ "draftCreatedAt",
+ "dsoNotes",
+ "dsoStatus",
+ "duplicateOverrideOf",
+ "duplicateOverrideReason",
+ "endDate",
+ "endingAct",
+ "erasureApprovedAt",
+ "erasureApprovedBy",
+ "erasureCounts",
+ "erasureDigest",
+ "erasureOutcome",
+ "erasurePreviewId",
+ "erasureProtected",
+ "extensionCount",
+ "externalApplication",
+ "externalIdentifier",
+ "externalUrl",
+ "firstResponseAt",
+ "firstResponseOverrunDays",
+ "firstResponseStatus",
+ "followUpCases",
+ "geometry",
+ "handoffSource",
+ "handoverPending",
+ "handoverRecord",
+ "handoverTo",
+ "hasAttentionMarkers",
+ "hasStartableFlows",
+ "heldUntil",
+ "holdReason",
+ "identifier",
+ "impact",
+ "initiatorDisplayName",
+ "initiatorSourceId",
+ "initiatorType",
+ "insightLevel",
+ "intakeChannel",
+ "intakeRefusal",
+ "intakeSubmission",
+ "isDraft",
+ "isFinalStatus",
+ "isIncomplete",
+ "isMajor",
+ "isTemplate",
+ "lastPaymentDate",
+ "lawfulPurposeEndDate",
+ "legalBasis",
+ "majorChannel",
+ "majorDeclaredAt",
+ "majorDeclaredBy",
+ "majorResponders",
+ "mergeRelinked",
+ "mergeState",
+ "mergedInto",
+ "missingFields",
+ "needsAttention",
+ "neighbourhood",
+ "outboundCommunications",
+ "parentCase",
+ "pauseWaitingOn",
+ "paymentIndication",
+ "paymentState",
+ "paymentStateCheckedAt",
+ "permitApplicationRef",
+ "plannedEndDate",
+ "portalDocuments",
+ "portalSubject",
+ "portalTurn",
+ "portalWrites",
+ "priority",
+ "priorityDerived",
+ "priorityFloor",
+ "priorityOrder",
+ "priorityOverride",
+ "priorityOverrideAt",
+ "priorityOverrideBy",
+ "priorityOverrideReason",
+ "priorityRaisedBy",
+ "procedureType",
+ "properties",
+ "publications",
+ "publishedAt",
+ "qualityScore",
+ "qualityStatus",
+ "receivedAt",
+ "receivedOutsideWorkingHours",
+ "relatedCases",
+ "requester",
+ "result",
+ "resultPublicDescription",
+ "resultPublicLabel",
+ "riskAssessment",
+ "riskLevel",
+ "sensitivity",
+ "skippedPhases",
+ "sourceOrganisation",
+ "splitFrom",
+ "splitInto",
+ "splitMovedItems",
+ "splitNote",
+ "startDate",
+ "startedFromTemplate",
+ "status",
+ "statusDwellBreached",
+ "statusDwellTotals",
+ "statusHiddenInLists",
+ "statusHistory",
+ "statusPublicDescription",
+ "statusPublicLabel",
+ "statusRole",
+ "statutoryTerm",
+ "subjectCases",
+ "subjectExportExpiresAt",
+ "subjectExportId",
+ "subjectExportReady",
+ "tags",
+ "taskAttachments",
+ "templateName",
+ "termStartsAt",
+ "title",
+ "toetsRegisterB",
+ "tweedeToets",
+ "urgency",
+ "voorbereiding",
+ "waitingOn",
+ "waitingOnApplicant",
+ "waitingOnApplicantSince",
+ "waitingSince",
+ "withdrawalReason",
+ "withdrawnAt",
+ "wooPublicationStatus",
+ "wooPublicationUrl",
+ "wooRequest",
+ "workflowTemplate",
+ "workflowVersion"
+ ]
+}
From 52883996caef0b521b803d20ce9ac3df536a182b Mon Sep 17 00:00:00 2001
From: Ruben van der Linde
Date: Mon, 5 Oct 2026 18:40:24 +0200
Subject: [PATCH 08/60] feat(attention): First today, one ranked list of what
your apps say needs attention (#778)
* wip: ship the Mijn werkdag template, an install command and a template round trip that keeps compulsory widgets
* wip: ready-made templates on the admin page, a download per template, the change record
* wip: the cross-app attention contract as an OpenSpec change, the declaration reader and its endpoint
* wip: the First today widget, the feed logic and template version 2 (frontend tests not yet run)
* wip: spec tag on the query writer
* wip: the templates page test runs, both files formatted
* wip(l10n): the eight new admin strings in the 35 other locales are English copies, not translations
* wip: formatted, lint clean, the twelve new strings copied as English into the 35 other locales
* docs(openspec): tick the built tasks of the attention feed change
* wip: a member who owns no dashboard gets their admin template from the page shell too
* wip: the template rung sits inside existing helpers, asks for the template first
* wip: REQ-TMPL-019, a test that asks both chains per page load, and a browser test for the member flow (not yet run)
* wip: the install command says so when personal dashboards are off
* wip: the template only uses widgets that render as installed, and its case lists name real fields
* test(e2e): a new account's first login needs more than the default minute
---
appinfo/routes.php | 4 +
data/templates/mijn-werkdag.json | 37 +-
l10n/be.js | 14 +-
l10n/be.json | 14 +-
l10n/bg.js | 14 +-
l10n/bg.json | 14 +-
l10n/bs.js | 14 +-
l10n/bs.json | 14 +-
l10n/ca.js | 14 +-
l10n/ca.json | 14 +-
l10n/cs.js | 14 +-
l10n/cs.json | 14 +-
l10n/da.js | 14 +-
l10n/da.json | 14 +-
l10n/de.js | 14 +-
l10n/de.json | 14 +-
l10n/el.js | 14 +-
l10n/el.json | 14 +-
l10n/en.js | 14 +-
l10n/en.json | 14 +-
l10n/es.js | 14 +-
l10n/es.json | 14 +-
l10n/et.js | 14 +-
l10n/et.json | 14 +-
l10n/fi.js | 14 +-
l10n/fi.json | 14 +-
l10n/fr.js | 14 +-
l10n/fr.json | 14 +-
l10n/ga.js | 14 +-
l10n/ga.json | 14 +-
l10n/hr.js | 14 +-
l10n/hr.json | 14 +-
l10n/hu.js | 14 +-
l10n/hu.json | 14 +-
l10n/is.js | 14 +-
l10n/is.json | 14 +-
l10n/it.js | 14 +-
l10n/it.json | 14 +-
l10n/lb.js | 14 +-
l10n/lb.json | 14 +-
l10n/lt.js | 14 +-
l10n/lt.json | 14 +-
l10n/lv.js | 14 +-
l10n/lv.json | 14 +-
l10n/mk.js | 14 +-
l10n/mk.json | 14 +-
l10n/mt.js | 14 +-
l10n/mt.json | 14 +-
l10n/nb.js | 14 +-
l10n/nb.json | 14 +-
l10n/nl.js | 14 +-
l10n/nl.json | 14 +-
l10n/pl.js | 14 +-
l10n/pl.json | 14 +-
l10n/pt.js | 14 +-
l10n/pt.json | 14 +-
l10n/rm.js | 14 +-
l10n/rm.json | 14 +-
l10n/ro.js | 14 +-
l10n/ro.json | 14 +-
l10n/ru.js | 14 +-
l10n/ru.json | 14 +-
l10n/sk.js | 14 +-
l10n/sk.json | 14 +-
l10n/sl.js | 14 +-
l10n/sl.json | 14 +-
l10n/sq.js | 14 +-
l10n/sq.json | 14 +-
l10n/sr.js | 14 +-
l10n/sr.json | 14 +-
l10n/sv.js | 14 +-
l10n/sv.json | 14 +-
l10n/tr.js | 14 +-
l10n/tr.json | 14 +-
l10n/uk.js | 14 +-
l10n/uk.json | 14 +-
lib/Controller/AttentionController.php | 83 ++++
lib/Service/AttentionDeclarationValidator.php | 293 ++++++++++++
lib/Service/AttentionSourceService.php | 191 ++++++++
lib/widget-types.json | 1 +
.../cross-app-attention-feed/design.md | 143 ++++++
.../cross-app-attention-feed/proposal.md | 54 +++
.../specs/attention-feed/spec.md | 173 +++++++
.../changes/cross-app-attention-feed/tasks.md | 9 +
openspec/specs/admin-templates/spec.md | 4 +-
openspec/specs/attention-feed/spec.md | 183 ++++++++
.../Widgets/Renderers/AttentionWidget.vue | 335 ++++++++++++++
.../Widgets/Renderers/AttentionWidgetForm.vue | 116 +++++
.../__tests__/AttentionWidget.spec.js | 237 ++++++++++
.../__tests__/AttentionWidgetForm.spec.js | 62 +++
src/constants/widgetRegistry.js | 15 +
src/services/__tests__/attentionFeed.spec.js | 422 ++++++++++++++++++
src/services/attentionFeed.js | 274 ++++++++++++
.../Controller/AttentionControllerTest.php | 66 +++
.../Service/AttentionSourceServiceTest.php | 345 ++++++++++++++
.../Service/ShippedTemplateServiceTest.php | 6 +-
96 files changed, 4000 insertions(+), 89 deletions(-)
create mode 100644 lib/Controller/AttentionController.php
create mode 100644 lib/Service/AttentionDeclarationValidator.php
create mode 100644 lib/Service/AttentionSourceService.php
create mode 100644 openspec/changes/cross-app-attention-feed/design.md
create mode 100644 openspec/changes/cross-app-attention-feed/proposal.md
create mode 100644 openspec/changes/cross-app-attention-feed/specs/attention-feed/spec.md
create mode 100644 openspec/changes/cross-app-attention-feed/tasks.md
create mode 100644 openspec/specs/attention-feed/spec.md
create mode 100644 src/components/Widgets/Renderers/AttentionWidget.vue
create mode 100644 src/components/Widgets/Renderers/AttentionWidgetForm.vue
create mode 100644 src/components/Widgets/Renderers/__tests__/AttentionWidget.spec.js
create mode 100644 src/components/Widgets/Renderers/__tests__/AttentionWidgetForm.spec.js
create mode 100644 src/services/__tests__/attentionFeed.spec.js
create mode 100644 src/services/attentionFeed.js
create mode 100644 tests/Unit/Controller/AttentionControllerTest.php
create mode 100644 tests/Unit/Service/AttentionSourceServiceTest.php
diff --git a/appinfo/routes.php b/appinfo/routes.php
index f7ec41e8b..9811a3ed8 100644
--- a/appinfo/routes.php
+++ b/appinfo/routes.php
@@ -587,6 +587,10 @@
'url' => '/api/admin/demo-showcases/{id}', 'verb' => 'DELETE',
'requirements' => ['id' => '[a-z0-9\-]+']],
+ // Attention feed: what the user's apps declare as needing attention
+ // (REQ-ATT-002). Declarations only; the widget runs the counts.
+ ['name' => 'attention#sources', 'url' => '/api/attention/sources', 'verb' => 'GET'],
+
// Weather widget — cached reading for one placement (REQ-WEATHER-001).
// View-time ACL guarded in the controller; never returns the provider
// API key or raw provider URL.
diff --git a/data/templates/mijn-werkdag.json b/data/templates/mijn-werkdag.json
index f287e6c36..e03869fdf 100644
--- a/data/templates/mijn-werkdag.json
+++ b/data/templates/mijn-werkdag.json
@@ -1,11 +1,11 @@
{
"templateId": "mijn-werkdag",
- "templateVersion": 1,
+ "templateVersion": 2,
"language": "nl",
"dashboard": {
"uuid": "6f0d1c1e-5b0a-4c56-9d55-0a1e6d2f7c01",
"name": "Mijn werkdag",
- "description": "Startpagina voor medewerkers: zaken over de termijn, uw open zaken en recente activiteit.",
+ "description": "Startpagina voor medewerkers: wat vandaag eerst moet uit al uw apps, zaken over de termijn, uw open zaken en recente activiteit.",
"icon": null,
"type": "admin_template",
"userId": null,
@@ -16,7 +16,7 @@
"sortOrder": 0,
"publicationStatus": "published",
"templateCategory": "Werkplek",
- "templateDescription": "Startpagina voor medewerkers van een gemeente. Toont uw dossiq-zaken over de termijn, uw open zaken en recente activiteit. Werkt zonder extra instellingen.",
+ "templateDescription": "Startpagina voor medewerkers van een gemeente. Toont wat vandaag aandacht vraagt uit alle apps, uw dossiq-zaken over de termijn, uw open zaken en recente activiteit. Werkt zonder extra instellingen.",
"widgets": [
{
"widgetId": "header",
@@ -40,12 +40,29 @@
"sortOrder": 0
},
{
- "widgetId": "object-list",
+ "widgetId": "attention",
"gridX": 0,
"gridY": 2,
+ "gridWidth": 12,
+ "gridHeight": 5,
+ "isCompulsory": 1,
+ "isVisible": 1,
+ "styleConfig": {},
+ "customTitle": "Vandaag eerst",
+ "customIcon": null,
+ "content": {
+ "limit": 5
+ },
+ "showTitle": 1,
+ "sortOrder": 1
+ },
+ {
+ "widgetId": "object-list",
+ "gridX": 0,
+ "gridY": 7,
"gridWidth": 6,
"gridHeight": 7,
- "isCompulsory": 1,
+ "isCompulsory": 0,
"isVisible": 1,
"styleConfig": {},
"customTitle": "Zaken over de termijn",
@@ -88,12 +105,12 @@
"emptyText": "Geen van uw zaken is over de termijn."
},
"showTitle": 1,
- "sortOrder": 1
+ "sortOrder": 2
},
{
"widgetId": "object-list",
"gridX": 6,
- "gridY": 2,
+ "gridY": 7,
"gridWidth": 6,
"gridHeight": 7,
"isCompulsory": 0,
@@ -138,12 +155,12 @@
"emptyText": "Er zijn geen zaken aan u toegewezen."
},
"showTitle": 1,
- "sortOrder": 2
+ "sortOrder": 3
},
{
"widgetId": "nc-widget",
"gridX": 0,
- "gridY": 9,
+ "gridY": 14,
"gridWidth": 12,
"gridHeight": 6,
"isCompulsory": 0,
@@ -157,7 +174,7 @@
"displayMode": "vertical"
},
"showTitle": 1,
- "sortOrder": 3
+ "sortOrder": 4
}
]
}
diff --git a/l10n/be.js b/l10n/be.js
index 5b89dd3f0..6abbab7a8 100644
--- a/l10n/be.js
+++ b/l10n/be.js
@@ -1292,6 +1292,18 @@ OC.L10N.register(
"Add template" : "Add template",
"The ready-made templates could not be loaded." : "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added." : "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded.",
+ "First today" : "First today",
+ "Checking your apps…" : "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again." : "This list could not be loaded. Reload the page to try again.",
+ "And {count} more." : "And {count} more.",
+ "Nothing needs your attention right now." : "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet." : "None of your apps reports attention items yet.",
+ "Could not check: {apps}" : "Could not check: {apps}",
+ "Urgent:" : "Urgent:",
+ "Soon:" : "Soon:",
+ "For your information:" : "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app." : "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show" : "Lines to show"
},
"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);");
diff --git a/l10n/be.json b/l10n/be.json
index dccce090e..02067be3b 100644
--- a/l10n/be.json
+++ b/l10n/be.json
@@ -1269,6 +1269,18 @@
"Add template": "Add template",
"The ready-made templates could not be loaded.": "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added.": "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded.",
+ "First today": "First today",
+ "Checking your apps…": "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again.": "This list could not be loaded. Reload the page to try again.",
+ "And {count} more.": "And {count} more.",
+ "Nothing needs your attention right now.": "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet.": "None of your apps reports attention items yet.",
+ "Could not check: {apps}": "Could not check: {apps}",
+ "Urgent:": "Urgent:",
+ "Soon:": "Soon:",
+ "For your information:": "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app.": "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show": "Lines to show"
}
}
diff --git a/l10n/bg.js b/l10n/bg.js
index 603141fe9..5d7be1a89 100644
--- a/l10n/bg.js
+++ b/l10n/bg.js
@@ -1301,7 +1301,19 @@ OC.L10N.register(
"Add template" : "Add template",
"The ready-made templates could not be loaded." : "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added." : "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded.",
+ "First today" : "First today",
+ "Checking your apps…" : "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again." : "This list could not be loaded. Reload the page to try again.",
+ "And {count} more." : "And {count} more.",
+ "Nothing needs your attention right now." : "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet." : "None of your apps reports attention items yet.",
+ "Could not check: {apps}" : "Could not check: {apps}",
+ "Urgent:" : "Urgent:",
+ "Soon:" : "Soon:",
+ "For your information:" : "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app." : "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show" : "Lines to show"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/bg.json b/l10n/bg.json
index 45f976ff1..0e69479c6 100644
--- a/l10n/bg.json
+++ b/l10n/bg.json
@@ -1268,6 +1268,18 @@
"Add template": "Add template",
"The ready-made templates could not be loaded.": "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added.": "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded.",
+ "First today": "First today",
+ "Checking your apps…": "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again.": "This list could not be loaded. Reload the page to try again.",
+ "And {count} more.": "And {count} more.",
+ "Nothing needs your attention right now.": "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet.": "None of your apps reports attention items yet.",
+ "Could not check: {apps}": "Could not check: {apps}",
+ "Urgent:": "Urgent:",
+ "Soon:": "Soon:",
+ "For your information:": "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app.": "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show": "Lines to show"
}
}
diff --git a/l10n/bs.js b/l10n/bs.js
index 02aecf52c..a6ef43ffb 100644
--- a/l10n/bs.js
+++ b/l10n/bs.js
@@ -1301,7 +1301,19 @@ OC.L10N.register(
"Add template" : "Add template",
"The ready-made templates could not be loaded." : "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added." : "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded.",
+ "First today" : "First today",
+ "Checking your apps…" : "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again." : "This list could not be loaded. Reload the page to try again.",
+ "And {count} more." : "And {count} more.",
+ "Nothing needs your attention right now." : "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet." : "None of your apps reports attention items yet.",
+ "Could not check: {apps}" : "Could not check: {apps}",
+ "Urgent:" : "Urgent:",
+ "Soon:" : "Soon:",
+ "For your information:" : "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app." : "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show" : "Lines to show"
},
"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"
);
diff --git a/l10n/bs.json b/l10n/bs.json
index 1355359c2..35ade86b4 100644
--- a/l10n/bs.json
+++ b/l10n/bs.json
@@ -1269,6 +1269,18 @@
"Add template": "Add template",
"The ready-made templates could not be loaded.": "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added.": "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded.",
+ "First today": "First today",
+ "Checking your apps…": "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again.": "This list could not be loaded. Reload the page to try again.",
+ "And {count} more.": "And {count} more.",
+ "Nothing needs your attention right now.": "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet.": "None of your apps reports attention items yet.",
+ "Could not check: {apps}": "Could not check: {apps}",
+ "Urgent:": "Urgent:",
+ "Soon:": "Soon:",
+ "For your information:": "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app.": "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show": "Lines to show"
}
}
diff --git a/l10n/ca.js b/l10n/ca.js
index 92c7d47bb..144abd806 100644
--- a/l10n/ca.js
+++ b/l10n/ca.js
@@ -1301,7 +1301,19 @@ OC.L10N.register(
"Add template" : "Add template",
"The ready-made templates could not be loaded." : "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added." : "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded.",
+ "First today" : "First today",
+ "Checking your apps…" : "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again." : "This list could not be loaded. Reload the page to try again.",
+ "And {count} more." : "And {count} more.",
+ "Nothing needs your attention right now." : "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet." : "None of your apps reports attention items yet.",
+ "Could not check: {apps}" : "Could not check: {apps}",
+ "Urgent:" : "Urgent:",
+ "Soon:" : "Soon:",
+ "For your information:" : "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app." : "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show" : "Lines to show"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/ca.json b/l10n/ca.json
index 7851acd05..d013ad946 100644
--- a/l10n/ca.json
+++ b/l10n/ca.json
@@ -1268,6 +1268,18 @@
"Add template": "Add template",
"The ready-made templates could not be loaded.": "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added.": "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded.",
+ "First today": "First today",
+ "Checking your apps…": "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again.": "This list could not be loaded. Reload the page to try again.",
+ "And {count} more.": "And {count} more.",
+ "Nothing needs your attention right now.": "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet.": "None of your apps reports attention items yet.",
+ "Could not check: {apps}": "Could not check: {apps}",
+ "Urgent:": "Urgent:",
+ "Soon:": "Soon:",
+ "For your information:": "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app.": "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show": "Lines to show"
}
}
diff --git a/l10n/cs.js b/l10n/cs.js
index f6867258f..c1c120c81 100644
--- a/l10n/cs.js
+++ b/l10n/cs.js
@@ -1301,7 +1301,19 @@ OC.L10N.register(
"Add template" : "Add template",
"The ready-made templates could not be loaded." : "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added." : "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded.",
+ "First today" : "First today",
+ "Checking your apps…" : "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again." : "This list could not be loaded. Reload the page to try again.",
+ "And {count} more." : "And {count} more.",
+ "Nothing needs your attention right now." : "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet." : "None of your apps reports attention items yet.",
+ "Could not check: {apps}" : "Could not check: {apps}",
+ "Urgent:" : "Urgent:",
+ "Soon:" : "Soon:",
+ "For your information:" : "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app." : "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show" : "Lines to show"
},
"nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;"
);
diff --git a/l10n/cs.json b/l10n/cs.json
index 645a9f74b..3ad4e4472 100644
--- a/l10n/cs.json
+++ b/l10n/cs.json
@@ -1269,6 +1269,18 @@
"Add template": "Add template",
"The ready-made templates could not be loaded.": "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added.": "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded.",
+ "First today": "First today",
+ "Checking your apps…": "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again.": "This list could not be loaded. Reload the page to try again.",
+ "And {count} more.": "And {count} more.",
+ "Nothing needs your attention right now.": "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet.": "None of your apps reports attention items yet.",
+ "Could not check: {apps}": "Could not check: {apps}",
+ "Urgent:": "Urgent:",
+ "Soon:": "Soon:",
+ "For your information:": "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app.": "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show": "Lines to show"
}
}
diff --git a/l10n/da.js b/l10n/da.js
index 83b7be017..e07895e66 100644
--- a/l10n/da.js
+++ b/l10n/da.js
@@ -1301,7 +1301,19 @@ OC.L10N.register(
"Add template" : "Add template",
"The ready-made templates could not be loaded." : "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added." : "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded.",
+ "First today" : "First today",
+ "Checking your apps…" : "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again." : "This list could not be loaded. Reload the page to try again.",
+ "And {count} more." : "And {count} more.",
+ "Nothing needs your attention right now." : "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet." : "None of your apps reports attention items yet.",
+ "Could not check: {apps}" : "Could not check: {apps}",
+ "Urgent:" : "Urgent:",
+ "Soon:" : "Soon:",
+ "For your information:" : "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app." : "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show" : "Lines to show"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/da.json b/l10n/da.json
index a5981715b..42197d268 100644
--- a/l10n/da.json
+++ b/l10n/da.json
@@ -1268,6 +1268,18 @@
"Add template": "Add template",
"The ready-made templates could not be loaded.": "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added.": "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded.",
+ "First today": "First today",
+ "Checking your apps…": "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again.": "This list could not be loaded. Reload the page to try again.",
+ "And {count} more.": "And {count} more.",
+ "Nothing needs your attention right now.": "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet.": "None of your apps reports attention items yet.",
+ "Could not check: {apps}": "Could not check: {apps}",
+ "Urgent:": "Urgent:",
+ "Soon:": "Soon:",
+ "For your information:": "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app.": "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show": "Lines to show"
}
}
diff --git a/l10n/de.js b/l10n/de.js
index 3bbbd1ae6..612de3c90 100644
--- a/l10n/de.js
+++ b/l10n/de.js
@@ -1301,7 +1301,19 @@ OC.L10N.register(
"Add template" : "Add template",
"The ready-made templates could not be loaded." : "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added." : "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded.",
+ "First today" : "First today",
+ "Checking your apps…" : "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again." : "This list could not be loaded. Reload the page to try again.",
+ "And {count} more." : "And {count} more.",
+ "Nothing needs your attention right now." : "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet." : "None of your apps reports attention items yet.",
+ "Could not check: {apps}" : "Could not check: {apps}",
+ "Urgent:" : "Urgent:",
+ "Soon:" : "Soon:",
+ "For your information:" : "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app." : "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show" : "Lines to show"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/de.json b/l10n/de.json
index b046811e8..88c1a0ae7 100644
--- a/l10n/de.json
+++ b/l10n/de.json
@@ -1268,6 +1268,18 @@
"Add template": "Add template",
"The ready-made templates could not be loaded.": "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added.": "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded.",
+ "First today": "First today",
+ "Checking your apps…": "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again.": "This list could not be loaded. Reload the page to try again.",
+ "And {count} more.": "And {count} more.",
+ "Nothing needs your attention right now.": "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet.": "None of your apps reports attention items yet.",
+ "Could not check: {apps}": "Could not check: {apps}",
+ "Urgent:": "Urgent:",
+ "Soon:": "Soon:",
+ "For your information:": "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app.": "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show": "Lines to show"
}
}
diff --git a/l10n/el.js b/l10n/el.js
index 7c94a536d..a87688241 100644
--- a/l10n/el.js
+++ b/l10n/el.js
@@ -1301,7 +1301,19 @@ OC.L10N.register(
"Add template" : "Add template",
"The ready-made templates could not be loaded." : "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added." : "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded.",
+ "First today" : "First today",
+ "Checking your apps…" : "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again." : "This list could not be loaded. Reload the page to try again.",
+ "And {count} more." : "And {count} more.",
+ "Nothing needs your attention right now." : "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet." : "None of your apps reports attention items yet.",
+ "Could not check: {apps}" : "Could not check: {apps}",
+ "Urgent:" : "Urgent:",
+ "Soon:" : "Soon:",
+ "For your information:" : "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app." : "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show" : "Lines to show"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/el.json b/l10n/el.json
index a2a1d4e93..8ddc386c8 100644
--- a/l10n/el.json
+++ b/l10n/el.json
@@ -1268,6 +1268,18 @@
"Add template": "Add template",
"The ready-made templates could not be loaded.": "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added.": "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded.",
+ "First today": "First today",
+ "Checking your apps…": "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again.": "This list could not be loaded. Reload the page to try again.",
+ "And {count} more.": "And {count} more.",
+ "Nothing needs your attention right now.": "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet.": "None of your apps reports attention items yet.",
+ "Could not check: {apps}": "Could not check: {apps}",
+ "Urgent:": "Urgent:",
+ "Soon:": "Soon:",
+ "For your information:": "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app.": "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show": "Lines to show"
}
}
diff --git a/l10n/en.js b/l10n/en.js
index 40c6fc3c0..ff660cf03 100644
--- a/l10n/en.js
+++ b/l10n/en.js
@@ -1281,7 +1281,19 @@ OC.L10N.register(
"Add template" : "Add template",
"The ready-made templates could not be loaded." : "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added." : "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded.",
+ "First today" : "First today",
+ "Checking your apps…" : "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again." : "This list could not be loaded. Reload the page to try again.",
+ "And {count} more." : "And {count} more.",
+ "Nothing needs your attention right now." : "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet." : "None of your apps reports attention items yet.",
+ "Could not check: {apps}" : "Could not check: {apps}",
+ "Urgent:" : "Urgent:",
+ "Soon:" : "Soon:",
+ "For your information:" : "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app." : "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show" : "Lines to show"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/en.json b/l10n/en.json
index 96712bef8..d744b7a47 100644
--- a/l10n/en.json
+++ b/l10n/en.json
@@ -1246,6 +1246,18 @@
"Add template": "Add template",
"The ready-made templates could not be loaded.": "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added.": "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded.",
+ "First today": "First today",
+ "Checking your apps…": "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again.": "This list could not be loaded. Reload the page to try again.",
+ "And {count} more.": "And {count} more.",
+ "Nothing needs your attention right now.": "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet.": "None of your apps reports attention items yet.",
+ "Could not check: {apps}": "Could not check: {apps}",
+ "Urgent:": "Urgent:",
+ "Soon:": "Soon:",
+ "For your information:": "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app.": "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show": "Lines to show"
}
}
diff --git a/l10n/es.js b/l10n/es.js
index a4fea6fdb..6f280e522 100644
--- a/l10n/es.js
+++ b/l10n/es.js
@@ -1301,7 +1301,19 @@ OC.L10N.register(
"Add template" : "Add template",
"The ready-made templates could not be loaded." : "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added." : "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded.",
+ "First today" : "First today",
+ "Checking your apps…" : "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again." : "This list could not be loaded. Reload the page to try again.",
+ "And {count} more." : "And {count} more.",
+ "Nothing needs your attention right now." : "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet." : "None of your apps reports attention items yet.",
+ "Could not check: {apps}" : "Could not check: {apps}",
+ "Urgent:" : "Urgent:",
+ "Soon:" : "Soon:",
+ "For your information:" : "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app." : "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show" : "Lines to show"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/es.json b/l10n/es.json
index beb3d30b4..ed69d7c81 100644
--- a/l10n/es.json
+++ b/l10n/es.json
@@ -1268,6 +1268,18 @@
"Add template": "Add template",
"The ready-made templates could not be loaded.": "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added.": "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded.",
+ "First today": "First today",
+ "Checking your apps…": "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again.": "This list could not be loaded. Reload the page to try again.",
+ "And {count} more.": "And {count} more.",
+ "Nothing needs your attention right now.": "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet.": "None of your apps reports attention items yet.",
+ "Could not check: {apps}": "Could not check: {apps}",
+ "Urgent:": "Urgent:",
+ "Soon:": "Soon:",
+ "For your information:": "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app.": "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show": "Lines to show"
}
}
diff --git a/l10n/et.js b/l10n/et.js
index 0c123aef7..3e0caa846 100644
--- a/l10n/et.js
+++ b/l10n/et.js
@@ -1301,7 +1301,19 @@ OC.L10N.register(
"Add template" : "Add template",
"The ready-made templates could not be loaded." : "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added." : "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded.",
+ "First today" : "First today",
+ "Checking your apps…" : "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again." : "This list could not be loaded. Reload the page to try again.",
+ "And {count} more." : "And {count} more.",
+ "Nothing needs your attention right now." : "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet." : "None of your apps reports attention items yet.",
+ "Could not check: {apps}" : "Could not check: {apps}",
+ "Urgent:" : "Urgent:",
+ "Soon:" : "Soon:",
+ "For your information:" : "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app." : "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show" : "Lines to show"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/et.json b/l10n/et.json
index 4933da059..04a3e833e 100644
--- a/l10n/et.json
+++ b/l10n/et.json
@@ -1268,6 +1268,18 @@
"Add template": "Add template",
"The ready-made templates could not be loaded.": "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added.": "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded.",
+ "First today": "First today",
+ "Checking your apps…": "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again.": "This list could not be loaded. Reload the page to try again.",
+ "And {count} more.": "And {count} more.",
+ "Nothing needs your attention right now.": "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet.": "None of your apps reports attention items yet.",
+ "Could not check: {apps}": "Could not check: {apps}",
+ "Urgent:": "Urgent:",
+ "Soon:": "Soon:",
+ "For your information:": "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app.": "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show": "Lines to show"
}
}
diff --git a/l10n/fi.js b/l10n/fi.js
index e272fd87a..825982c0e 100644
--- a/l10n/fi.js
+++ b/l10n/fi.js
@@ -1301,7 +1301,19 @@ OC.L10N.register(
"Add template" : "Add template",
"The ready-made templates could not be loaded." : "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added." : "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded.",
+ "First today" : "First today",
+ "Checking your apps…" : "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again." : "This list could not be loaded. Reload the page to try again.",
+ "And {count} more." : "And {count} more.",
+ "Nothing needs your attention right now." : "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet." : "None of your apps reports attention items yet.",
+ "Could not check: {apps}" : "Could not check: {apps}",
+ "Urgent:" : "Urgent:",
+ "Soon:" : "Soon:",
+ "For your information:" : "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app." : "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show" : "Lines to show"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/fi.json b/l10n/fi.json
index 44b2b6c1c..eb94e79e8 100644
--- a/l10n/fi.json
+++ b/l10n/fi.json
@@ -1268,6 +1268,18 @@
"Add template": "Add template",
"The ready-made templates could not be loaded.": "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added.": "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded.",
+ "First today": "First today",
+ "Checking your apps…": "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again.": "This list could not be loaded. Reload the page to try again.",
+ "And {count} more.": "And {count} more.",
+ "Nothing needs your attention right now.": "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet.": "None of your apps reports attention items yet.",
+ "Could not check: {apps}": "Could not check: {apps}",
+ "Urgent:": "Urgent:",
+ "Soon:": "Soon:",
+ "For your information:": "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app.": "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show": "Lines to show"
}
}
diff --git a/l10n/fr.js b/l10n/fr.js
index 55729c433..71ea0b2af 100644
--- a/l10n/fr.js
+++ b/l10n/fr.js
@@ -1301,7 +1301,19 @@ OC.L10N.register(
"Add template" : "Add template",
"The ready-made templates could not be loaded." : "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added." : "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded.",
+ "First today" : "First today",
+ "Checking your apps…" : "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again." : "This list could not be loaded. Reload the page to try again.",
+ "And {count} more." : "And {count} more.",
+ "Nothing needs your attention right now." : "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet." : "None of your apps reports attention items yet.",
+ "Could not check: {apps}" : "Could not check: {apps}",
+ "Urgent:" : "Urgent:",
+ "Soon:" : "Soon:",
+ "For your information:" : "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app." : "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show" : "Lines to show"
},
"nplurals=2; plural=(n > 1);"
);
diff --git a/l10n/fr.json b/l10n/fr.json
index e6da8c4e9..9451eed62 100644
--- a/l10n/fr.json
+++ b/l10n/fr.json
@@ -1268,6 +1268,18 @@
"Add template": "Add template",
"The ready-made templates could not be loaded.": "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added.": "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded.",
+ "First today": "First today",
+ "Checking your apps…": "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again.": "This list could not be loaded. Reload the page to try again.",
+ "And {count} more.": "And {count} more.",
+ "Nothing needs your attention right now.": "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet.": "None of your apps reports attention items yet.",
+ "Could not check: {apps}": "Could not check: {apps}",
+ "Urgent:": "Urgent:",
+ "Soon:": "Soon:",
+ "For your information:": "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app.": "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show": "Lines to show"
}
}
diff --git a/l10n/ga.js b/l10n/ga.js
index f695e90af..95368abbb 100644
--- a/l10n/ga.js
+++ b/l10n/ga.js
@@ -1301,7 +1301,19 @@ OC.L10N.register(
"Add template" : "Add template",
"The ready-made templates could not be loaded." : "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added." : "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded.",
+ "First today" : "First today",
+ "Checking your apps…" : "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again." : "This list could not be loaded. Reload the page to try again.",
+ "And {count} more." : "And {count} more.",
+ "Nothing needs your attention right now." : "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet." : "None of your apps reports attention items yet.",
+ "Could not check: {apps}" : "Could not check: {apps}",
+ "Urgent:" : "Urgent:",
+ "Soon:" : "Soon:",
+ "For your information:" : "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app." : "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show" : "Lines to show"
},
"nplurals=5; plural=(n==1 ? 0 : n==2 ? 1 : n<7 ? 2 : n<11 ? 3 : 4);"
);
diff --git a/l10n/ga.json b/l10n/ga.json
index f0dd9a2e9..349c4bf78 100644
--- a/l10n/ga.json
+++ b/l10n/ga.json
@@ -1271,6 +1271,18 @@
"Add template": "Add template",
"The ready-made templates could not be loaded.": "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added.": "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded.",
+ "First today": "First today",
+ "Checking your apps…": "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again.": "This list could not be loaded. Reload the page to try again.",
+ "And {count} more.": "And {count} more.",
+ "Nothing needs your attention right now.": "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet.": "None of your apps reports attention items yet.",
+ "Could not check: {apps}": "Could not check: {apps}",
+ "Urgent:": "Urgent:",
+ "Soon:": "Soon:",
+ "For your information:": "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app.": "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show": "Lines to show"
}
}
diff --git a/l10n/hr.js b/l10n/hr.js
index a6e07687d..16c37b916 100644
--- a/l10n/hr.js
+++ b/l10n/hr.js
@@ -1301,7 +1301,19 @@ OC.L10N.register(
"Add template" : "Add template",
"The ready-made templates could not be loaded." : "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added." : "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded.",
+ "First today" : "First today",
+ "Checking your apps…" : "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again." : "This list could not be loaded. Reload the page to try again.",
+ "And {count} more." : "And {count} more.",
+ "Nothing needs your attention right now." : "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet." : "None of your apps reports attention items yet.",
+ "Could not check: {apps}" : "Could not check: {apps}",
+ "Urgent:" : "Urgent:",
+ "Soon:" : "Soon:",
+ "For your information:" : "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app." : "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show" : "Lines to show"
},
"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"
);
diff --git a/l10n/hr.json b/l10n/hr.json
index 170a0a8a1..70dd9a9c4 100644
--- a/l10n/hr.json
+++ b/l10n/hr.json
@@ -1269,6 +1269,18 @@
"Add template": "Add template",
"The ready-made templates could not be loaded.": "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added.": "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded.",
+ "First today": "First today",
+ "Checking your apps…": "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again.": "This list could not be loaded. Reload the page to try again.",
+ "And {count} more.": "And {count} more.",
+ "Nothing needs your attention right now.": "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet.": "None of your apps reports attention items yet.",
+ "Could not check: {apps}": "Could not check: {apps}",
+ "Urgent:": "Urgent:",
+ "Soon:": "Soon:",
+ "For your information:": "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app.": "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show": "Lines to show"
}
}
diff --git a/l10n/hu.js b/l10n/hu.js
index b88cd9a3a..f480d0094 100644
--- a/l10n/hu.js
+++ b/l10n/hu.js
@@ -1301,7 +1301,19 @@ OC.L10N.register(
"Add template" : "Add template",
"The ready-made templates could not be loaded." : "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added." : "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded.",
+ "First today" : "First today",
+ "Checking your apps…" : "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again." : "This list could not be loaded. Reload the page to try again.",
+ "And {count} more." : "And {count} more.",
+ "Nothing needs your attention right now." : "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet." : "None of your apps reports attention items yet.",
+ "Could not check: {apps}" : "Could not check: {apps}",
+ "Urgent:" : "Urgent:",
+ "Soon:" : "Soon:",
+ "For your information:" : "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app." : "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show" : "Lines to show"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/hu.json b/l10n/hu.json
index 4f94b19bb..ec8c51030 100644
--- a/l10n/hu.json
+++ b/l10n/hu.json
@@ -1268,6 +1268,18 @@
"Add template": "Add template",
"The ready-made templates could not be loaded.": "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added.": "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded.",
+ "First today": "First today",
+ "Checking your apps…": "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again.": "This list could not be loaded. Reload the page to try again.",
+ "And {count} more.": "And {count} more.",
+ "Nothing needs your attention right now.": "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet.": "None of your apps reports attention items yet.",
+ "Could not check: {apps}": "Could not check: {apps}",
+ "Urgent:": "Urgent:",
+ "Soon:": "Soon:",
+ "For your information:": "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app.": "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show": "Lines to show"
}
}
diff --git a/l10n/is.js b/l10n/is.js
index 76f898791..134520dc7 100644
--- a/l10n/is.js
+++ b/l10n/is.js
@@ -1301,7 +1301,19 @@ OC.L10N.register(
"Add template" : "Add template",
"The ready-made templates could not be loaded." : "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added." : "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded.",
+ "First today" : "First today",
+ "Checking your apps…" : "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again." : "This list could not be loaded. Reload the page to try again.",
+ "And {count} more." : "And {count} more.",
+ "Nothing needs your attention right now." : "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet." : "None of your apps reports attention items yet.",
+ "Could not check: {apps}" : "Could not check: {apps}",
+ "Urgent:" : "Urgent:",
+ "Soon:" : "Soon:",
+ "For your information:" : "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app." : "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show" : "Lines to show"
},
"nplurals=2; plural=(n%10!=1 || n%100==11);"
);
diff --git a/l10n/is.json b/l10n/is.json
index bb45d46bf..7381121d9 100644
--- a/l10n/is.json
+++ b/l10n/is.json
@@ -1268,6 +1268,18 @@
"Add template": "Add template",
"The ready-made templates could not be loaded.": "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added.": "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded.",
+ "First today": "First today",
+ "Checking your apps…": "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again.": "This list could not be loaded. Reload the page to try again.",
+ "And {count} more.": "And {count} more.",
+ "Nothing needs your attention right now.": "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet.": "None of your apps reports attention items yet.",
+ "Could not check: {apps}": "Could not check: {apps}",
+ "Urgent:": "Urgent:",
+ "Soon:": "Soon:",
+ "For your information:": "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app.": "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show": "Lines to show"
}
}
diff --git a/l10n/it.js b/l10n/it.js
index 3abd5d556..6070ce9bf 100644
--- a/l10n/it.js
+++ b/l10n/it.js
@@ -1301,7 +1301,19 @@ OC.L10N.register(
"Add template" : "Add template",
"The ready-made templates could not be loaded." : "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added." : "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded.",
+ "First today" : "First today",
+ "Checking your apps…" : "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again." : "This list could not be loaded. Reload the page to try again.",
+ "And {count} more." : "And {count} more.",
+ "Nothing needs your attention right now." : "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet." : "None of your apps reports attention items yet.",
+ "Could not check: {apps}" : "Could not check: {apps}",
+ "Urgent:" : "Urgent:",
+ "Soon:" : "Soon:",
+ "For your information:" : "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app." : "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show" : "Lines to show"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/it.json b/l10n/it.json
index 14766b845..29c5f7837 100644
--- a/l10n/it.json
+++ b/l10n/it.json
@@ -1268,6 +1268,18 @@
"Add template": "Add template",
"The ready-made templates could not be loaded.": "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added.": "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded.",
+ "First today": "First today",
+ "Checking your apps…": "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again.": "This list could not be loaded. Reload the page to try again.",
+ "And {count} more.": "And {count} more.",
+ "Nothing needs your attention right now.": "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet.": "None of your apps reports attention items yet.",
+ "Could not check: {apps}": "Could not check: {apps}",
+ "Urgent:": "Urgent:",
+ "Soon:": "Soon:",
+ "For your information:": "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app.": "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show": "Lines to show"
}
}
diff --git a/l10n/lb.js b/l10n/lb.js
index d9a91946f..8486ab8b3 100644
--- a/l10n/lb.js
+++ b/l10n/lb.js
@@ -1292,7 +1292,19 @@ OC.L10N.register(
"Add template" : "Add template",
"The ready-made templates could not be loaded." : "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added." : "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded.",
+ "First today" : "First today",
+ "Checking your apps…" : "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again." : "This list could not be loaded. Reload the page to try again.",
+ "And {count} more." : "And {count} more.",
+ "Nothing needs your attention right now." : "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet." : "None of your apps reports attention items yet.",
+ "Could not check: {apps}" : "Could not check: {apps}",
+ "Urgent:" : "Urgent:",
+ "Soon:" : "Soon:",
+ "For your information:" : "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app." : "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show" : "Lines to show"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/lb.json b/l10n/lb.json
index 6924832a7..66aa43a5f 100644
--- a/l10n/lb.json
+++ b/l10n/lb.json
@@ -1268,6 +1268,18 @@
"Add template": "Add template",
"The ready-made templates could not be loaded.": "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added.": "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded.",
+ "First today": "First today",
+ "Checking your apps…": "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again.": "This list could not be loaded. Reload the page to try again.",
+ "And {count} more.": "And {count} more.",
+ "Nothing needs your attention right now.": "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet.": "None of your apps reports attention items yet.",
+ "Could not check: {apps}": "Could not check: {apps}",
+ "Urgent:": "Urgent:",
+ "Soon:": "Soon:",
+ "For your information:": "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app.": "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show": "Lines to show"
}
}
diff --git a/l10n/lt.js b/l10n/lt.js
index 50af3453c..9b417ff0c 100644
--- a/l10n/lt.js
+++ b/l10n/lt.js
@@ -1301,7 +1301,19 @@ OC.L10N.register(
"Add template" : "Add template",
"The ready-made templates could not be loaded." : "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added." : "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded.",
+ "First today" : "First today",
+ "Checking your apps…" : "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again." : "This list could not be loaded. Reload the page to try again.",
+ "And {count} more." : "And {count} more.",
+ "Nothing needs your attention right now." : "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet." : "None of your apps reports attention items yet.",
+ "Could not check: {apps}" : "Could not check: {apps}",
+ "Urgent:" : "Urgent:",
+ "Soon:" : "Soon:",
+ "For your information:" : "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app." : "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show" : "Lines to show"
},
"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2);"
);
diff --git a/l10n/lt.json b/l10n/lt.json
index 01d77e55b..7e956758d 100644
--- a/l10n/lt.json
+++ b/l10n/lt.json
@@ -1269,6 +1269,18 @@
"Add template": "Add template",
"The ready-made templates could not be loaded.": "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added.": "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded.",
+ "First today": "First today",
+ "Checking your apps…": "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again.": "This list could not be loaded. Reload the page to try again.",
+ "And {count} more.": "And {count} more.",
+ "Nothing needs your attention right now.": "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet.": "None of your apps reports attention items yet.",
+ "Could not check: {apps}": "Could not check: {apps}",
+ "Urgent:": "Urgent:",
+ "Soon:": "Soon:",
+ "For your information:": "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app.": "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show": "Lines to show"
}
}
diff --git a/l10n/lv.js b/l10n/lv.js
index 4db073afd..30d466c2c 100644
--- a/l10n/lv.js
+++ b/l10n/lv.js
@@ -1301,7 +1301,19 @@ OC.L10N.register(
"Add template" : "Add template",
"The ready-made templates could not be loaded." : "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added." : "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded.",
+ "First today" : "First today",
+ "Checking your apps…" : "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again." : "This list could not be loaded. Reload the page to try again.",
+ "And {count} more." : "And {count} more.",
+ "Nothing needs your attention right now." : "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet." : "None of your apps reports attention items yet.",
+ "Could not check: {apps}" : "Could not check: {apps}",
+ "Urgent:" : "Urgent:",
+ "Soon:" : "Soon:",
+ "For your information:" : "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app." : "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show" : "Lines to show"
},
"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2);"
);
diff --git a/l10n/lv.json b/l10n/lv.json
index 01c3b0102..6929d5a8a 100644
--- a/l10n/lv.json
+++ b/l10n/lv.json
@@ -1269,6 +1269,18 @@
"Add template": "Add template",
"The ready-made templates could not be loaded.": "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added.": "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded.",
+ "First today": "First today",
+ "Checking your apps…": "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again.": "This list could not be loaded. Reload the page to try again.",
+ "And {count} more.": "And {count} more.",
+ "Nothing needs your attention right now.": "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet.": "None of your apps reports attention items yet.",
+ "Could not check: {apps}": "Could not check: {apps}",
+ "Urgent:": "Urgent:",
+ "Soon:": "Soon:",
+ "For your information:": "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app.": "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show": "Lines to show"
}
}
diff --git a/l10n/mk.js b/l10n/mk.js
index c742f9f24..3f79600cd 100644
--- a/l10n/mk.js
+++ b/l10n/mk.js
@@ -1292,7 +1292,19 @@ OC.L10N.register(
"Add template" : "Add template",
"The ready-made templates could not be loaded." : "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added." : "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded.",
+ "First today" : "First today",
+ "Checking your apps…" : "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again." : "This list could not be loaded. Reload the page to try again.",
+ "And {count} more." : "And {count} more.",
+ "Nothing needs your attention right now." : "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet." : "None of your apps reports attention items yet.",
+ "Could not check: {apps}" : "Could not check: {apps}",
+ "Urgent:" : "Urgent:",
+ "Soon:" : "Soon:",
+ "For your information:" : "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app." : "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show" : "Lines to show"
},
"nplurals=2; plural=(n%10==1 && n%100!=11 ? 0 : 1);"
);
diff --git a/l10n/mk.json b/l10n/mk.json
index 1ed2a431d..3a6588ee3 100644
--- a/l10n/mk.json
+++ b/l10n/mk.json
@@ -1268,6 +1268,18 @@
"Add template": "Add template",
"The ready-made templates could not be loaded.": "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added.": "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded.",
+ "First today": "First today",
+ "Checking your apps…": "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again.": "This list could not be loaded. Reload the page to try again.",
+ "And {count} more.": "And {count} more.",
+ "Nothing needs your attention right now.": "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet.": "None of your apps reports attention items yet.",
+ "Could not check: {apps}": "Could not check: {apps}",
+ "Urgent:": "Urgent:",
+ "Soon:": "Soon:",
+ "For your information:": "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app.": "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show": "Lines to show"
}
}
diff --git a/l10n/mt.js b/l10n/mt.js
index 4d5e8a5bc..858ef2555 100644
--- a/l10n/mt.js
+++ b/l10n/mt.js
@@ -1301,7 +1301,19 @@ OC.L10N.register(
"Add template" : "Add template",
"The ready-made templates could not be loaded." : "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added." : "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded.",
+ "First today" : "First today",
+ "Checking your apps…" : "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again." : "This list could not be loaded. Reload the page to try again.",
+ "And {count} more." : "And {count} more.",
+ "Nothing needs your attention right now." : "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet." : "None of your apps reports attention items yet.",
+ "Could not check: {apps}" : "Could not check: {apps}",
+ "Urgent:" : "Urgent:",
+ "Soon:" : "Soon:",
+ "For your information:" : "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app." : "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show" : "Lines to show"
},
"nplurals=4; plural=(n==1 ? 0 : n==0 || (n%100>1 && n%100<11) ? 1 : (n%100>10 && n%100<20) ? 2 : 3);"
);
diff --git a/l10n/mt.json b/l10n/mt.json
index 67682ca44..0ab90ece0 100644
--- a/l10n/mt.json
+++ b/l10n/mt.json
@@ -1270,6 +1270,18 @@
"Add template": "Add template",
"The ready-made templates could not be loaded.": "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added.": "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded.",
+ "First today": "First today",
+ "Checking your apps…": "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again.": "This list could not be loaded. Reload the page to try again.",
+ "And {count} more.": "And {count} more.",
+ "Nothing needs your attention right now.": "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet.": "None of your apps reports attention items yet.",
+ "Could not check: {apps}": "Could not check: {apps}",
+ "Urgent:": "Urgent:",
+ "Soon:": "Soon:",
+ "For your information:": "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app.": "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show": "Lines to show"
}
}
diff --git a/l10n/nb.js b/l10n/nb.js
index 4eb769abd..1e28eafb9 100644
--- a/l10n/nb.js
+++ b/l10n/nb.js
@@ -1301,7 +1301,19 @@ OC.L10N.register(
"Add template" : "Add template",
"The ready-made templates could not be loaded." : "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added." : "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded.",
+ "First today" : "First today",
+ "Checking your apps…" : "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again." : "This list could not be loaded. Reload the page to try again.",
+ "And {count} more." : "And {count} more.",
+ "Nothing needs your attention right now." : "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet." : "None of your apps reports attention items yet.",
+ "Could not check: {apps}" : "Could not check: {apps}",
+ "Urgent:" : "Urgent:",
+ "Soon:" : "Soon:",
+ "For your information:" : "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app." : "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show" : "Lines to show"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/nb.json b/l10n/nb.json
index a47f4b1e7..91c816e3f 100644
--- a/l10n/nb.json
+++ b/l10n/nb.json
@@ -1268,6 +1268,18 @@
"Add template": "Add template",
"The ready-made templates could not be loaded.": "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added.": "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded.",
+ "First today": "First today",
+ "Checking your apps…": "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again.": "This list could not be loaded. Reload the page to try again.",
+ "And {count} more.": "And {count} more.",
+ "Nothing needs your attention right now.": "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet.": "None of your apps reports attention items yet.",
+ "Could not check: {apps}": "Could not check: {apps}",
+ "Urgent:": "Urgent:",
+ "Soon:": "Soon:",
+ "For your information:": "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app.": "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show": "Lines to show"
}
}
diff --git a/l10n/nl.js b/l10n/nl.js
index 5f4032084..cdb5f70ef 100644
--- a/l10n/nl.js
+++ b/l10n/nl.js
@@ -1351,7 +1351,19 @@ OC.L10N.register(
"Add template" : "Sjabloon toevoegen",
"The ready-made templates could not be loaded." : "De kant-en-klare sjablonen konden niet worden geladen.",
"The template \"{name}\" could not be added." : "Het sjabloon \"{name}\" kon niet worden toegevoegd.",
- "The template \"{name}\" could not be downloaded." : "Het sjabloon \"{name}\" kon niet worden gedownload."
+ "The template \"{name}\" could not be downloaded." : "Het sjabloon \"{name}\" kon niet worden gedownload.",
+ "First today" : "Vandaag eerst",
+ "Checking your apps…" : "Uw apps worden nagekeken…",
+ "This list could not be loaded. Reload the page to try again." : "Deze lijst kon niet worden geladen. Laad de pagina opnieuw om het nog eens te proberen.",
+ "And {count} more." : "En nog {count}.",
+ "Nothing needs your attention right now." : "Niets vraagt nu uw aandacht.",
+ "None of your apps reports attention items yet." : "Geen van uw apps meldt nog iets dat aandacht vraagt.",
+ "Could not check: {apps}" : "Niet kunnen nakijken: {apps}",
+ "Urgent:" : "Dringend:",
+ "Soon:" : "Binnenkort:",
+ "For your information:" : "Ter informatie:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app." : "Toont wat vandaag aandacht vraagt, uit elke app die dat meldt. Elke regel opent de app.",
+ "Lines to show" : "Aantal regels"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/nl.json b/l10n/nl.json
index 1d97de093..c3c88510b 100644
--- a/l10n/nl.json
+++ b/l10n/nl.json
@@ -1328,6 +1328,18 @@
"Add template": "Sjabloon toevoegen",
"The ready-made templates could not be loaded.": "De kant-en-klare sjablonen konden niet worden geladen.",
"The template \"{name}\" could not be added.": "Het sjabloon \"{name}\" kon niet worden toegevoegd.",
- "The template \"{name}\" could not be downloaded.": "Het sjabloon \"{name}\" kon niet worden gedownload."
+ "The template \"{name}\" could not be downloaded.": "Het sjabloon \"{name}\" kon niet worden gedownload.",
+ "First today": "Vandaag eerst",
+ "Checking your apps…": "Uw apps worden nagekeken…",
+ "This list could not be loaded. Reload the page to try again.": "Deze lijst kon niet worden geladen. Laad de pagina opnieuw om het nog eens te proberen.",
+ "And {count} more.": "En nog {count}.",
+ "Nothing needs your attention right now.": "Niets vraagt nu uw aandacht.",
+ "None of your apps reports attention items yet.": "Geen van uw apps meldt nog iets dat aandacht vraagt.",
+ "Could not check: {apps}": "Niet kunnen nakijken: {apps}",
+ "Urgent:": "Dringend:",
+ "Soon:": "Binnenkort:",
+ "For your information:": "Ter informatie:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app.": "Toont wat vandaag aandacht vraagt, uit elke app die dat meldt. Elke regel opent de app.",
+ "Lines to show": "Aantal regels"
}
}
diff --git a/l10n/pl.js b/l10n/pl.js
index 3ad63ca61..e8a29ac87 100644
--- a/l10n/pl.js
+++ b/l10n/pl.js
@@ -1301,7 +1301,19 @@ OC.L10N.register(
"Add template" : "Add template",
"The ready-made templates could not be loaded." : "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added." : "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded.",
+ "First today" : "First today",
+ "Checking your apps…" : "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again." : "This list could not be loaded. Reload the page to try again.",
+ "And {count} more." : "And {count} more.",
+ "Nothing needs your attention right now." : "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet." : "None of your apps reports attention items yet.",
+ "Could not check: {apps}" : "Could not check: {apps}",
+ "Urgent:" : "Urgent:",
+ "Soon:" : "Soon:",
+ "For your information:" : "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app." : "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show" : "Lines to show"
},
"nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"
);
diff --git a/l10n/pl.json b/l10n/pl.json
index 83af2bc25..863fa65df 100644
--- a/l10n/pl.json
+++ b/l10n/pl.json
@@ -1269,6 +1269,18 @@
"Add template": "Add template",
"The ready-made templates could not be loaded.": "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added.": "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded.",
+ "First today": "First today",
+ "Checking your apps…": "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again.": "This list could not be loaded. Reload the page to try again.",
+ "And {count} more.": "And {count} more.",
+ "Nothing needs your attention right now.": "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet.": "None of your apps reports attention items yet.",
+ "Could not check: {apps}": "Could not check: {apps}",
+ "Urgent:": "Urgent:",
+ "Soon:": "Soon:",
+ "For your information:": "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app.": "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show": "Lines to show"
}
}
diff --git a/l10n/pt.js b/l10n/pt.js
index 2a925077b..052d6c574 100644
--- a/l10n/pt.js
+++ b/l10n/pt.js
@@ -1301,7 +1301,19 @@ OC.L10N.register(
"Add template" : "Add template",
"The ready-made templates could not be loaded." : "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added." : "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded.",
+ "First today" : "First today",
+ "Checking your apps…" : "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again." : "This list could not be loaded. Reload the page to try again.",
+ "And {count} more." : "And {count} more.",
+ "Nothing needs your attention right now." : "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet." : "None of your apps reports attention items yet.",
+ "Could not check: {apps}" : "Could not check: {apps}",
+ "Urgent:" : "Urgent:",
+ "Soon:" : "Soon:",
+ "For your information:" : "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app." : "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show" : "Lines to show"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/pt.json b/l10n/pt.json
index da3d0af22..de37b6dbd 100644
--- a/l10n/pt.json
+++ b/l10n/pt.json
@@ -1268,6 +1268,18 @@
"Add template": "Add template",
"The ready-made templates could not be loaded.": "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added.": "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded.",
+ "First today": "First today",
+ "Checking your apps…": "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again.": "This list could not be loaded. Reload the page to try again.",
+ "And {count} more.": "And {count} more.",
+ "Nothing needs your attention right now.": "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet.": "None of your apps reports attention items yet.",
+ "Could not check: {apps}": "Could not check: {apps}",
+ "Urgent:": "Urgent:",
+ "Soon:": "Soon:",
+ "For your information:": "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app.": "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show": "Lines to show"
}
}
diff --git a/l10n/rm.js b/l10n/rm.js
index 4bb3340e1..c31fa7630 100644
--- a/l10n/rm.js
+++ b/l10n/rm.js
@@ -1301,7 +1301,19 @@ OC.L10N.register(
"Add template" : "Add template",
"The ready-made templates could not be loaded." : "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added." : "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded.",
+ "First today" : "First today",
+ "Checking your apps…" : "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again." : "This list could not be loaded. Reload the page to try again.",
+ "And {count} more." : "And {count} more.",
+ "Nothing needs your attention right now." : "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet." : "None of your apps reports attention items yet.",
+ "Could not check: {apps}" : "Could not check: {apps}",
+ "Urgent:" : "Urgent:",
+ "Soon:" : "Soon:",
+ "For your information:" : "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app." : "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show" : "Lines to show"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/rm.json b/l10n/rm.json
index 1dcf7525c..982b50daa 100644
--- a/l10n/rm.json
+++ b/l10n/rm.json
@@ -1268,6 +1268,18 @@
"Add template": "Add template",
"The ready-made templates could not be loaded.": "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added.": "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded.",
+ "First today": "First today",
+ "Checking your apps…": "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again.": "This list could not be loaded. Reload the page to try again.",
+ "And {count} more.": "And {count} more.",
+ "Nothing needs your attention right now.": "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet.": "None of your apps reports attention items yet.",
+ "Could not check: {apps}": "Could not check: {apps}",
+ "Urgent:": "Urgent:",
+ "Soon:": "Soon:",
+ "For your information:": "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app.": "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show": "Lines to show"
}
}
diff --git a/l10n/ro.js b/l10n/ro.js
index 2e6c82324..c82bc2db1 100644
--- a/l10n/ro.js
+++ b/l10n/ro.js
@@ -1301,7 +1301,19 @@ OC.L10N.register(
"Add template" : "Add template",
"The ready-made templates could not be loaded." : "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added." : "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded.",
+ "First today" : "First today",
+ "Checking your apps…" : "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again." : "This list could not be loaded. Reload the page to try again.",
+ "And {count} more." : "And {count} more.",
+ "Nothing needs your attention right now." : "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet." : "None of your apps reports attention items yet.",
+ "Could not check: {apps}" : "Could not check: {apps}",
+ "Urgent:" : "Urgent:",
+ "Soon:" : "Soon:",
+ "For your information:" : "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app." : "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show" : "Lines to show"
},
"nplurals=3; plural=(n==1 ? 0 : (n==0 || (n%100>0 && n%100<20)) ? 1 : 2);"
);
diff --git a/l10n/ro.json b/l10n/ro.json
index 3c2dfee25..ddff548c7 100644
--- a/l10n/ro.json
+++ b/l10n/ro.json
@@ -1269,6 +1269,18 @@
"Add template": "Add template",
"The ready-made templates could not be loaded.": "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added.": "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded.",
+ "First today": "First today",
+ "Checking your apps…": "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again.": "This list could not be loaded. Reload the page to try again.",
+ "And {count} more.": "And {count} more.",
+ "Nothing needs your attention right now.": "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet.": "None of your apps reports attention items yet.",
+ "Could not check: {apps}": "Could not check: {apps}",
+ "Urgent:": "Urgent:",
+ "Soon:": "Soon:",
+ "For your information:": "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app.": "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show": "Lines to show"
}
}
diff --git a/l10n/ru.js b/l10n/ru.js
index 59a950775..7a8fed64c 100644
--- a/l10n/ru.js
+++ b/l10n/ru.js
@@ -1301,7 +1301,19 @@ OC.L10N.register(
"Add template" : "Add template",
"The ready-made templates could not be loaded." : "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added." : "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded.",
+ "First today" : "First today",
+ "Checking your apps…" : "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again." : "This list could not be loaded. Reload the page to try again.",
+ "And {count} more." : "And {count} more.",
+ "Nothing needs your attention right now." : "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet." : "None of your apps reports attention items yet.",
+ "Could not check: {apps}" : "Could not check: {apps}",
+ "Urgent:" : "Urgent:",
+ "Soon:" : "Soon:",
+ "For your information:" : "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app." : "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show" : "Lines to show"
},
"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"
);
diff --git a/l10n/ru.json b/l10n/ru.json
index 602cf9ea9..5860f0f04 100644
--- a/l10n/ru.json
+++ b/l10n/ru.json
@@ -1269,6 +1269,18 @@
"Add template": "Add template",
"The ready-made templates could not be loaded.": "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added.": "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded.",
+ "First today": "First today",
+ "Checking your apps…": "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again.": "This list could not be loaded. Reload the page to try again.",
+ "And {count} more.": "And {count} more.",
+ "Nothing needs your attention right now.": "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet.": "None of your apps reports attention items yet.",
+ "Could not check: {apps}": "Could not check: {apps}",
+ "Urgent:": "Urgent:",
+ "Soon:": "Soon:",
+ "For your information:": "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app.": "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show": "Lines to show"
}
}
diff --git a/l10n/sk.js b/l10n/sk.js
index deb92b075..f94a990dd 100644
--- a/l10n/sk.js
+++ b/l10n/sk.js
@@ -1301,7 +1301,19 @@ OC.L10N.register(
"Add template" : "Add template",
"The ready-made templates could not be loaded." : "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added." : "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded.",
+ "First today" : "First today",
+ "Checking your apps…" : "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again." : "This list could not be loaded. Reload the page to try again.",
+ "And {count} more." : "And {count} more.",
+ "Nothing needs your attention right now." : "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet." : "None of your apps reports attention items yet.",
+ "Could not check: {apps}" : "Could not check: {apps}",
+ "Urgent:" : "Urgent:",
+ "Soon:" : "Soon:",
+ "For your information:" : "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app." : "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show" : "Lines to show"
},
"nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;"
);
diff --git a/l10n/sk.json b/l10n/sk.json
index 83c6ec2a8..5be2898f8 100644
--- a/l10n/sk.json
+++ b/l10n/sk.json
@@ -1269,6 +1269,18 @@
"Add template": "Add template",
"The ready-made templates could not be loaded.": "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added.": "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded.",
+ "First today": "First today",
+ "Checking your apps…": "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again.": "This list could not be loaded. Reload the page to try again.",
+ "And {count} more.": "And {count} more.",
+ "Nothing needs your attention right now.": "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet.": "None of your apps reports attention items yet.",
+ "Could not check: {apps}": "Could not check: {apps}",
+ "Urgent:": "Urgent:",
+ "Soon:": "Soon:",
+ "For your information:": "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app.": "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show": "Lines to show"
}
}
diff --git a/l10n/sl.js b/l10n/sl.js
index 644181fa7..05c778c47 100644
--- a/l10n/sl.js
+++ b/l10n/sl.js
@@ -1301,7 +1301,19 @@ OC.L10N.register(
"Add template" : "Add template",
"The ready-made templates could not be loaded." : "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added." : "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded.",
+ "First today" : "First today",
+ "Checking your apps…" : "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again." : "This list could not be loaded. Reload the page to try again.",
+ "And {count} more." : "And {count} more.",
+ "Nothing needs your attention right now." : "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet." : "None of your apps reports attention items yet.",
+ "Could not check: {apps}" : "Could not check: {apps}",
+ "Urgent:" : "Urgent:",
+ "Soon:" : "Soon:",
+ "For your information:" : "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app." : "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show" : "Lines to show"
},
"nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);"
);
diff --git a/l10n/sl.json b/l10n/sl.json
index af16a3551..0ed86e605 100644
--- a/l10n/sl.json
+++ b/l10n/sl.json
@@ -1270,6 +1270,18 @@
"Add template": "Add template",
"The ready-made templates could not be loaded.": "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added.": "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded.",
+ "First today": "First today",
+ "Checking your apps…": "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again.": "This list could not be loaded. Reload the page to try again.",
+ "And {count} more.": "And {count} more.",
+ "Nothing needs your attention right now.": "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet.": "None of your apps reports attention items yet.",
+ "Could not check: {apps}": "Could not check: {apps}",
+ "Urgent:": "Urgent:",
+ "Soon:": "Soon:",
+ "For your information:": "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app.": "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show": "Lines to show"
}
}
diff --git a/l10n/sq.js b/l10n/sq.js
index a009c620f..6a369e1e3 100644
--- a/l10n/sq.js
+++ b/l10n/sq.js
@@ -1301,7 +1301,19 @@ OC.L10N.register(
"Add template" : "Add template",
"The ready-made templates could not be loaded." : "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added." : "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded.",
+ "First today" : "First today",
+ "Checking your apps…" : "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again." : "This list could not be loaded. Reload the page to try again.",
+ "And {count} more." : "And {count} more.",
+ "Nothing needs your attention right now." : "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet." : "None of your apps reports attention items yet.",
+ "Could not check: {apps}" : "Could not check: {apps}",
+ "Urgent:" : "Urgent:",
+ "Soon:" : "Soon:",
+ "For your information:" : "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app." : "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show" : "Lines to show"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/sq.json b/l10n/sq.json
index 1afdc01cf..a49d73fbf 100644
--- a/l10n/sq.json
+++ b/l10n/sq.json
@@ -1268,6 +1268,18 @@
"Add template": "Add template",
"The ready-made templates could not be loaded.": "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added.": "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded.",
+ "First today": "First today",
+ "Checking your apps…": "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again.": "This list could not be loaded. Reload the page to try again.",
+ "And {count} more.": "And {count} more.",
+ "Nothing needs your attention right now.": "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet.": "None of your apps reports attention items yet.",
+ "Could not check: {apps}": "Could not check: {apps}",
+ "Urgent:": "Urgent:",
+ "Soon:": "Soon:",
+ "For your information:": "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app.": "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show": "Lines to show"
}
}
diff --git a/l10n/sr.js b/l10n/sr.js
index f761f6adb..39eb19147 100644
--- a/l10n/sr.js
+++ b/l10n/sr.js
@@ -1292,6 +1292,18 @@ OC.L10N.register(
"Add template" : "Add template",
"The ready-made templates could not be loaded." : "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added." : "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded.",
+ "First today" : "First today",
+ "Checking your apps…" : "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again." : "This list could not be loaded. Reload the page to try again.",
+ "And {count} more." : "And {count} more.",
+ "Nothing needs your attention right now." : "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet." : "None of your apps reports attention items yet.",
+ "Could not check: {apps}" : "Could not check: {apps}",
+ "Urgent:" : "Urgent:",
+ "Soon:" : "Soon:",
+ "For your information:" : "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app." : "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show" : "Lines to show"
},
"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);");
diff --git a/l10n/sr.json b/l10n/sr.json
index 3620d27ca..31073e25d 100644
--- a/l10n/sr.json
+++ b/l10n/sr.json
@@ -1269,6 +1269,18 @@
"Add template": "Add template",
"The ready-made templates could not be loaded.": "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added.": "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded.",
+ "First today": "First today",
+ "Checking your apps…": "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again.": "This list could not be loaded. Reload the page to try again.",
+ "And {count} more.": "And {count} more.",
+ "Nothing needs your attention right now.": "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet.": "None of your apps reports attention items yet.",
+ "Could not check: {apps}": "Could not check: {apps}",
+ "Urgent:": "Urgent:",
+ "Soon:": "Soon:",
+ "For your information:": "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app.": "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show": "Lines to show"
}
}
diff --git a/l10n/sv.js b/l10n/sv.js
index 8cad4c84d..1eb085058 100644
--- a/l10n/sv.js
+++ b/l10n/sv.js
@@ -1301,7 +1301,19 @@ OC.L10N.register(
"Add template" : "Add template",
"The ready-made templates could not be loaded." : "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added." : "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded.",
+ "First today" : "First today",
+ "Checking your apps…" : "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again." : "This list could not be loaded. Reload the page to try again.",
+ "And {count} more." : "And {count} more.",
+ "Nothing needs your attention right now." : "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet." : "None of your apps reports attention items yet.",
+ "Could not check: {apps}" : "Could not check: {apps}",
+ "Urgent:" : "Urgent:",
+ "Soon:" : "Soon:",
+ "For your information:" : "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app." : "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show" : "Lines to show"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/sv.json b/l10n/sv.json
index e25d15069..ab797d24a 100644
--- a/l10n/sv.json
+++ b/l10n/sv.json
@@ -1268,6 +1268,18 @@
"Add template": "Add template",
"The ready-made templates could not be loaded.": "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added.": "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded.",
+ "First today": "First today",
+ "Checking your apps…": "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again.": "This list could not be loaded. Reload the page to try again.",
+ "And {count} more.": "And {count} more.",
+ "Nothing needs your attention right now.": "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet.": "None of your apps reports attention items yet.",
+ "Could not check: {apps}": "Could not check: {apps}",
+ "Urgent:": "Urgent:",
+ "Soon:": "Soon:",
+ "For your information:": "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app.": "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show": "Lines to show"
}
}
diff --git a/l10n/tr.js b/l10n/tr.js
index fc546e9d5..192eabd03 100644
--- a/l10n/tr.js
+++ b/l10n/tr.js
@@ -1301,7 +1301,19 @@ OC.L10N.register(
"Add template" : "Add template",
"The ready-made templates could not be loaded." : "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added." : "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded.",
+ "First today" : "First today",
+ "Checking your apps…" : "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again." : "This list could not be loaded. Reload the page to try again.",
+ "And {count} more." : "And {count} more.",
+ "Nothing needs your attention right now." : "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet." : "None of your apps reports attention items yet.",
+ "Could not check: {apps}" : "Could not check: {apps}",
+ "Urgent:" : "Urgent:",
+ "Soon:" : "Soon:",
+ "For your information:" : "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app." : "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show" : "Lines to show"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/tr.json b/l10n/tr.json
index ce912c7aa..933468920 100644
--- a/l10n/tr.json
+++ b/l10n/tr.json
@@ -1268,6 +1268,18 @@
"Add template": "Add template",
"The ready-made templates could not be loaded.": "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added.": "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded.",
+ "First today": "First today",
+ "Checking your apps…": "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again.": "This list could not be loaded. Reload the page to try again.",
+ "And {count} more.": "And {count} more.",
+ "Nothing needs your attention right now.": "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet.": "None of your apps reports attention items yet.",
+ "Could not check: {apps}": "Could not check: {apps}",
+ "Urgent:": "Urgent:",
+ "Soon:": "Soon:",
+ "For your information:": "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app.": "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show": "Lines to show"
}
}
diff --git a/l10n/uk.js b/l10n/uk.js
index d52a5f754..1283b13fb 100644
--- a/l10n/uk.js
+++ b/l10n/uk.js
@@ -1301,7 +1301,19 @@ OC.L10N.register(
"Add template" : "Add template",
"The ready-made templates could not be loaded." : "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added." : "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded." : "The template \"{name}\" could not be downloaded.",
+ "First today" : "First today",
+ "Checking your apps…" : "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again." : "This list could not be loaded. Reload the page to try again.",
+ "And {count} more." : "And {count} more.",
+ "Nothing needs your attention right now." : "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet." : "None of your apps reports attention items yet.",
+ "Could not check: {apps}" : "Could not check: {apps}",
+ "Urgent:" : "Urgent:",
+ "Soon:" : "Soon:",
+ "For your information:" : "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app." : "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show" : "Lines to show"
},
"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"
);
diff --git a/l10n/uk.json b/l10n/uk.json
index b539422d5..d930c8296 100644
--- a/l10n/uk.json
+++ b/l10n/uk.json
@@ -1269,6 +1269,18 @@
"Add template": "Add template",
"The ready-made templates could not be loaded.": "The ready-made templates could not be loaded.",
"The template \"{name}\" could not be added.": "The template \"{name}\" could not be added.",
- "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded."
+ "The template \"{name}\" could not be downloaded.": "The template \"{name}\" could not be downloaded.",
+ "First today": "First today",
+ "Checking your apps…": "Checking your apps…",
+ "This list could not be loaded. Reload the page to try again.": "This list could not be loaded. Reload the page to try again.",
+ "And {count} more.": "And {count} more.",
+ "Nothing needs your attention right now.": "Nothing needs your attention right now.",
+ "None of your apps reports attention items yet.": "None of your apps reports attention items yet.",
+ "Could not check: {apps}": "Could not check: {apps}",
+ "Urgent:": "Urgent:",
+ "Soon:": "Soon:",
+ "For your information:": "For your information:",
+ "Shows what needs attention today, from every app that reports it. Each line links into the app.": "Shows what needs attention today, from every app that reports it. Each line links into the app.",
+ "Lines to show": "Lines to show"
}
}
diff --git a/lib/Controller/AttentionController.php b/lib/Controller/AttentionController.php
new file mode 100644
index 000000000..0fee76cbd
--- /dev/null
+++ b/lib/Controller/AttentionController.php
@@ -0,0 +1,83 @@
+
+ * @copyright 2026 Conduction b.v.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ * @version GIT:auto
+ * @link https://conduction.nl
+ *
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
+ * SPDX-License-Identifier: EUPL-1.2
+ */
+
+declare(strict_types=1);
+
+namespace OCA\LaunchPad\Controller;
+
+use OCA\LaunchPad\AppInfo\Application;
+use OCA\LaunchPad\Service\AttentionSourceService;
+use OCP\AppFramework\Controller;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\Attribute\NoAdminRequired;
+use OCP\AppFramework\Http\JSONResponse;
+use OCP\IRequest;
+use OCP\IUserSession;
+
+/**
+ * The attention sources endpoint.
+ *
+ * @spec openspec/specs/attention-feed/spec.md#req-att-002
+ */
+class AttentionController extends Controller {
+ /**
+ * Constructor.
+ *
+ * @param IRequest $request The request.
+ * @param AttentionSourceService $sources Reads the apps' declarations.
+ * @param IUserSession $userSession The user session.
+ */
+ public function __construct(
+ IRequest $request,
+ private readonly AttentionSourceService $sources,
+ private readonly IUserSession $userSession,
+ ) {
+ parent::__construct(
+ appName: Application::APP_ID,
+ request: $request
+ );
+ }//end __construct()
+
+ /**
+ * The attention items the signed-in user's apps declare.
+ *
+ * Any signed-in user may ask: the answer is what apps enabled for THIS
+ * user declare, and holds no object data.
+ *
+ * @return JSONResponse `{userId, sources, invalid}`, or 401 without a session.
+ *
+ * @spec openspec/specs/attention-feed/spec.md#req-att-002
+ */
+ #[NoAdminRequired]
+ public function sources(): JSONResponse {
+ $user = $this->userSession->getUser();
+ if ($user === null) {
+ return new JSONResponse(
+ data: ['error' => 'Not authenticated'],
+ statusCode: Http::STATUS_UNAUTHORIZED
+ );
+ }
+
+ // The widget resolves `@me` in a filter to this id before it counts.
+ return new JSONResponse(
+ data: (['userId' => $user->getUID()] + $this->sources->collect(user: $user))
+ );
+ }//end sources()
+}//end class
diff --git a/lib/Service/AttentionDeclarationValidator.php b/lib/Service/AttentionDeclarationValidator.php
new file mode 100644
index 000000000..264c14341
--- /dev/null
+++ b/lib/Service/AttentionDeclarationValidator.php
@@ -0,0 +1,293 @@
+
+ * @copyright 2026 Conduction b.v.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ * @version GIT:auto
+ * @link https://conduction.nl
+ *
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
+ * SPDX-License-Identifier: EUPL-1.2
+ */
+
+declare(strict_types=1);
+
+namespace OCA\LaunchPad\Service;
+
+use InvalidArgumentException;
+
+/**
+ * Check an attention declaration.
+ *
+ * @spec openspec/specs/attention-feed/spec.md#req-att-001
+ */
+class AttentionDeclarationValidator {
+ /**
+ * The declaration version this service reads.
+ *
+ * @var int
+ */
+ public const VERSION = 1;
+
+ /**
+ * The most items one app may declare.
+ *
+ * @var int
+ */
+ public const MAX_ITEMS = 10;
+
+ /**
+ * Severities, most urgent first.
+ *
+ * @var array
+ */
+ public const SEVERITIES = ['error', 'warning', 'info'];
+
+ /**
+ * Comparison operators an item may use.
+ *
+ * @var array
+ */
+ public const OPERATORS = ['gt', 'gte', 'lt', 'lte', 'eq', 'neq'];
+
+ /**
+ * Check one declaration and return its items with defaults filled in.
+ *
+ * @param string $appId The declaring app.
+ * @param string $raw The file's contents.
+ *
+ * @return array> The checked items.
+ *
+ * @throws InvalidArgumentException With the reason, when the file is not valid.
+ *
+ * @spec openspec/specs/attention-feed/spec.md#req-att-001
+ */
+ public function check(string $appId, string $raw): array {
+ $items = $this->readItems(raw: $raw);
+
+ $checked = [];
+ foreach ($items as $index => $item) {
+ $label = 'item ' . (string)$index;
+ if (is_array($item) === false) {
+ throw new InvalidArgumentException(message: $label . ' must be an object');
+ }
+
+ $id = $this->checkId(label: $label, id: ($item['id'] ?? null));
+ if (isset($checked[$id]) === true) {
+ throw new InvalidArgumentException(message: 'item "' . $id . '" is declared twice');
+ }
+
+ $checked[$id] = $this->checkItem(appId: $appId, id: $id, item: $item);
+ }//end foreach
+
+ return array_values(array: $checked);
+ }//end check()
+
+ /**
+ * Decode the file and return its list of items, unchecked.
+ *
+ * @param string $raw The file's contents.
+ *
+ * @return array The items.
+ *
+ * @throws InvalidArgumentException With the reason.
+ */
+ private function readItems(string $raw): array {
+ $decoded = json_decode(json: $raw, associative: true);
+ if (is_array($decoded) === false) {
+ throw new InvalidArgumentException(message: 'attention.json is not valid JSON');
+ }
+
+ if (($decoded['version'] ?? null) !== self::VERSION) {
+ throw new InvalidArgumentException(message: 'attention.json must say "version": ' . (string)self::VERSION);
+ }
+
+ $items = ($decoded['items'] ?? null);
+ if (is_array($items) === false || array_is_list(array: $items) === false) {
+ throw new InvalidArgumentException(message: '"items" must be a list');
+ }
+
+ if (count($items) > self::MAX_ITEMS) {
+ throw new InvalidArgumentException(
+ message: 'at most ' . (string)self::MAX_ITEMS . ' items, found ' . (string)count($items)
+ );
+ }
+
+ return $items;
+ }//end readItems()
+
+ /**
+ * Check an item id.
+ *
+ * @param string $label How to name the item in an error.
+ * @param mixed $id The decoded `id`.
+ *
+ * @return string The id.
+ *
+ * @throws InvalidArgumentException With the reason.
+ */
+ private function checkId(string $label, mixed $id): string {
+ if (is_string($id) === false || preg_match(pattern: '/^[a-z0-9][a-z0-9-]{0,63}$/', subject: $id) !== 1) {
+ throw new InvalidArgumentException(
+ message: $label . ': "id" must be lower-case letters, digits and hyphens'
+ );
+ }
+
+ return $id;
+ }//end checkId()
+
+ /**
+ * Check one item and fill in its defaults.
+ *
+ * @param string $appId The declaring app.
+ * @param string $id The item id, already checked.
+ * @param array $item The decoded item.
+ *
+ * @return array The item, with defaults.
+ *
+ * @throws InvalidArgumentException With the reason.
+ */
+ private function checkItem(string $appId, string $id, array $item): array {
+ $label = 'item "' . $id . '"';
+
+ $texts = [
+ 'title' => ($item['title'] ?? null),
+ 'reason' => ($item['reason'] ?? null),
+ 'action.label' => ($item['action']['label'] ?? null),
+ ];
+ foreach ($texts as $field => $text) {
+ if (is_string($text) === false || trim(string: $text) === '') {
+ throw new InvalidArgumentException(message: $label . ': "' . $field . '" must be a text');
+ }
+ }
+
+ $severity = ($item['severity'] ?? 'info');
+ if (in_array(needle: $severity, haystack: self::SEVERITIES, strict: true) === false) {
+ throw new InvalidArgumentException(message: $label . ': "severity" must be error, warning or info');
+ }
+
+ $operator = ($item['op'] ?? 'gt');
+ if (in_array(needle: $operator, haystack: self::OPERATORS, strict: true) === false) {
+ throw new InvalidArgumentException(message: $label . ': "op" must be gt, gte, lt, lte, eq or neq');
+ }
+
+ $value = ($item['value'] ?? 0);
+ if (is_int($value) === false && is_float($value) === false) {
+ throw new InvalidArgumentException(message: $label . ': "value" must be a number');
+ }
+
+ return [
+ 'id' => $id,
+ 'title' => $texts['title'],
+ 'reason' => $texts['reason'],
+ 'severity' => $severity,
+ 'source' => $this->checkSource(label: $label, source: ($item['source'] ?? null)),
+ 'op' => $operator,
+ 'value' => $value,
+ 'action' => [
+ 'label' => $texts['action.label'],
+ 'path' => '/apps/' . $appId . $this->checkPath(label: $label, path: ($item['action']['path'] ?? null)),
+ ],
+ ];
+ }//end checkItem()
+
+ /**
+ * Check an item's count source.
+ *
+ * @param string $label How to name the item in an error.
+ * @param mixed $source The decoded `source`.
+ *
+ * @return array{register: string, schema: string, filter: array}
+ *
+ * @throws InvalidArgumentException With the reason.
+ */
+ private function checkSource(string $label, mixed $source): array {
+ $slug = '/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/';
+ foreach (['register', 'schema'] as $field) {
+ $name = ($source[$field] ?? null);
+ if (is_string($name) === false || preg_match(pattern: $slug, subject: $name) !== 1) {
+ throw new InvalidArgumentException(message: $label . ': "source.' . $field . '" must be a slug');
+ }
+ }
+
+ $filter = ($source['filter'] ?? []);
+ if (is_array($filter) === false || ($filter !== [] && array_is_list(array: $filter) === true)) {
+ throw new InvalidArgumentException(message: $label . ': "source.filter" must be an object');
+ }
+
+ foreach ($filter as $key => $value) {
+ // The count and the link are written from this one filter. A nested
+ // operator ({"deadline": {"lt": ...}}) cannot be written into a
+ // link, and it is the form the app lanes hit as a live defect.
+ if ($this->isFlatValue(value: $value) === false) {
+ throw new InvalidArgumentException(
+ message: $label . ': filter "' . (string)$key
+ . '" must be a text, number, boolean or a list of those.'
+ . ' Write an operator in the key, as in "deadline[lt]"'
+ );
+ }
+ }
+
+ return ['register' => $source['register'], 'schema' => $source['schema'], 'filter' => $filter];
+ }//end checkSource()
+
+ /**
+ * Whether a filter value can be written into a query string as it is.
+ *
+ * @param mixed $value The filter value.
+ *
+ * @return bool True for a scalar, or a non-empty list of scalars.
+ */
+ private function isFlatValue(mixed $value): bool {
+ if (is_scalar($value) === true) {
+ return true;
+ }
+
+ if (is_array($value) === false || $value === [] || array_is_list(array: $value) === false) {
+ return false;
+ }
+
+ foreach ($value as $entry) {
+ if (is_scalar($entry) === false) {
+ return false;
+ }
+ }
+
+ return true;
+ }//end isFlatValue()
+
+ /**
+ * Check that a link path stays inside the declaring app.
+ *
+ * @param string $label How to name the item in an error.
+ * @param mixed $path The decoded `action.path`.
+ *
+ * @return string The path.
+ *
+ * @throws InvalidArgumentException With the reason.
+ */
+ private function checkPath(string $label, mixed $path): string {
+ if (is_string($path) === false
+ || preg_match(pattern: '#^(/[A-Za-z0-9._~-]+)+/?$#', subject: $path) !== 1
+ || str_contains(haystack: $path, needle: '..') === true
+ ) {
+ throw new InvalidArgumentException(
+ message: $label . ': "action.path" must be a path inside the app, such as "/cases"'
+ );
+ }
+
+ return $path;
+ }//end checkPath()
+}//end class
diff --git a/lib/Service/AttentionSourceService.php b/lib/Service/AttentionSourceService.php
new file mode 100644
index 000000000..4030d5ebb
--- /dev/null
+++ b/lib/Service/AttentionSourceService.php
@@ -0,0 +1,191 @@
+
+ * @copyright 2026 Conduction b.v.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ * @version GIT:auto
+ * @link https://conduction.nl
+ *
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
+ * SPDX-License-Identifier: EUPL-1.2
+ */
+
+declare(strict_types=1);
+
+namespace OCA\LaunchPad\Service;
+
+use InvalidArgumentException;
+use OCP\App\AppPathNotFoundException;
+use OCP\App\IAppManager;
+use OCP\IL10N;
+use OCP\IUser;
+use OCP\L10N\IFactory;
+use Psr\Log\LoggerInterface;
+
+/**
+ * Read and check the attention declarations of the user's apps.
+ *
+ * @spec openspec/specs/attention-feed/spec.md#req-att-001
+ */
+class AttentionSourceService {
+ /**
+ * Where an app keeps its declaration, relative to the app folder.
+ *
+ * @var string
+ */
+ public const DECLARATION_PATH = '/appinfo/attention.json';
+
+ /**
+ * Checks a declaration. It holds no state and has no dependencies, so it
+ * is constructed here rather than injected.
+ *
+ * @var AttentionDeclarationValidator
+ */
+ private readonly AttentionDeclarationValidator $validator;
+
+ /**
+ * Constructor.
+ *
+ * @param IAppManager $appManager Knows which apps are enabled for a user and where they live.
+ * @param IFactory $l10nFactory Gives each app's own translations.
+ * @param LoggerInterface $logger Logger.
+ */
+ public function __construct(
+ private readonly IAppManager $appManager,
+ private readonly IFactory $l10nFactory,
+ private readonly LoggerInterface $logger,
+ ) {
+ $this->validator = new AttentionDeclarationValidator();
+ }//end __construct()
+
+ /**
+ * Every attention item the user's apps declare.
+ *
+ * @param IUser $user The signed-in user.
+ *
+ * @return array{sources: array>, invalid: array>}
+ *
+ * @spec openspec/specs/attention-feed/spec.md#req-att-001
+ */
+ public function collect(IUser $user): array {
+ $sources = [];
+ $invalid = [];
+
+ $appIds = $this->appManager->getEnabledAppsForUser(user: $user);
+ sort(array: $appIds);
+
+ foreach ($appIds as $appId) {
+ $raw = $this->readDeclaration(appId: $appId);
+ if ($raw === null) {
+ continue;
+ }
+
+ $appName = $this->resolveAppName(appId: $appId);
+
+ try {
+ $items = $this->validator->check(appId: $appId, raw: $raw);
+ } catch (InvalidArgumentException $e) {
+ // One app's mistake must not take the list down, and it must
+ // not pass for "this app has nothing": it is named.
+ $invalid[] = ['appId' => $appId, 'appName' => $appName, 'message' => $e->getMessage()];
+ $this->logger->warning(
+ message: 'Invalid attention declaration',
+ context: ['app' => $appId, 'reason' => $e->getMessage()]
+ );
+ continue;
+ }
+
+ $l10n = $this->l10nFactory->get($appId);
+ foreach ($items as $item) {
+ $sources[] = $this->present(appId: $appId, appName: $appName, item: $item, l10n: $l10n);
+ }
+ }//end foreach
+
+ return ['sources' => $sources, 'invalid' => $invalid];
+ }//end collect()
+
+ /**
+ * Read an app's declaration file, or null when the app has none.
+ *
+ * @param string $appId The app id.
+ *
+ * @return string|null The file's contents.
+ */
+ private function readDeclaration(string $appId): ?string {
+ try {
+ $path = $this->appManager->getAppPath($appId) . self::DECLARATION_PATH;
+ } catch (AppPathNotFoundException) {
+ return null;
+ }
+
+ if (is_file(filename: $path) === false || is_readable(filename: $path) === false) {
+ return null;
+ }
+
+ $raw = file_get_contents(filename: $path);
+ if ($raw === false) {
+ return null;
+ }
+
+ return $raw;
+ }//end readDeclaration()
+
+ /**
+ * The app's display name, or its id when it has none.
+ *
+ * @param string $appId The app id.
+ *
+ * @return string The name to show.
+ */
+ private function resolveAppName(string $appId): string {
+ $info = $this->appManager->getAppInfo($appId);
+ $name = ($info['name'] ?? null);
+ if (is_string($name) === true && $name !== '') {
+ return $name;
+ }
+
+ return $appId;
+ }//end resolveAppName()
+
+ /**
+ * Shape one item for the widget, in the user's language.
+ *
+ * @param string $appId The declaring app.
+ * @param string $appName Its display name.
+ * @param array $item The checked item.
+ * @param IL10N $l10n The declaring app's translations.
+ *
+ * @return array The source as the widget reads it.
+ */
+ private function present(string $appId, string $appName, array $item, IL10N $l10n): array {
+ $item['appId'] = $appId;
+ $item['appName'] = $appName;
+ // phpcs:disable CustomSniffs.Functions.NamedParameters.RequireNamedParameters
+ $item['title'] = $l10n->t($item['title']);
+ $item['reason'] = $l10n->t($item['reason']);
+ $item['action']['label'] = $l10n->t($item['action']['label']);
+ // phpcs:enable CustomSniffs.Functions.NamedParameters.RequireNamedParameters
+ return $item;
+ }//end present()
+}//end class
diff --git a/lib/widget-types.json b/lib/widget-types.json
index e5771505d..1fc754c43 100644
--- a/lib/widget-types.json
+++ b/lib/widget-types.json
@@ -9,6 +9,7 @@
],
"types": [
"ai-assistant",
+ "attention",
"calendar",
"chart",
"clock",
diff --git a/openspec/changes/cross-app-attention-feed/design.md b/openspec/changes/cross-app-attention-feed/design.md
new file mode 100644
index 000000000..20aec43cc
--- /dev/null
+++ b/openspec/changes/cross-app-attention-feed/design.md
@@ -0,0 +1,143 @@
+# Design: cross-app attention feed
+
+## Decision 1: a data file, not a PHP interface
+
+The brief preferred reading a declaration the apps already have. They have one, but only inside their JavaScript bundle (see the proposal), so LaunchPad cannot reach it. The next smallest thing is a file.
+
+`appinfo/attention.json` was chosen over a PHP interface or an event because:
+
+- it needs no code in the app, so an app cannot break LaunchPad at load time and LaunchPad needs no class from the app (`class_exists` lookups go quiet when an app id moves);
+- it is the same data the app already wrote for its own card, so a test in the app can hold the two equal;
+- LaunchPad finds it through `IAppManager`, which already knows which apps are enabled for which user.
+
+## Decision 2: the browser counts, as the user
+
+LaunchPad's server does not query OpenRegister for another user's data. The widget asks OpenRegister directly, with the user's session: `GET /apps/openregister/api/objects/{register}/{schema}?&_limit=1` and reads `total`. This is the request the apps' own cards make (`readVisibleWhenValue` in the shared library), so the number is the same number, and OpenRegister's access rules decide what the user may count.
+
+## Decision 3: the link is built from the count's filter
+
+A lesson from all four app lanes: a number and the list it links to drift apart unless they share one filter. The declaration has no separate link query. LaunchPad writes the filter into the link as the query string, tokens unresolved (`assignee=@me`), which is what the apps' own links carry. The count resolves the tokens first. One filter, two uses.
+
+A filter must be flat for this: an operator is written in the key (`"deadline[lt]": "@today+1d"`), never nested (`"deadline": {"lt": ...}`). A nested filter is refused as an invalid declaration. The app lanes hit the nested form as a live defect.
+
+## Decision 4: failure is shown
+
+Three different things must not look alike:
+
+| What happened | What the employee sees |
+| --- | --- |
+| Every count ran and none needs attention | "Nothing needs your attention right now." |
+| A count needs attention | A line with the number, the reason, the app and a link |
+| A count could not run (network, server error, no access, unknown token) | A small line under the list: "Could not check: {app}" |
+| No app declares anything | "None of your apps reports attention items yet." |
+
+When one count fails and the others are clear, the widget shows the failure and does NOT show "nothing needs your attention".
+
+## Decision 5: ranking
+
+1. Severity: `error`, then `warning`, then `info`.
+2. Then the higher count.
+3. Then the app's name, then the item id, so the order is stable.
+
+The widget shows the first `limit` lines (default 5) and says how many more there are.
+
+## The declaration
+
+```json
+{
+ "version": 1,
+ "items": [
+ {
+ "id": "cases-past-deadline",
+ "title": "Cases past their deadline",
+ "reason": "{value} of your cases are past their deadline or end today.",
+ "severity": "error",
+ "source": {
+ "register": "dossiq",
+ "schema": "case",
+ "filter": { "assignee": "@me", "isFinalStatus": false, "deadline[lt]": "@today+1d" }
+ },
+ "op": "gt",
+ "value": 0,
+ "action": { "label": "Open these cases", "path": "/cases" }
+ }
+ ]
+}
+```
+
+- `title`, `reason` and `action.label` are English source strings. LaunchPad translates them with the declaring app's own translations, so the app keeps its wording. `{value}` in `reason` is the count.
+- `action.path` is a path inside the declaring app. LaunchPad prefixes `/apps/`. A path cannot leave the app.
+- `op` is `gt`, `gte`, `lt`, `lte`, `eq` or `neq`; default `gt`. `value` defaults to 0.
+- Tokens the count resolves: `@me`, `@now`, `@today`, `@today+Nd`, `@today-Nd`, `@monthStart`, `@quarterStart`, `@yearStart`. Any other `@` token fails that count, visibly.
+
+## What each app adds
+
+Written from each app's `src/menu-layout.simple.json` on 5 October 2026. The paths are the apps' own list routes (all four use history routing under `/apps/`).
+
+**dossiq** `appinfo/attention.json`
+
+```json
+{ "version": 1, "items": [ {
+ "id": "cases-past-deadline",
+ "title": "Cases past their deadline",
+ "reason": "{value} of your cases are past their deadline or end today.",
+ "severity": "error",
+ "source": { "register": "dossiq", "schema": "case", "filter": {
+ "assignee": "@me", "isFinalStatus": false, "statusHiddenInLists": false, "isDraft": false, "deadline[lt]": "@today+1d" } },
+ "action": { "label": "Open these cases", "path": "/cases" }
+} ] }
+```
+
+**pipelinq** `appinfo/attention.json`
+
+```json
+{ "version": 1, "items": [ {
+ "id": "tickets-past-deadline",
+ "title": "Tickets past their deadline",
+ "reason": "{value} of your tickets in progress are past their deadline or end today.",
+ "severity": "error",
+ "source": { "register": "pipelinq", "schema": "ticket", "filter": {
+ "assignee": "@me", "status": "in_progress", "slaDeadline[lt]": "@today+1d" } },
+ "action": { "label": "Open these tickets", "path": "/tickets" }
+} ] }
+```
+
+**decidiq** `appinfo/attention.json`
+
+```json
+{ "version": 1, "items": [ {
+ "id": "decisions-open-for-voting",
+ "title": "Decisions wait for the vote",
+ "reason": "{value} decisions are open for voting.",
+ "severity": "warning",
+ "source": { "register": "decidiq", "schema": "decision", "filter": { "lifecycle": "voting" } },
+ "action": { "label": "Open these decisions", "path": "/decisions" }
+} ] }
+```
+
+**learniq** `appinfo/attention.json`
+
+```json
+{ "version": 1, "items": [ {
+ "id": "attendance-flags-open",
+ "title": "Attendance flags nobody has picked up",
+ "reason": "{value} attendance flags are still open. Open one to start the follow-up.",
+ "severity": "warning",
+ "source": { "register": "learniq", "schema": "attendance-flag", "filter": { "lifecycle": "open" } },
+ "action": { "label": "Open the flags", "path": "/attendance/flags" }
+} ] }
+```
+
+Each app also needs:
+
+1. the three strings of each item in its `l10n/en.json` and `l10n/nl.json` (the title is there already; `reason` and the action label were added by hand for the card and may be there too);
+2. a test that the item's `source.filter` equals the filter of its "First today" card in `src/menu-layout.simple.json`;
+3. the file in its release package (`appinfo/` ships already).
+
+Two things for the owners to decide, not decided here: decidiq's and learniq's items count for everybody, not for "me" (their cards do the same), and their severity is set to `warning` here while their own cards use the error colour.
+
+## What was considered and not done
+
+- **Bundling the four declarations in LaunchPad.** It would make the widget show something before the apps add their file. It would also put a copy of each app's filter in LaunchPad, and copies drift. Not done.
+- **Reading `src/menu-layout.simple.json` from the app's folder on disk.** `src/` is not part of every release package, and the file's shape is the shared library's to change.
+- **A server-side count.** LaunchPad would have to ask OpenRegister on the user's behalf and cache per user. The browser already holds the session.
diff --git a/openspec/changes/cross-app-attention-feed/proposal.md b/openspec/changes/cross-app-attention-feed/proposal.md
new file mode 100644
index 000000000..b0088ee00
--- /dev/null
+++ b/openspec/changes/cross-app-attention-feed/proposal.md
@@ -0,0 +1,54 @@
+---
+kind: code
+depends_on: [mijn-werkdag-template]
+---
+
+# "First today" across apps: each app says what needs attention, LaunchPad shows one ranked list
+
+## Why
+
+The Zuiddrecht start page (`LpStart`) opens with "Vandaag eerst": a few lines from all the employee's apps that need attention today, each with a link into the app. The design notes say this contract does not exist yet.
+
+Measured on 5 October 2026:
+
+- dossiq, pipelinq, decidiq and learniq each show a "First today" card on their own simple dashboard. Each is a count of objects in an OpenRegister register with a filter (`visibleWhen.source` with `register`, `schema`, `filter`), a title, a reason and a link to the list.
+- Those four declarations live in each app's `src/menu-layout.simple.json`. That file is compiled into the app's JavaScript. Nothing serves it: pipelinq and decidiq have no manifest endpoint, and the ones dossiq and learniq have serve the base manifest without this layout. LaunchPad cannot read them from a running instance.
+- Nextcloud's dashboard widget interface (`OCP\Dashboard\IWidget`, `IAPIWidgetV2`) gives one widget per app. It has no way to rank items from several apps in one list.
+
+So LaunchPad needs a small declaration it can read, in the words the apps already use.
+
+## What changes
+
+- An app declares its attention items in one data file, `appinfo/attention.json`. No PHP interface and no registration call. The vocabulary is the count source the apps already write.
+- LaunchPad reads the file of every app that is enabled for the user and offers the list at `GET /api/attention/sources`.
+- A new widget, "First today" (`attention`), runs each count in the browser as the signed-in user, ranks the items that need attention and shows them with a link into the owning app.
+- The link's query is built from the same filter as the count, so the number and the list it opens cannot drift.
+- An app that is not installed, or declares nothing, does not appear. A count that fails is shown as failed. It is never shown as "nothing to do".
+- The shipped "Mijn werkdag" template (version 2) puts the widget at the top, compulsory.
+
+## Capabilities
+
+### New capabilities
+
+- `attention-feed`: the declaration, the sources endpoint, the ranking and the widget.
+
+### Modified capabilities
+
+- `admin-templates`: REQ-TMPL-018, the shipped template gains the widget.
+
+## What each app adds (not in this change)
+
+One file, `appinfo/attention.json`, and a test that its filter equals the filter of the app's own "First today" card. Nothing else. The four files, written from what the apps ship today, are in `design.md`.
+
+## Impact
+
+- `lib/Service/AttentionSourceService.php`, `lib/Controller/AttentionController.php`, `appinfo/routes.php` (new)
+- `src/components/Widgets/Renderers/AttentionWidget.vue`, `AttentionWidgetForm.vue`, `src/services/attentionFeed.js` (new), `src/constants/widgetRegistry.js`, `lib/widget-types.json`
+- `data/templates/mijn-werkdag.json` (version 2)
+- No database migration. No change to an app other than LaunchPad.
+
+## Out of scope
+
+- One line per object ("Parkeervergunningen binnenstad: the term ends today"), as the design draws it. Version 1 shows one line per declared item with its count ("3 of your cases are past their deadline"). A per-object line needs a title field and a detail link per schema; the declaration leaves room for it (`version`).
+- Items that are not an OpenRegister count (for example unread mail).
+- A personal order or hiding items.
diff --git a/openspec/changes/cross-app-attention-feed/specs/attention-feed/spec.md b/openspec/changes/cross-app-attention-feed/specs/attention-feed/spec.md
new file mode 100644
index 000000000..caee11aa8
--- /dev/null
+++ b/openspec/changes/cross-app-attention-feed/specs/attention-feed/spec.md
@@ -0,0 +1,173 @@
+# Delta for attention-feed: a new capability
+
+## ADDED Requirements
+
+### Requirement: REQ-ATT-001 An app declares its attention items in a file
+
+An app MAY ship `appinfo/attention.json`. LaunchPad MUST read that file for every app that is enabled for the signed-in user, and for no other app. LaunchPad MUST NOT require a PHP class, an interface or a registration call from the app.
+
+The file MUST be a JSON object with `version: 1` and `items`, a list of at most 10 items. Each item MUST have:
+
+- `id`: lower-case letters, digits and hyphens, unique within the file;
+- `title`, `reason` and `action.label`: non-empty English source strings. `reason` MAY contain `{value}`, the count;
+- `source.register` and `source.schema`: OpenRegister slugs;
+- `source.filter`: optional, a FLAT object. A value is a string, number, boolean or a list of those. An operator is part of the key (`"deadline[lt]"`). A nested object as a value is invalid;
+- `action.path`: a path inside the declaring app, starting with `/`, with no `..`, no `//`, no `?` and no `#`.
+
+Each item MAY have `severity` (`error`, `warning` or `info`, default `info`), `op` (`gt`, `gte`, `lt`, `lte`, `eq` or `neq`, default `gt`) and a numeric `value` (default `0`).
+
+LaunchPad MUST translate `title`, `reason` and `action.label` with the declaring app's own translations, in the user's language.
+
+A file that is not valid MUST NOT take the feed down. The app MUST be reported under `invalid` with a reason, its valid items MUST NOT be offered in part, and every other app MUST still be offered.
+
+#### Scenario: An app with a declaration is offered
+- GIVEN dossiq is enabled for Pieter and ships `appinfo/attention.json` with the item `cases-past-deadline`
+- WHEN Pieter's browser asks `GET /api/attention/sources`
+- THEN the answer MUST hold one source with `appId: "dossiq"`, the app's display name, the item's id, its translated title, reason and action label, its source, `op: "gt"`, `value: 0` and `action.path: "/apps/dossiq/cases"`
+
+@e2e exclude Needs an app that ships the file; no app does until the app lanes add it, and this change touches LaunchPad only. Pinned by AttentionSourceServiceTest::testAnEnabledAppWithADeclarationIsOffered, which reads a real file from a real app folder.
+
+#### Scenario: An app that is not installed or declares nothing does not appear
+- GIVEN pipelinq is not installed, and the Files app is enabled and ships no `appinfo/attention.json`
+- WHEN the sources are collected
+- THEN neither MUST appear under `sources`
+- AND neither MUST appear under `invalid`
+
+@e2e exclude Pinned by AttentionSourceServiceTest::testAnAppWithoutTheFileDoesNotAppear.
+
+#### Scenario: An app that is disabled for this user does not appear
+- GIVEN decidiq is enabled only for the group "bestuur" and Pieter is not in it
+- WHEN Pieter's sources are collected
+- THEN decidiq MUST NOT appear
+
+@e2e exclude Pinned by AttentionSourceServiceTest::testOnlyAppsEnabledForTheUserAreRead.
+
+#### Scenario: A broken declaration is reported and the others still work
+- GIVEN dossiq ships a valid file and learniq ships a file whose filter nests an operator (`"deadline": {"lt": "@today"}`)
+- WHEN the sources are collected
+- THEN dossiq's item MUST be under `sources`
+- AND learniq MUST be under `invalid` with a reason that names the item and the filter key
+- AND no learniq item MUST be under `sources`
+
+@e2e exclude Pinned by AttentionSourceServiceTest::testABrokenDeclarationIsReportedAndTheOthersStillWork and ::testEveryRuleOfTheDeclarationIsChecked.
+
+#### Scenario: A link cannot leave the declaring app
+- GIVEN an app declares `action.path: "/../settings/admin"` or `"//evil.example/x"`
+- WHEN the sources are collected
+- THEN the app MUST be reported under `invalid`
+
+@e2e exclude Pinned by AttentionSourceServiceTest::testEveryRuleOfTheDeclarationIsChecked.
+
+### Requirement: REQ-ATT-002 The sources endpoint
+
+`GET /api/attention/sources` MUST return `{ userId, sources: [...], invalid: [...] }` for the signed-in user. `userId` is the user's own id, which the widget writes where a filter says `@me`. It MUST require a signed-in user and MUST NOT require an administrator. It MUST NOT run any count and MUST NOT return any object data: it returns what the apps declared.
+
+#### Scenario: A signed-in employee gets the sources
+- GIVEN Pieter is signed in
+- WHEN his browser asks `GET /api/attention/sources`
+- THEN the answer MUST be HTTP 200 with `userId: "pieter"`, `sources` and `invalid`
+
+@e2e exclude Pinned by AttentionControllerTest::testASignedInUserGetsTheSources. Not run in a browser.
+
+#### Scenario: Nobody signed in
+- WHEN a request without a session asks `GET /api/attention/sources`
+- THEN the answer MUST be HTTP 401
+
+@e2e exclude Pinned by AttentionControllerTest::testNobodySignedInGets401.
+
+### Requirement: REQ-ATT-003 The count and the link come from one filter
+
+For each source the widget MUST ask OpenRegister, as the signed-in user, for `/apps/openregister/api/objects/{register}/{schema}` with the source's filter and `_limit=1`, and MUST read `total` as the count.
+
+Before the count is asked, the widget MUST resolve these tokens in filter values: `@me`, `@now`, `@today`, `@today+Nd`, `@today-Nd`, `@monthStart`, `@quarterStart` and `@yearStart`. A value that starts with `@` and is none of these MUST fail that source (REQ-ATT-005). It MUST NOT be sent as it is.
+
+The link into the app MUST be the declared path with the SAME filter as its query string, tokens not resolved, so the app's list resolves them as it does for its own links. A list value MUST be written as repeated `key[]` parameters in both.
+
+#### Scenario: The number and the list use the same filter
+- GIVEN the dossiq source with the filter `assignee: "@me"`, `isFinalStatus: false`, `"deadline[lt]": "@today+1d"`
+- WHEN the widget builds the count request and the link on 5 October 2026 for the user `pieter`
+- THEN the count request's query MUST be `assignee=pieter&isFinalStatus=false&deadline[lt]=2026-10-06&_limit=1`
+- AND the link MUST be `/apps/dossiq/cases?assignee=@me&isFinalStatus=false&deadline[lt]=@today%2B1d` (the `+` is written as `%2B`, or the app would read a space)
+- AND the link's parameter names MUST equal the count's parameter names without `_limit`
+
+@e2e exclude Pinned by attentionFeed.spec.js "the count and the link carry the same filter". Not run in a browser.
+
+#### Scenario: An unknown token fails the source
+- GIVEN a source whose filter holds `owner: "@manager"`
+- WHEN the widget prepares the count
+- THEN no request MUST be sent for that source
+- AND the source MUST be shown as failed
+
+@e2e exclude Pinned by attentionFeed.spec.js "an unknown token fails the source and sends nothing".
+
+### Requirement: REQ-ATT-004 Merging and ranking
+
+A source needs attention when its count compared with `value` by `op` is true. The widget MUST show the sources that need attention as one list across apps, ordered by severity (`error`, `warning`, `info`), then by the higher count, then by app name, then by item id.
+
+Each line MUST show the title, the reason with the count filled in, the app's name and a link with the action label. The widget MUST show at most `limit` lines (widget setting, 1 to 10, default 5) and MUST say how many more there are.
+
+#### Scenario: Items from three apps in one list
+- GIVEN dossiq reports 3 (error), pipelinq reports 5 (error) and decidiq reports 2 (warning)
+- WHEN the widget renders
+- THEN the lines MUST be pipelinq, dossiq, decidiq, in that order
+- AND the dossiq line MUST read "3 of your cases are past their deadline or end today." and link to dossiq
+
+@e2e exclude Needs three apps that ship the file. Pinned by attentionFeed.spec.js "ranks by severity, then count, then app" and AttentionWidget.spec.js "shows one line per item that needs attention".
+
+#### Scenario: A source that does not need attention is left out
+- GIVEN learniq's count is 0 and its item says `op: "gt"`, `value: 0`
+- WHEN the widget renders
+- THEN no learniq line MUST be shown
+
+@e2e exclude Pinned by AttentionWidget.spec.js "leaves out a source that does not need attention".
+
+### Requirement: REQ-ATT-005 A failure is shown as a failure
+
+A source fails when its count request does not answer with HTTP 2xx, when the answer has no numeric `total`, when the network fails, or when a token is unknown (REQ-ATT-003). A failed source MUST be named in a line under the list ("Could not check: {apps}"). An invalid declaration (REQ-ATT-001) MUST be named there too.
+
+The widget MUST show "Nothing needs your attention right now." only when every source was checked and none needs attention. When any source failed, it MUST NOT show that sentence.
+
+When no app declares anything, the widget MUST say so ("None of your apps reports attention items yet."). When the sources endpoint itself fails, the widget MUST say the list could not be loaded.
+
+One failing source MUST NOT stop the other sources from being shown.
+
+#### Scenario: One app fails and the others are clear
+- GIVEN dossiq's count answers HTTP 500 and pipelinq's count is 0
+- WHEN the widget renders
+- THEN it MUST show "Could not check: dossiq"
+- AND it MUST NOT show "Nothing needs your attention right now."
+
+@e2e exclude Staging a failing OpenRegister answer needs request interception against an app that ships the file. Pinned by AttentionWidget.spec.js "a failed source is named and never reads as nothing to do".
+
+#### Scenario: One app fails and another needs attention
+- GIVEN dossiq's count fails and pipelinq's count is 5
+- WHEN the widget renders
+- THEN the pipelinq line MUST be shown
+- AND "Could not check: dossiq" MUST be shown under it
+
+@e2e exclude Pinned by AttentionWidget.spec.js "a failed source does not hide the others".
+
+#### Scenario: Everything checked, nothing to do
+- GIVEN two sources, both checked, both with count 0
+- WHEN the widget renders
+- THEN it MUST show "Nothing needs your attention right now."
+
+@e2e exclude Pinned by AttentionWidget.spec.js "says nothing needs attention only when every source was checked".
+
+#### Scenario: No app declares anything
+- GIVEN the sources endpoint answers with no sources and no invalid declarations
+- WHEN the widget renders
+- THEN it MUST show "None of your apps reports attention items yet."
+
+@e2e exclude Pinned by AttentionWidget.spec.js "says so when no app declares anything". This is also what the widget shows on an instance today, until an app ships the file; that was not looked at in a browser.
+
+### Requirement: REQ-ATT-006 The widget type
+
+LaunchPad MUST offer the widget as type `attention`, named "First today", in the add-widget picker. Its only setting is `limit`. It MUST be on LaunchPad's widget type list (`lib/widget-types.json`), so templates, showcases and imports can place it. The line colour MUST come from Nextcloud's theme variables, and severity MUST also be said in text for assistive technology, not by colour alone.
+
+#### Scenario: The widget can be added
+- GIVEN an employee edits a dashboard
+- WHEN they open the add-widget picker
+- THEN "First today" MUST be offered
+
+@e2e exclude Not run in a browser. Pinned by widgetRegistry.completeness.spec.js, which holds the registry equal to lib/widget-types.json.
diff --git a/openspec/changes/cross-app-attention-feed/tasks.md b/openspec/changes/cross-app-attention-feed/tasks.md
new file mode 100644
index 000000000..ff05cbb9c
--- /dev/null
+++ b/openspec/changes/cross-app-attention-feed/tasks.md
@@ -0,0 +1,9 @@
+# Tasks: cross-app-attention-feed
+
+- [x] 1. `AttentionSourceService`: read and check `appinfo/attention.json` of every app enabled for the user, translate with the app's own strings, report invalid declarations. Verify: PHPUnit with real files in a temp app folder.
+- [x] 2. `GET /api/attention/sources`, signed-in users only. Verify: controller test.
+- [x] 3. `src/services/attentionFeed.js`: resolve tokens, build the count request and the link from one filter, compare, rank. Verify: Vitest, including that the count's query and the link's query are the same filter.
+- [x] 4. `AttentionWidget.vue` and its form, registered as `attention`, added to `lib/widget-types.json`. Verify: Vitest for the four states (items, nothing, failed, none declared).
+- [x] 5. `en` and `nl` strings.
+- [x] 6. "Mijn werkdag" version 2 with the widget on top. Verify: `ShippedTemplateServiceTest`.
+- [ ] 7. Live check in a browser with at least one app carrying the file. Not done in this change's PR.
diff --git a/openspec/specs/admin-templates/spec.md b/openspec/specs/admin-templates/spec.md
index f0eb0497d..82f0c688b 100644
--- a/openspec/specs/admin-templates/spec.md
+++ b/openspec/specs/admin-templates/spec.md
@@ -769,13 +769,13 @@ A definition that is missing or malformed MUST fail the install with an error. I
The listing MUST name, per template, the Nextcloud dashboard widgets it shows that no app on the instance registers (`missingWidgets`). Those widgets are still placed, so the template has one shape on every instance.
-The first shipped template is `mijn-werkdag`, a start page for a municipal employee, in Dutch. A shipped template MUST render as installed, with no further setting: it MUST NOT proxy a Nextcloud dashboard widget that has no items API, because LaunchPad paints those only when the legacy widget bridge is switched on (off by default), and every field its lists name MUST exist in the schema they read. `mijn-werkdag` holds a header, the employee's dossiq cases past their deadline, the employee's open dossiq cases and recent activity. The header and the list of cases past their deadline are compulsory. Its permission level is `add_only`.
+The first shipped template is `mijn-werkdag`, a start page for a municipal employee, in Dutch. A shipped template MUST render as installed, with no further setting: it MUST NOT proxy a Nextcloud dashboard widget that has no items API, because LaunchPad paints those only when the legacy widget bridge is switched on (off by default), and every field its lists name MUST exist in the schema they read. `mijn-werkdag` holds a header, the "First today" list across apps (`attention-feed`, since template version 2), the employee's dossiq cases past their deadline, the employee's open dossiq cases and recent activity. The header and the "First today" list are compulsory. Its permission level is `add_only`.
#### Scenario: An administrator adds the shipped template and gives it to a group
- GIVEN LaunchPad ships the template `mijn-werkdag` and it is not installed
- WHEN an administrator adds it from the Templates page, then edits it and picks the group "medewerkers"
- THEN the template list MUST show "Mijn werkdag" with the group "medewerkers"
-- AND a member of "medewerkers" who opens LaunchPad for the first time MUST get a dashboard made from it, with the header and the list of cases past their deadline marked compulsory
+- AND a member of "medewerkers" who opens LaunchPad for the first time MUST get a dashboard made from it, with the header and the "First today" list marked compulsory
@e2e exclude No Playwright test was written for this. The install is pinned by ShippedTemplateServiceTest::testInstallAddsTheTemplateThroughTheImporter, the compulsory flags by ::testTheShippedTemplateSurvivesExportAndImport, and the page by TemplatesPage.shipped.spec.js. It has not been run in a browser.
diff --git a/openspec/specs/attention-feed/spec.md b/openspec/specs/attention-feed/spec.md
new file mode 100644
index 000000000..6fbe8dc55
--- /dev/null
+++ b/openspec/specs/attention-feed/spec.md
@@ -0,0 +1,183 @@
+---
+status: done
+---
+
+# Attention feed specification
+
+## Purpose
+
+The attention feed is the "First today" list on a start page: what needs the employee's attention today, from all their apps, in one ranked list with a link into each app. An app says what needs attention in one data file. LaunchPad reads the files, the browser counts as the signed-in user, and the widget shows the result. A count that could not run is shown as failed, never as "nothing to do".
+
+The contract and the reasons behind it are in `openspec/changes/cross-app-attention-feed/design.md`.
+
+## Requirements
+
+### Requirement: REQ-ATT-001 An app declares its attention items in a file
+
+An app MAY ship `appinfo/attention.json`. LaunchPad MUST read that file for every app that is enabled for the signed-in user, and for no other app. LaunchPad MUST NOT require a PHP class, an interface or a registration call from the app.
+
+The file MUST be a JSON object with `version: 1` and `items`, a list of at most 10 items. Each item MUST have:
+
+- `id`: lower-case letters, digits and hyphens, unique within the file;
+- `title`, `reason` and `action.label`: non-empty English source strings. `reason` MAY contain `{value}`, the count;
+- `source.register` and `source.schema`: OpenRegister slugs;
+- `source.filter`: optional, a FLAT object. A value is a string, number, boolean or a list of those. An operator is part of the key (`"deadline[lt]"`). A nested object as a value is invalid;
+- `action.path`: a path inside the declaring app, starting with `/`, with no `..`, no `//`, no `?` and no `#`.
+
+Each item MAY have `severity` (`error`, `warning` or `info`, default `info`), `op` (`gt`, `gte`, `lt`, `lte`, `eq` or `neq`, default `gt`) and a numeric `value` (default `0`).
+
+LaunchPad MUST translate `title`, `reason` and `action.label` with the declaring app's own translations, in the user's language.
+
+A file that is not valid MUST NOT take the feed down. The app MUST be reported under `invalid` with a reason, its valid items MUST NOT be offered in part, and every other app MUST still be offered.
+
+#### Scenario: An app with a declaration is offered
+- GIVEN dossiq is enabled for Pieter and ships `appinfo/attention.json` with the item `cases-past-deadline`
+- WHEN Pieter's browser asks `GET /api/attention/sources`
+- THEN the answer MUST hold one source with `appId: "dossiq"`, the app's display name, the item's id, its translated title, reason and action label, its source, `op: "gt"`, `value: 0` and `action.path: "/apps/dossiq/cases"`
+
+@e2e exclude Needs an app that ships the file; no app does until the app lanes add it, and this change touches LaunchPad only. Pinned by AttentionSourceServiceTest::testAnEnabledAppWithADeclarationIsOffered, which reads a real file from a real app folder.
+
+#### Scenario: An app that is not installed or declares nothing does not appear
+- GIVEN pipelinq is not installed, and the Files app is enabled and ships no `appinfo/attention.json`
+- WHEN the sources are collected
+- THEN neither MUST appear under `sources`
+- AND neither MUST appear under `invalid`
+
+@e2e exclude Pinned by AttentionSourceServiceTest::testAnAppWithoutTheFileDoesNotAppear.
+
+#### Scenario: An app that is disabled for this user does not appear
+- GIVEN decidiq is enabled only for the group "bestuur" and Pieter is not in it
+- WHEN Pieter's sources are collected
+- THEN decidiq MUST NOT appear
+
+@e2e exclude Pinned by AttentionSourceServiceTest::testOnlyAppsEnabledForTheUserAreRead.
+
+#### Scenario: A broken declaration is reported and the others still work
+- GIVEN dossiq ships a valid file and learniq ships a file whose filter nests an operator (`"deadline": {"lt": "@today"}`)
+- WHEN the sources are collected
+- THEN dossiq's item MUST be under `sources`
+- AND learniq MUST be under `invalid` with a reason that names the item and the filter key
+- AND no learniq item MUST be under `sources`
+
+@e2e exclude Pinned by AttentionSourceServiceTest::testABrokenDeclarationIsReportedAndTheOthersStillWork and ::testEveryRuleOfTheDeclarationIsChecked.
+
+#### Scenario: A link cannot leave the declaring app
+- GIVEN an app declares `action.path: "/../settings/admin"` or `"//evil.example/x"`
+- WHEN the sources are collected
+- THEN the app MUST be reported under `invalid`
+
+@e2e exclude Pinned by AttentionSourceServiceTest::testEveryRuleOfTheDeclarationIsChecked.
+
+### Requirement: REQ-ATT-002 The sources endpoint
+
+`GET /api/attention/sources` MUST return `{ userId, sources: [...], invalid: [...] }` for the signed-in user. `userId` is the user's own id, which the widget writes where a filter says `@me`. It MUST require a signed-in user and MUST NOT require an administrator. It MUST NOT run any count and MUST NOT return any object data: it returns what the apps declared.
+
+#### Scenario: A signed-in employee gets the sources
+- GIVEN Pieter is signed in
+- WHEN his browser asks `GET /api/attention/sources`
+- THEN the answer MUST be HTTP 200 with `userId: "pieter"`, `sources` and `invalid`
+
+@e2e exclude Pinned by AttentionControllerTest::testASignedInUserGetsTheSources. Not run in a browser.
+
+#### Scenario: Nobody signed in
+- WHEN a request without a session asks `GET /api/attention/sources`
+- THEN the answer MUST be HTTP 401
+
+@e2e exclude Pinned by AttentionControllerTest::testNobodySignedInGets401.
+
+### Requirement: REQ-ATT-003 The count and the link come from one filter
+
+For each source the widget MUST ask OpenRegister, as the signed-in user, for `/apps/openregister/api/objects/{register}/{schema}` with the source's filter and `_limit=1`, and MUST read `total` as the count.
+
+Before the count is asked, the widget MUST resolve these tokens in filter values: `@me`, `@now`, `@today`, `@today+Nd`, `@today-Nd`, `@monthStart`, `@quarterStart` and `@yearStart`. A value that starts with `@` and is none of these MUST fail that source (REQ-ATT-005). It MUST NOT be sent as it is.
+
+The link into the app MUST be the declared path with the SAME filter as its query string, tokens not resolved, so the app's list resolves them as it does for its own links. A list value MUST be written as repeated `key[]` parameters in both.
+
+#### Scenario: The number and the list use the same filter
+- GIVEN the dossiq source with the filter `assignee: "@me"`, `isFinalStatus: false`, `"deadline[lt]": "@today+1d"`
+- WHEN the widget builds the count request and the link on 5 October 2026 for the user `pieter`
+- THEN the count request's query MUST be `assignee=pieter&isFinalStatus=false&deadline[lt]=2026-10-06&_limit=1`
+- AND the link MUST be `/apps/dossiq/cases?assignee=@me&isFinalStatus=false&deadline[lt]=@today%2B1d` (the `+` is written as `%2B`, or the app would read a space)
+- AND the link's parameter names MUST equal the count's parameter names without `_limit`
+
+@e2e exclude Pinned by attentionFeed.spec.js "the count and the link carry the same filter". Not run in a browser.
+
+#### Scenario: An unknown token fails the source
+- GIVEN a source whose filter holds `owner: "@manager"`
+- WHEN the widget prepares the count
+- THEN no request MUST be sent for that source
+- AND the source MUST be shown as failed
+
+@e2e exclude Pinned by attentionFeed.spec.js "an unknown token fails the source and sends nothing".
+
+### Requirement: REQ-ATT-004 Merging and ranking
+
+A source needs attention when its count compared with `value` by `op` is true. The widget MUST show the sources that need attention as one list across apps, ordered by severity (`error`, `warning`, `info`), then by the higher count, then by app name, then by item id.
+
+Each line MUST show the title, the reason with the count filled in, the app's name and a link with the action label. The widget MUST show at most `limit` lines (widget setting, 1 to 10, default 5) and MUST say how many more there are.
+
+#### Scenario: Items from three apps in one list
+- GIVEN dossiq reports 3 (error), pipelinq reports 5 (error) and decidiq reports 2 (warning)
+- WHEN the widget renders
+- THEN the lines MUST be pipelinq, dossiq, decidiq, in that order
+- AND the dossiq line MUST read "3 of your cases are past their deadline or end today." and link to dossiq
+
+@e2e exclude Needs three apps that ship the file. Pinned by attentionFeed.spec.js "ranks by severity, then count, then app" and AttentionWidget.spec.js "shows one line per item that needs attention".
+
+#### Scenario: A source that does not need attention is left out
+- GIVEN learniq's count is 0 and its item says `op: "gt"`, `value: 0`
+- WHEN the widget renders
+- THEN no learniq line MUST be shown
+
+@e2e exclude Pinned by AttentionWidget.spec.js "leaves out a source that does not need attention".
+
+### Requirement: REQ-ATT-005 A failure is shown as a failure
+
+A source fails when its count request does not answer with HTTP 2xx, when the answer has no numeric `total`, when the network fails, or when a token is unknown (REQ-ATT-003). A failed source MUST be named in a line under the list ("Could not check: {apps}"). An invalid declaration (REQ-ATT-001) MUST be named there too.
+
+The widget MUST show "Nothing needs your attention right now." only when every source was checked and none needs attention. When any source failed, it MUST NOT show that sentence.
+
+When no app declares anything, the widget MUST say so ("None of your apps reports attention items yet."). When the sources endpoint itself fails, the widget MUST say the list could not be loaded.
+
+One failing source MUST NOT stop the other sources from being shown.
+
+#### Scenario: One app fails and the others are clear
+- GIVEN dossiq's count answers HTTP 500 and pipelinq's count is 0
+- WHEN the widget renders
+- THEN it MUST show "Could not check: dossiq"
+- AND it MUST NOT show "Nothing needs your attention right now."
+
+@e2e exclude Staging a failing OpenRegister answer needs request interception against an app that ships the file. Pinned by AttentionWidget.spec.js "a failed source is named and never reads as nothing to do".
+
+#### Scenario: One app fails and another needs attention
+- GIVEN dossiq's count fails and pipelinq's count is 5
+- WHEN the widget renders
+- THEN the pipelinq line MUST be shown
+- AND "Could not check: dossiq" MUST be shown under it
+
+@e2e exclude Pinned by AttentionWidget.spec.js "a failed source does not hide the others".
+
+#### Scenario: Everything checked, nothing to do
+- GIVEN two sources, both checked, both with count 0
+- WHEN the widget renders
+- THEN it MUST show "Nothing needs your attention right now."
+
+@e2e exclude Pinned by AttentionWidget.spec.js "says nothing needs attention only when every source was checked".
+
+#### Scenario: No app declares anything
+- GIVEN the sources endpoint answers with no sources and no invalid declarations
+- WHEN the widget renders
+- THEN it MUST show "None of your apps reports attention items yet."
+
+@e2e exclude Pinned by AttentionWidget.spec.js "says so when no app declares anything". This is also what the widget shows on an instance today, until an app ships the file; that was not looked at in a browser.
+
+### Requirement: REQ-ATT-006 The widget type
+
+LaunchPad MUST offer the widget as type `attention`, named "First today", in the add-widget picker. Its only setting is `limit`. It MUST be on LaunchPad's widget type list (`lib/widget-types.json`), so templates, showcases and imports can place it. The line colour MUST come from Nextcloud's theme variables, and severity MUST also be said in text for assistive technology, not by colour alone.
+
+#### Scenario: The widget can be added
+- GIVEN an employee edits a dashboard
+- WHEN they open the add-widget picker
+- THEN "First today" MUST be offered
+
+@e2e exclude Not run in a browser. Pinned by widgetRegistry.completeness.spec.js, which holds the registry equal to lib/widget-types.json.
diff --git a/src/components/Widgets/Renderers/AttentionWidget.vue b/src/components/Widgets/Renderers/AttentionWidget.vue
new file mode 100644
index 000000000..09e70e034
--- /dev/null
+++ b/src/components/Widgets/Renderers/AttentionWidget.vue
@@ -0,0 +1,335 @@
+
+
+
+
+
+ {{ t('launchpad', 'Checking your apps…') }}
+
+
+
+ {{
+ t(
+ 'launchpad',
+ 'This list could not be loaded. Reload the page to try again.',
+ )
+ }}
+
+ {{
+ t(
+ 'launchpad',
+ 'Shows what needs attention today, from every app that reports it. Each line links into the app.',
+ )
+ }}
+
+
+
+
+
+
+
+
+
diff --git a/src/components/Widgets/Renderers/__tests__/AttentionWidget.spec.js b/src/components/Widgets/Renderers/__tests__/AttentionWidget.spec.js
new file mode 100644
index 000000000..4d8057fd2
--- /dev/null
+++ b/src/components/Widgets/Renderers/__tests__/AttentionWidget.spec.js
@@ -0,0 +1,237 @@
+/**
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
+ * SPDX-License-Identifier: EUPL-1.2
+ *
+ * AttentionWidget: what the employee is told in each state
+ * (openspec/specs/attention-feed REQ-ATT-004, REQ-ATT-005). The feed's rules
+ * are attentionFeed.spec.js; here the feed is given and the screen is read.
+ */
+
+import { flushPromises, mount } from '@vue/test-utils'
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+
+vi.mock('@nextcloud/l10n', () => ({
+ translate: (_app, text, vars = {}) =>
+ text.replace(/\{(\w+)\}/g, (_m, k) => vars[k] ?? `{${k}}`),
+}))
+vi.mock('../../../../utils/logger.js', () => ({ logger: { error: vi.fn() } }))
+
+const loadAttentionFeed = vi.fn()
+vi.mock('../../../../services/attentionFeed.js', () => ({
+ loadAttentionFeed: (...args) => loadAttentionFeed(...args),
+}))
+
+import AttentionWidget from '../AttentionWidget.vue'
+
+function item(appId, appName, severity, count, overrides = {}) {
+ return {
+ key: `${appId}/x`,
+ id: 'x',
+ appId,
+ appName,
+ title: `${appName} title`,
+ severity,
+ count,
+ attention: true,
+ failed: false,
+ reason: `${count} things need you.`,
+ actionLabel: `Open ${appName}`,
+ href: `/index.php/apps/${appId}/list?assignee=@me`,
+ ...overrides,
+ }
+}
+
+const CLEAR = 'Nothing needs your attention right now.'
+
+async function mountWith(feed, content = {}) {
+ loadAttentionFeed.mockResolvedValue(feed)
+ const wrapper = mount(AttentionWidget, { props: { content } })
+ await flushPromises()
+ return wrapper
+}
+
+beforeEach(() => {
+ loadAttentionFeed.mockReset()
+})
+
+describe('AttentionWidget', () => {
+ it('shows one line per item that needs attention', async () => {
+ const wrapper = await mountWith({
+ items: [
+ item('pipelinq', 'Pipelinq', 'error', 5),
+ item('dossiq', 'Dossiq', 'error', 3),
+ item('decidiq', 'Decidiq', 'warning', 2),
+ ],
+ failedApps: [],
+ sourceCount: 3,
+ })
+
+ const lines = wrapper.findAll('.attention-widget__item')
+ expect(
+ lines.map((line) => line.find('.attention-widget__app').text()),
+ ).toEqual(['Pipelinq', 'Dossiq', 'Decidiq'])
+ const dossiq = wrapper.find('[data-testid="attention-item-dossiq-x"]')
+ expect(dossiq.text()).toContain('Dossiq title')
+ expect(dossiq.text()).toContain('3 things need you.')
+ expect(dossiq.find('a').text()).toBe('Open Dossiq')
+ expect(dossiq.find('a').attributes('href')).toBe(
+ '/index.php/apps/dossiq/list?assignee=@me',
+ )
+ expect(dossiq.classes()).toContain('attention-widget__item--error')
+ expect(
+ wrapper.find('[data-testid="attention-item-decidiq-x"]').classes(),
+ ).toContain('attention-widget__item--warning')
+ expect(wrapper.text()).not.toContain(CLEAR)
+ expect(wrapper.find('[data-testid="attention-failed"]').exists()).toBe(false)
+ })
+
+ it('says the severity in words, not by colour alone', async () => {
+ const wrapper = await mountWith({
+ items: [
+ item('a', 'A', 'error', 1),
+ item('b', 'B', 'warning', 1),
+ item('c', 'C', 'info', 1),
+ ],
+ failedApps: [],
+ sourceCount: 3,
+ })
+
+ expect(
+ wrapper.findAll('.attention-widget__sr').map((el) => el.text()),
+ ).toEqual(['Urgent:', 'Soon:', 'For your information:'])
+ })
+
+ it('leaves out a source that does not need attention', async () => {
+ // The feed only hands over what needs attention; the widget shows
+ // exactly that and does not invent a line for the rest.
+ const wrapper = await mountWith({
+ items: [item('dossiq', 'Dossiq', 'error', 3)],
+ failedApps: [],
+ sourceCount: 2,
+ })
+
+ expect(wrapper.findAll('.attention-widget__item')).toHaveLength(1)
+ expect(wrapper.text()).not.toContain('Learniq')
+ })
+
+ it('says nothing needs attention only when every source was checked', async () => {
+ const wrapper = await mountWith({
+ items: [],
+ failedApps: [],
+ sourceCount: 2,
+ })
+
+ expect(wrapper.find('[data-testid="attention-clear"]').text()).toBe(CLEAR)
+ expect(wrapper.find('[data-testid="attention-none"]').exists()).toBe(false)
+ expect(wrapper.find('[data-testid="attention-failed"]').exists()).toBe(false)
+ })
+
+ it('a failed source is named and never reads as nothing to do', async () => {
+ const wrapper = await mountWith({
+ items: [],
+ failedApps: ['Dossiq'],
+ sourceCount: 2,
+ })
+
+ expect(wrapper.find('[data-testid="attention-failed"]').text()).toBe(
+ 'Could not check: Dossiq',
+ )
+ expect(wrapper.find('[data-testid="attention-clear"]').exists()).toBe(false)
+ expect(wrapper.find('[data-testid="attention-none"]').exists()).toBe(false)
+ expect(wrapper.text()).not.toContain(CLEAR)
+ })
+
+ it('a failed source does not hide the others', async () => {
+ const wrapper = await mountWith({
+ items: [item('pipelinq', 'Pipelinq', 'error', 5)],
+ failedApps: ['Dossiq', 'Learniq'],
+ sourceCount: 3,
+ })
+
+ expect(
+ wrapper.find('[data-testid="attention-item-pipelinq-x"]').exists(),
+ ).toBe(true)
+ expect(wrapper.find('[data-testid="attention-failed"]').text()).toBe(
+ 'Could not check: Dossiq, Learniq',
+ )
+ expect(wrapper.text()).not.toContain(CLEAR)
+ })
+
+ it('says so when no app declares anything', async () => {
+ const wrapper = await mountWith({
+ items: [],
+ failedApps: [],
+ sourceCount: 0,
+ })
+
+ expect(wrapper.find('[data-testid="attention-none"]').text()).toBe(
+ 'None of your apps reports attention items yet.',
+ )
+ expect(wrapper.find('[data-testid="attention-clear"]').exists()).toBe(false)
+ })
+
+ it('says the list could not be loaded when the sources endpoint fails', async () => {
+ loadAttentionFeed.mockRejectedValue(new Error('500'))
+ const wrapper = mount(AttentionWidget)
+ await flushPromises()
+
+ expect(wrapper.find('[data-testid="attention-error"]').text()).toBe(
+ 'This list could not be loaded. Reload the page to try again.',
+ )
+ expect(wrapper.text()).not.toContain(CLEAR)
+ expect(wrapper.find('[data-testid="attention-none"]').exists()).toBe(false)
+ })
+
+ it('shows a loading line until the feed answers', async () => {
+ let resolve
+ loadAttentionFeed.mockReturnValue(
+ new Promise((r) => {
+ resolve = r
+ }),
+ )
+ const wrapper = mount(AttentionWidget)
+ await wrapper.vm.$nextTick()
+
+ expect(wrapper.find('[data-testid="attention-loading"]').exists()).toBe(true)
+ expect(wrapper.text()).not.toContain(CLEAR)
+
+ resolve({ items: [], failedApps: [], sourceCount: 1 })
+ await flushPromises()
+ expect(wrapper.find('[data-testid="attention-loading"]').exists()).toBe(
+ false,
+ )
+ expect(wrapper.find('[data-testid="attention-clear"]').exists()).toBe(true)
+ })
+
+ it('shows at most the configured number of lines and counts the rest', async () => {
+ const items = Array.from({ length: 7 }, (_v, i) =>
+ item(`app${i}`, `App ${i}`, 'info', 1),
+ )
+
+ const five = await mountWith({ items, failedApps: [], sourceCount: 7 })
+ expect(five.findAll('.attention-widget__item')).toHaveLength(5)
+ expect(five.find('[data-testid="attention-more"]').text()).toBe(
+ 'And 2 more.',
+ )
+
+ const two = await mountWith(
+ { items, failedApps: [], sourceCount: 7 },
+ { limit: 2 },
+ )
+ expect(two.findAll('.attention-widget__item')).toHaveLength(2)
+ expect(two.find('[data-testid="attention-more"]').text()).toBe('And 5 more.')
+
+ const capped = await mountWith(
+ { items, failedApps: [], sourceCount: 7 },
+ { limit: 500 },
+ )
+ expect(capped.findAll('.attention-widget__item')).toHaveLength(7)
+ expect(capped.find('[data-testid="attention-more"]').exists()).toBe(false)
+
+ const nonsense = await mountWith(
+ { items, failedApps: [], sourceCount: 7 },
+ { limit: 'many' },
+ )
+ expect(nonsense.findAll('.attention-widget__item')).toHaveLength(5)
+ })
+})
diff --git a/src/components/Widgets/Renderers/__tests__/AttentionWidgetForm.spec.js b/src/components/Widgets/Renderers/__tests__/AttentionWidgetForm.spec.js
new file mode 100644
index 000000000..9fb01091e
--- /dev/null
+++ b/src/components/Widgets/Renderers/__tests__/AttentionWidgetForm.spec.js
@@ -0,0 +1,62 @@
+/**
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
+ * SPDX-License-Identifier: EUPL-1.2
+ *
+ * AttentionWidgetForm: the one setting, lines to show (REQ-ATT-004), in the
+ * protocol the add-widget modal speaks (`editingWidget`, `value`,
+ * `update:content`, `validate()`).
+ */
+
+import { mount } from '@vue/test-utils'
+import { describe, expect, it, vi } from 'vitest'
+
+vi.mock('@nextcloud/l10n', () => ({ translate: (_app, text) => text }))
+vi.mock('@nextcloud/vue', () => ({
+ NcSelect: {
+ name: 'NcSelect',
+ props: ['modelValue', 'options', 'inputLabel', 'clearable'],
+ emits: ['update:modelValue'],
+ template:
+ '',
+ },
+}))
+
+import AttentionWidgetForm from '../AttentionWidgetForm.vue'
+
+describe('AttentionWidgetForm', () => {
+ it('starts at five lines, with a labelled select', () => {
+ const wrapper = mount(AttentionWidgetForm)
+
+ const select = wrapper.find('.select-stub')
+ expect(select.attributes('data-value')).toBe('5')
+ expect(select.attributes('data-label')).toBe('Lines to show')
+ expect(wrapper.vm.validate()).toEqual([])
+ })
+
+ it('opens on the limit of the widget being edited', () => {
+ const wrapper = mount(AttentionWidgetForm, {
+ props: { editingWidget: { content: { limit: 8 } } },
+ })
+
+ expect(wrapper.find('.select-stub').attributes('data-value')).toBe('8')
+ })
+
+ it('falls back to five for a limit outside 1 to 10', () => {
+ const wrapper = mount(AttentionWidgetForm, {
+ props: { editingWidget: { content: { limit: 40 } } },
+ })
+
+ expect(wrapper.find('.select-stub').attributes('data-value')).toBe('5')
+ })
+
+ it('emits the content when the limit changes', async () => {
+ const wrapper = mount(AttentionWidgetForm)
+
+ await wrapper
+ .findComponent({ name: 'NcSelect' })
+ .vm.$emit('update:modelValue', 3)
+
+ expect(wrapper.emitted('update:content')).toEqual([[{ limit: 3 }]])
+ expect(wrapper.find('.select-stub').attributes('data-value')).toBe('3')
+ })
+})
diff --git a/src/constants/widgetRegistry.js b/src/constants/widgetRegistry.js
index 5effd6b88..ae0f9c637 100644
--- a/src/constants/widgetRegistry.js
+++ b/src/constants/widgetRegistry.js
@@ -64,6 +64,8 @@ import {
// registration pattern as `clock`/`weather`.
import AiAssistantWidget from '../components/Widgets/Renderers/AiAssistantWidget.vue'
import AiAssistantWidgetForm from '../components/Widgets/Renderers/AiAssistantWidgetForm.vue'
+import AttentionWidget from '../components/Widgets/Renderers/AttentionWidget.vue'
+import AttentionWidgetForm from '../components/Widgets/Renderers/AttentionWidgetForm.vue'
import ChartHost from '../components/Widgets/Renderers/ChartHost.vue'
import ClockWidget from '../components/Widgets/Renderers/ClockWidget.vue'
import ClockWidgetForm from '../components/Widgets/Renderers/ClockWidgetForm.vue'
@@ -168,6 +170,19 @@ registerDashboardWidget('ai-assistant', {
icon: 'RobotOutline',
})
+// `attention`: "First today", the ranked list of what the user's apps say
+// needs attention (openspec/specs/attention-feed). LaunchPad-only: it reads
+// LaunchPad's own sources endpoint. Same registration pattern as `clock`.
+registerDashboardWidget('attention', {
+ renderer: AttentionWidget,
+ form: AttentionWidgetForm,
+ defaultContent: {
+ limit: 5,
+ },
+ displayName: 'First today',
+ icon: 'AlertCircleOutline',
+})
+
registerDashboardWidget('clock', {
renderer: ClockWidget,
form: ClockWidgetForm,
diff --git a/src/services/__tests__/attentionFeed.spec.js b/src/services/__tests__/attentionFeed.spec.js
new file mode 100644
index 000000000..5646434d4
--- /dev/null
+++ b/src/services/__tests__/attentionFeed.spec.js
@@ -0,0 +1,422 @@
+/**
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
+ * SPDX-License-Identifier: EUPL-1.2
+ *
+ * The attention feed's rules (openspec/specs/attention-feed REQ-ATT-003..005):
+ * tokens, the one filter behind both the count and the link, the comparison,
+ * the ranking, and what a failure becomes.
+ *
+ * The sources are the declarations the proposal asks dossiq, pipelinq,
+ * decidiq and learniq to ship, in the shape AttentionSourceService returns.
+ */
+
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+
+const get = vi.fn()
+vi.mock('@nextcloud/axios', () => ({ default: { get: (...args) => get(...args) } }))
+vi.mock('@nextcloud/router', () => ({ generateUrl: (path) => `/index.php${path}` }))
+
+import {
+ actionPath,
+ checkSource,
+ countPath,
+ filterPairs,
+ loadAttentionFeed,
+ needsAttention,
+ rank,
+ resolveToken,
+} from '../attentionFeed.js'
+
+// Monday 5 October 2026, 14:30 local time.
+const ctx = { userId: 'pieter', now: new Date(2026, 9, 5, 14, 30, 0) }
+
+const dossiq = {
+ appId: 'dossiq',
+ appName: 'Dossiq',
+ id: 'cases-past-deadline',
+ title: 'Cases past their deadline',
+ reason: '{value} of your cases are past their deadline or end today.',
+ severity: 'error',
+ source: {
+ register: 'dossiq',
+ schema: 'case',
+ filter: {
+ assignee: '@me',
+ isFinalStatus: false,
+ statusHiddenInLists: false,
+ isDraft: false,
+ 'deadline[lt]': '@today+1d',
+ },
+ },
+ op: 'gt',
+ value: 0,
+ action: { label: 'Open these cases', path: '/apps/dossiq/cases' },
+}
+const pipelinq = {
+ appId: 'pipelinq',
+ appName: 'Pipelinq',
+ id: 'tickets-past-deadline',
+ title: 'Tickets past their deadline',
+ reason: '{value} of your tickets in progress are past their deadline or end today.',
+ severity: 'error',
+ source: {
+ register: 'pipelinq',
+ schema: 'ticket',
+ filter: {
+ assignee: '@me',
+ status: 'in_progress',
+ 'slaDeadline[lt]': '@today+1d',
+ },
+ },
+ op: 'gt',
+ value: 0,
+ action: { label: 'Open these tickets', path: '/apps/pipelinq/tickets' },
+}
+const decidiq = {
+ appId: 'decidiq',
+ appName: 'Decidiq',
+ id: 'decisions-open-for-voting',
+ title: 'Decisions wait for the vote',
+ reason: '{value} decisions are open for voting.',
+ severity: 'warning',
+ source: {
+ register: 'decidiq',
+ schema: 'decision',
+ filter: { lifecycle: 'voting' },
+ },
+ op: 'gt',
+ value: 0,
+ action: { label: 'Open these decisions', path: '/apps/decidiq/decisions' },
+}
+
+/** Split `a=b&c=d` into decoded pairs. */
+function pairsOf(url) {
+ const query = url.includes('?') ? url.slice(url.indexOf('?') + 1) : ''
+ return query === ''
+ ? []
+ : query.split('&').map((part) => part.split('=').map(decodeURIComponent))
+}
+
+/** Answer each OpenRegister count by register, or reject. */
+function answerCounts(byRegister) {
+ get.mockImplementation((url) => {
+ if (url.endsWith('/api/attention/sources')) {
+ return Promise.resolve({ data: byRegister.__sources })
+ }
+ const register = /\/api\/objects\/([^/]+)\//.exec(url)[1]
+ const answer = byRegister[register]
+ return answer instanceof Error
+ ? Promise.reject(answer)
+ : Promise.resolve({ data: answer })
+ })
+}
+
+beforeEach(() => {
+ get.mockReset()
+})
+
+describe('resolveToken', () => {
+ it('resolves every token the contract names', () => {
+ expect(resolveToken('@me', ctx)).toBe('pieter')
+ expect(resolveToken('@today', ctx)).toBe('2026-10-05')
+ expect(resolveToken('@today+1d', ctx)).toBe('2026-10-06')
+ expect(resolveToken('@today-7d', ctx)).toBe('2026-09-28')
+ expect(resolveToken('@today+30d', ctx)).toBe('2026-11-04')
+ expect(resolveToken('@monthStart', ctx)).toBe('2026-10-01')
+ expect(resolveToken('@quarterStart', ctx)).toBe('2026-10-01')
+ expect(resolveToken('@yearStart', ctx)).toBe('2026-01-01')
+ expect(resolveToken('@now', ctx)).toBe(ctx.now.toISOString())
+ })
+
+ it('leaves everything that is not a token alone', () => {
+ expect(resolveToken('in_progress', ctx)).toBe('in_progress')
+ expect(resolveToken(false, ctx)).toBe(false)
+ expect(resolveToken(3, ctx)).toBe(3)
+ expect(resolveToken('mail@example.org', ctx)).toBe('mail@example.org')
+ })
+
+ it('throws on a token it does not know, and on @me without a user', () => {
+ expect(() => resolveToken('@manager', ctx)).toThrow('unknown token @manager')
+ expect(() => resolveToken('@today+1w', ctx)).toThrow('unknown token')
+ expect(() => resolveToken('@me', { userId: '', now: ctx.now })).toThrow(
+ '@me needs a signed-in user',
+ )
+ })
+})
+
+describe('the count and the link', () => {
+ it('the count and the link carry the same filter', () => {
+ const count = countPath(dossiq, ctx)
+ const link = actionPath(dossiq)
+
+ expect(count).toBe(
+ '/apps/openregister/api/objects/dossiq/case'
+ + '?assignee=pieter&isFinalStatus=false&statusHiddenInLists=false&isDraft=false&deadline[lt]=2026-10-06&_limit=1',
+ )
+ expect(link).toBe(
+ '/apps/dossiq/cases'
+ + '?assignee=@me&isFinalStatus=false&statusHiddenInLists=false&isDraft=false&deadline[lt]=@today%2B1d',
+ )
+
+ // The serialised addresses, compared: same keys in the same order,
+ // and each link value resolves to the count's value. This is the
+ // check all four app lanes had to add after a live defect.
+ const countPairs = pairsOf(count).filter(([key]) => key !== '_limit')
+ const linkPairs = pairsOf(link)
+ expect(linkPairs.map(([key]) => key)).toEqual(countPairs.map(([key]) => key))
+ expect(
+ linkPairs.map(([, value]) => String(resolveToken(value, ctx))),
+ ).toEqual(countPairs.map(([, value]) => value))
+ })
+
+ it('holds for every declaration the apps are asked to ship', () => {
+ for (const source of [dossiq, pipelinq, decidiq]) {
+ const countPairs = pairsOf(countPath(source, ctx)).filter(
+ ([key]) => key !== '_limit',
+ )
+ const linkPairs = pairsOf(actionPath(source))
+ expect(
+ linkPairs.map(([key]) => key),
+ source.appId,
+ ).toEqual(Object.keys(source.source.filter))
+ expect(
+ linkPairs.map(([, value]) => String(resolveToken(value, ctx))),
+ source.appId,
+ ).toEqual(countPairs.map(([, value]) => value))
+ }
+ })
+
+ it('writes a list as repeated key[] pairs in both', () => {
+ const source = {
+ ...decidiq,
+ source: {
+ register: 'decidiq',
+ schema: 'governance-commitment',
+ filter: { lifecycle: ['open', 'in-execution'] },
+ },
+ }
+
+ expect(filterPairs(source.source.filter)).toEqual([
+ ['lifecycle[]', 'open'],
+ ['lifecycle[]', 'in-execution'],
+ ])
+ expect(countPath(source, ctx)).toContain(
+ '?lifecycle[]=open&lifecycle[]=in-execution&_limit=1',
+ )
+ expect(actionPath(source)).toBe(
+ '/apps/decidiq/decisions?lifecycle[]=open&lifecycle[]=in-execution',
+ )
+ })
+
+ it('links to the bare path when there is no filter', () => {
+ const source = {
+ ...decidiq,
+ source: { register: 'decidiq', schema: 'decision', filter: {} },
+ }
+
+ expect(actionPath(source)).toBe('/apps/decidiq/decisions')
+ expect(countPath(source, ctx)).toBe(
+ '/apps/openregister/api/objects/decidiq/decision?_limit=1',
+ )
+ })
+
+ it('encodes a value that could break out of the query', () => {
+ const source = {
+ ...decidiq,
+ source: { register: 'r', schema: 's', filter: { title: 'a&b=c #d' } },
+ }
+
+ expect(actionPath(source)).toBe(
+ '/apps/decidiq/decisions?title=a%26b%3Dc%20%23d',
+ )
+ expect(pairsOf(countPath(source, ctx))).toEqual([
+ ['title', 'a&b=c #d'],
+ ['_limit', '1'],
+ ])
+ })
+})
+
+describe('needsAttention', () => {
+ it('compares with every operator', () => {
+ expect(needsAttention(1, 'gt', 0)).toBe(true)
+ expect(needsAttention(0, 'gt', 0)).toBe(false)
+ expect(needsAttention(0, 'gte', 0)).toBe(true)
+ expect(needsAttention(2, 'lt', 3)).toBe(true)
+ expect(needsAttention(3, 'lt', 3)).toBe(false)
+ expect(needsAttention(3, 'lte', 3)).toBe(true)
+ expect(needsAttention(3, 'eq', 3)).toBe(true)
+ expect(needsAttention(3, 'neq', 3)).toBe(false)
+ expect(() => needsAttention(1, 'contains', 0)).toThrow('unknown operator')
+ })
+})
+
+describe('rank', () => {
+ it('ranks by severity, then count, then app', () => {
+ const entry = (appName, severity, count, id = 'x') => ({
+ appName,
+ severity,
+ count,
+ id,
+ })
+ const ranked = rank([
+ entry('Decidiq', 'warning', 2),
+ entry('Learniq', 'info', 99),
+ entry('Dossiq', 'error', 3),
+ entry('Pipelinq', 'error', 5),
+ entry('Alpha', 'warning', 2),
+ entry('Alpha', 'warning', 2, 'a'),
+ ])
+
+ expect(ranked.map((e) => `${e.appName}/${e.id}`)).toEqual([
+ 'Pipelinq/x',
+ 'Dossiq/x',
+ 'Alpha/a',
+ 'Alpha/x',
+ 'Decidiq/x',
+ 'Learniq/x',
+ ])
+ })
+})
+
+describe('checkSource', () => {
+ it('fills the count into the reason and links into the app', async () => {
+ get.mockResolvedValue({ data: { total: 3, results: [{}] } })
+
+ const entry = await checkSource(dossiq, ctx)
+
+ expect(get).toHaveBeenCalledTimes(1)
+ expect(get.mock.calls[0][0]).toBe(`/index.php${countPath(dossiq, ctx)}`)
+ expect(entry).toMatchObject({
+ key: 'dossiq/cases-past-deadline',
+ failed: false,
+ count: 3,
+ attention: true,
+ reason: '3 of your cases are past their deadline or end today.',
+ actionLabel: 'Open these cases',
+ href: `/index.php${actionPath(dossiq)}`,
+ })
+ })
+
+ it('a zero is checked and needs no attention', async () => {
+ get.mockResolvedValue({ data: { total: 0, results: [] } })
+
+ expect(await checkSource(dossiq, ctx)).toMatchObject({
+ failed: false,
+ count: 0,
+ attention: false,
+ })
+ })
+
+ it.each([
+ [
+ 'a server error',
+ () => Promise.reject(new Error('Request failed with status code 500')),
+ ],
+ [
+ 'no access',
+ () => Promise.reject(new Error('Request failed with status code 403')),
+ ],
+ [
+ 'an answer without a total',
+ () => Promise.resolve({ data: { results: [] } }),
+ ],
+ [
+ 'a total that is not a number',
+ () => Promise.resolve({ data: { total: '3' } }),
+ ],
+ ['an empty answer', () => Promise.resolve({ data: null })],
+ ])('%s is a failure, never a zero', async (_name, answer) => {
+ get.mockImplementation(answer)
+
+ const entry = await checkSource(dossiq, ctx)
+
+ expect(entry.failed).toBe(true)
+ expect(entry).not.toHaveProperty('count')
+ expect(entry).not.toHaveProperty('attention')
+ })
+
+ it('an unknown token fails the source and sends nothing', async () => {
+ const source = {
+ ...dossiq,
+ source: { ...dossiq.source, filter: { owner: '@manager' } },
+ }
+
+ const entry = await checkSource(source, ctx)
+
+ expect(get).not.toHaveBeenCalled()
+ expect(entry.failed).toBe(true)
+ expect(entry.error).toContain('@manager')
+ })
+})
+
+describe('loadAttentionFeed', () => {
+ it('merges the apps, ranks them and uses the user the endpoint names', async () => {
+ answerCounts({
+ __sources: {
+ userId: 'pieter',
+ sources: [decidiq, dossiq, pipelinq],
+ invalid: [],
+ },
+ dossiq: { total: 3 },
+ pipelinq: { total: 5 },
+ decidiq: { total: 2 },
+ })
+
+ const feed = await loadAttentionFeed({ now: ctx.now })
+
+ expect(get.mock.calls[0][0]).toBe(
+ '/index.php/apps/launchpad/api/attention/sources',
+ )
+ expect(feed.items.map((item) => item.appId)).toEqual([
+ 'pipelinq',
+ 'dossiq',
+ 'decidiq',
+ ])
+ expect(feed.failedApps).toEqual([])
+ expect(feed.sourceCount).toBe(3)
+ const dossiqCall = get.mock.calls
+ .map(([url]) => url)
+ .find((url) => url.includes('/objects/dossiq/'))
+ expect(dossiqCall).toContain('assignee=pieter')
+ })
+
+ it('names a failed app and an invalid declaration, and keeps the others', async () => {
+ answerCounts({
+ __sources: {
+ userId: 'pieter',
+ sources: [dossiq, pipelinq],
+ invalid: [
+ {
+ appId: 'learniq',
+ appName: 'Learniq',
+ message: 'item "late": ...',
+ },
+ ],
+ },
+ dossiq: new Error('Request failed with status code 500'),
+ pipelinq: { total: 5 },
+ })
+
+ const feed = await loadAttentionFeed({ now: ctx.now })
+
+ expect(feed.items.map((item) => item.appId)).toEqual(['pipelinq'])
+ expect(feed.failedApps).toEqual(['Dossiq', 'Learniq'])
+ expect(feed.sourceCount).toBe(3)
+ })
+
+ it('reports no sources as no sources', async () => {
+ answerCounts({ __sources: { userId: 'pieter', sources: [], invalid: [] } })
+
+ expect(await loadAttentionFeed({ now: ctx.now })).toEqual({
+ items: [],
+ failedApps: [],
+ sourceCount: 0,
+ })
+ })
+
+ it('rejects when the sources endpoint itself fails', async () => {
+ get.mockRejectedValue(new Error('Request failed with status code 500'))
+
+ await expect(loadAttentionFeed()).rejects.toThrow('500')
+ })
+})
diff --git a/src/services/attentionFeed.js b/src/services/attentionFeed.js
new file mode 100644
index 000000000..2a701e34f
--- /dev/null
+++ b/src/services/attentionFeed.js
@@ -0,0 +1,274 @@
+/**
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
+ * SPDX-License-Identifier: EUPL-1.2
+ *
+ * The attention feed's browser half (openspec/specs/attention-feed). Pure
+ * functions plus one loader: no component state lives here, so every rule can
+ * be tested without mounting anything.
+ *
+ * A source is what an app declared in its `appinfo/attention.json`, as
+ * `GET /api/attention/sources` returns it. For each one the feed asks
+ * OpenRegister for a count as the signed-in user, decides whether it needs
+ * attention, and builds the link into the app FROM THE SAME FILTER, so the
+ * number and the list it opens cannot drift (REQ-ATT-003).
+ *
+ * A count that cannot run is a FAILED entry. It is never a zero: a zero says
+ * "nothing to do", and that is the one thing a failure must not say
+ * (REQ-ATT-005).
+ */
+
+import axios from '@nextcloud/axios'
+import { generateUrl } from '@nextcloud/router'
+
+/** Most urgent first. */
+export const SEVERITIES = ['error', 'warning', 'info']
+
+const DAY_TOKEN = /^@today([+-])(\d+)d$/
+
+/**
+ * Format a date as local `YYYY-MM-DD`.
+ *
+ * @param {Date} date The date.
+ * @return {string} The date-only string.
+ */
+function ymd(date) {
+ const month = String(date.getMonth() + 1).padStart(2, '0')
+ const day = String(date.getDate()).padStart(2, '0')
+ return `${date.getFullYear()}-${month}-${day}`
+}
+
+/**
+ * Resolve one filter value. A value that starts with `@` MUST be a token this
+ * function knows; anything else throws, so an unknown token fails its source
+ * instead of being sent to OpenRegister as a literal (REQ-ATT-003).
+ *
+ * @param {unknown} value The filter value.
+ * @param {{userId: string, now: Date}} ctx Who asks, and when.
+ * @return {unknown} The resolved value.
+ * @spec openspec/specs/attention-feed/spec.md#req-att-003
+ */
+export function resolveToken(value, ctx) {
+ if (typeof value !== 'string' || value.charAt(0) !== '@') {
+ return value
+ }
+ const now = ctx.now
+ if (value === '@me') {
+ if (!ctx.userId) {
+ throw new Error('@me needs a signed-in user')
+ }
+ return ctx.userId
+ }
+ if (value === '@now') {
+ return now.toISOString()
+ }
+ if (value === '@today') {
+ return ymd(now)
+ }
+ const delta = DAY_TOKEN.exec(value)
+ if (delta) {
+ const days = Number(delta[2]) * (delta[1] === '-' ? -1 : 1)
+ return ymd(new Date(now.getFullYear(), now.getMonth(), now.getDate() + days))
+ }
+ if (value === '@monthStart') {
+ return ymd(new Date(now.getFullYear(), now.getMonth(), 1))
+ }
+ if (value === '@quarterStart') {
+ return ymd(
+ new Date(now.getFullYear(), Math.floor(now.getMonth() / 3) * 3, 1),
+ )
+ }
+ if (value === '@yearStart') {
+ return ymd(new Date(now.getFullYear(), 0, 1))
+ }
+ throw new Error(`unknown token ${value}`)
+}
+
+/**
+ * Write a flat filter as query pairs, in the filter's own key order. A list
+ * becomes repeated `key[]` pairs, the form OpenRegister reads as an IN-list.
+ * `resolve` is applied to every value: the count passes the token resolver,
+ * the link passes nothing.
+ *
+ * @param {object} filter The declared filter.
+ * @param {(value: unknown) => unknown} [resolve] Maps each value before it is written.
+ * @return {Array<[string, string]>} The pairs.
+ * @spec openspec/specs/attention-feed/spec.md#req-att-003
+ */
+export function filterPairs(filter, resolve = (value) => value) {
+ const pairs = []
+ for (const [key, value] of Object.entries(filter || {})) {
+ if (Array.isArray(value)) {
+ for (const entry of value) {
+ pairs.push([`${key}[]`, String(resolve(entry))])
+ }
+ } else {
+ pairs.push([key, String(resolve(value))])
+ }
+ }
+ return pairs
+}
+
+/**
+ * Join pairs into a query string. Brackets in keys and `@` in values stay
+ * readable (`deadline[lt]=@today%2B1d`); everything else is percent-encoded,
+ * `+` included, because a bare `+` in a query reads as a space.
+ *
+ * @param {Array<[string, string]>} pairs The pairs.
+ * @return {string} `a=b&c=d`, or an empty string.
+ * @spec openspec/specs/attention-feed/spec.md#req-att-003
+ */
+export function toQuery(pairs) {
+ return pairs
+ .map(
+ ([key, value]) =>
+ `${encodeURIComponent(key).replace(/%5B/g, '[').replace(/%5D/g, ']')}=${encodeURIComponent(value).replace(/%40/g, '@')}`,
+ )
+ .join('&')
+}
+
+/**
+ * The OpenRegister request that counts a source, tokens resolved.
+ *
+ * @param {object} source A source from the sources endpoint.
+ * @param {{userId: string, now: Date}} ctx Who asks, and when.
+ * @return {string} App-relative URL, before `generateUrl`.
+ * @spec openspec/specs/attention-feed/spec.md#req-att-003
+ */
+export function countPath(source, ctx) {
+ const { register, schema, filter } = source.source
+ const pairs = filterPairs(filter, (value) => resolveToken(value, ctx))
+ pairs.push(['_limit', '1'])
+ return `/apps/openregister/api/objects/${encodeURIComponent(register)}/${encodeURIComponent(schema)}?${toQuery(pairs)}`
+}
+
+/**
+ * The link into the owning app: the declared path with the SAME filter as its
+ * query, tokens left as written, which is what the apps' own links carry.
+ *
+ * @param {object} source A source from the sources endpoint.
+ * @return {string} App-relative URL, before `generateUrl`.
+ * @spec openspec/specs/attention-feed/spec.md#req-att-003
+ */
+export function actionPath(source) {
+ const query = toQuery(filterPairs(source.source.filter))
+ return query === '' ? source.action.path : `${source.action.path}?${query}`
+}
+
+/**
+ * Whether a count needs attention under the source's rule.
+ *
+ * @param {number} count The count.
+ * @param {string} op `gt`, `gte`, `lt`, `lte`, `eq` or `neq`.
+ * @param {number} value The right-hand side.
+ * @return {boolean} True when the item should be shown.
+ * @spec openspec/specs/attention-feed/spec.md#req-att-004
+ */
+export function needsAttention(count, op, value) {
+ switch (op) {
+ case 'gt':
+ return count > value
+ case 'gte':
+ return count >= value
+ case 'lt':
+ return count < value
+ case 'lte':
+ return count <= value
+ case 'eq':
+ return count === value
+ case 'neq':
+ return count !== value
+ default:
+ throw new Error(`unknown operator ${op}`)
+ }
+}
+
+/**
+ * Order the entries that need attention: severity, then the higher count,
+ * then app name, then item id, so the order is stable.
+ *
+ * @param {Array
@@ -26,6 +27,7 @@
import { CnAppRoot } from '@conduction/nextcloud-vue'
import customComponents from './customComponents.js'
import { ICON_CATALOGUE } from './services/iconCatalogue.js'
+import { permits } from './utils/permissions.js'
/**
* Root component — mounts `CnAppRoot`, which renders the shared chrome
@@ -163,6 +165,24 @@ export default {
},
computed: {
+ /**
+ * Whether the shared first-open support note ("Support LaunchPad":
+ * donate, get support, suggest a feature, review) is mounted for this
+ * account: administrators only (REQ-SHELL-008).
+ *
+ * It used to open for everyone, over the start page an administrator
+ * had just rolled out to a group (#782). The note asks for things only
+ * an administrator can act on, so it is theirs. `CnAppRoot` takes
+ * `false` to opt out; any other value keeps its default, which is the
+ * library's per-user "once ever" behaviour for the administrator.
+ *
+ * @return {boolean} false for a non-admin account.
+ * @spec openspec/specs/runtime-shell/spec.md#req-shell-008
+ */
+ supportNoteForThisAccount() {
+ return permits('admin', this.permissions)
+ },
+
/**
* The manifest CnAppRoot should render.
*
diff --git a/src/__tests__/App.supportNote.spec.js b/src/__tests__/App.supportNote.spec.js
new file mode 100644
index 000000000..31c6eea01
--- /dev/null
+++ b/src/__tests__/App.supportNote.spec.js
@@ -0,0 +1,46 @@
+/**
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
+ * SPDX-License-Identifier: EUPL-1.2
+ *
+ * The first-open support note is mounted for administrators only
+ * (runtime-shell REQ-SHELL-008): `App.vue` hands `CnAppRoot` `false` for
+ * every other account, which is the library's documented opt-out.
+ */
+
+import { describe, expect, it, vi } from 'vitest'
+
+vi.mock('@conduction/nextcloud-vue', () => ({
+ CnAppRoot: { name: 'CnAppRoot', template: '' },
+}))
+vi.mock('../customComponents.js', () => ({ default: {} }))
+vi.mock('../services/iconCatalogue.js', () => ({ ICON_CATALOGUE: [] }))
+
+import App from '../App.vue'
+
+describe('App support note', () => {
+ it('is off for a member', () => {
+ expect(
+ App.computed.supportNoteForThisAccount.call({ permissions: ['user'] }),
+ ).toBe(false)
+ })
+
+ it('is off when the permissions are missing', () => {
+ expect(
+ App.computed.supportNoteForThisAccount.call({ permissions: undefined }),
+ ).toBe(false)
+ })
+
+ it('stays on for an administrator', () => {
+ expect(
+ App.computed.supportNoteForThisAccount.call({
+ permissions: ['user', 'admin'],
+ }),
+ ).toBe(true)
+ })
+
+ it('is what the root receives', () => {
+ // The template binding is the wiring; a computed nobody binds is a
+ // guard with no call site.
+ expect(App.render?.toString() ?? '').toContain('supportNoteForThisAccount')
+ })
+})
diff --git a/src/views/Views.vue b/src/views/Views.vue
index 56ba27b3c..887db29c9 100644
--- a/src/views/Views.vue
+++ b/src/views/Views.vue
@@ -2188,6 +2188,29 @@ export default {
* @spec openspec/specs/dashboards/spec.md
*/
async removeWidget(placementId) {
+ // A compulsory widget cannot be removed from a copy made from an
+ // admin template unless the template allows everything; the
+ // server answers 403 and the store does not even ask it. Say so
+ // instead of doing nothing (#780, REQ-TMPL-023).
+ const placement = (this.widgetPlacements || []).find(
+ (p) => p.id === placementId,
+ )
+ if (
+ Number(placement?.isCompulsory) === 1
+ && this.permissionLevel !== 'full'
+ ) {
+ showError(
+ t(
+ 'launchpad',
+ '"{title}" is part of the start page your administrator set for your group and cannot be removed.',
+ {
+ title:
+ placement?.customTitle || placement?.widgetId || '',
+ },
+ ),
+ )
+ return
+ }
await this.removeWidgetFromDashboard(placementId)
},
diff --git a/src/views/__tests__/ViewsCompulsoryRemove.spec.js b/src/views/__tests__/ViewsCompulsoryRemove.spec.js
new file mode 100644
index 000000000..1938b0061
--- /dev/null
+++ b/src/views/__tests__/ViewsCompulsoryRemove.spec.js
@@ -0,0 +1,84 @@
+/**
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
+ * SPDX-License-Identifier: EUPL-1.2
+ *
+ * Deleting a compulsory widget from a template copy says why it cannot be
+ * done and sends nothing; any other widget is removed as before
+ * (admin-templates REQ-TMPL-023). Both menus end in `removeWidget`, which is
+ * what is called here, the way the menus call it.
+ */
+
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+
+vi.mock('@nextcloud/dialogs', () => ({ showError: vi.fn(), showSuccess: vi.fn() }))
+vi.mock('@nextcloud/l10n', async (importOriginal) => ({
+ ...(await importOriginal()),
+ t: (_app, text, vars = {}) =>
+ text.replace(/\{(\w+)\}/g, (_m, k) => vars[k] ?? `{${k}}`),
+}))
+
+let Views
+let showError
+
+/**
+ * @param {object[]} placements The dashboard's placements.
+ * @param {string} permissionLevel The copy's permission level.
+ * @return {object} host with the real Views methods.
+ */
+function makeHost(placements, permissionLevel = 'add_only') {
+ const host = {
+ widgetPlacements: placements,
+ permissionLevel,
+ }
+ for (const [name, fn] of Object.entries(Views.methods)) {
+ host[name] = fn.bind(host)
+ }
+ // The store action is mapped into `methods`; the double goes on after
+ // the bind, as ViewsPersonalHide.spec.js does with `switchDashboard`.
+ host.removeWidgetFromDashboard = vi.fn().mockResolvedValue()
+ return host
+}
+
+const attention = {
+ id: 7,
+ widgetId: 'attention',
+ customTitle: 'Vandaag eerst',
+ isCompulsory: 1,
+}
+const cases = {
+ id: 8,
+ widgetId: 'object-list',
+ customTitle: 'Mijn zaken',
+ isCompulsory: 0,
+}
+
+beforeEach(async () => {
+ vi.clearAllMocks()
+ Views = (await import('../Views.vue')).default
+ showError = (await import('@nextcloud/dialogs')).showError
+})
+
+describe('Views removes a widget', () => {
+ it('explains a compulsory widget and sends nothing', async () => {
+ const host = makeHost([attention, cases])
+ await host.removeWidget(7)
+ expect(showError).toHaveBeenCalledWith(
+ '"Vandaag eerst" is part of the start page your administrator set for your group and cannot be removed.',
+ )
+ expect(host.removeWidgetFromDashboard).not.toHaveBeenCalled()
+ })
+
+ it('removes a widget that is not compulsory, with no message', async () => {
+ const host = makeHost([attention, cases])
+ await host.removeWidget(8)
+ expect(showError).not.toHaveBeenCalled()
+ expect(host.removeWidgetFromDashboard).toHaveBeenCalledWith(8)
+ })
+
+ it('lets a full-permission copy remove a compulsory widget', async () => {
+ const host = makeHost([attention], 'full')
+ await host.removeWidget(7)
+ expect(showError).not.toHaveBeenCalled()
+ expect(host.removeWidgetFromDashboard).toHaveBeenCalledWith(7)
+ })
+})
From f47ed8235e8a479be6399fca6e3a8eff2f40d35e Mon Sep 17 00:00:00 2001
From: Ruben van der Linde
Date: Tue, 6 Oct 2026 21:22:26 +0200
Subject: [PATCH 16/60] feat(shell): an admin option for a start page without
the navigation panel, its destinations in a menu in the top row (#798)
* feat(settings): an admin option for the start page without the navigation panel, stored with the other layout options and pushed to the workspace initial state
* feat(shell): the dashboard view without the navigation panel, its destinations in a menu in the top row, and the admin switch (6 strings, 35 locales carry the English copy)
* docs(spec): REQ-SHELL-009 start page without navigation panel, the thirteenth admin setting, and its e2e spec
* style(e2e): sort the playwright imports
* style(php): inline comments start with a capital letter
* test(shell): off hands the root no menu slot, the e2e proves no destination is lost and that other pages keep the panel; phpmd budgets kept
---
l10n/be.js | 8 +-
l10n/be.json | 8 +-
l10n/bg.js | 8 +-
l10n/bg.json | 8 +-
l10n/bs.js | 8 +-
l10n/bs.json | 8 +-
l10n/ca.js | 8 +-
l10n/ca.json | 8 +-
l10n/cs.js | 8 +-
l10n/cs.json | 8 +-
l10n/da.js | 8 +-
l10n/da.json | 8 +-
l10n/de.js | 8 +-
l10n/de.json | 8 +-
l10n/el.js | 8 +-
l10n/el.json | 8 +-
l10n/en.js | 8 +-
l10n/en.json | 8 +-
l10n/es.js | 8 +-
l10n/es.json | 8 +-
l10n/et.js | 8 +-
l10n/et.json | 8 +-
l10n/fi.js | 8 +-
l10n/fi.json | 8 +-
l10n/fr.js | 8 +-
l10n/fr.json | 8 +-
l10n/ga.js | 8 +-
l10n/ga.json | 8 +-
l10n/hr.js | 8 +-
l10n/hr.json | 8 +-
l10n/hu.js | 8 +-
l10n/hu.json | 8 +-
l10n/is.js | 8 +-
l10n/is.json | 8 +-
l10n/it.js | 8 +-
l10n/it.json | 8 +-
l10n/lb.js | 8 +-
l10n/lb.json | 8 +-
l10n/lt.js | 8 +-
l10n/lt.json | 8 +-
l10n/lv.js | 8 +-
l10n/lv.json | 8 +-
l10n/mk.js | 8 +-
l10n/mk.json | 8 +-
l10n/mt.js | 8 +-
l10n/mt.json | 8 +-
l10n/nb.js | 8 +-
l10n/nb.json | 8 +-
l10n/nl.js | 8 +-
l10n/nl.json | 8 +-
l10n/pl.js | 8 +-
l10n/pl.json | 8 +-
l10n/pt.js | 8 +-
l10n/pt.json | 8 +-
l10n/rm.js | 8 +-
l10n/rm.json | 8 +-
l10n/ro.js | 8 +-
l10n/ro.json | 8 +-
l10n/ru.js | 8 +-
l10n/ru.json | 8 +-
l10n/sk.js | 8 +-
l10n/sk.json | 8 +-
l10n/sl.js | 8 +-
l10n/sl.json | 8 +-
l10n/sq.js | 8 +-
l10n/sq.json | 8 +-
l10n/sr.js | 8 +-
l10n/sr.json | 8 +-
l10n/sv.js | 8 +-
l10n/sv.json | 8 +-
l10n/tr.js | 8 +-
l10n/tr.json | 8 +-
l10n/uk.js | 8 +-
l10n/uk.json | 8 +-
lib/Controller/AdminController.php | 5 +-
lib/Controller/PageController.php | 1 +
lib/Db/AdminSetting.php | 9 +
lib/Db/AdminSettingKey.php | 1 +
lib/Service/AdminSettingsService.php | 18 ++
lib/Service/InitialStateBuilder.php | 21 ++
.../proposal.md | 26 ++
.../tasks.md | 5 +
openspec/specs/admin-settings/spec.md | 10 +-
openspec/specs/runtime-shell/spec.md | 40 ++++
src/App.vue | 120 +++++++++-
src/__tests__/App.navigationPanel.spec.js | 221 +++++++++++++++++
src/components/Workspace/StartPageMenu.vue | 224 ++++++++++++++++++
.../Workspace/__tests__/StartPageMenu.spec.js | 210 ++++++++++++++++
src/components/admin/AdminSettings.vue | 23 ++
src/utils/loadInitialState.js | 4 +
src/views/Views.vue | 8 +
src/views/WorkspaceApp.vue | 16 ++
.../AdminControllerSettingsAliasTest.php | 28 +++
.../Unit/Service/AdminSettingsServiceTest.php | 46 +++-
.../Unit/Service/InitialStateBuilderTest.php | 3 +
.../e2e/start-page-without-navigation.spec.ts | 167 +++++++++++++
96 files changed, 1716 insertions(+), 82 deletions(-)
create mode 100644 openspec/changes/start-page-without-navigation-panel/proposal.md
create mode 100644 openspec/changes/start-page-without-navigation-panel/tasks.md
create mode 100644 src/__tests__/App.navigationPanel.spec.js
create mode 100644 src/components/Workspace/StartPageMenu.vue
create mode 100644 src/components/Workspace/__tests__/StartPageMenu.spec.js
create mode 100644 tests/e2e/start-page-without-navigation.spec.ts
diff --git a/l10n/be.js b/l10n/be.js
index 2d271fe48..acb4eb5f7 100644
--- a/l10n/be.js
+++ b/l10n/be.js
@@ -1348,6 +1348,12 @@ OC.L10N.register(
"Update to version {version}" : "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}." : "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members." : "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel" : "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel." : "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings" : "Personal settings",
+ "Store" : "Store",
+ "Reports" : "Reports",
+ "Flows" : "Flows"
},
"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);");
diff --git a/l10n/be.json b/l10n/be.json
index 408e402d0..77714ff6e 100644
--- a/l10n/be.json
+++ b/l10n/be.json
@@ -1325,6 +1325,12 @@
"Update to version {version}": "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}.": "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.": "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel": "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.": "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings": "Personal settings",
+ "Store": "Store",
+ "Reports": "Reports",
+ "Flows": "Flows"
}
}
diff --git a/l10n/bg.js b/l10n/bg.js
index 5167a4b03..87b74012b 100644
--- a/l10n/bg.js
+++ b/l10n/bg.js
@@ -1357,7 +1357,13 @@ OC.L10N.register(
"Update to version {version}" : "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}." : "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members." : "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel" : "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel." : "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings" : "Personal settings",
+ "Store" : "Store",
+ "Reports" : "Reports",
+ "Flows" : "Flows"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/bg.json b/l10n/bg.json
index 271a2c9eb..3da51a67f 100644
--- a/l10n/bg.json
+++ b/l10n/bg.json
@@ -1324,6 +1324,12 @@
"Update to version {version}": "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}.": "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.": "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel": "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.": "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings": "Personal settings",
+ "Store": "Store",
+ "Reports": "Reports",
+ "Flows": "Flows"
}
}
diff --git a/l10n/bs.js b/l10n/bs.js
index b998e2660..cb3bc1f36 100644
--- a/l10n/bs.js
+++ b/l10n/bs.js
@@ -1357,7 +1357,13 @@ OC.L10N.register(
"Update to version {version}" : "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}." : "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members." : "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel" : "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel." : "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings" : "Personal settings",
+ "Store" : "Store",
+ "Reports" : "Reports",
+ "Flows" : "Flows"
},
"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"
);
diff --git a/l10n/bs.json b/l10n/bs.json
index 8aab5cf14..ab5bdc588 100644
--- a/l10n/bs.json
+++ b/l10n/bs.json
@@ -1325,6 +1325,12 @@
"Update to version {version}": "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}.": "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.": "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel": "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.": "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings": "Personal settings",
+ "Store": "Store",
+ "Reports": "Reports",
+ "Flows": "Flows"
}
}
diff --git a/l10n/ca.js b/l10n/ca.js
index d6dd9a0af..8d3c9d34f 100644
--- a/l10n/ca.js
+++ b/l10n/ca.js
@@ -1357,7 +1357,13 @@ OC.L10N.register(
"Update to version {version}" : "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}." : "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members." : "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel" : "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel." : "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings" : "Personal settings",
+ "Store" : "Store",
+ "Reports" : "Reports",
+ "Flows" : "Flows"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/ca.json b/l10n/ca.json
index e5cc7c789..27156682f 100644
--- a/l10n/ca.json
+++ b/l10n/ca.json
@@ -1324,6 +1324,12 @@
"Update to version {version}": "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}.": "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.": "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel": "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.": "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings": "Personal settings",
+ "Store": "Store",
+ "Reports": "Reports",
+ "Flows": "Flows"
}
}
diff --git a/l10n/cs.js b/l10n/cs.js
index 55f254172..8cabbb3d3 100644
--- a/l10n/cs.js
+++ b/l10n/cs.js
@@ -1357,7 +1357,13 @@ OC.L10N.register(
"Update to version {version}" : "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}." : "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members." : "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel" : "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel." : "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings" : "Personal settings",
+ "Store" : "Store",
+ "Reports" : "Reports",
+ "Flows" : "Flows"
},
"nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;"
);
diff --git a/l10n/cs.json b/l10n/cs.json
index 7221b2a17..5ee2db728 100644
--- a/l10n/cs.json
+++ b/l10n/cs.json
@@ -1325,6 +1325,12 @@
"Update to version {version}": "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}.": "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.": "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel": "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.": "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings": "Personal settings",
+ "Store": "Store",
+ "Reports": "Reports",
+ "Flows": "Flows"
}
}
diff --git a/l10n/da.js b/l10n/da.js
index 7dcb569ac..2f800d597 100644
--- a/l10n/da.js
+++ b/l10n/da.js
@@ -1357,7 +1357,13 @@ OC.L10N.register(
"Update to version {version}" : "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}." : "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members." : "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel" : "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel." : "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings" : "Personal settings",
+ "Store" : "Store",
+ "Reports" : "Reports",
+ "Flows" : "Flows"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/da.json b/l10n/da.json
index 182ac65d5..7cf891549 100644
--- a/l10n/da.json
+++ b/l10n/da.json
@@ -1324,6 +1324,12 @@
"Update to version {version}": "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}.": "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.": "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel": "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.": "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings": "Personal settings",
+ "Store": "Store",
+ "Reports": "Reports",
+ "Flows": "Flows"
}
}
diff --git a/l10n/de.js b/l10n/de.js
index 71636d9b2..ce48ea0c1 100644
--- a/l10n/de.js
+++ b/l10n/de.js
@@ -1357,7 +1357,13 @@ OC.L10N.register(
"Update to version {version}" : "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}." : "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members." : "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel" : "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel." : "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings" : "Personal settings",
+ "Store" : "Store",
+ "Reports" : "Reports",
+ "Flows" : "Flows"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/de.json b/l10n/de.json
index fc8f69973..7bd079f06 100644
--- a/l10n/de.json
+++ b/l10n/de.json
@@ -1324,6 +1324,12 @@
"Update to version {version}": "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}.": "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.": "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel": "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.": "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings": "Personal settings",
+ "Store": "Store",
+ "Reports": "Reports",
+ "Flows": "Flows"
}
}
diff --git a/l10n/el.js b/l10n/el.js
index a41461957..085475e25 100644
--- a/l10n/el.js
+++ b/l10n/el.js
@@ -1357,7 +1357,13 @@ OC.L10N.register(
"Update to version {version}" : "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}." : "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members." : "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel" : "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel." : "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings" : "Personal settings",
+ "Store" : "Store",
+ "Reports" : "Reports",
+ "Flows" : "Flows"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/el.json b/l10n/el.json
index 56ad092ee..b7bd82a87 100644
--- a/l10n/el.json
+++ b/l10n/el.json
@@ -1324,6 +1324,12 @@
"Update to version {version}": "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}.": "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.": "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel": "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.": "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings": "Personal settings",
+ "Store": "Store",
+ "Reports": "Reports",
+ "Flows": "Flows"
}
}
diff --git a/l10n/en.js b/l10n/en.js
index 559789158..ea036701f 100644
--- a/l10n/en.js
+++ b/l10n/en.js
@@ -1337,7 +1337,13 @@ OC.L10N.register(
"Update to version {version}" : "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}." : "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members." : "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel" : "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel." : "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings" : "Personal settings",
+ "Store" : "Store",
+ "Reports" : "Reports",
+ "Flows" : "Flows"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/en.json b/l10n/en.json
index 49a1ed717..4fb4149c9 100644
--- a/l10n/en.json
+++ b/l10n/en.json
@@ -1302,6 +1302,12 @@
"Update to version {version}": "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}.": "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.": "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel": "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.": "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings": "Personal settings",
+ "Store": "Store",
+ "Reports": "Reports",
+ "Flows": "Flows"
}
}
diff --git a/l10n/es.js b/l10n/es.js
index 6858e3c46..9960fc610 100644
--- a/l10n/es.js
+++ b/l10n/es.js
@@ -1357,7 +1357,13 @@ OC.L10N.register(
"Update to version {version}" : "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}." : "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members." : "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel" : "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel." : "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings" : "Personal settings",
+ "Store" : "Store",
+ "Reports" : "Reports",
+ "Flows" : "Flows"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/es.json b/l10n/es.json
index 99637ca02..2cb682b7f 100644
--- a/l10n/es.json
+++ b/l10n/es.json
@@ -1324,6 +1324,12 @@
"Update to version {version}": "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}.": "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.": "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel": "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.": "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings": "Personal settings",
+ "Store": "Store",
+ "Reports": "Reports",
+ "Flows": "Flows"
}
}
diff --git a/l10n/et.js b/l10n/et.js
index 66675750d..8063f77bc 100644
--- a/l10n/et.js
+++ b/l10n/et.js
@@ -1357,7 +1357,13 @@ OC.L10N.register(
"Update to version {version}" : "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}." : "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members." : "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel" : "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel." : "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings" : "Personal settings",
+ "Store" : "Store",
+ "Reports" : "Reports",
+ "Flows" : "Flows"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/et.json b/l10n/et.json
index cbb1fb868..97d295ded 100644
--- a/l10n/et.json
+++ b/l10n/et.json
@@ -1324,6 +1324,12 @@
"Update to version {version}": "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}.": "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.": "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel": "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.": "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings": "Personal settings",
+ "Store": "Store",
+ "Reports": "Reports",
+ "Flows": "Flows"
}
}
diff --git a/l10n/fi.js b/l10n/fi.js
index 3a52be6cf..414148c9c 100644
--- a/l10n/fi.js
+++ b/l10n/fi.js
@@ -1357,7 +1357,13 @@ OC.L10N.register(
"Update to version {version}" : "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}." : "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members." : "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel" : "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel." : "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings" : "Personal settings",
+ "Store" : "Store",
+ "Reports" : "Reports",
+ "Flows" : "Flows"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/fi.json b/l10n/fi.json
index e4ae27603..3c3009d99 100644
--- a/l10n/fi.json
+++ b/l10n/fi.json
@@ -1324,6 +1324,12 @@
"Update to version {version}": "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}.": "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.": "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel": "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.": "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings": "Personal settings",
+ "Store": "Store",
+ "Reports": "Reports",
+ "Flows": "Flows"
}
}
diff --git a/l10n/fr.js b/l10n/fr.js
index b63d3053e..c041d7711 100644
--- a/l10n/fr.js
+++ b/l10n/fr.js
@@ -1357,7 +1357,13 @@ OC.L10N.register(
"Update to version {version}" : "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}." : "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members." : "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel" : "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel." : "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings" : "Personal settings",
+ "Store" : "Store",
+ "Reports" : "Reports",
+ "Flows" : "Flows"
},
"nplurals=2; plural=(n > 1);"
);
diff --git a/l10n/fr.json b/l10n/fr.json
index 7b0f47463..f446a22a9 100644
--- a/l10n/fr.json
+++ b/l10n/fr.json
@@ -1324,6 +1324,12 @@
"Update to version {version}": "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}.": "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.": "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel": "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.": "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings": "Personal settings",
+ "Store": "Store",
+ "Reports": "Reports",
+ "Flows": "Flows"
}
}
diff --git a/l10n/ga.js b/l10n/ga.js
index 349c5ad3e..1e4c9c39b 100644
--- a/l10n/ga.js
+++ b/l10n/ga.js
@@ -1357,7 +1357,13 @@ OC.L10N.register(
"Update to version {version}" : "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}." : "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members." : "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel" : "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel." : "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings" : "Personal settings",
+ "Store" : "Store",
+ "Reports" : "Reports",
+ "Flows" : "Flows"
},
"nplurals=5; plural=(n==1 ? 0 : n==2 ? 1 : n<7 ? 2 : n<11 ? 3 : 4);"
);
diff --git a/l10n/ga.json b/l10n/ga.json
index a7654ad61..cab2a132a 100644
--- a/l10n/ga.json
+++ b/l10n/ga.json
@@ -1327,6 +1327,12 @@
"Update to version {version}": "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}.": "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.": "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel": "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.": "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings": "Personal settings",
+ "Store": "Store",
+ "Reports": "Reports",
+ "Flows": "Flows"
}
}
diff --git a/l10n/hr.js b/l10n/hr.js
index 6d16f2bb9..2a8857d55 100644
--- a/l10n/hr.js
+++ b/l10n/hr.js
@@ -1357,7 +1357,13 @@ OC.L10N.register(
"Update to version {version}" : "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}." : "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members." : "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel" : "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel." : "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings" : "Personal settings",
+ "Store" : "Store",
+ "Reports" : "Reports",
+ "Flows" : "Flows"
},
"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"
);
diff --git a/l10n/hr.json b/l10n/hr.json
index 3272fae15..8fbf01850 100644
--- a/l10n/hr.json
+++ b/l10n/hr.json
@@ -1325,6 +1325,12 @@
"Update to version {version}": "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}.": "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.": "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel": "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.": "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings": "Personal settings",
+ "Store": "Store",
+ "Reports": "Reports",
+ "Flows": "Flows"
}
}
diff --git a/l10n/hu.js b/l10n/hu.js
index c24678c15..263c90107 100644
--- a/l10n/hu.js
+++ b/l10n/hu.js
@@ -1357,7 +1357,13 @@ OC.L10N.register(
"Update to version {version}" : "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}." : "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members." : "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel" : "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel." : "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings" : "Personal settings",
+ "Store" : "Store",
+ "Reports" : "Reports",
+ "Flows" : "Flows"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/hu.json b/l10n/hu.json
index 5434171d2..db7aed4b0 100644
--- a/l10n/hu.json
+++ b/l10n/hu.json
@@ -1324,6 +1324,12 @@
"Update to version {version}": "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}.": "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.": "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel": "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.": "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings": "Personal settings",
+ "Store": "Store",
+ "Reports": "Reports",
+ "Flows": "Flows"
}
}
diff --git a/l10n/is.js b/l10n/is.js
index 0f5489bfc..160c212b8 100644
--- a/l10n/is.js
+++ b/l10n/is.js
@@ -1357,7 +1357,13 @@ OC.L10N.register(
"Update to version {version}" : "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}." : "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members." : "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel" : "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel." : "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings" : "Personal settings",
+ "Store" : "Store",
+ "Reports" : "Reports",
+ "Flows" : "Flows"
},
"nplurals=2; plural=(n%10!=1 || n%100==11);"
);
diff --git a/l10n/is.json b/l10n/is.json
index 6480747e8..f04e7fe9f 100644
--- a/l10n/is.json
+++ b/l10n/is.json
@@ -1324,6 +1324,12 @@
"Update to version {version}": "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}.": "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.": "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel": "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.": "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings": "Personal settings",
+ "Store": "Store",
+ "Reports": "Reports",
+ "Flows": "Flows"
}
}
diff --git a/l10n/it.js b/l10n/it.js
index e09254b00..85143eb7f 100644
--- a/l10n/it.js
+++ b/l10n/it.js
@@ -1357,7 +1357,13 @@ OC.L10N.register(
"Update to version {version}" : "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}." : "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members." : "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel" : "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel." : "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings" : "Personal settings",
+ "Store" : "Store",
+ "Reports" : "Reports",
+ "Flows" : "Flows"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/it.json b/l10n/it.json
index 2b619fe6a..39bf8dec9 100644
--- a/l10n/it.json
+++ b/l10n/it.json
@@ -1324,6 +1324,12 @@
"Update to version {version}": "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}.": "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.": "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel": "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.": "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings": "Personal settings",
+ "Store": "Store",
+ "Reports": "Reports",
+ "Flows": "Flows"
}
}
diff --git a/l10n/lb.js b/l10n/lb.js
index f352cf32a..88a18e9fb 100644
--- a/l10n/lb.js
+++ b/l10n/lb.js
@@ -1348,7 +1348,13 @@ OC.L10N.register(
"Update to version {version}" : "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}." : "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members." : "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel" : "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel." : "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings" : "Personal settings",
+ "Store" : "Store",
+ "Reports" : "Reports",
+ "Flows" : "Flows"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/lb.json b/l10n/lb.json
index bcee4589c..a1ec1ebcf 100644
--- a/l10n/lb.json
+++ b/l10n/lb.json
@@ -1324,6 +1324,12 @@
"Update to version {version}": "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}.": "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.": "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel": "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.": "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings": "Personal settings",
+ "Store": "Store",
+ "Reports": "Reports",
+ "Flows": "Flows"
}
}
diff --git a/l10n/lt.js b/l10n/lt.js
index 170e9de34..af2a987e5 100644
--- a/l10n/lt.js
+++ b/l10n/lt.js
@@ -1357,7 +1357,13 @@ OC.L10N.register(
"Update to version {version}" : "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}." : "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members." : "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel" : "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel." : "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings" : "Personal settings",
+ "Store" : "Store",
+ "Reports" : "Reports",
+ "Flows" : "Flows"
},
"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2);"
);
diff --git a/l10n/lt.json b/l10n/lt.json
index 465280ef5..48d140036 100644
--- a/l10n/lt.json
+++ b/l10n/lt.json
@@ -1325,6 +1325,12 @@
"Update to version {version}": "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}.": "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.": "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel": "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.": "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings": "Personal settings",
+ "Store": "Store",
+ "Reports": "Reports",
+ "Flows": "Flows"
}
}
diff --git a/l10n/lv.js b/l10n/lv.js
index 1a0cd0601..64ba546fd 100644
--- a/l10n/lv.js
+++ b/l10n/lv.js
@@ -1357,7 +1357,13 @@ OC.L10N.register(
"Update to version {version}" : "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}." : "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members." : "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel" : "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel." : "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings" : "Personal settings",
+ "Store" : "Store",
+ "Reports" : "Reports",
+ "Flows" : "Flows"
},
"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2);"
);
diff --git a/l10n/lv.json b/l10n/lv.json
index ac3149456..893c514a4 100644
--- a/l10n/lv.json
+++ b/l10n/lv.json
@@ -1325,6 +1325,12 @@
"Update to version {version}": "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}.": "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.": "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel": "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.": "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings": "Personal settings",
+ "Store": "Store",
+ "Reports": "Reports",
+ "Flows": "Flows"
}
}
diff --git a/l10n/mk.js b/l10n/mk.js
index 9ef8010c3..80b3ac41d 100644
--- a/l10n/mk.js
+++ b/l10n/mk.js
@@ -1348,7 +1348,13 @@ OC.L10N.register(
"Update to version {version}" : "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}." : "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members." : "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel" : "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel." : "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings" : "Personal settings",
+ "Store" : "Store",
+ "Reports" : "Reports",
+ "Flows" : "Flows"
},
"nplurals=2; plural=(n%10==1 && n%100!=11 ? 0 : 1);"
);
diff --git a/l10n/mk.json b/l10n/mk.json
index 560671dd6..e6c1b13dc 100644
--- a/l10n/mk.json
+++ b/l10n/mk.json
@@ -1324,6 +1324,12 @@
"Update to version {version}": "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}.": "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.": "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel": "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.": "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings": "Personal settings",
+ "Store": "Store",
+ "Reports": "Reports",
+ "Flows": "Flows"
}
}
diff --git a/l10n/mt.js b/l10n/mt.js
index 66146ea0b..89b245f07 100644
--- a/l10n/mt.js
+++ b/l10n/mt.js
@@ -1357,7 +1357,13 @@ OC.L10N.register(
"Update to version {version}" : "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}." : "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members." : "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel" : "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel." : "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings" : "Personal settings",
+ "Store" : "Store",
+ "Reports" : "Reports",
+ "Flows" : "Flows"
},
"nplurals=4; plural=(n==1 ? 0 : n==0 || (n%100>1 && n%100<11) ? 1 : (n%100>10 && n%100<20) ? 2 : 3);"
);
diff --git a/l10n/mt.json b/l10n/mt.json
index af19c2a19..41f497916 100644
--- a/l10n/mt.json
+++ b/l10n/mt.json
@@ -1326,6 +1326,12 @@
"Update to version {version}": "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}.": "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.": "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel": "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.": "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings": "Personal settings",
+ "Store": "Store",
+ "Reports": "Reports",
+ "Flows": "Flows"
}
}
diff --git a/l10n/nb.js b/l10n/nb.js
index 3d37b5968..232711a65 100644
--- a/l10n/nb.js
+++ b/l10n/nb.js
@@ -1357,7 +1357,13 @@ OC.L10N.register(
"Update to version {version}" : "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}." : "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members." : "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel" : "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel." : "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings" : "Personal settings",
+ "Store" : "Store",
+ "Reports" : "Reports",
+ "Flows" : "Flows"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/nb.json b/l10n/nb.json
index b207b3446..c15ea424b 100644
--- a/l10n/nb.json
+++ b/l10n/nb.json
@@ -1324,6 +1324,12 @@
"Update to version {version}": "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}.": "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.": "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel": "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.": "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings": "Personal settings",
+ "Store": "Store",
+ "Reports": "Reports",
+ "Flows": "Flows"
}
}
diff --git a/l10n/nl.js b/l10n/nl.js
index d42201948..598a0d3d5 100644
--- a/l10n/nl.js
+++ b/l10n/nl.js
@@ -1407,7 +1407,13 @@ OC.L10N.register(
"Update to version {version}" : "Bijwerken naar versie {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}." : "\"{name}\" is bijgewerkt naar versie {version}. Medewerkers met een kopie: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members." : "De lijsten lezen deze registers: {registers}. Een lijst waarvan het register niet op deze omgeving staat, wordt voor medewerkers verborgen.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" hoort bij de startpagina die uw beheerder voor uw groep heeft ingesteld en kan niet worden verwijderd."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" hoort bij de startpagina die uw beheerder voor uw groep heeft ingesteld en kan niet worden verwijderd.",
+ "Start page without navigation panel" : "Startpagina zonder navigatiepaneel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel." : "Als dit aanstaat heeft de dashboardweergave geen navigatiepaneel links en gebruikt hij de volle breedte. De dashboardkiezer en de bestemmingen Documentatie, Store, Rapporten, Functies en roadmap en instellingen staan dan in de knop Menu in de bovenste rij. Andere pagina's houden het paneel.",
+ "Personal settings" : "Persoonlijke instellingen",
+ "Store" : "Store",
+ "Reports" : "Rapporten",
+ "Flows" : "Flows"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/nl.json b/l10n/nl.json
index a2737f99e..47ea5a5a4 100644
--- a/l10n/nl.json
+++ b/l10n/nl.json
@@ -1384,6 +1384,12 @@
"Update to version {version}": "Bijwerken naar versie {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}.": "\"{name}\" is bijgewerkt naar versie {version}. Medewerkers met een kopie: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.": "De lijsten lezen deze registers: {registers}. Een lijst waarvan het register niet op deze omgeving staat, wordt voor medewerkers verborgen.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" hoort bij de startpagina die uw beheerder voor uw groep heeft ingesteld en kan niet worden verwijderd."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" hoort bij de startpagina die uw beheerder voor uw groep heeft ingesteld en kan niet worden verwijderd.",
+ "Start page without navigation panel": "Startpagina zonder navigatiepaneel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.": "Als dit aanstaat heeft de dashboardweergave geen navigatiepaneel links en gebruikt hij de volle breedte. De dashboardkiezer en de bestemmingen Documentatie, Store, Rapporten, Functies en roadmap en instellingen staan dan in de knop Menu in de bovenste rij. Andere pagina's houden het paneel.",
+ "Personal settings": "Persoonlijke instellingen",
+ "Store": "Store",
+ "Reports": "Rapporten",
+ "Flows": "Flows"
}
}
diff --git a/l10n/pl.js b/l10n/pl.js
index 1b8c25a30..9a8705cf1 100644
--- a/l10n/pl.js
+++ b/l10n/pl.js
@@ -1357,7 +1357,13 @@ OC.L10N.register(
"Update to version {version}" : "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}." : "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members." : "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel" : "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel." : "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings" : "Personal settings",
+ "Store" : "Store",
+ "Reports" : "Reports",
+ "Flows" : "Flows"
},
"nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"
);
diff --git a/l10n/pl.json b/l10n/pl.json
index cc22c3cf8..fe9f9f1da 100644
--- a/l10n/pl.json
+++ b/l10n/pl.json
@@ -1325,6 +1325,12 @@
"Update to version {version}": "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}.": "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.": "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel": "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.": "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings": "Personal settings",
+ "Store": "Store",
+ "Reports": "Reports",
+ "Flows": "Flows"
}
}
diff --git a/l10n/pt.js b/l10n/pt.js
index 5a2099be2..274bf36e1 100644
--- a/l10n/pt.js
+++ b/l10n/pt.js
@@ -1357,7 +1357,13 @@ OC.L10N.register(
"Update to version {version}" : "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}." : "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members." : "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel" : "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel." : "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings" : "Personal settings",
+ "Store" : "Store",
+ "Reports" : "Reports",
+ "Flows" : "Flows"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/pt.json b/l10n/pt.json
index 6a9d8dcd2..8c1f0535d 100644
--- a/l10n/pt.json
+++ b/l10n/pt.json
@@ -1324,6 +1324,12 @@
"Update to version {version}": "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}.": "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.": "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel": "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.": "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings": "Personal settings",
+ "Store": "Store",
+ "Reports": "Reports",
+ "Flows": "Flows"
}
}
diff --git a/l10n/rm.js b/l10n/rm.js
index 1619e7b24..da9498be9 100644
--- a/l10n/rm.js
+++ b/l10n/rm.js
@@ -1357,7 +1357,13 @@ OC.L10N.register(
"Update to version {version}" : "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}." : "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members." : "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel" : "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel." : "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings" : "Personal settings",
+ "Store" : "Store",
+ "Reports" : "Reports",
+ "Flows" : "Flows"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/rm.json b/l10n/rm.json
index c2a8b31d8..ce4b9a7dc 100644
--- a/l10n/rm.json
+++ b/l10n/rm.json
@@ -1324,6 +1324,12 @@
"Update to version {version}": "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}.": "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.": "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel": "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.": "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings": "Personal settings",
+ "Store": "Store",
+ "Reports": "Reports",
+ "Flows": "Flows"
}
}
diff --git a/l10n/ro.js b/l10n/ro.js
index 14a91f1bf..537e0dd8a 100644
--- a/l10n/ro.js
+++ b/l10n/ro.js
@@ -1357,7 +1357,13 @@ OC.L10N.register(
"Update to version {version}" : "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}." : "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members." : "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel" : "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel." : "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings" : "Personal settings",
+ "Store" : "Store",
+ "Reports" : "Reports",
+ "Flows" : "Flows"
},
"nplurals=3; plural=(n==1 ? 0 : (n==0 || (n%100>0 && n%100<20)) ? 1 : 2);"
);
diff --git a/l10n/ro.json b/l10n/ro.json
index 7ce02313f..8864aaf78 100644
--- a/l10n/ro.json
+++ b/l10n/ro.json
@@ -1325,6 +1325,12 @@
"Update to version {version}": "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}.": "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.": "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel": "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.": "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings": "Personal settings",
+ "Store": "Store",
+ "Reports": "Reports",
+ "Flows": "Flows"
}
}
diff --git a/l10n/ru.js b/l10n/ru.js
index 948779db3..fb8047a75 100644
--- a/l10n/ru.js
+++ b/l10n/ru.js
@@ -1357,7 +1357,13 @@ OC.L10N.register(
"Update to version {version}" : "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}." : "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members." : "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel" : "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel." : "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings" : "Personal settings",
+ "Store" : "Store",
+ "Reports" : "Reports",
+ "Flows" : "Flows"
},
"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"
);
diff --git a/l10n/ru.json b/l10n/ru.json
index 802b1a957..db33b6f40 100644
--- a/l10n/ru.json
+++ b/l10n/ru.json
@@ -1325,6 +1325,12 @@
"Update to version {version}": "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}.": "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.": "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel": "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.": "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings": "Personal settings",
+ "Store": "Store",
+ "Reports": "Reports",
+ "Flows": "Flows"
}
}
diff --git a/l10n/sk.js b/l10n/sk.js
index 3f1f544d6..662b55503 100644
--- a/l10n/sk.js
+++ b/l10n/sk.js
@@ -1357,7 +1357,13 @@ OC.L10N.register(
"Update to version {version}" : "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}." : "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members." : "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel" : "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel." : "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings" : "Personal settings",
+ "Store" : "Store",
+ "Reports" : "Reports",
+ "Flows" : "Flows"
},
"nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;"
);
diff --git a/l10n/sk.json b/l10n/sk.json
index 9522d0b02..f7ae46bbb 100644
--- a/l10n/sk.json
+++ b/l10n/sk.json
@@ -1325,6 +1325,12 @@
"Update to version {version}": "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}.": "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.": "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel": "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.": "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings": "Personal settings",
+ "Store": "Store",
+ "Reports": "Reports",
+ "Flows": "Flows"
}
}
diff --git a/l10n/sl.js b/l10n/sl.js
index 0ce2b107b..46c48e27a 100644
--- a/l10n/sl.js
+++ b/l10n/sl.js
@@ -1357,7 +1357,13 @@ OC.L10N.register(
"Update to version {version}" : "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}." : "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members." : "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel" : "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel." : "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings" : "Personal settings",
+ "Store" : "Store",
+ "Reports" : "Reports",
+ "Flows" : "Flows"
},
"nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);"
);
diff --git a/l10n/sl.json b/l10n/sl.json
index 009a22fdc..a31dcaa79 100644
--- a/l10n/sl.json
+++ b/l10n/sl.json
@@ -1326,6 +1326,12 @@
"Update to version {version}": "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}.": "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.": "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel": "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.": "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings": "Personal settings",
+ "Store": "Store",
+ "Reports": "Reports",
+ "Flows": "Flows"
}
}
diff --git a/l10n/sq.js b/l10n/sq.js
index 68df5674c..27e4e125d 100644
--- a/l10n/sq.js
+++ b/l10n/sq.js
@@ -1357,7 +1357,13 @@ OC.L10N.register(
"Update to version {version}" : "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}." : "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members." : "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel" : "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel." : "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings" : "Personal settings",
+ "Store" : "Store",
+ "Reports" : "Reports",
+ "Flows" : "Flows"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/sq.json b/l10n/sq.json
index d1f7e88af..3e1738c79 100644
--- a/l10n/sq.json
+++ b/l10n/sq.json
@@ -1324,6 +1324,12 @@
"Update to version {version}": "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}.": "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.": "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel": "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.": "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings": "Personal settings",
+ "Store": "Store",
+ "Reports": "Reports",
+ "Flows": "Flows"
}
}
diff --git a/l10n/sr.js b/l10n/sr.js
index cc6e8331d..d038903e7 100644
--- a/l10n/sr.js
+++ b/l10n/sr.js
@@ -1348,6 +1348,12 @@ OC.L10N.register(
"Update to version {version}" : "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}." : "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members." : "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel" : "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel." : "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings" : "Personal settings",
+ "Store" : "Store",
+ "Reports" : "Reports",
+ "Flows" : "Flows"
},
"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);");
diff --git a/l10n/sr.json b/l10n/sr.json
index eff3224d2..af33753c4 100644
--- a/l10n/sr.json
+++ b/l10n/sr.json
@@ -1325,6 +1325,12 @@
"Update to version {version}": "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}.": "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.": "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel": "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.": "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings": "Personal settings",
+ "Store": "Store",
+ "Reports": "Reports",
+ "Flows": "Flows"
}
}
diff --git a/l10n/sv.js b/l10n/sv.js
index 3a8d21317..c0875623f 100644
--- a/l10n/sv.js
+++ b/l10n/sv.js
@@ -1357,7 +1357,13 @@ OC.L10N.register(
"Update to version {version}" : "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}." : "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members." : "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel" : "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel." : "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings" : "Personal settings",
+ "Store" : "Store",
+ "Reports" : "Reports",
+ "Flows" : "Flows"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/sv.json b/l10n/sv.json
index 1e3a99cd7..7dde9c0a5 100644
--- a/l10n/sv.json
+++ b/l10n/sv.json
@@ -1324,6 +1324,12 @@
"Update to version {version}": "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}.": "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.": "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel": "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.": "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings": "Personal settings",
+ "Store": "Store",
+ "Reports": "Reports",
+ "Flows": "Flows"
}
}
diff --git a/l10n/tr.js b/l10n/tr.js
index 20faecd96..b280f9d36 100644
--- a/l10n/tr.js
+++ b/l10n/tr.js
@@ -1357,7 +1357,13 @@ OC.L10N.register(
"Update to version {version}" : "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}." : "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members." : "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel" : "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel." : "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings" : "Personal settings",
+ "Store" : "Store",
+ "Reports" : "Reports",
+ "Flows" : "Flows"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/tr.json b/l10n/tr.json
index c2af4da3a..94f40c7ed 100644
--- a/l10n/tr.json
+++ b/l10n/tr.json
@@ -1324,6 +1324,12 @@
"Update to version {version}": "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}.": "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.": "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel": "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.": "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings": "Personal settings",
+ "Store": "Store",
+ "Reports": "Reports",
+ "Flows": "Flows"
}
}
diff --git a/l10n/uk.js b/l10n/uk.js
index cb8b488df..cd872be42 100644
--- a/l10n/uk.js
+++ b/l10n/uk.js
@@ -1357,7 +1357,13 @@ OC.L10N.register(
"Update to version {version}" : "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}." : "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members." : "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed." : "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel" : "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel." : "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings" : "Personal settings",
+ "Store" : "Store",
+ "Reports" : "Reports",
+ "Flows" : "Flows"
},
"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"
);
diff --git a/l10n/uk.json b/l10n/uk.json
index bc580ed08..e1275f8e1 100644
--- a/l10n/uk.json
+++ b/l10n/uk.json
@@ -1325,6 +1325,12 @@
"Update to version {version}": "Update to version {version}",
"Updated \"{name}\" to version {version}. Members with a copy: {count}.": "Updated \"{name}\" to version {version}. Members with a copy: {count}.",
"Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.": "Its lists read these registers: {registers}. A list whose register is not on this instance is hidden for members.",
- "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed."
+ "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.": "\"{title}\" is part of the start page your administrator set for your group and cannot be removed.",
+ "Start page without navigation panel": "Start page without navigation panel",
+ "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.": "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.",
+ "Personal settings": "Personal settings",
+ "Store": "Store",
+ "Reports": "Reports",
+ "Flows": "Flows"
}
}
diff --git a/lib/Controller/AdminController.php b/lib/Controller/AdminController.php
index c04d59c1e..0118f228d 100644
--- a/lib/Controller/AdminController.php
+++ b/lib/Controller/AdminController.php
@@ -468,6 +468,7 @@ public function getSettings(): JSONResponse {
* @param bool|null $allowMultipleDashboards Alias of `$allowMultiDash`.
* @param int|null $defaultGridColumns Alias of `$defaultGridCols`.
* @param array|null $linkCreateFileExtensions Alias of `$linkCreateFileExts`.
+ * @param bool|null $startPageWithoutNavigation Dashboard view without the navigation panel (REQ-SHELL-009).
*
* @return JSONResponse The update confirmation.
*
@@ -491,7 +492,7 @@ public function updateSettings(
?bool $allowUserDashboards = null,
?bool $allowMultipleDashboards = null,
?int $defaultGridColumns = null,
- ?array $linkCreateFileExtensions = null,
+ ?array $linkCreateFileExtensions = null, ?bool $startPageWithoutNavigation = null,
): JSONResponse {
// Read-side aliases, see the docblock. Short form wins.
$defaultPermLevel ??= $defaultPermissionLevel;
@@ -513,7 +514,7 @@ public function updateSettings(
legacyWidgetBridgeEnabled: $legacyWidgetBridgeEnabled,
maxDashboardsPerUser: $maxDashboardsPerUser,
maxWidgetsPerDashboard: $maxWidgetsPerDashboard,
- quicksearchFallbackTarget: $quicksearchFallbackTarget
+ quicksearchFallbackTarget: $quicksearchFallbackTarget, startPageWithoutNavigation: $startPageWithoutNavigation
);
return ResponseHelper::success(data: ['status' => 'ok']);
diff --git a/lib/Controller/PageController.php b/lib/Controller/PageController.php
index 32e61dcb6..c74852dfd 100644
--- a/lib/Controller/PageController.php
+++ b/lib/Controller/PageController.php
@@ -227,6 +227,7 @@ public function index(string $deepLink = ''): TemplateResponse {
->setAllowedWidgets($allowedWidgets)
->setDeepLinkPath($activeState['deepLinkPath'])
->setQuicksearchFallbackTarget($quicksearchFallback)
+ ->setStartPageWithoutNavigation(($settings['startPageWithoutNavigation'] ?? false) === true)
->apply();
// 🔴 NO CHROME SLOT IDS. This used to pass
diff --git a/lib/Db/AdminSetting.php b/lib/Db/AdminSetting.php
index 2deb06c23..f322b537c 100644
--- a/lib/Db/AdminSetting.php
+++ b/lib/Db/AdminSetting.php
@@ -221,6 +221,15 @@ class AdminSetting extends Entity implements JsonSerializable {
*/
public const KEY_QUICKSEARCH_FALLBACK_TARGET = AdminSettingKey::QUICKSEARCH_FALLBACK_TARGET->value;
+ /**
+ * BC alias for AdminSettingKey::START_PAGE_WITHOUT_NAVIGATION (runtime-shell REQ-SHELL-009).
+ *
+ * @var string
+ *
+ * @see AdminSettingKey::START_PAGE_WITHOUT_NAVIGATION
+ */
+ public const KEY_START_PAGE_WITHOUT_NAVIGATION = AdminSettingKey::START_PAGE_WITHOUT_NAVIGATION->value;
+
/**
* The setting key.
*
diff --git a/lib/Db/AdminSettingKey.php b/lib/Db/AdminSettingKey.php
index 5abb9f3e6..7996eebb0 100644
--- a/lib/Db/AdminSettingKey.php
+++ b/lib/Db/AdminSettingKey.php
@@ -62,4 +62,5 @@ enum AdminSettingKey: string {
case MAX_DASHBOARDS_PER_USER = 'max_dashboards_per_user';
case MAX_WIDGETS_PER_DASHBOARD = 'max_widgets_per_dashboard';
case QUICKSEARCH_FALLBACK_TARGET = 'quicksearch_fallback_target';
+ case START_PAGE_WITHOUT_NAVIGATION = 'start_page_without_navigation';
}//end enum
diff --git a/lib/Service/AdminSettingsService.php b/lib/Service/AdminSettingsService.php
index 6188a6705..0cc8cd44e 100644
--- a/lib/Service/AdminSettingsService.php
+++ b/lib/Service/AdminSettingsService.php
@@ -107,6 +107,7 @@ public function getSettings(): array {
// a value the frontend can't safely act on.
$fallbackKey = AdminSetting::KEY_QUICKSEARCH_FALLBACK_TARGET;
$storedFallbackTarget = $settings[$fallbackKey] ?? null;
+ $noRailKey = AdminSetting::KEY_START_PAGE_WITHOUT_NAVIGATION;
$quicksearchFallbackTarget = self::DEFAULT_QUICKSEARCH_FALLBACK_TARGET;
if (is_string($storedFallbackTarget) === true
@@ -146,6 +147,10 @@ public function getSettings(): array {
// Tile-quick-search REQ-QSEARCH-004: 'none' | 'unified-search'
// | an https URL template containing '{query}'.
'quicksearchFallbackTarget' => $quicksearchFallbackTarget,
+ // Runtime-shell REQ-SHELL-009: the dashboard view without the left
+ // navigation panel. Off by default so an existing instance keeps its
+ // rail; a stored value that is not a boolean reads as off.
+ 'startPageWithoutNavigation' => ($settings[$noRailKey] ?? false) === true,
];
}//end getSettings()
@@ -327,6 +332,11 @@ private function isValidQuicksearchFallbackUrlTemplate(string $value): bool {
* `{query}`.
* tile-quick-search
* REQ-QSEARCH-004.
+ * @param bool|null $startPageWithoutNavigation Render the dashboard
+ * view without the left
+ * navigation panel
+ * (runtime-shell
+ * REQ-SHELL-009).
*
* @return void
*
@@ -350,6 +360,7 @@ public function updateSettings(
?int $maxDashboardsPerUser = null,
?int $maxWidgetsPerDashboard = null,
?string $quicksearchFallbackTarget = null,
+ ?bool $startPageWithoutNavigation = null,
): void {
if ($defaultPermLevel !== null) {
$this->settingMapper->setSetting(
@@ -410,6 +421,13 @@ public function updateSettings(
forcedShareGroups: $forcedShareGroups,
legacyWidgetBridgeEnabled: $legacyWidgetBridgeEnabled
);
+
+ if ($startPageWithoutNavigation !== null) {
+ $this->settingMapper->setSetting(
+ key: AdminSetting::KEY_START_PAGE_WITHOUT_NAVIGATION,
+ value: $startPageWithoutNavigation
+ );
+ }
}//end updateSettings()
/**
diff --git a/lib/Service/InitialStateBuilder.php b/lib/Service/InitialStateBuilder.php
index b849806c1..0322232ff 100644
--- a/lib/Service/InitialStateBuilder.php
+++ b/lib/Service/InitialStateBuilder.php
@@ -338,6 +338,27 @@ public function setQuicksearchFallbackTarget(string $fallbackTarget): self {
return $this;
}//end setQuicksearchFallbackTarget()
+ /**
+ * Set whether the dashboard view renders without the left navigation
+ * panel (workspace).
+ *
+ * Optional key, like {@see self::setQuicksearchFallbackTarget()}: NOT
+ * in {@see self::REQUIRED_KEYS}, so a server that has not called this
+ * setter still passes {@see self::apply()}, and the JS reader's `false`
+ * default keeps the rail for an older deploy (runtime-shell
+ * REQ-SHELL-009).
+ *
+ * @param bool $withoutNavigation True to leave the panel out of the dashboard view.
+ *
+ * @return self Fluent.
+ *
+ * @spec openspec/specs/runtime-shell/spec.md#req-shell-009
+ */
+ public function setStartPageWithoutNavigation(bool $withoutNavigation): self {
+ $this->values['startPageWithoutNavigation'] = $withoutNavigation;
+ return $this;
+ }//end setStartPageWithoutNavigation()
+
/**
* Set every Nextcloud group (admin).
*
diff --git a/openspec/changes/start-page-without-navigation-panel/proposal.md b/openspec/changes/start-page-without-navigation-panel/proposal.md
new file mode 100644
index 000000000..0af4fb1b6
--- /dev/null
+++ b/openspec/changes/start-page-without-navigation-panel/proposal.md
@@ -0,0 +1,26 @@
+---
+kind: code
+depends_on: []
+---
+
+# Start page without navigation panel
+
+## Why
+
+The Zuiddrecht design (`LpStart`) shows the employee's start page at full width under the top bar: no left navigation panel, the dashboard switcher and the footer destinations not in a rail. LaunchPad roots on `CnAppRoot`, which renders `CnAppNav` with ADR-114's four footer destinations, and the library has no prop to leave it out. For the demo the panel was hidden with one CSS line on the instance, which keeps an invisible landmark and takes Documentation, Store, Reports, Features & roadmap and the settings entries with it.
+
+## What changes
+
+- An admin option "Start page without navigation panel" (`start_page_without_navigation`, API `startPageWithoutNavigation`, default off) on the admin settings page beside the other layout options, stored with the other admin settings, pushed to the workspace initial state as an optional key (runtime-shell REQ-SHELL-009, admin-settings REQ-ASET-001).
+- On, the dashboard view (`/`, `/dashboards/:id`) renders without the panel: `App.vue` fills `CnAppRoot`'s `#menu` slot with a hidden, empty node, so the panel is not rendered and the content takes the full width. Other pages keep the panel.
+- `StartPageMenu` in the workspace's top-right controls, beside the dashboard switcher (and in the empty state), offers every entry the panel would have shown: the manifest's main, footer and settings entries after the panel's permission filter, the personal settings dialog, and for an administrator the link to Nextcloud's admin settings.
+- Tests: option off renders the panel, on does not, other routes keep it; every panel entry has a destination and every router destination is a declared page (route no loss).
+
+## Library gap
+
+`@conduction/nextcloud-vue` 2.57.1, `src/components/CnAppRoot/CnAppRoot.vue` lines 202 to 213: the `#menu` slot defaults to `` and there is no prop to render the shell without it. Vue renders a slot's default content when the override holds no real node (`@vue/runtime-core` `ensureValidVNode`), so an empty override does not drop the panel; the hidden span is the smallest thing that does.
+
+## Out of scope
+
+- The design's "Startpagina aanpassen" button and widgets; this change is about the chrome.
+- A settings export: the app's export is of dashboards and the site, not of admin settings, so there is nothing to add the option to.
diff --git a/openspec/changes/start-page-without-navigation-panel/tasks.md b/openspec/changes/start-page-without-navigation-panel/tasks.md
new file mode 100644
index 000000000..7b3723dca
--- /dev/null
+++ b/openspec/changes/start-page-without-navigation-panel/tasks.md
@@ -0,0 +1,5 @@
+# Tasks: start-page-without-navigation-panel
+
+- [x] 1. The option: key, default off, persistence, controller parameter, initial-state setter and wiring. Verify: `AdminSettingsServiceTest::testGetSettingsStartPageWithoutNavigationDefaultsOff`, `::testUpdateSettingsPersistsStartPageWithoutNavigation`, `InitialStateBuilderTest`, `AdminControllerSettingsAliasTest::testStartPageWithoutNavigationIsPassedThrough`.
+- [x] 2. The dashboard view without the panel, the menu in the top row, the admin switch, strings in 37 locales. Verify: `App.navigationPanel.spec.js`, `StartPageMenu.spec.js`.
+- [ ] 3. Live: option on, a member's start page without the panel, the menu with every destination; option off, the panel back. See the PR body.
diff --git a/openspec/specs/admin-settings/spec.md b/openspec/specs/admin-settings/spec.md
index ea1406cf2..3a4bab5ca 100644
--- a/openspec/specs/admin-settings/spec.md
+++ b/openspec/specs/admin-settings/spec.md
@@ -35,18 +35,19 @@ Settings are stored as key-value pairs:
| `max_dashboards_per_user` | `maxDashboardsPerUser` | integer | `0` | Personal dashboard quota per user, `0` = unlimited (REQ-QUOTA-001) |
| `max_widgets_per_dashboard` | `maxWidgetsPerDashboard` | integer | `0` | Placement quota per dashboard, `0` = unlimited (REQ-QUOTA-001) |
| `quicksearch_fallback_target` | `quicksearchFallbackTarget` | string | `none` | Quick-search no-match fallback: `none`, `unified-search` or an https URL template (REQ-QSEARCH-004). A stored value that no longer validates reads as `none`. |
+| `start_page_without_navigation` | `startPageWithoutNavigation` | boolean | `false` | Whether the dashboard view renders without the left navigation panel (runtime-shell REQ-SHELL-009). Only a stored `true` turns it on. |
| `group_order` | n/a (separate `/api/admin/groups` endpoints) | `string[]` (JSON) | `[]` | Ordered list of Nextcloud group IDs that are "active" for LaunchPad workspace routing (REQ-ASET-012). Read via `AdminSettingsService::getGroupOrder()`; written via `setGroupOrder()`. Corrupt JSON resolves to `[]`. |
NOTE: The DB stores settings with snake_case keys, but the API response returns camelCase keys. The factory default for `defaultPermissionLevel` is `add_only` (Dashboard::PERMISSION_ADD_ONLY), NOT `full`. The API update endpoint accepts each setting under the key the GET response uses (`defaultPermissionLevel`, `allowUserDashboards`, `allowMultipleDashboards`, `defaultGridColumns`, `linkCreateFileExtensions`) and under its short parameter name (`defaultPermLevel`, `allowUserDash`, `allowMultiDash`, `defaultGridCols`, `linkCreateFileExts`). When both arrive in one body the short name wins. See REQ-ASET-002.
## Requirements
### Requirement: Retrieve Admin Settings (REQ-ASET-001)
-Administrators MUST be able to retrieve all current admin settings via the API. The endpoint returns a flat JSON object with the twelve settings listed under Defined Settings, each under its API response key. `group_order` is not among them; it has its own endpoints (REQ-ASET-012).
+Administrators MUST be able to retrieve all current admin settings via the API. The endpoint returns a flat JSON object with the thirteen settings listed under Defined Settings, each under its API response key. `group_order` is not among them; it has its own endpoints (REQ-ASET-012).
#### Scenario: Get all settings with defaults
- GIVEN no admin settings have been explicitly configured (fresh installation)
- WHEN the admin sends GET /api/admin/settings
-- THEN the system MUST return HTTP 200 with all twelve settings at their default values:
+- THEN the system MUST return HTTP 200 with all thirteen settings at their default values:
```json
{
"defaultPermissionLevel": "add_only",
@@ -60,7 +61,8 @@ Administrators MUST be able to retrieve all current admin settings via the API.
"legacyWidgetBridgeEnabled": false,
"maxDashboardsPerUser": 0,
"maxWidgetsPerDashboard": 0,
- "quicksearchFallbackTarget": "none"
+ "quicksearchFallbackTarget": "none",
+ "startPageWithoutNavigation": false
}
```
- NOTE: `allowUserDashboards` defaults to `false` (REQ-ASET-003) — admins MUST opt in to personal dashboard creation.
@@ -86,7 +88,7 @@ Administrators MUST be able to retrieve all current admin settings via the API.
#### Scenario: Settings response format consistency
- GIVEN the admin has configured various settings at different times
- WHEN GET /api/admin/settings is called
-- THEN the response MUST always return exactly the twelve keys listed under Defined Settings: `defaultPermissionLevel`, `allowUserDashboards`, `allowMultipleDashboards`, `defaultGridColumns`, `linkCreateFileExtensions`, `launchpad.content_storage`, `defaultSharePermissionLevel`, `forcedShareGroups`, `legacyWidgetBridgeEnabled`, `maxDashboardsPerUser`, `maxWidgetsPerDashboard`, `quicksearchFallbackTarget`
+- THEN the response MUST always return exactly the thirteen keys listed under Defined Settings: `defaultPermissionLevel`, `allowUserDashboards`, `allowMultipleDashboards`, `defaultGridColumns`, `linkCreateFileExtensions`, `launchpad.content_storage`, `defaultSharePermissionLevel`, `forcedShareGroups`, `legacyWidgetBridgeEnabled`, `maxDashboardsPerUser`, `maxWidgetsPerDashboard`, `quicksearchFallbackTarget`, `startPageWithoutNavigation`
- AND no additional keys MUST be present in the response
- NOTE: This scenario said "exactly four keys" long after the response had grown to twelve. The list above is taken from `AdminSettingsService::getSettings()`; a key added there MUST be added here and to the Defined Settings table.
- AND the response MUST be a flat JSON object (no nesting)
diff --git a/openspec/specs/runtime-shell/spec.md b/openspec/specs/runtime-shell/spec.md
index b77f8f35a..244bfbf53 100644
--- a/openspec/specs/runtime-shell/spec.md
+++ b/openspec/specs/runtime-shell/spec.md
@@ -194,3 +194,43 @@ The shared library's first-open support note (`CnSupportDialog`: donate, get sup
@e2e exclude Pinned by src/__tests__/App.supportNote.spec.js.
+
+### Requirement: REQ-SHELL-009 Start page without navigation panel
+
+An administrator MUST be able to turn on the option "Start page without navigation panel" (`start_page_without_navigation`, API key `startPageWithoutNavigation`, default `false`) on the admin settings page beside the other layout options. The option is stored with the other admin settings and pushed to the workspace initial state as the optional key `startPageWithoutNavigation`.
+
+When the option is on, the dashboard view (`/` and `/dashboards/:id`) MUST render without the left navigation panel: `App.vue` overrides `CnAppRoot`'s `#menu` slot with a hidden, empty node, so `CnAppNav` is not rendered and `NcAppContent` takes the full width. Every other page (Store, Reports, Flows, the admin pages) keeps the panel.
+
+Everything the panel offered MUST stay reachable on the dashboard view: the dashboards through the dashboard switcher that is already in the workspace's top-right controls, and the panel's other entries (`manifest.menu` in the sections main, footer and settings, after the same permission filter the panel applies; the personal settings dialog; for an administrator the link to Nextcloud's admin settings) through a "Menu" button (`StartPageMenu`) beside the switcher. A member without an active dashboard gets the same button in the empty state. The menu is keyboard reachable with visible focus and uses no colour of its own. An external destination opens in a new tab, as the panel's did.
+
+When the option is off, or the initial state does not carry the key, nothing changes: the panel renders as before and the menu button is not rendered.
+
+**Why.** The Zuiddrecht design (`LpStart`) is a full-width start page under the top bar; the shared library has no prop to leave the panel out (`CnAppRoot.vue`: the `#menu` slot is the override, and Vue renders the slot's default content when the override holds no real node), and hiding the panel with CSS keeps a landmark nobody can see and takes the footer destinations with it.
+
+#### Scenario: The option is off
+- GIVEN the option is off (the default)
+- WHEN a member opens the dashboard view
+- THEN the navigation panel MUST render with its dashboards, footer destinations and settings foldout, and no "Menu" button MUST be rendered
+
+@e2e openspec/specs/runtime-shell/spec.md#req-shell-009
+
+#### Scenario: The option is on
+- GIVEN an administrator turned the option on
+- WHEN a member opens the dashboard view
+- THEN no navigation panel MUST be rendered, the content MUST take the full width, and the "Menu" button MUST offer Documentation, Store, Reports, Features & roadmap and the personal settings; the dashboard switcher MUST still be beside it
+
+@e2e openspec/specs/runtime-shell/spec.md#req-shell-009
+
+#### Scenario: Other pages keep the panel
+- GIVEN the option is on
+- WHEN a member opens the Store page
+- THEN the navigation panel MUST render
+
+@e2e openspec/specs/runtime-shell/spec.md#req-shell-009
+
+#### Scenario: No destination is lost
+- GIVEN the option is on
+- WHEN the menu is built from the app's manifest for a member and for an administrator
+- THEN every entry the panel would have shown MUST have a destination in the menu, and every router destination MUST be a page the app declares
+
+@e2e openspec/specs/runtime-shell/spec.md#req-shell-009
diff --git a/src/App.vue b/src/App.vue
index 4fd8a9357..b54cfb4fc 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -20,15 +20,72 @@
:customComponents="customComponents"
:permissions="permissions"
:supportDialog="supportNoteForThisAccount"
- appId="launchpad" />
+ appId="launchpad">
+
+
+
+
+
diff --git a/src/components/Workspace/__tests__/StartPageMenu.spec.js b/src/components/Workspace/__tests__/StartPageMenu.spec.js
new file mode 100644
index 000000000..2a9986997
--- /dev/null
+++ b/src/components/Workspace/__tests__/StartPageMenu.spec.js
@@ -0,0 +1,210 @@
+/**
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
+ * SPDX-License-Identifier: EUPL-1.2
+ *
+ * The start-page menu stands in for the navigation panel when the panel is
+ * left out (runtime-shell REQ-SHELL-009): nothing while the panel is there,
+ * and otherwise every destination, grouped as the panel groups them.
+ */
+
+import { mount } from '@vue/test-utils'
+import { describe, expect, it, vi } from 'vitest'
+
+vi.mock('@nextcloud/l10n', () => ({ t: (_app, text) => text }))
+vi.mock('@nextcloud/router', () => ({
+ generateUrl: (tpl, vars) => tpl.replace('{appId}', vars.appId),
+}))
+vi.mock('@nextcloud/vue', () => ({
+ NcActions: {
+ name: 'NcActions',
+ props: ['ariaLabel'],
+ template: '
+ {{
+ t(
+ 'launchpad',
+ 'When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.',
+ )
+ }}
+
+
+
+
+
+
+
{{ t('launchpad', 'No dashboards available') }}
@@ -108,6 +114,7 @@ import MenuIcon from 'vue-material-design-icons/Menu.vue'
import DashboardFooter from '../components/DashboardFooter.vue'
import OrgNavigationPanel from '../components/OrgNavigationPanel.vue'
import SidebarBackdrop from '../components/Workspace/SidebarBackdrop.vue'
+import StartPageMenu from '../components/Workspace/StartPageMenu.vue'
import Views from './Views.vue'
import { useDashboardStore } from '../stores/dashboard.js'
import { useOrgNavigationStore } from '../stores/orgNavigation.js'
@@ -148,6 +155,7 @@ export default {
DashboardFooter,
OrgNavigationPanel,
SidebarBackdrop,
+ StartPageMenu,
},
inject: {
@@ -566,6 +574,14 @@ export default {
gap: 12px;
}
+/* runtime-shell REQ-SHELL-009: the start-page menu sits where the grid's
+ top-right controls would be, so a member finds it in the same place with
+ or without a dashboard. Empty when the panel is there. */
+.workspace-shell__empty-menu {
+ align-self: flex-end;
+ margin-inline-end: 36px;
+}
+
.workspace-shell__empty-title {
font-size: 1.3em;
font-weight: 600;
diff --git a/tests/Unit/Controller/AdminControllerSettingsAliasTest.php b/tests/Unit/Controller/AdminControllerSettingsAliasTest.php
index 0d62bc564..a1a58a52b 100644
--- a/tests/Unit/Controller/AdminControllerSettingsAliasTest.php
+++ b/tests/Unit/Controller/AdminControllerSettingsAliasTest.php
@@ -155,4 +155,32 @@ public function testUnsuppliedSettingsStayNull(): void {
$this->controller->updateSettings(allowUserDash: null);
}//end testUnsuppliedSettingsStayNull()
+
+ /**
+ * runtime-shell REQ-SHELL-009: the start-page option reaches the service
+ * under its own name, and only when it was sent.
+ *
+ * @return void
+ */
+ public function testStartPageWithoutNavigationIsPassedThrough(): void {
+ $this->settingsService->expects($this->once())
+ ->method('updateSettings')
+ ->with(
+ $this->isNull(),
+ $this->isNull(),
+ $this->isNull(),
+ $this->isNull(),
+ $this->isNull(),
+ $this->isNull(),
+ $this->isNull(),
+ $this->isNull(),
+ $this->isNull(),
+ $this->isNull(),
+ $this->isNull(),
+ $this->isNull(),
+ $this->isTrue(),
+ );
+
+ $this->controller->updateSettings(startPageWithoutNavigation: true);
+ }//end testStartPageWithoutNavigationIsPassedThrough()
}//end class
diff --git a/tests/Unit/Service/AdminSettingsServiceTest.php b/tests/Unit/Service/AdminSettingsServiceTest.php
index 72b15535c..131e203d3 100644
--- a/tests/Unit/Service/AdminSettingsServiceTest.php
+++ b/tests/Unit/Service/AdminSettingsServiceTest.php
@@ -79,6 +79,7 @@ public function testGetSettingsReturnsExactlyTheDocumentedKeysAtTheirDefaults():
'maxDashboardsPerUser' => 0,
'maxWidgetsPerDashboard' => 0,
'quicksearchFallbackTarget' => 'none',
+ 'startPageWithoutNavigation' => false,
],
$this->service->getSettings(),
'GET /api/admin/settings no longer matches REQ-ASET-001; update the spec and this list together'
@@ -165,7 +166,9 @@ public function testGetSettingsReturnsCamelCaseKeys(): void {
$this->assertArrayHasKey('maxWidgetsPerDashboard', $settings);
// tile-quick-search REQ-QSEARCH-004.
$this->assertArrayHasKey('quicksearchFallbackTarget', $settings);
- $this->assertCount(12, $settings);
+ // runtime-shell REQ-SHELL-009.
+ $this->assertArrayHasKey('startPageWithoutNavigation', $settings);
+ $this->assertCount(13, $settings);
}//end testGetSettingsReturnsCamelCaseKeys()
// ----- dashboard-quota-limits REQ-QUOTA-001 -----
@@ -313,6 +316,47 @@ public function testUpdateSettingsPersistsBridgeToggle(): void {
);
}//end testUpdateSettingsPersistsBridgeToggle()
+ /**
+ * runtime-shell REQ-SHELL-009: off by default, so an instance that
+ * upgrades keeps its navigation panel.
+ */
+ public function testGetSettingsStartPageWithoutNavigationDefaultsOff(): void {
+ $this->settingMapper->method('getAllAsArray')->willReturn([]);
+
+ $this->assertFalse($this->service->getSettings()['startPageWithoutNavigation']);
+ }//end testGetSettingsStartPageWithoutNavigationDefaultsOff()
+
+ public function testGetSettingsReadsStoredStartPageWithoutNavigation(): void {
+ $this->settingMapper->method('getAllAsArray')->willReturn(
+ [AdminSetting::KEY_START_PAGE_WITHOUT_NAVIGATION => true]
+ );
+
+ $this->assertTrue($this->service->getSettings()['startPageWithoutNavigation']);
+ }//end testGetSettingsReadsStoredStartPageWithoutNavigation()
+
+ /**
+ * A hand-edited row that is not a boolean must not take the panel away:
+ * only a stored `true` does.
+ */
+ public function testGetSettingsTreatsANonBooleanStartPageValueAsOff(): void {
+ $this->settingMapper->method('getAllAsArray')->willReturn(
+ [AdminSetting::KEY_START_PAGE_WITHOUT_NAVIGATION => 'yes']
+ );
+
+ $this->assertFalse($this->service->getSettings()['startPageWithoutNavigation']);
+ }//end testGetSettingsTreatsANonBooleanStartPageValueAsOff()
+
+ public function testUpdateSettingsPersistsStartPageWithoutNavigation(): void {
+ $this->settingMapper->expects($this->once())
+ ->method('setSetting')
+ ->with(
+ AdminSetting::KEY_START_PAGE_WITHOUT_NAVIGATION,
+ true
+ );
+
+ $this->service->updateSettings(startPageWithoutNavigation: true);
+ }//end testUpdateSettingsPersistsStartPageWithoutNavigation()
+
public function testUpdateSettingsRejectsInvalidSharePermissionLevel(): void {
$this->expectException(InvalidArgumentException::class);
diff --git a/tests/Unit/Service/InitialStateBuilderTest.php b/tests/Unit/Service/InitialStateBuilderTest.php
index ee7e7d3a6..b021e2bd1 100644
--- a/tests/Unit/Service/InitialStateBuilderTest.php
+++ b/tests/Unit/Service/InitialStateBuilderTest.php
@@ -104,6 +104,7 @@ public function testWorkspaceBuilderWritesEveryKeyWithCorrectValues(): void {
->setUserDashboards($userDashboards)
->setAllowUserDashboards(true)
->setAllowedWidgets(null)
+ ->setStartPageWithoutNavigation(true)
->apply();
$this->assertSame($widgets, $sink['widgets']);
@@ -116,6 +117,8 @@ public function testWorkspaceBuilderWritesEveryKeyWithCorrectValues(): void {
$this->assertSame($groupDashboards, $sink['groupDashboards']);
$this->assertSame($userDashboards, $sink['userDashboards']);
$this->assertTrue($sink['allowUserDashboards']);
+ // runtime-shell REQ-SHELL-009, an optional key: pushed when set.
+ $this->assertTrue($sink['startPageWithoutNavigation']);
}
public function testAdminBuilderWritesEveryKeyWithCorrectValues(): void {
diff --git a/tests/e2e/start-page-without-navigation.spec.ts b/tests/e2e/start-page-without-navigation.spec.ts
new file mode 100644
index 000000000..7dbe9a8cc
--- /dev/null
+++ b/tests/e2e/start-page-without-navigation.spec.ts
@@ -0,0 +1,167 @@
+/*
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
+ * SPDX-License-Identifier: EUPL-1.2
+ *
+ * The start page without the navigation panel (runtime-shell REQ-SHELL-009).
+ * Off, a member's dashboard view has the panel and no "Menu" button; on, it
+ * has no panel and the button offers the panel's destinations beside the
+ * dashboard switcher. The option is put back to what it was.
+ *
+ * @e2e openspec/specs/runtime-shell/spec.md#req-shell-009
+ */
+import type { APIRequestContext } from '@playwright/test'
+
+import { expect, request as pwRequest, test } from '@playwright/test'
+import {
+ deprovisionUser,
+ loginAs,
+ provisionThrowawayUser,
+} from './fixtures/secondary-user.ts'
+import { BASE_URL as BASE } from './support/baseUrl.ts'
+
+const ADMIN = {
+ user: process.env.NC_ADMIN_USER ?? 'admin',
+ pass: process.env.NC_ADMIN_PASS ?? 'admin',
+}
+const API = '/index.php/apps/launchpad/api'
+
+let api: APIRequestContext
+let member: { username: string; password: string } | null = null
+let before: boolean | null = null
+
+test.setTimeout(180_000)
+
+test.beforeAll(async () => {
+ api = await pwRequest.newContext({
+ baseURL: BASE,
+ httpCredentials: { username: ADMIN.user, password: ADMIN.pass },
+ extraHTTPHeaders: { 'OCS-APIRequest': 'true' },
+ })
+ const current = await api.get(`${API}/admin/settings`)
+ expect(current.ok(), `settings answered ${current.status()}`).toBe(true)
+ const body = await current.json()
+ before = Boolean((body.data ?? body).startPageWithoutNavigation)
+ member = await provisionThrowawayUser('e2e-norail')
+})
+
+test.afterAll(async () => {
+ if (before !== null) {
+ await api.put(`${API}/admin/settings`, {
+ data: { startPageWithoutNavigation: before },
+ })
+ }
+ if (member !== null) {
+ await deprovisionUser(member.username)
+ }
+ await api.dispose()
+})
+
+async function setOption(on: boolean): Promise {
+ const put = await api.put(`${API}/admin/settings`, {
+ data: { startPageWithoutNavigation: on },
+ })
+ expect(put.ok(), `PUT answered ${put.status()}`).toBe(true)
+ const read = await api.get(`${API}/admin/settings`)
+ const body = await read.json()
+ expect((body.data ?? body).startPageWithoutNavigation).toBe(on)
+}
+
+const railIds: string[] = []
+
+// @e2e runtime-shell::the-option-is-off
+test('off: the panel is there and there is no menu button', async ({ browser }) => {
+ await setOption(false)
+ const { context, page } = await loginAs(
+ browser,
+ member!.username,
+ member!.password,
+ )
+ try {
+ await page.goto('/index.php/apps/launchpad/')
+ await expect(page.locator('[data-testid="cn-nav"]')).toBeVisible({
+ timeout: 30_000,
+ })
+ await expect(
+ page.locator('[data-testid="launchpad-start-page-menu"]'),
+ ).toHaveCount(0)
+ railIds.push(
+ ...(await page
+ .locator('[data-testid^="cn-nav-entry-"]')
+ .evaluateAll((els) =>
+ els.map((el) =>
+ (el.getAttribute('data-testid') ?? '').replace(
+ 'cn-nav-entry-',
+ '',
+ ),
+ ),
+ )),
+ )
+ expect(railIds.length).toBeGreaterThan(0)
+ } finally {
+ await context.close()
+ }
+})
+
+// @e2e runtime-shell::the-option-is-on
+// @e2e runtime-shell::no-destination-is-lost
+test('on: no panel, the content at the left edge, every panel entry in the menu', async ({
+ browser,
+}) => {
+ await setOption(true)
+ const { context, page } = await loginAs(
+ browser,
+ member!.username,
+ member!.password,
+ )
+ try {
+ await page.goto('/index.php/apps/launchpad/')
+ const menu = page.locator('[data-testid="launchpad-start-page-menu"]')
+ await expect(menu).toBeVisible({ timeout: 30_000 })
+ await expect(page.locator('[data-testid="cn-nav"]')).toHaveCount(0)
+ await expect(page.locator('#app-navigation-vue')).toHaveCount(0)
+ // The content starts where the panel used to.
+ const contentBox = await page.locator('#app-content-vue').boundingBox()
+ expect(contentBox?.x ?? 999).toBeLessThan(40)
+ // The switcher is beside the menu.
+ await expect(page.getByRole('button', { name: 'Dashboards' })).toBeVisible()
+ await menu.getByRole('button').first().click()
+ const menuIds = await page
+ .locator('[data-testid^="launchpad-start-page-menu-"]')
+ .evaluateAll((els) =>
+ els.map((el) =>
+ (el.getAttribute('data-testid') ?? '').replace(
+ 'launchpad-start-page-menu-',
+ '',
+ ),
+ ),
+ )
+ // Every entry the panel showed with the option off is in the menu.
+ for (const id of railIds) {
+ expect(menuIds, `panel entry ${id} is not in the menu`).toContain(id)
+ }
+ expect(menuIds).toContain('personal-settings')
+ } finally {
+ await context.close()
+ }
+})
+
+// @e2e runtime-shell::other-pages-keep-the-panel
+test('on: the Store page keeps the panel', async ({ browser }) => {
+ await setOption(true)
+ const { context, page } = await loginAs(
+ browser,
+ member!.username,
+ member!.password,
+ )
+ try {
+ await page.goto('/index.php/apps/launchpad/store')
+ await expect(page.locator('[data-testid="cn-nav"]')).toBeVisible({
+ timeout: 30_000,
+ })
+ await expect(
+ page.locator('[data-testid="launchpad-start-page-menu"]'),
+ ).toHaveCount(0)
+ } finally {
+ await context.close()
+ }
+})
From ae285609e3f76ebd1fd293624d722e4cc9b669ec Mon Sep 17 00:00:00 2001
From: Ruben van der Linde
Date: Wed, 7 Oct 2026 09:37:46 +0200
Subject: [PATCH 17/60] feat(setup): each example data card loads itself (#800)
* feat(setup): each example data card loads itself
The demo-data cards step declares loadAction: load-demo-data and the
separate run-action load step is gone. The load action accepts
{ dataset } in the body, refuses an unknown dataset, and records the
pick only after a successful load; a call without a body still loads
the stored pick. Setup status reports every manifest step id.
@conduction/nextcloud-vue is raised to ^2.65.0.
OpenSpec change: wizard-dataset-card-load.
* style(e2e): prettier on the demo data setup step spec
---
l10n/nl.js | 4 +-
l10n/nl.json | 4 +-
lib/Controller/SetupController.php | 98 +-
.../wizard-dataset-card-load/proposal.md | 25 +
.../specs/first-time-setup/spec.md | 51 +
.../changes/wizard-dataset-card-load/tasks.md | 12 +
package-lock.json | 1296 ++++++++++++++++-
package.json | 2 +-
src/manifest.json | 19 +-
tests/Unit/Controller/SetupControllerTest.php | 113 +-
.../demo-data-setup-step.spec.ts | 27 +-
11 files changed, 1578 insertions(+), 73 deletions(-)
create mode 100644 openspec/changes/wizard-dataset-card-load/proposal.md
create mode 100644 openspec/changes/wizard-dataset-card-load/specs/first-time-setup/spec.md
create mode 100644 openspec/changes/wizard-dataset-card-load/tasks.md
diff --git a/l10n/nl.js b/l10n/nl.js
index 598a0d3d5..d7a189596 100644
--- a/l10n/nl.js
+++ b/l10n/nl.js
@@ -1168,9 +1168,7 @@ OC.L10N.register(
"Token removed.": "Token verwijderd.",
"The registry could not be saved.": "Het register kon niet worden opgeslagen.",
"Load example data?": "Voorbeeldgegevens laden?",
- "Example data fills the lists, detail pages and dashboards so you can see the app working straight away. Pick \"None\" on a production install.": "Voorbeeldgegevens vullen de lijsten, detailpagina’s en dashboards, zodat je de app meteen ziet werken. Kies \"Geen\" op een productieomgeving.",
- "Load the example data": "Laad de voorbeeldgegevens",
- "Loads what you picked. The data is obviously sample data, it is safe to run more than once, and you can delete it afterwards.": "Laadt wat je koos. De gegevens zijn herkenbaar voorbeeldgegevens, je kunt dit meer dan een keer uitvoeren en je kunt ze daarna verwijderen.",
+ "Example data fills the lists, detail pages and dashboards so you can see the app working straight away. Each card has its own Load button. Pick \"None\" on a production install.": "Voorbeeldgegevens vullen de lijsten, detailpagina’s en dashboards, zodat je de app meteen ziet werken. Elke kaart heeft een eigen knop Laden. Kies \"Geen\" op een productieomgeving.",
"None, I will set this up myself": "Geen, ik richt dit zelf in",
"Nothing is imported. You start with an empty app and add your own data.": "Er wordt niets geïmporteerd. Je begint met een lege app en voegt zelf gegevens toe.",
"Example data": "Voorbeeldgegevens",
diff --git a/l10n/nl.json b/l10n/nl.json
index 47ea5a5a4..80cbd6ff0 100644
--- a/l10n/nl.json
+++ b/l10n/nl.json
@@ -1,9 +1,7 @@
{
"translations": {
"Load example data?": "Voorbeeldgegevens laden?",
- "Example data fills the lists, detail pages and dashboards so you can see the app working straight away. Pick \"None\" on a production install.": "Voorbeeldgegevens vullen de lijsten, detailpagina’s en dashboards, zodat je de app meteen ziet werken. Kies \"Geen\" op een productieomgeving.",
- "Load the example data": "Laad de voorbeeldgegevens",
- "Loads what you picked. The data is obviously sample data, it is safe to run more than once, and you can delete it afterwards.": "Laadt wat je koos. De gegevens zijn herkenbaar voorbeeldgegevens, je kunt dit meer dan een keer uitvoeren en je kunt ze daarna verwijderen.",
+ "Example data fills the lists, detail pages and dashboards so you can see the app working straight away. Each card has its own Load button. Pick \"None\" on a production install.": "Voorbeeldgegevens vullen de lijsten, detailpagina’s en dashboards, zodat je de app meteen ziet werken. Elke kaart heeft een eigen knop Laden. Kies \"Geen\" op een productieomgeving.",
"None, I will set this up myself": "Geen, ik richt dit zelf in",
"Nothing is imported. You start with an empty app and add your own data.": "Er wordt niets geïmporteerd. Je begint met een lege app en voegt zelf gegevens toe.",
"Example data": "Voorbeeldgegevens",
diff --git a/lib/Controller/SetupController.php b/lib/Controller/SetupController.php
index d9a080f24..edb011e83 100644
--- a/lib/Controller/SetupController.php
+++ b/lib/Controller/SetupController.php
@@ -68,11 +68,10 @@ class SetupController extends Controller {
/**
* App-config key holding the dataset the operator picked.
*
- * The wizard's `choice` step writes it through `POST /api/setup/config`, and
- * the `run-action` step that follows reads it back. Two steps rather than
- * one because `CnSetupWizard::runAction()` posts to
- * `/api/setup/action/{action}` with no body: an action cannot carry the
- * answer, so the answer has to be stored before the action runs.
+ * The wizard's `choice` step writes it through `POST /api/setup/config`.
+ * Each card's Load button posts `{ dataset }` to the `load-demo-data`
+ * action, which stores the same key once the load succeeds, so both routes
+ * land in one place (`loadAction` on the step, wizard-dataset-card-load).
*
* @var string
*/
@@ -107,7 +106,7 @@ public function __construct(
*
* @return JSONResponse The status document.
*
- * @spec exclude Setup status document; ADR-042 contract, no per-app behavioural spec.
+ * @spec openspec/changes/wizard-dataset-card-load/specs/first-time-setup/spec.md
*/
#[AuthorizedAdminSetting(LaunchPadAdmin::class)]
public function status(): JSONResponse {
@@ -122,13 +121,16 @@ public function status(): JSONResponse {
// `optionsSource: datasets` and no options of its own, so a
// dataset missing from this list is a dataset nobody can pick.
'datasets' => $this->demoDataService->listChoices(),
+ // Every id of `manifest.setup.steps`, asserted by the status
+ // contract test: a step the server never reports stays open,
+ // and an open step reopens the wizard on every page.
'steps' => [
- 'demo-data' => ['done' => ($picked !== '')],
- // "None" is an ANSWER, so the load step is finished the moment
- // it is chosen: there is nothing left for the operator to run.
- 'load-demo-data' => [
- 'done' => ($demoDecided === true || $picked === DemoDataService::NONE_DATASET),
- ],
+ 'welcome' => ['done' => true],
+ // Answered once a card was picked ("None" included) or a load
+ // ran. A pick without a load still counts: a wizard that
+ // predates `loadAction` can only record the pick.
+ 'demo-data' => ['done' => ($demoDecided === true || $picked !== '')],
+ 'done' => ['done' => true],
],
]
);
@@ -140,7 +142,7 @@ public function status(): JSONResponse {
*
* @return JSONResponse `{ success, config }`.
*
- * @spec exclude Setup config write; ADR-042 contract, no per-app behavioural spec.
+ * @spec openspec/changes/wizard-dataset-card-load/specs/first-time-setup/spec.md
*/
#[AuthorizedAdminSetting(LaunchPadAdmin::class)]
public function saveConfig(): JSONResponse {
@@ -161,22 +163,12 @@ public function saveConfig(): JSONResponse {
$submitted = ($value[0] ?? null);
}
- if (is_scalar($submitted) === false) {
- return new JSONResponse(
- data: ['success' => false, 'message' => 'A dataset is named by a string.'],
- statusCode: Http::STATUS_BAD_REQUEST,
- );
+ $refusal = $this->refuseDataset(value: $submitted);
+ if ($refusal !== null) {
+ return $refusal;
}
$datasetId = (string)$submitted;
- $known = array_column($this->demoDataService->listChoices(), 'id');
- if (in_array($datasetId, $known, true) === false) {
- return new JSONResponse(
- data: ['success' => false, 'message' => 'No dataset is called "' . $datasetId . '".'],
- statusCode: Http::STATUS_BAD_REQUEST,
- );
- }
-
$this->appConfig->setValueString(Application::APP_ID, self::DATASET_KEY, $datasetId);
return new JSONResponse(data: ['success' => true, 'config' => [self::DATASET_KEY => $datasetId]]);
@@ -223,7 +215,8 @@ public function runAction(string $actionId): JSONResponse {
}//end runAction()
/**
- * Import the dataset the operator picked in the previous step.
+ * Import the dataset a card's Load button posted as `dataset`, or the
+ * stored pick when nothing is posted.
*
* @param string $actionId The action that asked, which decides whether an
* unanswered choice is refused or means the shipped set.
@@ -233,10 +226,26 @@ public function runAction(string $actionId): JSONResponse {
* throws instead of returning an empty result.
*
* @return JSONResponse `{ success, message }`.
+ *
+ * @spec openspec/changes/wizard-dataset-card-load/specs/first-time-setup/spec.md
*/
private function loadDataset(string $actionId): JSONResponse {
$picked = $this->appConfig->getValueString(Application::APP_ID, self::DATASET_KEY, '');
+ // The card's Load button names its dataset in the body. An older wizard
+ // posts nothing and relies on the choice stored a step earlier. Nothing
+ // is stored before the load succeeds: a failed load must leave the step
+ // open for an operator who asked for data and got none.
+ $posted = $this->request->getParam('dataset');
+ if ($posted !== null) {
+ $refusal = $this->refuseDataset(value: $posted);
+ if ($refusal !== null) {
+ return $refusal;
+ }
+
+ $picked = (string)$posted;
+ }
+
// The legacy id carries no answer, so it means the shipped dataset. A
// caller that posts it has said which one by posting it.
if ($actionId === 'install-demo-data' && $picked === '') {
@@ -254,6 +263,7 @@ private function loadDataset(string $actionId): JSONResponse {
}
if ($picked === DemoDataService::NONE_DATASET) {
+ $this->appConfig->setValueString(Application::APP_ID, self::DATASET_KEY, DemoDataService::NONE_DATASET);
$this->appConfig->setValueString(Application::APP_ID, self::DEMO_DECIDED_KEY, 'skipped');
return new JSONResponse(data: ['success' => true, 'message' => 'No example data was loaded.']);
@@ -273,6 +283,8 @@ private function loadDataset(string $actionId): JSONResponse {
);
}
+ // Loading IS choosing the set, so the pick is recorded too.
+ $this->appConfig->setValueString(Application::APP_ID, self::DATASET_KEY, $picked);
$this->appConfig->setValueString(Application::APP_ID, self::DEMO_DECIDED_KEY, 'installed');
return new JSONResponse(
@@ -283,4 +295,36 @@ private function loadDataset(string $actionId): JSONResponse {
);
}//end loadDataset()
+
+ /**
+ * Refuse a dataset id no dataset answers to.
+ *
+ * Shared by the choice step's config write and the card's Load button, so
+ * both refuse the same values with the same message.
+ *
+ * @param mixed $value The posted value.
+ *
+ * @return JSONResponse|null The refusal, or null when the dataset is known.
+ *
+ * @spec openspec/changes/wizard-dataset-card-load/specs/first-time-setup/spec.md
+ */
+ private function refuseDataset(mixed $value): ?JSONResponse {
+ if (is_scalar($value) === false) {
+ return new JSONResponse(
+ data: ['success' => false, 'message' => 'A dataset is named by a string.'],
+ statusCode: Http::STATUS_BAD_REQUEST,
+ );
+ }
+
+ $known = array_column($this->demoDataService->listChoices(), 'id');
+ if (in_array((string)$value, $known, true) === true) {
+ return null;
+ }
+
+ return new JSONResponse(
+ data: ['success' => false, 'message' => 'No dataset is called "' . (string)$value . '".'],
+ statusCode: Http::STATUS_BAD_REQUEST,
+ );
+
+ }//end refuseDataset()
}//end class
diff --git a/openspec/changes/wizard-dataset-card-load/proposal.md b/openspec/changes/wizard-dataset-card-load/proposal.md
new file mode 100644
index 000000000..028529ca5
--- /dev/null
+++ b/openspec/changes/wizard-dataset-card-load/proposal.md
@@ -0,0 +1,25 @@
+---
+kind: code
+---
+
+# Proposal: wizard-dataset-card-load
+
+## Summary
+
+The example data step in the LaunchPad setup wizard becomes one cards step whose cards load themselves. The separate "load the example data" step goes away.
+
+## Why
+
+The wizard asked for example data in two steps: a choice step that stored the pick, then a run-action step that loaded it. Two steps for one decision is one step too many, and the operator could not see which card was loading. `@conduction/nextcloud-vue` 2.65.0 gives a cards choice step a `loadAction`: every card except "None" gets its own Load button that posts `{ dataset }` to the action and spins while it runs. The fleet adopts that pattern from pipelinq (pipelinq#2177).
+
+## What Changes
+
+- The `demo-data` cards step declares `loadAction: load-demo-data`. The run-action step that loaded the pick is removed from the manifest.
+- `POST /api/setup/action/load-demo-data` accepts `{ dataset }` in the body. It refuses a dataset no card offers, loads the named set, and records the pick only after the load succeeds. A call without a body still loads the stored pick, so older wizards and scripts keep working.
+- `GET /api/setup/status` reports exactly the step ids of `manifest.setup.steps`. A step the server never reports stays open, and an open step reopens the wizard on every page.
+- `@conduction/nextcloud-vue` is raised to `^2.65.0`, the first release with `loadAction`.
+
+## Impact
+
+- Affected code: `src/manifest.json`, the setup controller, its unit tests and the demo-data e2e spec.
+- No data migration. The `demo_dataset` app-config key keeps its meaning.
diff --git a/openspec/changes/wizard-dataset-card-load/specs/first-time-setup/spec.md b/openspec/changes/wizard-dataset-card-load/specs/first-time-setup/spec.md
new file mode 100644
index 000000000..a516c3429
--- /dev/null
+++ b/openspec/changes/wizard-dataset-card-load/specs/first-time-setup/spec.md
@@ -0,0 +1,51 @@
+# first-time-setup Specification (delta)
+
+## ADDED Requirements
+
+### Requirement: Each example data card loads itself
+
+The `demo-data` setup step MUST be a cards choice step with `loadAction: load-demo-data`. The setup wizard MUST NOT carry a separate run-action step that loads the picked dataset.
+
+#### Scenario: The operator loads a dataset from its card
+
+- GIVEN the setup wizard shows the example data cards
+- WHEN the operator presses Load on a card
+- THEN the wizard posts `{ "dataset": }` to `/api/setup/action/load-demo-data`
+- AND the server loads that dataset
+- AND the server records the dataset as the pick only after the load succeeds
+- @e2e exclude the card and its spinner are CnSetupWizard UI, tested in nextcloud-vue; the posted body is covered by tests/Unit/Controller/SetupControllerTest.php
+
+#### Scenario: An unknown dataset is refused
+
+- GIVEN a dataset id that no card offers
+- WHEN it is posted to `/api/setup/action/load-demo-data`
+- THEN the server answers 400 with `success: false`
+- AND nothing is loaded or stored
+- @e2e tests/e2e/spec-coverage/demo-data-setup-step.spec.ts
+
+#### Scenario: A call without a body keeps working
+
+- GIVEN a dataset was stored through `/api/setup/config`
+- WHEN `/api/setup/action/load-demo-data` is called without a body
+- THEN the stored dataset is loaded
+- @e2e tests/e2e/spec-coverage/demo-data-setup-step.spec.ts
+
+#### Scenario: A failed load leaves the step open
+
+- GIVEN the load of the posted dataset fails
+- WHEN the server answers
+- THEN the answer carries `success: false`
+- AND no pick or decision is stored
+- @e2e exclude needs a load that fails on a live instance; covered by tests/Unit/Controller/SetupControllerTest.php
+
+### Requirement: Setup status reports every manifest step
+
+`GET /api/setup/status` MUST report a `done` state for every step id in `manifest.setup.steps`.
+
+#### Scenario: The status ids match the manifest
+
+- GIVEN the LaunchPad manifest
+- WHEN an administrator reads `/api/setup/status`
+- THEN `steps` holds an entry for every manifest step id
+- AND the retired load step is not reported
+- @e2e tests/e2e/spec-coverage/demo-data-setup-step.spec.ts
diff --git a/openspec/changes/wizard-dataset-card-load/tasks.md b/openspec/changes/wizard-dataset-card-load/tasks.md
new file mode 100644
index 000000000..f1b648fe1
--- /dev/null
+++ b/openspec/changes/wizard-dataset-card-load/tasks.md
@@ -0,0 +1,12 @@
+# Tasks: wizard-dataset-card-load
+
+## 1. Manifest
+
+- [x] 1.1 Merge the dataset choice step and its run-action load step into one cards step with `loadAction: load-demo-data`
+- [x] 1.2 Raise `@conduction/nextcloud-vue` to `^2.65.0` and validate the manifest against its schema
+
+## 2. Server
+
+- [x] 2.1 Accept `{ dataset }` on `load-demo-data`, refuse an unknown dataset, record the pick after a successful load
+- [x] 2.2 Report every manifest step id from `/api/setup/status`
+- [x] 2.3 Unit tests: the posted dataset, the refusal, a failed load stores nothing, status ids equal manifest ids
diff --git a/package-lock.json b/package-lock.json
index 9e9b48cde..daec55822 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,7 +9,7 @@
"version": "1.0.0",
"license": "EUPL-1.2",
"dependencies": {
- "@conduction/nextcloud-vue": "^2.57.1",
+ "@conduction/nextcloud-vue": "^2.65.0",
"@mdi/js": "^7.4.47",
"@nextcloud/axios": "^2.6.0",
"@nextcloud/dialogs": "^7.4.1",
@@ -1973,9 +1973,9 @@
}
},
"node_modules/@conduction/nextcloud-vue": {
- "version": "2.57.1",
- "resolved": "https://registry.npmjs.org/@conduction/nextcloud-vue/-/nextcloud-vue-2.57.1.tgz",
- "integrity": "sha512-yYN+ZZZqeN8Aj+ncgcMv4P3XWrU69vBDFnYDX4ZIHpGfC9pYpXXuLd+kDKiveap/ingjkD5Ign3miK+dutHhQA==",
+ "version": "2.65.0",
+ "resolved": "https://registry.npmjs.org/@conduction/nextcloud-vue/-/nextcloud-vue-2.65.0.tgz",
+ "integrity": "sha512-TsI4q/VFOXvsPnquc+QEVUgW8sitMY49L1oMB+/sJt2OJUs9eWhywJ7ANmPpw3+OA313G/L3wd5r5ukXFJmikw==",
"license": "EUPL-1.2",
"dependencies": {
"@ckpack/vue-color": "^1.6.0",
@@ -2387,6 +2387,7 @@
"cpu": [
"ppc64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2403,6 +2404,7 @@
"cpu": [
"arm"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2419,6 +2421,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2435,6 +2438,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2451,6 +2455,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2467,6 +2472,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2483,6 +2489,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2499,6 +2506,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2515,6 +2523,7 @@
"cpu": [
"arm"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2531,6 +2540,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2547,6 +2557,7 @@
"cpu": [
"ia32"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2563,6 +2574,7 @@
"cpu": [
"loong64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2579,6 +2591,7 @@
"cpu": [
"mips64el"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2595,6 +2608,7 @@
"cpu": [
"ppc64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2611,6 +2625,7 @@
"cpu": [
"riscv64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2627,6 +2642,7 @@
"cpu": [
"s390x"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2643,6 +2659,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2659,6 +2676,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2675,6 +2693,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2691,6 +2710,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2707,6 +2727,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2723,6 +2744,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2739,6 +2761,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2755,6 +2778,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2771,6 +2795,7 @@
"cpu": [
"ia32"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2787,6 +2812,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3775,6 +3801,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4232,6 +4259,448 @@
"vue": "^3"
}
},
+ "node_modules/@nextcloud/vue/node_modules/@esbuild/aix-ppc64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz",
+ "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@nextcloud/vue/node_modules/@esbuild/android-arm": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz",
+ "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@nextcloud/vue/node_modules/@esbuild/android-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz",
+ "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@nextcloud/vue/node_modules/@esbuild/android-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz",
+ "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@nextcloud/vue/node_modules/@esbuild/darwin-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz",
+ "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@nextcloud/vue/node_modules/@esbuild/darwin-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz",
+ "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@nextcloud/vue/node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz",
+ "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@nextcloud/vue/node_modules/@esbuild/freebsd-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz",
+ "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@nextcloud/vue/node_modules/@esbuild/linux-arm": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz",
+ "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@nextcloud/vue/node_modules/@esbuild/linux-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz",
+ "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@nextcloud/vue/node_modules/@esbuild/linux-ia32": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz",
+ "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@nextcloud/vue/node_modules/@esbuild/linux-loong64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz",
+ "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@nextcloud/vue/node_modules/@esbuild/linux-mips64el": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz",
+ "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==",
+ "cpu": [
+ "mips64el"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@nextcloud/vue/node_modules/@esbuild/linux-ppc64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz",
+ "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@nextcloud/vue/node_modules/@esbuild/linux-riscv64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz",
+ "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@nextcloud/vue/node_modules/@esbuild/linux-s390x": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz",
+ "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@nextcloud/vue/node_modules/@esbuild/linux-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz",
+ "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@nextcloud/vue/node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz",
+ "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@nextcloud/vue/node_modules/@esbuild/netbsd-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz",
+ "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@nextcloud/vue/node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz",
+ "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@nextcloud/vue/node_modules/@esbuild/openbsd-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz",
+ "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@nextcloud/vue/node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz",
+ "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@nextcloud/vue/node_modules/@esbuild/sunos-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz",
+ "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@nextcloud/vue/node_modules/@esbuild/win32-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz",
+ "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@nextcloud/vue/node_modules/@esbuild/win32-ia32": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz",
+ "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@nextcloud/vue/node_modules/@esbuild/win32-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz",
+ "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@nextcloud/vue/node_modules/@nextcloud/initial-state": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@nextcloud/initial-state/-/initial-state-3.0.0.tgz",
@@ -4241,6 +4710,128 @@
"node": "^20.0.0 || ^22.0.0 || ^24.0.0"
}
},
+ "node_modules/@nextcloud/vue/node_modules/esbuild": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz",
+ "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.28.2",
+ "@esbuild/android-arm": "0.28.2",
+ "@esbuild/android-arm64": "0.28.2",
+ "@esbuild/android-x64": "0.28.2",
+ "@esbuild/darwin-arm64": "0.28.2",
+ "@esbuild/darwin-x64": "0.28.2",
+ "@esbuild/freebsd-arm64": "0.28.2",
+ "@esbuild/freebsd-x64": "0.28.2",
+ "@esbuild/linux-arm": "0.28.2",
+ "@esbuild/linux-arm64": "0.28.2",
+ "@esbuild/linux-ia32": "0.28.2",
+ "@esbuild/linux-loong64": "0.28.2",
+ "@esbuild/linux-mips64el": "0.28.2",
+ "@esbuild/linux-ppc64": "0.28.2",
+ "@esbuild/linux-riscv64": "0.28.2",
+ "@esbuild/linux-s390x": "0.28.2",
+ "@esbuild/linux-x64": "0.28.2",
+ "@esbuild/netbsd-arm64": "0.28.2",
+ "@esbuild/netbsd-x64": "0.28.2",
+ "@esbuild/openbsd-arm64": "0.28.2",
+ "@esbuild/openbsd-x64": "0.28.2",
+ "@esbuild/openharmony-arm64": "0.28.2",
+ "@esbuild/sunos-x64": "0.28.2",
+ "@esbuild/win32-arm64": "0.28.2",
+ "@esbuild/win32-ia32": "0.28.2",
+ "@esbuild/win32-x64": "0.28.2"
+ }
+ },
+ "node_modules/@nextcloud/vue/node_modules/vite": {
+ "version": "8.3.3",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.3.tgz",
+ "integrity": "sha512-cTAldKPImjg6c+gk48U19POPn3GCBzZwpdsN8ZMEEcbpes+6/wvfqUd0C2y3qYY4wsj8PwgFwrZ/1jBPVttMSg==",
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "lightningcss": "^1.33.0",
+ "picomatch": "^4.0.7",
+ "postcss": "^8.5.28",
+ "rolldown": "~1.2.11",
+ "tinyglobby": "^0.2.17"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "@vitejs/devtools": "^0.7.1",
+ "esbuild": "^0.27.0 || ^0.28.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "@vitejs/devtools": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@nextcloud/vue/node_modules/vue-router": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.3.1.tgz",
@@ -4519,10 +5110,22 @@
"node": ">=20.0"
}
},
+ "node_modules/@oxc-project/types": {
+ "version": "0.152.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.152.0.tgz",
+ "integrity": "sha512-oM/5rLBm2tPkg0iBgkH/FOeR3PCDpY19GTgAZjMFM8h9WI9VW7cLgzp6nwtarYKmovavIQZ+Fe/RKX/8C8O/Rw==",
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "funding": {
+ "url": "https://github.com/sponsors/oxc-project"
+ }
+ },
"node_modules/@parcel/watcher": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.6.0.tgz",
"integrity": "sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==",
+ "dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
@@ -4562,6 +5165,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4583,6 +5187,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4604,6 +5209,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4625,6 +5231,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4646,6 +5253,7 @@
"cpu": [
"arm"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4667,6 +5275,7 @@
"cpu": [
"arm"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4688,6 +5297,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4709,6 +5319,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4730,6 +5341,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4751,6 +5363,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4772,6 +5385,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4793,6 +5407,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5052,6 +5667,261 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@rolldown/binding-android-arm-eabi": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.12.tgz",
+ "integrity": "sha512-dB/a1214qKfHMXCpgqR4OZT+jS4kTyEXbQGJPqzobt5EwH5rX080pxE37alt3RzvR1bf1Yz/yGqRfrYAxuPw0A==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "peer": true,
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.12.tgz",
+ "integrity": "sha512-7KHFgQ5VJxIHcLlrwrc3Xbds7oTNQT7Pgi9gQCJKrd2VGab/UksIOYp6VD8MzCstGxOKMgNamPwUCfxPdP1OHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "peer": true,
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-arm64": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.12.tgz",
+ "integrity": "sha512-3YIhqHD96nA5SaYNRBR16HnGv4oavZvXfD/ayHM+oYZ0WD/8lBAtf6zQua4kEyAvpqrluKXl0lnOBoiNby7x9w==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "peer": true,
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-x64": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.12.tgz",
+ "integrity": "sha512-UuuJ35MFw4gmFOrE9pEqIV+K3syIKveph+Qc1/ljHZVdoDW4pz/JHR/eMVom+TZGl/5OOvGJOWaOCVt3ZfqhxA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "peer": true,
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-freebsd-x64": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.12.tgz",
+ "integrity": "sha512-uMvssit0a4W+/7D8CbHUvG719mH3R2jwXAlh/XcPvuHTE0g++LymF88DCGNX0HM2rBOn0xrzgXktIB6fLSJBTQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "peer": true,
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.12.tgz",
+ "integrity": "sha512-XcFu0R0xWnwzSf4IQgFH1rJIckPN1pLy2R+4r9IDB7Yfu/ys9cVqfa4pBrMHj7a3gl8mIR4nRNPg0e5IvEVs6g==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.12.tgz",
+ "integrity": "sha512-260UrKgn8tz39ak+SMDOirKzr7V04M9dWPw5llW00SwBivCZoWcRBKV1d8cXnRkUmSZA3BdiUmBHWk7734Ulpw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.12.tgz",
+ "integrity": "sha512-5YK1I9SqDkbPgc1IA8BgDl34suqUS2q0KWnBrirm0E51YjOs6eo6dV6jbQfNE/argHRSvd0QUGgtpIoYx+WWpw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.12.tgz",
+ "integrity": "sha512-Rkcrmp7eFRg74yL5fXEU91JEWbdEPLevWwGtXpmhbjlD1StScbWTmO94Bhly+Mo+ketKYkdmM1vNUKeWSlx8cQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.12.tgz",
+ "integrity": "sha512-qvK4DuAsQc2BSjlx+Xr+IzOIvvxbGZqxFwdWfG6F518Erj0GGISyQbJ6pIappnOxlNPzNHvo/L0BwB30GZ+zVw==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.12.tgz",
+ "integrity": "sha512-Q9uLBO53Xd4QIq1WOycVQyPP1O4HhraEV2qqb3uTrnVw6QZih9duY4vNXOivL1xoUS1/z+W8eF4NMfl2a8Sdjw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-musl": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.12.tgz",
+ "integrity": "sha512-3IBxWFMjbOZskDPKv8Lf9BCnahlKuHthWkYnyIxOH/QcJrFcS4EmcenthApkwr/5+nEqZlLzeYbxeMaX7A5u4g==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-openharmony-arm64": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.12.tgz",
+ "integrity": "sha512-xtX61xg4LKPkPWilZU1ynKClz5Gj4bf74LML4r3eVLWumKnGjoEr1OSHQhMdbBDoYTi+yjrujvpZe2pUnqCrrA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "peer": true,
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.12.tgz",
+ "integrity": "sha512-At7fPB6PCaIjzgIhEZFxuT+BBFqiQibJDT4d3PhiR3f4E7bbMZF4aKblbFfEM3sETRDd1YiQx/+U/g/B/ou5Ew==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "peer": true,
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.12.tgz",
+ "integrity": "sha512-WIw2haVKwjuYdXkHaoC0mF8Le71TuCBxjrdKqLbJGctbBABj+ClfmNvtbOnzpq3RokNo5+V1qhtSzJyXorsklQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "peer": true,
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
"node_modules/@rolldown/pluginutils": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
@@ -5066,6 +5936,7 @@
"cpu": [
"arm"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5079,6 +5950,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5092,6 +5964,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5105,6 +5978,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5118,6 +5992,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5131,6 +6006,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5144,6 +6020,7 @@
"cpu": [
"arm"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5157,6 +6034,7 @@
"cpu": [
"arm"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5170,6 +6048,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5183,6 +6062,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5196,6 +6076,7 @@
"cpu": [
"loong64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5209,6 +6090,7 @@
"cpu": [
"loong64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5222,6 +6104,7 @@
"cpu": [
"ppc64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5235,6 +6118,7 @@
"cpu": [
"ppc64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5248,6 +6132,7 @@
"cpu": [
"riscv64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5261,6 +6146,7 @@
"cpu": [
"riscv64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5274,6 +6160,7 @@
"cpu": [
"s390x"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5287,6 +6174,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5300,6 +6188,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5313,6 +6202,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5326,6 +6216,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5339,6 +6230,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5352,6 +6244,7 @@
"cpu": [
"ia32"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5365,6 +6258,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5378,6 +6272,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5421,7 +6316,7 @@
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.10.0.tgz",
"integrity": "sha512-nPK52ZHvot8Ju/0A4ucSX1dcPV2/1clx0kLcH5wDmrE4naKso7TUC/voUyU1O9OTKTrR6MYip6LP0ogEMQ9jPQ==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
@@ -5841,7 +6736,7 @@
"version": "8.70.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.70.0.tgz",
"integrity": "sha512-zYvrmj9Yxd63UGaXw+kdt6A0F0s0qveJyuatIM77bYC2DE4pgmg7a50u8LR7PRtXd0x+h+Tl3eXabGm06SWd3Q==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.70.0",
@@ -5866,7 +6761,7 @@
"version": "8.70.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.70.0.tgz",
"integrity": "sha512-hFHbTNqhU9G+2eKFXCBVb1tjFT/LceiJ4+HfLO4pTpDI0KHi6iajpcFFkaSQ9gXmCh7n82A0PthaayEdN6mspQ==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.70.0",
@@ -5888,7 +6783,7 @@
"version": "8.70.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.70.0.tgz",
"integrity": "sha512-8nP3Kwh5hlgZ4FicGvmznAmJe8UL4sdU8tLukrPaMuQmDuk4Y8xYfzu/aYZW4xT2JCgc7H/TpDI5cGlxcWJSqQ==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.70.0",
@@ -5906,7 +6801,7 @@
"version": "8.70.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.70.0.tgz",
"integrity": "sha512-adnkeeNq9Sq1sUf4+FRVc0KdgYghzsgFpZSQVZVvY0LCuUuN0FnQgyGzCJeC4fW1cdXseBAjU2EOqUIjbNcZUw==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -5923,7 +6818,7 @@
"version": "8.70.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.70.0.tgz",
"integrity": "sha512-asTOIYhDg4zdzOScCyaytrsV3cR6B4ecPQlXw/dJIm7J/MZTtCtfVII9JD8Geh4jTCrK/Xe6cg5UevoleMcoJQ==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -5937,7 +6832,7 @@
"version": "8.70.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.70.0.tgz",
"integrity": "sha512-d9NmHMPEKQ7QCLLm1jI3zmoQBwT5KwFYjXBJ9ymZfKCUU+5rmTRykKAFvH5Qn/ZCds3CEAFS9OC9M/jkl0X2bA==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.70.0",
@@ -5965,7 +6860,7 @@
"version": "8.70.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.70.0.tgz",
"integrity": "sha512-BoC8PiO4Hkdo0TVJh9Ntxr5MxPDI7/oFsrygN5ADelFSeXG/qgNuucIGA+L5Z6JpPTE/uRfcTWtscjbUaufepQ==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.70.0",
@@ -5983,7 +6878,7 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
- "devOptional": true,
+ "dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
@@ -5996,7 +6891,7 @@
"version": "7.8.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
- "devOptional": true,
+ "dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -6091,7 +6986,7 @@
"version": "8.68.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.68.0.tgz",
"integrity": "sha512-9RnpsGJjrAllCMefGVVsImJM24YurhC0Q1h4UbvivtvOqXmR/vEJge2OoE++z9m6hyg8T1Q8t5SNT6tHSbrxcg==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -9059,6 +9954,21 @@
}
}
},
+ "node_modules/data-urls/node_modules/@noble/hashes": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.4.0.tgz",
+ "integrity": "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "engines": {
+ "node": ">= 20.19.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
"node_modules/data-urls/node_modules/whatwg-url": {
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz",
@@ -11419,6 +12329,21 @@
}
}
},
+ "node_modules/html-encoding-sniffer/node_modules/@noble/hashes": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.4.0.tgz",
+ "integrity": "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "engines": {
+ "node": ">= 20.19.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
"node_modules/html-entities": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz",
@@ -12432,6 +13357,21 @@
}
}
},
+ "node_modules/jsdom/node_modules/@noble/hashes": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.4.0.tgz",
+ "integrity": "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "engines": {
+ "node": ">= 20.19.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
"node_modules/jsdom/node_modules/lru-cache": {
"version": "11.5.2",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz",
@@ -12583,6 +13523,279 @@
"node": ">=22"
}
},
+ "node_modules/lightningcss": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
+ "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==",
+ "license": "MPL-2.0",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.33.0",
+ "lightningcss-darwin-arm64": "1.33.0",
+ "lightningcss-darwin-x64": "1.33.0",
+ "lightningcss-freebsd-x64": "1.33.0",
+ "lightningcss-linux-arm-gnueabihf": "1.33.0",
+ "lightningcss-linux-arm64-gnu": "1.33.0",
+ "lightningcss-linux-arm64-musl": "1.33.0",
+ "lightningcss-linux-x64-gnu": "1.33.0",
+ "lightningcss-linux-x64-musl": "1.33.0",
+ "lightningcss-win32-arm64-msvc": "1.33.0",
+ "lightningcss-win32-x64-msvc": "1.33.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz",
+ "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz",
+ "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz",
+ "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz",
+ "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz",
+ "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz",
+ "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz",
+ "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz",
+ "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz",
+ "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz",
+ "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz",
+ "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
"node_modules/lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
@@ -14162,6 +15375,7 @@
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
+ "dev": true,
"license": "MIT",
"optional": true,
"peer": true
@@ -16377,6 +17591,41 @@
"license": "MIT",
"peer": true
},
+ "node_modules/rolldown": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.12.tgz",
+ "integrity": "sha512-8wafseiaG80xmXSfqidUNqZcylTlhmPZZt+za2m+js2sFZ8dTNlhIOV2WcbIPx2hgwPBJpEUGFAMZ9bgBBLTSQ==",
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "@oxc-project/types": "=0.152.0",
+ "@rolldown/pluginutils": "^1.0.0"
+ },
+ "bin": {
+ "rolldown": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "optionalDependencies": {
+ "@rolldown/binding-android-arm-eabi": "1.2.12",
+ "@rolldown/binding-android-arm64": "1.2.12",
+ "@rolldown/binding-darwin-arm64": "1.2.12",
+ "@rolldown/binding-darwin-x64": "1.2.12",
+ "@rolldown/binding-freebsd-x64": "1.2.12",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.2.12",
+ "@rolldown/binding-linux-arm64-gnu": "1.2.12",
+ "@rolldown/binding-linux-arm64-musl": "1.2.12",
+ "@rolldown/binding-linux-ppc64-gnu": "1.2.12",
+ "@rolldown/binding-linux-s390x-gnu": "1.2.12",
+ "@rolldown/binding-linux-x64-gnu": "1.2.12",
+ "@rolldown/binding-linux-x64-musl": "1.2.12",
+ "@rolldown/binding-openharmony-arm64": "1.2.12",
+ "@rolldown/binding-win32-arm64-msvc": "1.2.12",
+ "@rolldown/binding-win32-x64-msvc": "1.2.12"
+ }
+ },
"node_modules/rollup": {
"version": "4.63.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.1.tgz",
@@ -18542,7 +19791,7 @@
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
"integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=18.12"
@@ -20065,6 +21314,21 @@
}
}
},
+ "node_modules/whatwg-url/node_modules/@noble/hashes": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.4.0.tgz",
+ "integrity": "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "engines": {
+ "node": ">= 20.19.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
diff --git a/package.json b/package.json
index 84b6ba3ff..539404aa7 100644
--- a/package.json
+++ b/package.json
@@ -35,7 +35,7 @@
"prepare": "git config core.hooksPath .githooks || true"
},
"dependencies": {
- "@conduction/nextcloud-vue": "^2.57.1",
+ "@conduction/nextcloud-vue": "^2.65.0",
"@mdi/js": "^7.4.47",
"@nextcloud/axios": "^2.6.0",
"@nextcloud/dialogs": "^7.4.1",
diff --git a/src/manifest.json b/src/manifest.json
index 1a120bf9a..7386ec2e4 100644
--- a/src/manifest.json
+++ b/src/manifest.json
@@ -16,17 +16,10 @@
"display": "cards",
"optionsSource": "datasets",
"configKey": "demo_dataset",
+ "loadAction": "load-demo-data",
"title": "Load example data?",
"required": false,
- "body": "Example data fills the lists, detail pages and dashboards so you can see the app working straight away. Pick \"None\" on a production install."
- },
- {
- "id": "load-demo-data",
- "type": "run-action",
- "action": "load-demo-data",
- "title": "Load the example data",
- "required": false,
- "body": "Loads what you picked. The data is obviously sample data, it is safe to run more than once, and you can delete it afterwards."
+ "body": "Example data fills the lists, detail pages and dashboards so you can see the app working straight away. Each card has its own Load button. Pick \"None\" on a production install."
},
{
"id": "done",
@@ -36,7 +29,13 @@
]
},
"version": "0.2.0",
- "dependencies": [],
+ "dependencies": [
+ {
+ "id": "openregister",
+ "required": false,
+ "name": "OpenRegister"
+ }
+ ],
"observability": {
"_note": "LaunchPad consumes the OpenRegister AppHost observability engine (ADR-040). These descriptors reproduce the output of the now-deleted hand-written HealthController + MetricsController. The /api/health and /api/metrics routes are aliased to the AppHost generic controllers in lib/AppInfo/Application.php, so the engine reads this block at request time. The Prometheus prefix is the runtime app id `launchpad`. Parity: implicit launchpad_info/launchpad_up plus launchpad_dashboards_total/widgets_total/tiles_total match the pre-adoption contract; documented intentional improvements are the now-public health endpoint and the added health `app`/`version` fields (ADR-006).",
"health": {
diff --git a/tests/Unit/Controller/SetupControllerTest.php b/tests/Unit/Controller/SetupControllerTest.php
index 0b81875af..bb97f5edd 100644
--- a/tests/Unit/Controller/SetupControllerTest.php
+++ b/tests/Unit/Controller/SetupControllerTest.php
@@ -38,23 +38,39 @@ protected function setUp(): void {
);
}
- public function testStatusReportsBothDemoDataSteps(): void {
+ public function testStatusReportsEveryManifestStepId(): void {
$this->appConfig->method('getValueString')->willReturn('');
$this->demoData->method('listChoices')->willReturn([]);
$data = $this->controller->status()->getData();
// Absence is the defect this guards: a step the wizard is never told
- // about cannot be offered and cannot be completed.
- $this->assertArrayHasKey('demo-data', $data['steps']);
- $this->assertArrayHasKey('load-demo-data', $data['steps']);
+ // about stays open, and an open step reopens the wizard on every page.
+ // So the ids are read from the manifest the wizard renders, not listed
+ // here by hand.
+ $manifest = json_decode((string)file_get_contents(__DIR__ . '/../../../src/manifest.json'), true);
+ $stepIds = array_column($manifest['setup']['steps'], 'id');
+ $reported = array_values(array_keys($data['steps']));
+ sort($stepIds);
+ sort($reported);
+ $this->assertSame($stepIds, $reported);
+
$this->assertFalse($data['steps']['demo-data']['done']);
- $this->assertFalse($data['steps']['load-demo-data']['done']);
// This app declares no REQUIRED step, so setup must never gate the app.
$this->assertTrue($data['completed']);
$this->assertSame(1, $data['version']);
}
+ public function testTheDatasetStepLoadsFromItsCards(): void {
+ // One cards step with `loadAction` replaces the choice step plus the
+ // run-action step that loaded the pick (wizard-dataset-card-load).
+ $manifest = json_decode((string)file_get_contents(__DIR__ . '/../../../src/manifest.json'), true);
+ $steps = array_column($manifest['setup']['steps'], null, 'id');
+
+ $this->assertSame('load-demo-data', $steps['demo-data']['loadAction'] ?? null);
+ $this->assertArrayNotHasKey('load-demo-data', $steps);
+ }
+
public function testStatusCarriesTheOptionListTheChoiceStepReads(): void {
// 🔴 THIS RESPONSE *IS* THE OPTION LIST. The step declares
// `optionsSource: datasets` and carries no options of its own, so a
@@ -74,7 +90,7 @@ public function testStatusCarriesTheOptionListTheChoiceStepReads(): void {
$this->assertSame('DatabaseOutline', $data['datasets'][1]['icon']);
}
- public function testChoosingNoneClosesBothStepsWithoutRunningAnything(): void {
+ public function testChoosingNoneClosesTheStepWithoutRunningAnything(): void {
// 🔴 THE DEFECT THIS FIXES. Every app in this fleet implemented
// `skip-demo-data` and NO manifest step could reach it, so declining was
// unsayable: the step stayed `done: false` and CnAppRoot reopened the
@@ -88,7 +104,7 @@ public function testChoosingNoneClosesBothStepsWithoutRunningAnything(): void {
$data = $this->controller->status()->getData();
$this->assertTrue($data['steps']['demo-data']['done']);
- $this->assertTrue($data['steps']['load-demo-data']['done']);
+ $this->assertArrayNotHasKey('load-demo-data', $data['steps']);
}
public function testTheChoiceIsPersisted(): void {
@@ -250,12 +266,18 @@ public function testLoadReportsHowMuchLanded(): void {
$this->demoData->method('install')
->willReturn(['objects' => 30, 'registers' => 1, 'schemas' => 4]);
- $this->appConfig->expects($this->once())
- ->method('setValueString')
- ->with('launchpad', 'demo_data_decided', 'installed');
+ $written = [];
+ $this->appConfig->method('setValueString')
+ ->willReturnCallback(static function (string $app, string $key, string $value) use (&$written): bool {
+ $written[$key] = $value;
+
+ return true;
+ });
$data = $this->controller->runAction('load-demo-data')->getData();
+ $this->assertSame(['demo_dataset' => 'demo', 'demo_data_decided' => 'installed'], $written);
+
$this->assertTrue($data['success']);
// A success message that names no count cannot be told apart from an
// import that wrote nothing — the defect this programme already shipped.
@@ -280,4 +302,75 @@ public function testAFailedLoadIsReportedAndLeavesTheStepUNDECIDED(): void {
$this->assertFalse($response->getData()['success']);
$this->assertStringContainsString('OpenRegister is not installed.', $response->getData()['message']);
}
+
+ /**
+ * Build a controller whose request carries the given body params.
+ *
+ * @param array $params The posted body.
+ *
+ * @return SetupController
+ */
+ private function controllerPosting(array $params): SetupController {
+ $request = $this->createMock(IRequest::class);
+ $request->method('getParam')
+ ->willReturnCallback(static fn (string $key) => ($params[$key] ?? null));
+
+ return new SetupController($request, $this->appConfig, $this->logger, $this->demoData);
+ }
+
+ public function testTheCardPostsItsDatasetAndTheLoadRecordsTheChoice(): void {
+ // The card's Load button posts `{ dataset }` to the step's
+ // `loadAction`. Nothing was stored before: the card IS the choice.
+ $this->appConfig->method('getValueString')->willReturn('');
+ $this->demoData->method('listChoices')->willReturn([
+ ['id' => 'none', 'label' => 'None', 'description' => '', 'objectCount' => 0, 'icon' => ''],
+ ['id' => 'demo', 'label' => 'Example data', 'description' => '', 'objectCount' => 66, 'icon' => ''],
+ ]);
+ $this->demoData->expects($this->once())->method('install')
+ ->willReturn(['objects' => 66, 'registers' => 1, 'schemas' => 4]);
+
+ $written = [];
+ $this->appConfig->method('setValueString')
+ ->willReturnCallback(static function (string $app, string $key, string $value) use (&$written): bool {
+ $written[$key] = $value;
+
+ return true;
+ });
+
+ $data = $this->controllerPosting(['dataset' => 'demo'])->runAction('load-demo-data')->getData();
+
+ $this->assertTrue($data['success']);
+ $this->assertStringContainsString('66', $data['message']);
+ $this->assertSame(['demo_dataset' => 'demo', 'demo_data_decided' => 'installed'], $written);
+ }
+
+ public function testAnUnknownPostedDatasetIsRefusedAndNothingLoads(): void {
+ $this->appConfig->method('getValueString')->willReturn('demo');
+ $this->demoData->method('listChoices')->willReturn([
+ ['id' => 'none', 'label' => 'None', 'description' => '', 'objectCount' => 0, 'icon' => ''],
+ ['id' => 'demo', 'label' => 'Example data', 'description' => '', 'objectCount' => 66, 'icon' => ''],
+ ]);
+ $this->demoData->expects($this->never())->method('install');
+ $this->appConfig->expects($this->never())->method('setValueString');
+
+ $response = $this->controllerPosting(['dataset' => 'atlantis'])->runAction('load-demo-data');
+
+ $this->assertSame(400, $response->getStatus());
+ $this->assertStringContainsString('atlantis', $response->getData()['message']);
+ }
+
+ public function testAFailedCardLoadStoresNothing(): void {
+ // The pick is recorded only after a successful load, so a failed card
+ // leaves the step open for an operator who asked for data.
+ $this->appConfig->method('getValueString')->willReturn('');
+ $this->demoData->method('listChoices')->willReturn([
+ ['id' => 'demo', 'label' => 'Example data', 'description' => '', 'objectCount' => 66, 'icon' => ''],
+ ]);
+ $this->demoData->method('install')->willThrowException(new RuntimeException('OpenRegister is not installed.'));
+ $this->appConfig->expects($this->never())->method('setValueString');
+
+ $response = $this->controllerPosting(['dataset' => 'demo'])->runAction('load-demo-data');
+
+ $this->assertSame(500, $response->getStatus());
+ }
}
diff --git a/tests/e2e/spec-coverage/demo-data-setup-step.spec.ts b/tests/e2e/spec-coverage/demo-data-setup-step.spec.ts
index 9db2d9d2e..b408f5f43 100644
--- a/tests/e2e/spec-coverage/demo-data-setup-step.spec.ts
+++ b/tests/e2e/spec-coverage/demo-data-setup-step.spec.ts
@@ -78,7 +78,7 @@ async function api(
* Choose the shipped dataset, and answer with the id that was chosen.
*
* 🔴 THE TEST HAS TO MAKE THE DECISION IT ASSERTS AGAINST. The demo-data step
- * is a choice followed by a load step now, and the CI seed settles the optional
+ * is a cards choice whose cards load themselves, and the CI seed settles the optional
* steps by posting `skip-demo-data` — which records "none". A load that follows
* correctly imports nothing, so an install test that skips this arranges no
* precondition and measures the seed instead of the app.
@@ -137,6 +137,25 @@ test.describe('ADR-111 demo data', () => {
Object.keys(res.json?.steps ?? {}),
'setup/status must report a demo-data step',
).toContain('demo-data')
+ // The cards load themselves (`loadAction`), so the separate load step is
+ // gone from the manifest and from the status document.
+ expect(
+ Object.keys(res.json?.steps ?? {}),
+ 'the run-action load step is retired',
+ ).not.toContain('load-demo-data')
+ })
+
+ test('a card that names an unknown dataset loads nothing', async ({ page }) => {
+ // The card's Load button posts `{ dataset }` to the step's loadAction.
+ const res = await api(
+ page,
+ 'POST',
+ `${BASE}/api/setup/action/load-demo-data`,
+ { dataset: 'atlantis' },
+ )
+
+ expect(res.status).toBe(400)
+ expect(res.json?.success).toBe(false)
})
test('installing the demo data reports HOW MUCH landed, not just success', async ({
@@ -183,12 +202,14 @@ test.describe('ADR-111 demo data', () => {
// The step body tells the operator it is "safe to run more than once".
// That sentence is a contract; this asserts the server keeps it rather
// than erroring or reporting failure on a second pass.
- await pickShippedDataset(page)
+ const shipped = await pickShippedDataset(page)
+ // Posted the way a dataset card's Load button posts it.
const again = await api(
page,
'POST',
- `${BASE}/api/setup/action/install-demo-data`,
+ `${BASE}/api/setup/action/load-demo-data`,
+ { dataset: shipped },
)
expect(again.status).toBe(200)
From 205bd5e0afc124714982cd6e7e0239d4ca2ebdad Mon Sep 17 00:00:00 2001
From: Ruben van der Linde
Date: Wed, 7 Oct 2026 09:49:30 +0200
Subject: [PATCH 18/60] feat(nav): Reports moves under Advanced in the
navigation (#802)
The generic Reports entry leaves the footer for the settings foldout,
which nextcloud-vue now labels Advanced.
---
src/__tests__/App.navigationPanel.spec.js | 7 ++++---
src/manifest.json | 2 +-
tests/e2e/app-chrome.spec.ts | 8 ++++----
3 files changed, 9 insertions(+), 8 deletions(-)
diff --git a/src/__tests__/App.navigationPanel.spec.js b/src/__tests__/App.navigationPanel.spec.js
index 301a03014..18f29c600 100644
--- a/src/__tests__/App.navigationPanel.spec.js
+++ b/src/__tests__/App.navigationPanel.spec.js
@@ -186,16 +186,17 @@ describe('Route no loss: every panel destination stays reachable', () => {
expect(ids).toContain('FeaturesRoadmapMenu')
})
- it('names the four footer destinations of ADR-114', () => {
- const footer = panelEntriesFor(manifest, ['user'])
+ it('names the three footer destinations, with Reports under Advanced', () => {
+ const entries = panelEntriesFor(manifest, ['user'])
+ const footer = entries
.filter((e) => e.section === 'footer')
.map((e) => e.label)
expect(footer).toEqual([
'Documentation',
'Store',
- 'Reports',
'Features & roadmap',
])
+ expect(entries.find((e) => e.id === 'ReportsMenu').section).toBe('settings')
})
it('opens a scheme-prefixed link in a new tab and an app path in the same one', () => {
diff --git a/src/manifest.json b/src/manifest.json
index 7386ec2e4..9439c93f5 100644
--- a/src/manifest.json
+++ b/src/manifest.json
@@ -137,7 +137,7 @@
"label": "Reports",
"icon": "ChartBoxOutline",
"route": "Reports",
- "section": "footer",
+ "section": "settings",
"order": 95
},
{
diff --git a/tests/e2e/app-chrome.spec.ts b/tests/e2e/app-chrome.spec.ts
index 1f379c484..37bff9c97 100644
--- a/tests/e2e/app-chrome.spec.ts
+++ b/tests/e2e/app-chrome.spec.ts
@@ -95,7 +95,7 @@ test.describe('app chrome (ADR-114)', () => {
await expect(page.locator('#launchpad-main-content')).toBeAttached()
})
- test('the chrome declares Documentation, Store, Reports and Features & roadmap', async () => {
+ test('the chrome declares Documentation, Store and Features & roadmap', async () => {
// The manifest is read here rather than restated, so a renamed or
// dropped entry is a failure instead of a silently stale literal.
@@ -113,8 +113,8 @@ test.describe('app chrome (ADR-114)', () => {
expect(
footer.map((e: any) => e.label),
- 'ADR-114 declares four footer destinations, in this order',
- ).toEqual(['Documentation', 'Store', 'Reports', 'Features & roadmap'])
+ 'the footer declares three destinations, in this order; Reports sits in the Advanced foldout',
+ ).toEqual(['Documentation', 'Store', 'Features & roadmap'])
// A GLYPH ON EVERY ONE. An icon name that is not registered renders no
// glyph — not a fallback, not a console error; this app shipped one.
@@ -123,7 +123,7 @@ test.describe('app chrome (ADR-114)', () => {
}
// Documentation leaves the app, so it is an href and there is nothing
- // here to render. The other three name a page this app must host.
+ // here to render. The other two name a page this app must host.
expect(footer[0].href, 'Documentation must be an external href').toMatch(
/^https:\/\//,
)
From f824400e2626c25729cd3f485288442cf2244d88 Mon Sep 17 00:00:00 2001
From: Ruben van der Linde
Date: Wed, 7 Oct 2026 14:28:25 +0200
Subject: [PATCH 19/60] feat(start): Mijn werkdag version 4 and the start page
as LpStart draws it (nextcloud-vue ^2.67.0) (#801)
* fix(start): the case lists keep Status and Termijn inside their card, and the attention button keeps its label (nextcloud-vue ^2.65.0)
A list widget on a dashboard takes a fixed table layout, so a long case
number or title ends in an ellipsis instead of pushing Status and Termijn
past the card edge. The attention widget's action is a link drawn as a
button; a theme that colours every link with !important painted its text
the button's own blue, so it read as an empty bar. The button now keeps
the text colour of its background, without the theme's underline.
* fix(start): the start page without the navigation panel is one centred column, as LpStart draws it
* style: prettier on the attention widget
* feat(templates): Mijn werkdag version 4, as LpStart draws it
The greeting on the page ground with the date and Startpagina aanpassen,
Vandaag eerst, Mijn werk as one number per app (dossiq, pipelinq,
decidiq), Agenda vandaag, Verder waar u was and Alle apps. An app tile
hides when its register is not on the instance, as the lists did.
?edit=1 opens the start page in edit mode, so the greeting's button can
link to it.
* feat(templates): Mijn werkdag 4 on the page ground: greeting and labels without a card, Startpagina aanpassen at the right, app chips, no card menus in view mode
* fix(start): the app tiles fill their cell and the app chips sit on the page ground
* fix(start): section labels start at the card edge
* test(templates): the shipped-template tests read Mijn werkdag 4's app tiles
Version 4 replaced the four lists with three stat tiles and grew to ten
widgets. The render check now checks a tile's source register, filter
fields and enum values the way it checks a list's, and the round-trip
control pins ten compulsory flags.
* chore(deps): @conduction/nextcloud-vue ^2.67.0
* style: prettier on the start page styles this branch adds
---
data/templates/mijn-werkdag.json | 381 ++++++++++--------
lib/Service/ShippedTemplateService.php | 25 +-
package-lock.json | 8 +-
package.json | 2 +-
src/components/WidgetWrapper.vue | 15 +-
.../Widgets/Renderers/AttentionWidget.vue | 16 +
src/services/sourceAvailability.js | 18 +-
src/styles/workspace.css | 161 ++++++++
src/views/Views.vue | 11 +
.../ViewsHideUnavailableSource.spec.js | 35 ++
.../Service/ShippedTemplateServiceTest.php | 23 +-
11 files changed, 502 insertions(+), 193 deletions(-)
diff --git a/data/templates/mijn-werkdag.json b/data/templates/mijn-werkdag.json
index 2bd55f29e..401104b1d 100644
--- a/data/templates/mijn-werkdag.json
+++ b/data/templates/mijn-werkdag.json
@@ -1,11 +1,11 @@
{
"templateId": "mijn-werkdag",
- "templateVersion": 3,
+ "templateVersion": 4,
"language": "nl",
"dashboard": {
"uuid": "6f0d1c1e-5b0a-4c56-9d55-0a1e6d2f7c01",
"name": "Mijn werkdag",
- "description": "Startpagina voor medewerkers: wat vandaag eerst moet uit al uw apps, zaken over de termijn, uw open zaken, uw tickets, besluiten die op uw stem wachten en recente activiteit.",
+ "description": "Startpagina voor medewerkers: wat vandaag eerst moet uit al uw apps, uw werk per app, uw agenda van vandaag, waar u gebleven was en al uw apps.",
"icon": null,
"type": "admin_template",
"userId": null,
@@ -16,7 +16,7 @@
"sortOrder": 0,
"publicationStatus": "published",
"templateCategory": "Werkplek",
- "templateDescription": "Startpagina voor medewerkers van een gemeente. Toont wat vandaag aandacht vraagt uit alle apps, uw dossiq-zaken over de termijn, uw open zaken, uw pipelinq-tickets, de decidiq-besluiten die op uw stem wachten en recente activiteit. Een lijst van een app die hier niet is, wordt niet getoond. Werkt zonder extra instellingen.",
+ "templateDescription": "Startpagina voor medewerkers van een gemeente. Begroet u met de datum, toont wat vandaag aandacht vraagt uit alle apps, uw werk per app (dossiq-zaken, pipelinq-vragen, decidiq-besluiten die op uw stem wachten), uw agenda van vandaag, waar u gebleven was en al uw apps. Een tegel van een app die hier niet is, wordt niet getoond. Werkt zonder extra instellingen.",
"widgets": [
{
"widgetId": "header",
@@ -30,11 +30,14 @@
"customTitle": null,
"customIcon": null,
"content": {
- "title": "Mijn werkdag",
- "subtitle": "Uw zaken en wat vandaag eerst moet, op één pagina",
- "textAlign": "left",
- "verticalAlign": "middle",
- "height": "small"
+ "greeting": true,
+ "showDate": true,
+ "ground": true,
+ "cta": {
+ "label": "Startpagina aanpassen",
+ "url": "/index.php/apps/launchpad/?edit=1",
+ "style": "secondary"
+ }
},
"showTitle": 0,
"sortOrder": 0
@@ -44,224 +47,179 @@
"gridX": 0,
"gridY": 2,
"gridWidth": 12,
- "gridHeight": 5,
+ "gridHeight": 4,
"isCompulsory": 1,
"isVisible": 1,
"styleConfig": {},
"customTitle": "Vandaag eerst",
"customIcon": null,
"content": {
- "limit": 5
+ "limit": 5,
+ "showActions": false
},
"showTitle": 1,
"sortOrder": 1
},
{
- "widgetId": "object-list",
+ "widgetId": "label",
"gridX": 0,
- "gridY": 7,
- "gridWidth": 6,
- "gridHeight": 7,
+ "gridY": 6,
+ "gridWidth": 12,
+ "gridHeight": 1,
"isCompulsory": 0,
"isVisible": 1,
"styleConfig": {},
- "customTitle": "Zaken over de termijn",
+ "customTitle": null,
"customIcon": null,
"content": {
- "title": "Zaken over de termijn",
- "register": "dossiq",
- "schema": "case",
- "filter": {
- "assignee": "@me",
- "isFinalStatus": false,
- "statusHiddenInLists": false,
- "isDraft": false,
- "deadline[lt]": "@today+1d"
- },
- "sort": {
- "field": "deadline",
- "dir": "asc"
- },
- "limit": 10,
- "columns": [
- {
- "key": "identifier",
- "label": "Nummer"
- },
- {
- "key": "title",
- "label": "Zaak"
- },
- {
- "key": "statusPublicLabel",
- "label": "Status"
- },
- {
- "key": "deadline",
- "label": "Termijn"
- }
- ],
- "allowCreate": false,
- "emptyText": "Geen van uw zaken is over de termijn.",
- "hideWhenUnavailable": true
+ "text": "Mijn werk",
+ "textAlign": "left",
+ "fontSize": "18px",
+ "fontWeight": "700"
},
- "showTitle": 1,
+ "showTitle": 0,
"sortOrder": 2
},
{
- "widgetId": "object-list",
- "gridX": 6,
+ "widgetId": "stat",
+ "gridX": 0,
"gridY": 7,
- "gridWidth": 6,
- "gridHeight": 7,
+ "gridWidth": 4,
+ "gridHeight": 2,
"isCompulsory": 0,
"isVisible": 1,
"styleConfig": {},
- "customTitle": "Mijn zaken",
+ "customTitle": null,
"customIcon": null,
"content": {
- "title": "Mijn zaken",
- "register": "dossiq",
- "schema": "case",
- "filter": {
- "assignee": "@me",
- "isFinalStatus": false,
- "statusHiddenInLists": false,
- "isDraft": false
+ "label": "dossiq · Zaken",
+ "caption": "open zaken",
+ "icon": "BriefcaseOutline",
+ "layout": "stacked",
+ "format": {
+ "style": "decimal",
+ "decimals": 0
},
- "sort": {
- "field": "deadline",
- "dir": "asc"
- },
- "limit": 10,
- "columns": [
- {
- "key": "identifier",
- "label": "Nummer"
- },
- {
- "key": "title",
- "label": "Zaak"
- },
- {
- "key": "statusPublicLabel",
- "label": "Status"
- },
- {
- "key": "deadline",
- "label": "Termijn"
+ "source": {
+ "register": "dossiq",
+ "schema": "case",
+ "metric": "count",
+ "filter": {
+ "assignee": "@me",
+ "isFinalStatus": false,
+ "statusHiddenInLists": false,
+ "isDraft": false
}
- ],
- "allowCreate": false,
- "emptyText": "Er zijn geen zaken aan u toegewezen.",
+ },
+ "link": "/index.php/apps/dossiq/",
"hideWhenUnavailable": true
},
- "showTitle": 1,
+ "showTitle": 0,
"sortOrder": 3
},
{
- "widgetId": "object-list",
- "gridX": 0,
- "gridY": 14,
- "gridWidth": 6,
- "gridHeight": 7,
+ "widgetId": "stat",
+ "gridX": 4,
+ "gridY": 7,
+ "gridWidth": 4,
+ "gridHeight": 2,
"isCompulsory": 0,
"isVisible": 1,
"styleConfig": {},
- "customTitle": "Mijn tickets",
+ "customTitle": null,
"customIcon": null,
"content": {
- "title": "Mijn tickets",
- "register": "pipelinq",
- "schema": "ticket",
- "filter": {
- "assignee": "@me",
- "status": [
- "new",
- "in_progress",
- "awaiting_customer"
- ]
- },
- "sort": {
- "field": "slaDeadline",
- "dir": "asc"
+ "label": "pipelinq · Klantcontact",
+ "caption": "open vragen",
+ "icon": "MessageOutline",
+ "layout": "stacked",
+ "format": {
+ "style": "decimal",
+ "decimals": 0
},
- "limit": 10,
- "columns": [
- {
- "key": "title",
- "label": "Ticket"
- },
- {
- "key": "status",
- "label": "Status"
- },
- {
- "key": "priority",
- "label": "Prioriteit"
- },
- {
- "key": "slaDeadline",
- "label": "Termijn"
+ "source": {
+ "register": "pipelinq",
+ "schema": "ticket",
+ "metric": "count",
+ "filter": {
+ "assignee": "@me",
+ "status": [
+ "new",
+ "in_progress",
+ "awaiting_customer"
+ ]
}
- ],
- "allowCreate": false,
- "emptyText": "Er zijn geen open tickets aan u toegewezen.",
+ },
+ "link": "/index.php/apps/pipelinq/",
"hideWhenUnavailable": true
},
- "showTitle": 1,
+ "showTitle": 0,
"sortOrder": 4
},
{
- "widgetId": "object-list",
- "gridX": 6,
- "gridY": 14,
- "gridWidth": 6,
- "gridHeight": 7,
+ "widgetId": "stat",
+ "gridX": 8,
+ "gridY": 7,
+ "gridWidth": 4,
+ "gridHeight": 2,
"isCompulsory": 0,
"isVisible": 1,
"styleConfig": {},
- "customTitle": "Wacht op uw stem",
+ "customTitle": null,
"customIcon": null,
"content": {
- "title": "Wacht op uw stem",
- "register": "decidiq",
- "schema": "decision",
- "filter": {
- "lifecycle": "voting"
+ "label": "decidiq · Besluitvorming",
+ "caption": "wachten op uw stem",
+ "icon": "Gavel",
+ "layout": "stacked",
+ "format": {
+ "style": "decimal",
+ "decimals": 0
},
- "sort": {
- "field": "submittedAt",
- "dir": "asc"
- },
- "limit": 10,
- "columns": [
- {
- "key": "title",
- "label": "Besluit"
- },
- {
- "key": "resolutionNumber",
- "label": "Nummer"
- },
- {
- "key": "submittedAt",
- "label": "Ingediend"
+ "source": {
+ "register": "decidiq",
+ "schema": "decision",
+ "metric": "count",
+ "filter": {
+ "lifecycle": "voting"
}
- ],
- "allowCreate": false,
- "emptyText": "Er wacht geen besluit op uw stem.",
+ },
+ "link": "/index.php/apps/decidiq/",
"hideWhenUnavailable": true
},
- "showTitle": 1,
+ "showTitle": 0,
"sortOrder": 5
},
{
- "widgetId": "nc-widget",
+ "widgetId": "calendar",
"gridX": 0,
- "gridY": 21,
- "gridWidth": 12,
- "gridHeight": 6,
+ "gridY": 9,
+ "gridWidth": 6,
+ "gridHeight": 5,
+ "isCompulsory": 0,
+ "isVisible": 1,
+ "styleConfig": {},
+ "customTitle": "Agenda vandaag",
+ "customIcon": null,
+ "content": {
+ "title": "Agenda vandaag",
+ "viewMode": "agenda",
+ "daysAhead": 1,
+ "internalCalendars": [
+ "personal"
+ ],
+ "externalIcsUrls": [],
+ "showActions": false
+ },
+ "showTitle": 1,
+ "sortOrder": 6
+ },
+ {
+ "widgetId": "nc-widget",
+ "gridX": 6,
+ "gridY": 9,
+ "gridWidth": 6,
+ "gridHeight": 5,
"isCompulsory": 0,
"isVisible": 1,
"styleConfig": {},
@@ -270,10 +228,95 @@
"content": {
"title": "Verder waar u was",
"widgetId": "activity",
- "displayMode": "vertical"
+ "displayMode": "vertical",
+ "showActions": false
},
"showTitle": 1,
- "sortOrder": 6
+ "sortOrder": 7
+ },
+ {
+ "widgetId": "label",
+ "gridX": 0,
+ "gridY": 14,
+ "gridWidth": 12,
+ "gridHeight": 1,
+ "isCompulsory": 0,
+ "isVisible": 1,
+ "styleConfig": {},
+ "customTitle": null,
+ "customIcon": null,
+ "content": {
+ "text": "Alle apps",
+ "textAlign": "left",
+ "fontSize": "18px",
+ "fontWeight": "700"
+ },
+ "showTitle": 0,
+ "sortOrder": 8
+ },
+ {
+ "widgetId": "links",
+ "gridX": 0,
+ "gridY": 15,
+ "gridWidth": 12,
+ "gridHeight": 2,
+ "isCompulsory": 0,
+ "isVisible": 1,
+ "styleConfig": {},
+ "customTitle": null,
+ "customIcon": null,
+ "content": {
+ "linkLayout": "inline",
+ "columns": 9,
+ "openInNewTab": false,
+ "showSectionTitles": false,
+ "sections": [
+ {
+ "title": "Alle apps",
+ "links": [
+ {
+ "label": "dossiq",
+ "url": "/index.php/apps/dossiq/"
+ },
+ {
+ "label": "pipelinq",
+ "url": "/index.php/apps/pipelinq/"
+ },
+ {
+ "label": "decidiq",
+ "url": "/index.php/apps/decidiq/"
+ },
+ {
+ "label": "learniq",
+ "url": "/index.php/apps/learniq/"
+ },
+ {
+ "label": "Bestanden",
+ "url": "/index.php/apps/files/"
+ },
+ {
+ "label": "Agenda",
+ "url": "/index.php/apps/calendar/"
+ },
+ {
+ "label": "Mail",
+ "url": "/index.php/apps/mail/"
+ },
+ {
+ "label": "Talk",
+ "url": "/index.php/apps/spreed/"
+ },
+ {
+ "label": "Contacten",
+ "url": "/index.php/apps/contacts/"
+ }
+ ]
+ }
+ ],
+ "showActions": false
+ },
+ "showTitle": 0,
+ "sortOrder": 9
}
]
}
diff --git a/lib/Service/ShippedTemplateService.php b/lib/Service/ShippedTemplateService.php
index 8d6e379bc..33c144dec 100644
--- a/lib/Service/ShippedTemplateService.php
+++ b/lib/Service/ShippedTemplateService.php
@@ -398,14 +398,23 @@ private function findMissingWidgets(array $widgets): array {
* @spec openspec/specs/admin-templates/spec.md#req-tmpl-022
*/
private function findRegisters(array $widgets): array {
- $lists = array_filter(
- $widgets,
- static fn (mixed $widget): bool => is_array($widget) === true && ($widget['widgetId'] ?? '') === 'object-list'
- );
- $registers = array_map(
- static fn (array $widget): string => (string)($widget['content']['register'] ?? ''),
- $lists
- );
+ $registers = [];
+ foreach ($widgets as $widget) {
+ if (is_array($widget) === false) {
+ continue;
+ }
+
+ // A list names its register at the top of its content, a number
+ // (the app tiles of Mijn werkdag 4) under `source`.
+ $content = (array)($widget['content'] ?? []);
+ if (($widget['widgetId'] ?? '') === 'object-list') {
+ $registers[] = (string)($content['register'] ?? '');
+ }
+
+ if (($widget['widgetId'] ?? '') === 'stat') {
+ $registers[] = (string)(((array)($content['source'] ?? []))['register'] ?? '');
+ }
+ }
return array_values(array: array_unique(array: array_filter($registers)));
}//end findRegisters()
diff --git a/package-lock.json b/package-lock.json
index daec55822..aef43ba98 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,7 +9,7 @@
"version": "1.0.0",
"license": "EUPL-1.2",
"dependencies": {
- "@conduction/nextcloud-vue": "^2.65.0",
+ "@conduction/nextcloud-vue": "^2.67.0",
"@mdi/js": "^7.4.47",
"@nextcloud/axios": "^2.6.0",
"@nextcloud/dialogs": "^7.4.1",
@@ -1973,9 +1973,9 @@
}
},
"node_modules/@conduction/nextcloud-vue": {
- "version": "2.65.0",
- "resolved": "https://registry.npmjs.org/@conduction/nextcloud-vue/-/nextcloud-vue-2.65.0.tgz",
- "integrity": "sha512-TsI4q/VFOXvsPnquc+QEVUgW8sitMY49L1oMB+/sJt2OJUs9eWhywJ7ANmPpw3+OA313G/L3wd5r5ukXFJmikw==",
+ "version": "2.67.0",
+ "resolved": "https://registry.npmjs.org/@conduction/nextcloud-vue/-/nextcloud-vue-2.67.0.tgz",
+ "integrity": "sha512-teoKqWYKkYuAY8hXLJHjp3wGXCK4BEg2bmCSMGza/gslWbV9r3PblVqa/+kmv+j4As87x84m8Eo8uFA7llgYig==",
"license": "EUPL-1.2",
"dependencies": {
"@ckpack/vue-color": "^1.6.0",
diff --git a/package.json b/package.json
index 539404aa7..c5156a777 100644
--- a/package.json
+++ b/package.json
@@ -35,7 +35,7 @@
"prepare": "git config core.hooksPath .githooks || true"
},
"dependencies": {
- "@conduction/nextcloud-vue": "^2.65.0",
+ "@conduction/nextcloud-vue": "^2.67.0",
"@mdi/js": "^7.4.47",
"@nextcloud/axios": "^2.6.0",
"@nextcloud/dialogs": "^7.4.1",
diff --git a/src/components/WidgetWrapper.vue b/src/components/WidgetWrapper.vue
index a7eadd1bc..d1f9dcd68 100644
--- a/src/components/WidgetWrapper.vue
+++ b/src/components/WidgetWrapper.vue
@@ -34,7 +34,8 @@
:borderless="isChromelessFrame"
:flush="isChromelessFrame || rendersOwnHeader"
:showRefresh="false"
- :showRequestFeature="false">
+ :showRequestFeature="false"
+ :showActions="showActionsMenu">
@@ -232,6 +233,18 @@ export default {
return this.placement?.widgetId === 'nc-widget'
},
+ /**
+ * Whether the card's overflow menu shows. On by default; a placement
+ * whose content says `showActions: false` (Mijn werkdag 4, as LpStart
+ * draws it) hides it outside edit mode. The cog stays for editing.
+ *
+ * @spec openspec/specs/widgets/spec.md
+ * @return {boolean} false only for an opted-out placement in view mode.
+ */
+ showActionsMenu() {
+ return this.editMode || this.placement?.content?.showActions !== false
+ },
+
/** @spec openspec/specs/widgets/spec.md */
isChromelessFrame() {
return this.isTileWidget || this.isChromelessType
diff --git a/src/components/Widgets/Renderers/AttentionWidget.vue b/src/components/Widgets/Renderers/AttentionWidget.vue
index 09e70e034..3e7bdd320 100644
--- a/src/components/Widgets/Renderers/AttentionWidget.vue
+++ b/src/components/Widgets/Renderers/AttentionWidget.vue
@@ -309,6 +309,22 @@ export default {
background: var(--color-primary-element-hover);
}
+/*
+ * The action is a link drawn as a button. A theme that colours every link
+ * with `!important` (thematiq's nldesign sheets: `a:not(#header a)...`,
+ * which carries an id in its specificity) painted its text the link blue on
+ * the blue button, so the button read as an empty bar (:8080, 7 October),
+ * and underlined it.
+ * The two `:not(#…)` raise this rule above that one, so the button keeps
+ * the text colour that belongs to its own background in every theme.
+ */
+.attention-widget__action.attention-widget__action:not(#attention-a):not(
+ #attention-b
+ ) {
+ color: var(--color-primary-element-text) !important;
+ text-decoration: none !important;
+}
+
.attention-widget__action:focus-visible {
outline: 2px solid var(--color-main-text);
outline-offset: 2px;
diff --git a/src/services/sourceAvailability.js b/src/services/sourceAvailability.js
index aff3f2a2f..497f178c9 100644
--- a/src/services/sourceAvailability.js
+++ b/src/services/sourceAvailability.js
@@ -23,6 +23,12 @@ import { generateUrl } from '@nextcloud/router'
*/
const probes = new Map()
+/**
+ * The widget types that read one register and schema and can hide when it
+ * is not here: a list, and a number (the app tiles of Mijn werkdag v4).
+ */
+const HIDEABLE_TYPES = ['object-list', 'stat']
+
/**
* The key a placement's source is remembered under.
*
@@ -31,8 +37,12 @@ const probes = new Map()
* @spec openspec/specs/admin-templates/spec.md#req-tmpl-022
*/
export function sourceKey(content) {
- const register = content && content.register
- const schema = content && content.schema
+ // An object-list names its source at the top of its content; a stat
+ // widget names it under `source`.
+ const from =
+ content && !content.register && content.source ? content.source : content
+ const register = from && from.register
+ const schema = from && from.schema
if (!register || !schema) {
return ''
}
@@ -43,13 +53,13 @@ export function sourceKey(content) {
* Whether a placement asks to be hidden when its source is not here.
*
* @param {object} placement A widget placement.
- * @return {boolean} True for an object-list with `hideWhenUnavailable`.
+ * @return {boolean} True for an object-list or a stat with `hideWhenUnavailable`.
* @spec openspec/specs/admin-templates/spec.md#req-tmpl-022
*/
export function hidesWhenUnavailable(placement) {
return (
!!placement
- && placement.widgetId === 'object-list'
+ && HIDEABLE_TYPES.includes(placement.widgetId)
&& !!placement.content
&& placement.content.hideWhenUnavailable === true
&& sourceKey(placement.content) !== ''
diff --git a/src/styles/workspace.css b/src/styles/workspace.css
index 86d8d8346..dfc2fe40c 100644
--- a/src/styles/workspace.css
+++ b/src/styles/workspace.css
@@ -144,3 +144,164 @@
.vs__dropdown-menu {
--vs-dropdown-z-index: 10100;
}
+
+/* =========================================================
+ A list widget on a dashboard keeps every column inside its card.
+ The case lists of "Mijn werkdag" are six columns wide and hold four
+ columns (number, case, status, deadline). Under the table's automatic
+ layout a long case number or title widened its column and pushed
+ Status and Termijn past the card edge, where they were cut off
+ (LpStart, :8080 07 Oct). A fixed layout shares the width between the
+ columns, and a value too long for its column ends in an ellipsis; the
+ whole value stays in the cell's title on the row's detail page.
+ ========================================================= */
+.launchpad-container .cn-object-list-widget table.cn-data-table {
+ table-layout: fixed;
+ width: 100%;
+}
+
+.launchpad-container .cn-object-list-widget table.cn-data-table td,
+.launchpad-container .cn-object-list-widget table.cn-data-table th {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+/* =========================================================
+ The start page without the navigation panel (REQ-SHELL-009) reads as
+ one centred column, as LpStart draws it: 1214px of content between
+ even margins on a 1440px screen, instead of a grid that runs from edge
+ to edge. Only that page carries the hidden marker, so a dashboard with
+ the panel keeps its full width.
+ ========================================================= */
+.app-launchpad:has(.launchpad-rail-suppressed) .launchpad-container {
+ box-sizing: border-box;
+ width: 100%;
+ max-width: 1214px;
+ margin-inline: auto;
+}
+
+/* =========================================================
+ Mijn werkdag 4 on the start page, as LpStart draws it. Each rule keys on
+ a widget the template declares, so a dashboard that declares none of
+ them looks as before.
+ ========================================================= */
+
+/* The greeting and the section labels sit on the page ground, not in a
+ card: a theme that paints every element with "widget" in its class
+ (thematiq's solid-background rule) made them white boxes. */
+.launchpad-container
+ .launchpad-widget:has(.cn-header-widget--ground):not(#lp-ground-a):not(
+ #lp-ground-b
+ ),
+.launchpad-container
+ .launchpad-widget:has(.cn-header-widget--ground)
+ .cn-widget-wrapper:not(#lp-ground-a):not(#lp-ground-b),
+.launchpad-container
+ .launchpad-widget:has(.cn-header-widget--ground)
+ [class*='widget']:not([class*='widget__']):not(#lp-ground-a):not(#lp-ground-b),
+.launchpad-container
+ .launchpad-widget:has(.cn-label-widget):not(#lp-ground-a):not(#lp-ground-b),
+.launchpad-container
+ .launchpad-widget:has(.cn-label-widget)
+ .cn-widget-wrapper:not(#lp-ground-a):not(#lp-ground-b),
+.launchpad-container
+ .launchpad-widget:has(.cn-label-widget)
+ [class*='widget']:not(#lp-ground-a):not(#lp-ground-b) {
+ background: transparent;
+ border: none;
+ box-shadow: none;
+}
+
+/* "Startpagina aanpassen" at the right of the greeting row, as a plain
+ outlined button. */
+.launchpad-container .cn-header-widget--ground {
+ position: relative;
+}
+
+.launchpad-container .cn-header-widget--ground .cn-header-widget__cta {
+ position: absolute;
+ inset-inline-end: 0;
+ top: 50%;
+ transform: translateY(-50%);
+ padding: 8px 16px;
+ border: 1px solid var(--color-border-dark);
+ border-radius: var(--border-radius-element, 8px);
+ background: var(--color-main-background);
+ color: var(--color-main-text) !important;
+ font-weight: 600;
+ text-decoration: none !important;
+}
+
+/* A section label reads as a heading at the left edge. */
+.launchpad-container .cn-label-widget {
+ justify-content: flex-start !important;
+ align-items: flex-end !important;
+ padding-inline: 0;
+}
+
+/* "Alle apps": the links are chips in one row, without the placeholder
+ icon a link without an icon gets. */
+.launchpad-container .cn-links-widget .cn-links-widget__items {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+}
+
+.launchpad-container .cn-links-widget .cn-links-widget__icon-wrap {
+ display: none;
+}
+
+.launchpad-container
+ .cn-links-widget
+ .cn-links-widget__link.cn-links-widget__link:not(#lp-chip-a) {
+ padding: 10px 18px;
+ border: 1px solid var(--color-border);
+ border-radius: var(--border-radius-pill, 999px);
+ background: var(--color-main-background);
+ color: var(--color-main-text) !important;
+ font-weight: 600;
+ text-decoration: none !important;
+}
+
+.launchpad-container
+ .launchpad-widget:has(.cn-links-widget):not(#lp-ground-a):not(#lp-ground-b),
+.launchpad-container
+ .launchpad-widget:has(.cn-links-widget)
+ .cn-widget-wrapper:not(#lp-ground-a):not(#lp-ground-b),
+.launchpad-container
+ .launchpad-widget:has(.cn-links-widget)
+ .cn-widget-wrapper__content:not(#lp-ground-a):not(#lp-ground-b) {
+ background: transparent;
+ border: none;
+ box-shadow: none;
+ padding: 0;
+}
+
+/* The app tiles: the number card fills its cell. The wrapper's and the
+ renderer's 16px padding left a 64px card for an 81px tile, which cut
+ the label and the caption off. */
+.launchpad-container
+ .launchpad-widget:has(.cn-stat-widget)
+ .cn-widget-wrapper__content,
+.launchpad-container .launchpad-widget:has(.cn-stat-widget) .widget-renderer {
+ padding: 0;
+}
+
+.launchpad-container
+ .launchpad-widget:has(.cn-links-widget)
+ .widget-renderer:not(#lp-ground-a):not(#lp-ground-b),
+.launchpad-container
+ .cn-links-widget.cn-links-widget:not(#lp-ground-a):not(#lp-ground-b) {
+ background: transparent;
+ padding: 0;
+}
+
+/* A section label starts at the card edge below it, as LpStart draws it. */
+.launchpad-container
+ .launchpad-widget:has(.cn-label-widget)
+ .cn-widget-wrapper__content,
+.launchpad-container .launchpad-widget:has(.cn-label-widget) .widget-renderer,
+.launchpad-container .cn-label-widget.cn-label-widget {
+ padding: 0;
+}
diff --git a/src/views/Views.vue b/src/views/Views.vue
index dae90de5a..e3308f04a 100644
--- a/src/views/Views.vue
+++ b/src/views/Views.vue
@@ -1198,6 +1198,13 @@ export default {
// instantiated in `setup()` which has no access to `this`.
this.grid._host = this
+ // `?edit=1` opens the start page in edit mode: the "Startpagina
+ // aanpassen" button of Mijn werkdag (LpStart) is a link in the
+ // greeting, and a link can only carry its wish in the address.
+ // Read now, because mounted() rewrites the address.
+ const editFromAddress =
+ new URLSearchParams(window.location.search).get('edit') === '1'
+
const dashboardStore = useDashboardStore()
const widgetStore = useWidgetStore()
const tileStore = useTileStore()
@@ -1227,6 +1234,10 @@ export default {
error,
)
}
+
+ if (editFromAddress && this.canEdit) {
+ await this.enterEditMode()
+ }
},
/** @spec openspec/specs/dashboards/spec.md */
diff --git a/src/views/__tests__/ViewsHideUnavailableSource.spec.js b/src/views/__tests__/ViewsHideUnavailableSource.spec.js
index 510781aac..a478400b4 100644
--- a/src/views/__tests__/ViewsHideUnavailableSource.spec.js
+++ b/src/views/__tests__/ViewsHideUnavailableSource.spec.js
@@ -121,3 +121,38 @@ describe('Views hides a list whose register is not here', () => {
expect(host.shownPlacements).toEqual([])
})
})
+
+describe('Views hides an app tile whose register is not here', () => {
+ it('leaves out a stat that names its source under `source`, and keeps one whose source is here', async () => {
+ // Mijn werkdag 4: one number per app, each reading its own register.
+ const caseTile = {
+ id: 5,
+ widgetId: 'stat',
+ content: {
+ source: { register: 'dossiq', schema: 'case', metric: 'count' },
+ hideWhenUnavailable: true,
+ },
+ }
+ const ticketTile = {
+ id: 6,
+ widgetId: 'stat',
+ content: {
+ source: { register: 'pipelinq', schema: 'ticket', metric: 'count' },
+ hideWhenUnavailable: true,
+ },
+ }
+ const plainTile = {
+ id: 7,
+ widgetId: 'stat',
+ content: {
+ source: { register: 'pipelinq', schema: 'lead', metric: 'count' },
+ },
+ }
+ const host = makeHost([header, caseTile, ticketTile, plainTile])
+
+ await host.probeHideableSources()
+
+ expect(host.shownPlacements.map((p) => p.id)).toEqual([1, 5, 7])
+ expect(get).toHaveBeenCalledTimes(2)
+ })
+})
diff --git a/tests/Unit/Service/ShippedTemplateServiceTest.php b/tests/Unit/Service/ShippedTemplateServiceTest.php
index a71a06b87..145f67beb 100644
--- a/tests/Unit/Service/ShippedTemplateServiceTest.php
+++ b/tests/Unit/Service/ShippedTemplateServiceTest.php
@@ -333,7 +333,7 @@ public function testTheShippedTemplateSurvivesExportAndImport(): void {
// The control: the comparison would also pass if both sides were
// empty, so pin that the compulsory flags really crossed.
self::assertSame(
- [1, 1, 0, 0, 0, 0, 0],
+ [1, 1, 0, 0, 0, 0, 0, 0, 0, 0],
array_column(self::definitionOf($onB)['widgets'], 'isCompulsory')
);
}
@@ -455,7 +455,7 @@ public function testTheListingNamesWidgetsNothingRegisters(): void {
self::assertSame('mijn-werkdag', $listing[0]['id']);
self::assertSame('Mijn werkdag', $listing[0]['name']);
self::assertSame('nl', $listing[0]['language']);
- self::assertSame(7, $listing[0]['widgetCount']);
+ self::assertSame(10, $listing[0]['widgetCount']);
self::assertSame(['dossiq', 'pipelinq', 'decidiq'], $listing[0]['registers']);
self::assertFalse($listing[0]['isInstalled']);
self::assertNull($listing[0]['installedVersion']);
@@ -519,14 +519,25 @@ public function testAShippedTemplateRendersAsInstalled(): void {
if ($widget['widgetId'] === 'nc-widget') {
self::assertContains($content['widgetId'], $itemsApiWidgets, 'this widget cannot paint without the legacy bridge');
}
- if ($widget['widgetId'] !== 'object-list') {
+ if ($widget['widgetId'] !== 'object-list' && $widget['widgetId'] !== 'stat') {
continue;
}
+ // A list names its register at the top of its content and
+ // shows columns; a number (the app tiles of Mijn werkdag 4)
+ // names it under `source` and only filters.
+ $title = (string)($widget['customTitle'] ?? $content['label'] ?? $widget['widgetId']);
+ $named = [];
+ if ($widget['widgetId'] === 'stat') {
+ $content = array_merge($content, (array)$content['source']);
+ } else {
+ $named = array_column($content['columns'], 'key');
+ $named[] = $content['sort']['field'];
+ }
+
+ $widget['customTitle'] = $title;
$key = $content['register'] . '/' . $content['schema'];
self::assertArrayHasKey($key, $schemas, 'no field list for ' . $key . ': add tests/fixtures/registers');
- $named = array_column($content['columns'], 'key');
- $named[] = $content['sort']['field'];
foreach (array_keys($content['filter']) as $filterKey) {
// `deadline[lt]` names the field `deadline`.
$named[] = preg_replace('/\[.*$/', '', (string)$filterKey);
@@ -556,7 +567,7 @@ public function testAShippedTemplateRendersAsInstalled(): void {
}
}
- self::assertSame(4, $listsChecked, 'two dossiq lists, the pipelinq list and the decidiq list');
+ self::assertSame(3, $listsChecked, 'the dossiq, pipelinq and decidiq tiles');
}
/**
From d443633d6bd0c05bb8c9f3645f06059c13164dda Mon Sep 17 00:00:00 2001
From: Ruben van der Linde
Date: Wed, 7 Oct 2026 21:06:52 +0200
Subject: [PATCH 20/60] docs(parity): link the five unbuilt capability rows to
the changes that specify them
---
openspec/parity/capabilities.json | 15 ++++++++++-----
1 file changed, 10 insertions(+), 5 deletions(-)
diff --git a/openspec/parity/capabilities.json b/openspec/parity/capabilities.json
index 24cd01e13..53bbb585e 100644
--- a/openspec/parity/capabilities.json
+++ b/openspec/parity/capabilities.json
@@ -2319,7 +2319,8 @@
"built": {
"state": "building",
"evidence": "lib/Settings/connections.json:14; src/manifest.d/connection-registry.json:23",
- "owner": "ConductionNL/integriq"
+ "owner": "ConductionNL/integriq",
+ "change": "openspec/changes/adopt-connection-registry"
},
"reachedOn": "Integrations page (/apps/launchpad/settings/integrations), shown only when the integriq app is installed",
"provider": "integriq",
@@ -3041,7 +3042,8 @@
"built": {
"state": "specified",
"evidence": "lib/Controller/StoreController.php:134; src/manifest.json:164",
- "owner": "ConductionNL/openregister"
+ "owner": "ConductionNL/openregister",
+ "change": "openregister/store-over-federated-config"
},
"reachedOn": "Store page (/apps/launchpad/store), once an administrator sets a registry URL on the Sharing tab",
"provider": "openregister",
@@ -3801,7 +3803,8 @@
"built": {
"state": "specified",
"evidence": "openspec/changes/mcp-tool-surface/specs/mcp-tools/spec.md:226",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "change": "openspec/changes/mcp-tool-surface"
},
"reachedOn": "nothing: the overlay lists it as soon",
"provider": "launchpad",
@@ -4859,7 +4862,8 @@
"built": {
"state": "specified",
"owner": "ConductionNL/nextcloud-vue",
- "evidence": "lib/widget-types.json:37"
+ "evidence": "lib/widget-types.json:37",
+ "change": "nextcloud-vue/dashboard-notepad-widget"
},
"reachedOn": "Workspace page, Add widget, Text, on a personal dashboard",
"evidence": {
@@ -5291,7 +5295,8 @@
"built": {
"state": "specified",
"owner": "ConductionNL/dossiq",
- "evidence": "src/services/widgetBridge.js:29"
+ "evidence": "src/services/widgetBridge.js:29",
+ "change": "dossiq/widgets-my-open-cases-on-the-start-page"
},
"reachedOn": "Workspace page, Add widget, Nextcloud widget, a case app's own widget, with that app installed",
"evidence": {
From 79bc2987821e7fd571f7b37f1c1965247a7d911f Mon Sep 17 00:00:00 2001
From: Ruben van der Linde
Date: Wed, 7 Oct 2026 21:09:13 +0200
Subject: [PATCH 21/60] docs(parity): link every built capability row to the
spec or change that delivered it
---
openspec/parity/capabilities.json | 298 ++++++++++++++++++++----------
1 file changed, 201 insertions(+), 97 deletions(-)
diff --git a/openspec/parity/capabilities.json b/openspec/parity/capabilities.json
index 53bbb585e..6ccc632ab 100644
--- a/openspec/parity/capabilities.json
+++ b/openspec/parity/capabilities.json
@@ -545,7 +545,8 @@
"built": {
"state": "built",
"evidence": "src/views/Views.vue:183",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "spec": "openspec/specs/grid-layout"
},
"reachedOn": "Workspace page (/apps/launchpad/), after choosing Edit dashboard in the dashboard menu",
"evidence": {
@@ -574,7 +575,8 @@
"built": {
"state": "built",
"evidence": "src/views/Views.vue:183",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "spec": "openspec/specs/grid-layout"
},
"reachedOn": "Workspace page in edit mode, the same grid as build-drag",
"evidence": {
@@ -603,7 +605,8 @@
"built": {
"state": "built",
"evidence": "src/modals/WidgetMovePanel.vue:27; src/components/Widgets/WidgetContextMenu.vue:27",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "change": "openspec/changes/keyboard-accessible-widget-repositioning"
},
"reachedOn": "Workspace page, widget context menu, Move",
"provider": "launchpad",
@@ -632,7 +635,8 @@
"built": {
"state": "built",
"evidence": "lib/Service/AdminSettingsService.php:124; src/components/admin/AdminSettings.vue:96",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "spec": "openspec/specs/admin-settings"
},
"reachedOn": "Admin settings (/settings/admin/launchpad), Default settings, Default grid columns",
"provider": "launchpad",
@@ -662,7 +666,8 @@
"built": {
"state": "built",
"evidence": "src/views/Views.vue:650",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "change": "openspec/changes/archive/2026-06-14-responsive-grid-breakpoints"
},
"reachedOn": "Workspace page at any viewport width",
"evidence": {
@@ -691,7 +696,8 @@
"built": {
"state": "built",
"evidence": "src/components/Widgets/Renderers/ContainerWidget.vue:52; src/constants/widgetRegistry.js:16",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "change": "openspec/changes/archive/2026-05-03-container-widget"
},
"reachedOn": "Workspace page, Add widget, Container",
"evidence": {
@@ -720,7 +726,8 @@
"built": {
"state": "built",
"evidence": "src/views/Views.vue:309",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "spec": "openspec/specs/widgets"
},
"reachedOn": "Workspace page, widget context menu, Edit",
"provider": "launchpad",
@@ -749,7 +756,8 @@
"built": {
"state": "built",
"evidence": "src/modals/WidgetPickerModal.vue:21; src/views/Views.vue:273",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "change": "openspec/changes/archive/2026-05-03-unified-add-widget-flow"
},
"reachedOn": "Workspace page, dashboard menu, Add widget",
"provider": "launchpad",
@@ -778,7 +786,8 @@
"built": {
"state": "built",
"evidence": "lib/Service/WidgetService.php:86; src/constants/widgetRegistry.js:141",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "change": "openspec/changes/archive/2026-06-14-nc-dashboard-widget-proxy"
},
"reachedOn": "Workspace page, Add widget, Nextcloud widget",
"evidence": {
@@ -807,7 +816,8 @@
"built": {
"state": "built",
"evidence": "src/composables/useGridManager.js:221",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "change": "openspec/changes/archive/2026-06-14-widget-collision-placement"
},
"reachedOn": "Workspace page, any Add widget action",
"provider": "launchpad",
@@ -897,7 +907,8 @@
"built": {
"state": "built",
"evidence": "lib/Service/ImportService.php:138; lib/Command/ImportCommand.php:36",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "spec": "openspec/specs/dashboard-export-import"
},
"reachedOn": "Admin settings, Operations tab, Export and import; and occ launchpad import",
"evidence": {
@@ -958,7 +969,8 @@
"built": {
"state": "built",
"evidence": "src/components/Workspace/DashboardSwitcherSidebar.vue:224",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "change": "openspec/changes/archive/2026-06-14-dashboard-switcher-sidebar"
},
"reachedOn": "Workspace page, Dashboards sidebar",
"evidence": {
@@ -987,7 +999,8 @@
"built": {
"state": "built",
"evidence": "src/components/Workspace/DashboardRowActions.vue:90",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "change": "openspec/changes/archive/2026-06-14-default-dashboard-flag"
},
"reachedOn": "Workspace page, dashboard menu, Set as default",
"provider": "launchpad",
@@ -1016,7 +1029,8 @@
"built": {
"state": "built",
"evidence": "src/modals/DashboardConfigModal.vue:75; src/modals/DashboardConfigModal.vue:87",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "spec": "openspec/specs/dashboards"
},
"reachedOn": "Workspace page, dashboard menu, Dashboard configuration, General tab",
"provider": "launchpad",
@@ -1045,7 +1059,8 @@
"built": {
"state": "built",
"evidence": "lib/Service/DashboardService.php:2190; src/views/Views.vue:2037",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "change": "openspec/changes/archive/2026-06-14-fork-current-as-personal"
},
"reachedOn": "Workspace page, Dashboards sidebar, Add dashboard",
"provider": "launchpad",
@@ -1074,7 +1089,8 @@
"built": {
"state": "built",
"evidence": "lib/Controller/PageController.php:127; src/manifest.json:176",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "spec": "openspec/specs/dashboard-deeplinking"
},
"reachedOn": "/apps/launchpad/dashboards/{id} and /apps/launchpad/{path}",
"provider": "launchpad",
@@ -1103,7 +1119,8 @@
"built": {
"state": "built",
"evidence": "src/components/Workspace/DashboardSwitcherSidebar.vue:326",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "spec": "openspec/specs/dashboard-sharing"
},
"reachedOn": "Workspace page, Dashboards sidebar, Shared with you",
"evidence": {
@@ -1162,7 +1179,8 @@
"built": {
"state": "built",
"evidence": "src/views/WorkspaceApp.vue:81",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "spec": "openspec/specs/runtime-shell"
},
"reachedOn": "Workspace page with no dashboards",
"provider": "launchpad",
@@ -1251,7 +1269,8 @@
"built": {
"state": "built",
"evidence": "src/modals/TileEditor.vue:71; src/modals/TileEditor.vue:111",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "spec": "openspec/specs/tiles"
},
"reachedOn": "Workspace page, dashboard menu, Add custom widget, tile editor",
"evidence": {
@@ -1280,7 +1299,8 @@
"built": {
"state": "built",
"evidence": "src/modals/TileEditor.vue:123; lib/Service/HealthPingService.php:11",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "change": "openspec/changes/archive/2026-07-23-service-health-ping"
},
"reachedOn": "Workspace page, tile badge; switched on in the tile editor. Only hosts in healthping_allowed_hosts are pinged, set with occ",
"evidence": {
@@ -1309,7 +1329,8 @@
"built": {
"state": "built",
"evidence": "src/components/Widgets/Renderers/SearchWidget.vue:157",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "change": "openspec/changes/archive/2026-07-23-tile-quick-search"
},
"reachedOn": "Workspace page, Search widget",
"evidence": {
@@ -1338,7 +1359,8 @@
"built": {
"state": "built",
"evidence": "src/components/Widgets/Renderers/SearchWidgetForm.vue:41",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "change": "openspec/changes/archive/2026-08-20-tile-search-widget"
},
"reachedOn": "Workspace page, Search widget, falls back to a configured https search template or Nextcloud unified search",
"evidence": {
@@ -1367,7 +1389,8 @@
"built": {
"state": "built",
"evidence": "lib/widget-types.json:24; lib/widget-types.json:31; package.json:38",
- "owner": "ConductionNL/nextcloud-vue"
+ "owner": "ConductionNL/nextcloud-vue",
+ "change": "openspec/changes/archive/2026-05-03-links-widget"
},
"reachedOn": "Workspace page, Add widget, Links or Quick links",
"provider": "nextcloud-vue",
@@ -1594,7 +1617,8 @@
"built": {
"state": "built",
"evidence": "src/views/WorkspaceApp.vue:20; lib/Service/OrgNavigationService.php:204",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "change": "openspec/changes/archive/2026-05-03-navigation-editor-org"
},
"reachedOn": "Workspace page, navigation panel; edited on Admin settings, Organisation navigation tab",
"provider": "launchpad",
@@ -1623,7 +1647,8 @@
"built": {
"state": "built",
"evidence": "lib/widget-types.json:37; package.json:38",
- "owner": "ConductionNL/nextcloud-vue"
+ "owner": "ConductionNL/nextcloud-vue",
+ "change": "openspec/changes/archive/2026-06-14-text-display-widget"
},
"reachedOn": "Workspace page, Add widget, Text",
"provider": "nextcloud-vue",
@@ -1652,7 +1677,8 @@
"built": {
"state": "built",
"evidence": "lib/widget-types.json:21; lib/Controller/ResourceController.php:63",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "change": "openspec/changes/archive/2026-06-14-image-widget"
},
"reachedOn": "Workspace page, Add widget, Image",
"provider": "launchpad",
@@ -1681,7 +1707,8 @@
"built": {
"state": "built",
"evidence": "lib/widget-types.json:39; package.json:38",
- "owner": "ConductionNL/nextcloud-vue"
+ "owner": "ConductionNL/nextcloud-vue",
+ "change": "openspec/changes/archive/2026-05-03-video-widget"
},
"reachedOn": "Workspace page, Add widget, Video",
"evidence": {
@@ -1710,7 +1737,8 @@
"built": {
"state": "built",
"evidence": "lib/Service/IframeService.php:9; src/constants/widgetRegistry.js:206",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "change": "openspec/changes/archive/2026-07-23-iframe-embed-widget"
},
"reachedOn": "Workspace page, Add widget, Embedded page; the allow-list is an app config value",
"evidence": {
@@ -1739,7 +1767,8 @@
"built": {
"state": "built",
"evidence": "lib/Service/NewsWidgetService.php:133; src/constants/widgetRegistry.js:244",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "change": "openspec/changes/archive/2026-05-03-news-widget"
},
"reachedOn": "Workspace page, Add widget, News",
"evidence": {
@@ -1768,7 +1797,8 @@
"built": {
"state": "built",
"evidence": "lib/Service/WeatherService.php:208; src/constants/widgetRegistry.js:171",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "change": "openspec/changes/archive/2026-07-23-clock-weather-widgets"
},
"reachedOn": "Workspace page, Add widget, Weather",
"evidence": {
@@ -1797,7 +1827,8 @@
"built": {
"state": "built",
"evidence": "src/constants/widgetRegistry.js:156",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "change": "openspec/changes/archive/2026-07-23-clock-weather-widgets"
},
"reachedOn": "Workspace page, Add widget, Clock",
"evidence": {
@@ -1826,7 +1857,8 @@
"built": {
"state": "built",
"evidence": "lib/Service/CalendarWidgetService.php:77; lib/widget-types.json:11",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "change": "openspec/changes/archive/2026-05-03-calendar-widget"
},
"reachedOn": "Workspace page, Add widget, Calendar",
"evidence": {
@@ -1855,7 +1887,8 @@
"built": {
"state": "built",
"evidence": "lib/Controller/FilesWidgetController.php:179; lib/widget-types.json:17",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "change": "openspec/changes/archive/2026-05-03-files-widget"
},
"reachedOn": "Workspace page, Add widget, Files",
"evidence": {
@@ -1884,7 +1917,8 @@
"built": {
"state": "built",
"evidence": "lib/Service/PeopleWidgetService.php:8; lib/widget-types.json:30",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "change": "openspec/changes/archive/2026-05-03-people-widget"
},
"reachedOn": "Workspace page, Add widget, People",
"evidence": {
@@ -1913,7 +1947,8 @@
"built": {
"state": "built",
"evidence": "lib/Service/PeopleWidgetService.php:410",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "spec": "openspec/specs/people-widget"
},
"reachedOn": "Workspace page, People widget",
"evidence": {
@@ -1942,7 +1977,12 @@
"built": {
"state": "built",
"evidence": "lib/widget-types.json:19; lib/widget-types.json:16; package.json:38",
- "owner": "ConductionNL/nextcloud-vue"
+ "owner": "ConductionNL/nextcloud-vue",
+ "change": [
+ "openspec/changes/archive/2026-05-03-header-widget",
+ "openspec/changes/archive/2026-06-14-label-widget",
+ "openspec/changes/archive/2026-05-03-divider-widget"
+ ]
},
"reachedOn": "Workspace page, Add widget, Header, Label or Divider",
"provider": "nextcloud-vue",
@@ -1971,7 +2011,8 @@
"built": {
"state": "built",
"evidence": "src/constants/widgetRegistry.js:14; lib/Controller/FileController.php:87",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "change": "openspec/changes/archive/2026-06-14-link-button-widget"
},
"reachedOn": "Workspace page, Add widget, Link button",
"provider": "launchpad",
@@ -2000,7 +2041,8 @@
"built": {
"state": "built",
"evidence": "lib/Service/LiveTileService.php:22; src/constants/widgetRegistry.js:184",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "change": "openspec/changes/archive/2026-07-23-live-data-tile-widget"
},
"reachedOn": "Workspace page, Add widget, Live tile; only hosts in livetile_allowed_hosts are fetched, set with occ",
"evidence": {
@@ -2029,7 +2071,8 @@
"built": {
"state": "built",
"evidence": "lib/widget-types.json:12; src/services/graphqlClient.js:16",
- "owner": "ConductionNL/openregister"
+ "owner": "ConductionNL/openregister",
+ "spec": "none"
},
"reachedOn": "Workspace page, Add widget, Chart, Stat or Table, only with OpenRegister installed",
"provider": "openregister",
@@ -2059,7 +2102,8 @@
"built": {
"state": "built",
"evidence": "lib/Service/IframeService.php:9",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "change": "openspec/changes/archive/2026-07-23-iframe-embed-widget"
},
"reachedOn": "Workspace page, Embedded page widget, when the report host is on the allow-list",
"evidence": {
@@ -2089,7 +2133,8 @@
"built": {
"state": "built",
"evidence": "src/services/spendAnalytics.js:10; lib/widget-types.json:33",
- "owner": "ConductionNL/openregister"
+ "owner": "ConductionNL/openregister",
+ "change": "openspec/changes/archive/2026-06-14-add-launchpad-discovered-and-spend-analytics"
},
"reachedOn": "Workspace page, Add widget, Spend analytics, only with those apps installed",
"provider": "openregister",
@@ -2204,7 +2249,8 @@
"built": {
"state": "built",
"evidence": "src/constants/widgetRegistry.js:141",
- "owner": "nextcloud/server"
+ "owner": "nextcloud/server",
+ "change": "openspec/changes/archive/2026-06-14-nc-dashboard-widget-proxy"
},
"reachedOn": "Workspace page, Add widget, Nextcloud widget, the Mail app's widget, with Nextcloud Mail installed",
"evidence": {
@@ -2233,7 +2279,8 @@
"built": {
"state": "built",
"evidence": "src/constants/widgetRegistry.js:141",
- "owner": "nextcloud/server"
+ "owner": "nextcloud/server",
+ "change": "openspec/changes/archive/2026-06-14-nc-dashboard-widget-proxy"
},
"reachedOn": "Workspace page, Add widget, Nextcloud widget, Deck's task widget, with Deck installed",
"evidence": {
@@ -2350,7 +2397,8 @@
"built": {
"state": "built",
"evidence": "lib/Service/RuleEvaluatorService.php:125; src/components/Widgets/WidgetContextMenu.vue:35",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "spec": "openspec/specs/conditional-visibility"
},
"reachedOn": "Workspace page, widget context menu, Visibility rules",
"evidence": {
@@ -2379,7 +2427,8 @@
"built": {
"state": "built",
"evidence": "lib/Service/RuleEvaluatorService.php:170; src/components/Widgets/VisibilityRuleRow.vue:56",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "spec": "openspec/specs/conditional-visibility"
},
"reachedOn": "Workspace page, Visibility rules, time rule",
"provider": "launchpad",
@@ -2408,7 +2457,8 @@
"built": {
"state": "built",
"evidence": "src/components/Widgets/VisibilityRuleRow.vue:111",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "spec": "openspec/specs/conditional-visibility"
},
"reachedOn": "Workspace page, Visibility rules, date rule",
"provider": "launchpad",
@@ -2437,7 +2487,8 @@
"built": {
"state": "built",
"evidence": "lib/Service/RuleEvaluatorService.php:238; lib/Service/UserAttributeResolver.php:36",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "spec": "openspec/specs/conditional-visibility"
},
"reachedOn": "Workspace page, Visibility rules, attribute rule",
"evidence": {
@@ -2466,7 +2517,8 @@
"built": {
"state": "built",
"evidence": "lib/Controller/VisibilityPreviewController.php:102; src/components/Widgets/ConditionalVisibilityEditor.vue:240",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "change": "openspec/changes/archive/2026-07-23-conditional-visibility-editor"
},
"reachedOn": "Workspace page, Visibility rules editor",
"provider": "launchpad",
@@ -2495,7 +2547,8 @@
"built": {
"state": "built",
"evidence": "src/components/admin/tabs/VersioningAuditTab.vue:8",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "change": "openspec/changes/archive/2026-06-14-refactor-launchpad-ia-alignment"
},
"reachedOn": "Admin settings, Versioning and audit tab",
"provider": "launchpad",
@@ -2524,7 +2577,8 @@
"built": {
"state": "built",
"evidence": "lib/Service/RoleFeaturePermissionService.php:612; src/components/admin/tabs/RolesPermissionsTab.vue:11",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "spec": "openspec/specs/role-feature-permissions"
},
"reachedOn": "Admin settings, Roles and permissions tab",
"provider": "launchpad",
@@ -2553,7 +2607,8 @@
"built": {
"state": "built",
"evidence": "lib/Service/RoleFeaturePermissionService.php:694; src/components/admin/tabs/RolesPermissionsTab.vue:13",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "spec": "openspec/specs/role-feature-permissions"
},
"reachedOn": "Admin settings, Roles and permissions tab, layout defaults",
"evidence": {
@@ -2582,7 +2637,8 @@
"built": {
"state": "built",
"evidence": "lib/Service/PermissionService.php:254; src/dialogs/RoleLayoutDefaultEditorDialog.vue:89",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "spec": "openspec/specs/permissions"
},
"reachedOn": "Admin settings, Roles and permissions, layout default editor",
"evidence": {
@@ -2666,7 +2722,8 @@
"built": {
"state": "built",
"evidence": "src/components/admin/tabs/SharingTab.vue:10; lib/Service/DashboardResolver.php:47",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "change": "openspec/changes/archive/2026-06-14-group-priority-order"
},
"reachedOn": "Admin settings, Sharing tab, group order",
"provider": "launchpad",
@@ -2695,7 +2752,8 @@
"built": {
"state": "built",
"evidence": "src/components/admin/tabs/RolesPermissionsTab.vue:15; lib/Service/ActionAuthService.php:95",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "spec": "none"
},
"reachedOn": "Admin settings, Roles and permissions tab, action matrix",
"provider": "launchpad",
@@ -2724,7 +2782,8 @@
"built": {
"state": "built",
"evidence": "lib/Service/AdminTemplateService.php:80; src/components/admin/AdminSettings.vue:289",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "spec": "openspec/specs/admin-templates"
},
"reachedOn": "Admin settings, Templates tab",
"evidence": {
@@ -2753,7 +2812,8 @@
"built": {
"state": "built",
"evidence": "lib/Service/PermissionService.php:245; src/components/admin/AdminSettings.vue:60",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "spec": "openspec/specs/permissions"
},
"reachedOn": "Admin settings, Default permission level; per template in the template editor",
"evidence": {
@@ -2782,7 +2842,8 @@
"built": {
"state": "built",
"evidence": "lib/Service/TemplateResyncService.php:247; src/components/admin/tabs/TemplatesPage.vue:70",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "change": "openspec/changes/archive/2026-07-23-admin-template-resync"
},
"reachedOn": "Admin settings, Templates tab, Resync",
"provider": "launchpad",
@@ -2811,7 +2872,8 @@
"built": {
"state": "built",
"evidence": "src/components/admin/AdminSettings.vue:245; lib/Controller/DashboardApiController.php:960",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "spec": "openspec/specs/dashboards"
},
"reachedOn": "Admin settings, Group dashboards tab; users see it in the Dashboards sidebar",
"evidence": {
@@ -2840,7 +2902,8 @@
"built": {
"state": "built",
"evidence": "src/components/admin/AdminSettings.vue:22; lib/Service/SetupWizardService.php:78",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "change": "openspec/changes/archive/2026-05-03-setup-wizard"
},
"reachedOn": "Admin settings, Run setup wizard banner",
"provider": "launchpad",
@@ -2869,7 +2932,8 @@
"built": {
"state": "built",
"evidence": "lib/Service/DemoShowcasesService.php:314; src/components/admin/tabs/DemoDataTab.vue:8",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "change": "openspec/changes/archive/2026-05-03-demo-data-showcases"
},
"reachedOn": "Admin settings, Demo data tab; also occ launchpad demo-showcases install",
"provider": "launchpad",
@@ -2898,7 +2962,8 @@
"built": {
"state": "built",
"evidence": "lib/Db/RoleAssignment.php:57; src/modals/SetupWizardModal.vue:131",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "spec": "openspec/specs/admin-roles"
},
"reachedOn": "Admin settings, setup wizard, roles step",
"evidence": {
@@ -2928,7 +2993,8 @@
"built": {
"state": "built",
"evidence": "lib/Service/BulkOperationService.php:169; src/components/admin/tabs/OperationsTab.vue:21",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "change": "openspec/changes/archive/2026-05-03-dashboard-bulk-operations"
},
"reachedOn": "Admin settings, Operations tab",
"provider": "launchpad",
@@ -2957,7 +3023,8 @@
"built": {
"state": "built",
"evidence": "lib/Service/ExportService.php:93; src/components/admin/tabs/OperationsTab.vue:18; lib/Command/ExportCommand.php:40",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "change": "openspec/changes/archive/2026-05-02-dashboard-export-import"
},
"reachedOn": "Admin settings, Operations tab; and occ launchpad export and import",
"evidence": {
@@ -3013,7 +3080,8 @@
"built": {
"state": "built",
"evidence": "lib/Service/ConfluenceImportService.php:94; src/components/admin/tabs/OperationsTab.vue:24",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "change": "openspec/changes/archive/2026-05-02-confluence-html-import"
},
"reachedOn": "Admin settings, Operations tab, Confluence import",
"provider": "launchpad",
@@ -3073,7 +3141,8 @@
"built": {
"state": "built",
"evidence": "lib/Service/QuotaService.php:144; src/components/admin/AdminSettings.vue:112",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "change": "openspec/changes/archive/2026-06-14-dashboard-quota-limits"
},
"reachedOn": "Admin settings, Default settings, quota fields",
"provider": "launchpad",
@@ -3102,7 +3171,8 @@
"built": {
"state": "built",
"evidence": "src/components/admin/AdminSettings.vue:74",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "change": "openspec/changes/archive/2026-06-14-allow-personal-dashboards-flag"
},
"reachedOn": "Admin settings, Default settings",
"provider": "launchpad",
@@ -3131,7 +3201,8 @@
"built": {
"state": "built",
"evidence": "lib/Service/DashboardShareService.php:105; src/modals/DashboardConfigModal.vue:141",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "spec": "openspec/specs/dashboard-sharing"
},
"reachedOn": "Workspace page, dashboard menu, Share",
"evidence": {
@@ -3160,7 +3231,8 @@
"built": {
"state": "built",
"evidence": "lib/Service/PublicShareService.php:121; src/modals/DashboardConfigModal.vue:320",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "change": "openspec/changes/archive/2026-06-14-dashboard-public-share"
},
"reachedOn": "Dashboard configuration, Sharing tab, Public link; opened at /apps/launchpad/s/{token}",
"provider": "launchpad",
@@ -3189,7 +3261,8 @@
"built": {
"state": "built",
"evidence": "src/modals/DashboardConfigModal.vue:306; src/modals/DashboardConfigModal.vue:312",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "change": "openspec/changes/archive/2026-06-14-dashboard-public-share"
},
"reachedOn": "Dashboard configuration, Sharing tab, Public link",
"provider": "launchpad",
@@ -3218,7 +3291,8 @@
"built": {
"state": "built",
"evidence": "lib/Controller/PublicShareController.php:37",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "change": "openspec/changes/archive/2026-06-14-dashboard-public-share"
},
"reachedOn": "/apps/launchpad/s/{token}/unlock",
"provider": "launchpad",
@@ -3247,7 +3321,8 @@
"built": {
"state": "built",
"evidence": "lib/Service/DashboardShareService.php:497; lib/Notification/Notifier.php:135",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "spec": "openspec/specs/dashboard-sharing"
},
"reachedOn": "Nextcloud notifications bell",
"provider": "launchpad",
@@ -3338,7 +3413,8 @@
"built": {
"state": "built",
"evidence": "src/components/admin/tabs/SharingTab.vue:8",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "spec": "openspec/specs/dashboard-sharing"
},
"reachedOn": "Admin settings, Sharing tab",
"provider": "launchpad",
@@ -3367,7 +3443,8 @@
"built": {
"state": "built",
"evidence": "lib/Listener/UserDeletedListener.php:8; lib/Service/DashboardShareService.php:342",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "spec": "openspec/specs/dashboard-sharing"
},
"reachedOn": "automatic when a Nextcloud user is deleted",
"provider": "launchpad",
@@ -3454,7 +3531,8 @@
"built": {
"state": "built",
"evidence": "lib/Service/AcknowledgementService.php:251; src/views/Views.vue:163",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "change": "openspec/changes/archive/2026-07-07-dashboard-acknowledgements"
},
"reachedOn": "Workspace page, Read receipts button",
"provider": "launchpad",
@@ -3713,7 +3791,8 @@
"built": {
"state": "built",
"evidence": "lib/Search/LaunchPadSearchProvider.php:173",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "change": "openspec/changes/archive/2026-05-03-nc-unified-search-integration"
},
"reachedOn": "Nextcloud unified search in the header",
"provider": "launchpad",
@@ -3919,7 +3998,8 @@
"built": {
"state": "built",
"evidence": "lib/Service/AnalyticsService.php:281; src/components/admin/tabs/VersioningAuditTab.vue:10",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "change": "openspec/changes/archive/2026-05-02-dashboard-view-analytics"
},
"reachedOn": "Admin settings, Versioning and audit tab",
"provider": "launchpad",
@@ -3948,7 +4028,8 @@
"built": {
"state": "built",
"evidence": "lib/Service/AnalyticsService.php:140; lib/BackgroundJob/PurgeViewsJob.php:50",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "change": "openspec/changes/archive/2026-05-02-dashboard-view-analytics"
},
"reachedOn": "the purge runs as a background job; the opt-out is a user preference no page sets, since /api/preferences/{key} has no caller in src/",
"provider": "launchpad",
@@ -3977,7 +4058,8 @@
"built": {
"state": "built",
"evidence": "src/components/admin/tabs/OperationsTab.vue:9; lib/Controller/HealthController.php:53",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "spec": "openspec/specs/prometheus-metrics"
},
"reachedOn": "Admin settings, Operations tab",
"provider": "launchpad",
@@ -4006,7 +4088,8 @@
"built": {
"state": "built",
"evidence": "lib/Controller/MetricsController.php:93; src/components/admin/tabs/OperationsTab.vue:12",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "change": "openspec/changes/archive/2026-03-21-prometheus-metrics"
},
"reachedOn": "/apps/launchpad/api/metrics; explained on Admin settings, Operations tab",
"provider": "launchpad",
@@ -4065,7 +4148,8 @@
"built": {
"state": "built",
"evidence": "lib/Activity/ActivityPublisher.php:82; lib/Service/TemplateResyncService.php:867; Corrections round 8 (2026-09-28), launchpad#713: only 2 of the 14 event types declared at lib/Activity/Extension.php:80-93 are ever sent, dashboard_updated for bulk operations and template re-sync and dashboard_acknowledged; lib/Service/BulkOperationService.php:949, lib/Service/TemplateResyncService.php:867 and lib/Service/AcknowledgementService.php:532 are the only callers of ActivityPublisher::publish() at e74f421, so creating, deleting, publishing, sharing, commenting on or restoring a dashboard leaves no entry.",
- "owner": "nextcloud/server"
+ "owner": "nextcloud/server",
+ "change": "openspec/changes/archive/2026-05-02-activity-feed-integration"
},
"reachedOn": "Nextcloud Activity app",
"provider": "nextcloud",
@@ -4095,7 +4179,8 @@
"built": {
"state": "built",
"evidence": "lib/BackgroundJob/OrphanedDataCleanupJob.php:111; lib/Listener/GroupDeletedListener.php:55",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "change": "openspec/changes/archive/2026-05-03-orphaned-data-cleanup"
},
"reachedOn": "background job and user or group deletion; occ launchpad cleanup scan and purge",
"provider": "launchpad",
@@ -4124,7 +4209,8 @@
"built": {
"state": "built",
"evidence": "lib/Command/DashboardListCommand.php:38; lib/Command/ExportCommand.php:40",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "change": "openspec/changes/archive/2026-05-03-cli-commands"
},
"reachedOn": "occ launchpad commands",
"provider": "launchpad",
@@ -4153,7 +4239,8 @@
"built": {
"state": "built",
"evidence": "src/views/WorkspaceApp.vue:100; lib/Service/FooterService.php:237",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "change": "openspec/changes/archive/2026-05-03-footer-customization"
},
"reachedOn": "the footer renders on the Workspace page; /api/admin/footer-settings has no caller in src/, so no screen edits it",
"provider": "launchpad",
@@ -4182,7 +4269,11 @@
"built": {
"state": "built",
"evidence": "lib/Settings/connections.json:43; lib/Service/IframeService.php:9",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "change": [
+ "openspec/changes/adopt-connection-registry",
+ "openspec/changes/archive/2026-07-23-iframe-embed-widget"
+ ]
},
"reachedOn": "occ config:app:set only; no admin screen edits the host lists",
"provider": "launchpad",
@@ -4211,7 +4302,8 @@
"built": {
"state": "built",
"evidence": "lib/Service/SvgSanitiser.php:166",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "change": "openspec/changes/archive/2026-06-14-svg-sanitisation"
},
"reachedOn": "every icon upload through /api/resources",
"provider": "launchpad",
@@ -4240,7 +4332,8 @@
"built": {
"state": "built",
"evidence": "lib/Controller/DashboardApiController.php:158",
- "owner": "nextcloud/server"
+ "owner": "nextcloud/server",
+ "spec": "none"
},
"reachedOn": "Nextcloud login; every LaunchPad route runs behind the Nextcloud session",
"evidence": {
@@ -4297,7 +4390,8 @@
"built": {
"state": "built",
"evidence": "appinfo/info.xml:56",
- "owner": "nextcloud/server"
+ "owner": "nextcloud/server",
+ "spec": "none"
},
"reachedOn": "installed as a Nextcloud app on your own instance",
"evidence": {
@@ -4324,7 +4418,8 @@
"built": {
"state": "built",
"evidence": "src/views/WorkspaceApp.vue:552",
- "owner": "nextcloud/server"
+ "owner": "nextcloud/server",
+ "spec": "none"
},
"reachedOn": "every page; colours come from Nextcloud theming or the NL Design System app",
"evidence": {
@@ -4406,7 +4501,8 @@
"built": {
"state": "built",
"evidence": "l10n/nl.json:2",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "spec": "none"
},
"reachedOn": "every page, following the Nextcloud language setting",
"evidence": {
@@ -4435,7 +4531,8 @@
"built": {
"state": "built",
"evidence": "appinfo/info.xml:75",
- "owner": "ConductionNL/launchpad"
+ "owner": "ConductionNL/launchpad",
+ "spec": "none"
},
"reachedOn": "the app installs and boots with no OpenRegister; OpenRegister-backed widgets are feature-detected",
"provider": "launchpad",
@@ -4491,7 +4588,8 @@
"built": {
"state": "built",
"owner": "ConductionNL/launchpad",
- "evidence": "src/modals/WidgetPickerModal.vue:36; lib/Migration/PlacementTableBuilder.php:239"
+ "evidence": "src/modals/WidgetPickerModal.vue:36; lib/Migration/PlacementTableBuilder.php:239",
+ "spec": "openspec/specs/widgets"
},
"reachedOn": "Workspace page, Edit dashboard, Add widget: a widget already placed is only marked as placed and can be added again, each placement with its own settings",
"evidence": {
@@ -4614,7 +4712,8 @@
"built": {
"state": "built",
"owner": "ConductionNL/launchpad",
- "evidence": "src/components/WidgetRenderer.vue:442"
+ "evidence": "src/components/WidgetRenderer.vue:442",
+ "spec": "openspec/specs/widgets"
},
"reachedOn": "Workspace page, any Nextcloud widget that declares a reload interval, and live value tiles",
"evidence": {
@@ -4645,7 +4744,8 @@
"built": {
"state": "built",
"owner": "nextcloud/server",
- "evidence": "lib/Service/RuleEvaluatorService.php:125"
+ "evidence": "lib/Service/RuleEvaluatorService.php:125",
+ "spec": "none"
},
"reachedOn": "every targeting rule and group dashboard; group membership itself comes from the Nextcloud login app",
"evidence": {
@@ -4676,7 +4776,8 @@
"built": {
"state": "built",
"owner": "ConductionNL/nextcloud-vue",
- "evidence": "src/manifest.json:475"
+ "evidence": "src/manifest.json:475",
+ "spec": "none"
},
"reachedOn": "Workspace page on first visit, 'Getting started' tour, and again from the help menu",
"evidence": {
@@ -4707,7 +4808,8 @@
"built": {
"state": "built",
"owner": "nextcloud/server",
- "evidence": "src/views/WorkspaceApp.vue:480"
+ "evidence": "src/views/WorkspaceApp.vue:480",
+ "spec": "none"
},
"reachedOn": "any LaunchPad page, through the browser's install option",
"evidence": {
@@ -4770,7 +4872,8 @@
"built": {
"state": "built",
"owner": "ConductionNL/launchpad",
- "evidence": "lib/Service/LiveTileService.php:455"
+ "evidence": "lib/Service/LiveTileService.php:455",
+ "spec": "openspec/specs/background-job-feed-refresh"
},
"reachedOn": "every widget that fetches from outside: news, calendar feeds, live tiles, weather, health pings, iframe checks",
"evidence": {
@@ -5020,7 +5123,8 @@
"built": {
"state": "built",
"owner": "nextcloud/server",
- "evidence": "src/views/WorkspaceApp.vue:480"
+ "evidence": "src/views/WorkspaceApp.vue:480",
+ "spec": "none"
},
"reachedOn": "every page",
"evidence": {
From 371eb268172a64f0d1b7b442d6429fb96fdf55c1 Mon Sep 17 00:00:00 2001
From: Ruben van der Linde
Date: Wed, 7 Oct 2026 21:10:31 +0200
Subject: [PATCH 22/60] docs(parity): add rows for four built capabilities that
had a spec but no row
---
openspec/parity/capabilities.json | 81 ++++++++++++++++++++++++++++++-
1 file changed, 80 insertions(+), 1 deletion(-)
diff --git a/openspec/parity/capabilities.json b/openspec/parity/capabilities.json
index 6ccc632ab..3feeb6e31 100644
--- a/openspec/parity/capabilities.json
+++ b/openspec/parity/capabilities.json
@@ -1030,7 +1030,10 @@
"state": "built",
"evidence": "src/modals/DashboardConfigModal.vue:75; src/modals/DashboardConfigModal.vue:87",
"owner": "ConductionNL/launchpad",
- "spec": "openspec/specs/dashboards"
+ "spec": [
+ "openspec/specs/dashboards",
+ "openspec/specs/dashboard-icons"
+ ]
},
"reachedOn": "Workspace page, dashboard menu, Dashboard configuration, General tab",
"provider": "launchpad",
@@ -1256,6 +1259,25 @@
"microsoft-viva": "not checked: create-dashboard for custom dashboard properties"
}
},
+ {
+ "id": "dash-starter-widgets",
+ "area": "dashboards",
+ "name": "A new personal dashboard starts with a useful set of widgets instead of an empty grid.",
+ "launchpad": "yes",
+ "workspace-365": "unchecked",
+ "microsoft-viva": "unchecked",
+ "nextcloud-dashboard": "unchecked",
+ "homarr": "unchecked",
+ "dashy": "unchecked",
+ "built": {
+ "state": "built",
+ "evidence": "lib/Service/DashboardService.php:3085",
+ "owner": "ConductionNL/launchpad",
+ "spec": "openspec/specs/default-widget-bundle"
+ },
+ "reachedOn": "Workspace page, after creating a new personal dashboard",
+ "source": "spec-round-2026-10-07"
+ },
{
"id": "tile-pin",
"area": "launcher",
@@ -5418,6 +5440,63 @@
"originUrl": "https://www.tenderned.nl/aankondigingen/overzicht/296963",
"note": "LaunchPad has no work queue of its own; it can show a case app's Nextcloud widget. dossiq registers 'My tasks' and 'Cases overview' widgets (ConductionNL/dossiq development 00869dd7 lib/AppInfo/Registrar/AppHostRegistrar.php:126-132, lib/Dashboard/MyTasksWidget.php:42 a legacy IWidget) and LaunchPad's bridge catches OCA.Dashboard.register for such widgets (widgetBridge.js:29-62). Whether they render end to end is pending q-sibling-widgets. Also mined from https://www.tenderned.nl/aankondigingen/overzicht/237144, https://www.tenderned.nl/aankondigingen/overzicht/416075. Tender text: Noorderzijlvest 2023: 'Startscherm/medewerkersportaal met doorklik naar werkvoorraad'; Nationale ombudsman 2021: 'persoonlijk dashboard ... Overzicht lopende (eigen) zaken'; Súdwest-Fryslân 2026: dashboard with 'alle openstaande signaleringen, actiepunten, issues' at a glance, set up by the user with widgets. Specified 2026-09-27 (openspec pass, coordinator) in ConductionNL/dossiq openspec/changes/widgets-my-open-cases-on-the-start-page."
},
+ {
+ "id": "w-attention",
+ "area": "widgets",
+ "name": "See what needs your attention today from all your apps in one ranked list.",
+ "launchpad": "yes",
+ "workspace-365": "unchecked",
+ "microsoft-viva": "unchecked",
+ "nextcloud-dashboard": "unchecked",
+ "homarr": "unchecked",
+ "dashy": "unchecked",
+ "built": {
+ "state": "built",
+ "evidence": "lib/Controller/AttentionController.php:39; src/constants/widgetRegistry.js:176",
+ "owner": "ConductionNL/launchpad",
+ "spec": "openspec/specs/attention-feed"
+ },
+ "reachedOn": "Workspace page, Add widget, First today",
+ "source": "spec-round-2026-10-07"
+ },
+ {
+ "id": "w-menu",
+ "area": "widgets",
+ "name": "Put a navigation menu with nested links on a dashboard.",
+ "launchpad": "yes",
+ "workspace-365": "unchecked",
+ "microsoft-viva": "unchecked",
+ "nextcloud-dashboard": "unchecked",
+ "homarr": "unchecked",
+ "dashy": "unchecked",
+ "built": {
+ "state": "built",
+ "evidence": "lib/widget-types.json:28; src/components/WidgetRenderer.vue:10",
+ "owner": "ConductionNL/nextcloud-vue",
+ "change": "openspec/changes/archive/2026-05-03-menu-widget"
+ },
+ "reachedOn": "Workspace page, Add widget, Menu",
+ "source": "spec-round-2026-10-07"
+ },
+ {
+ "id": "w-quicklinks",
+ "area": "widgets",
+ "name": "Show a compact grid of icon shortcuts in one widget.",
+ "launchpad": "yes",
+ "workspace-365": "unchecked",
+ "microsoft-viva": "unchecked",
+ "nextcloud-dashboard": "unchecked",
+ "homarr": "unchecked",
+ "dashy": "unchecked",
+ "built": {
+ "state": "built",
+ "evidence": "lib/widget-types.json:33; src/components/WidgetRenderer.vue:9",
+ "owner": "ConductionNL/nextcloud-vue",
+ "change": "openspec/changes/archive/2026-05-03-quicklinks-widget"
+ },
+ "reachedOn": "Workspace page, Add widget, Quick links",
+ "source": "spec-round-2026-10-07"
+ },
{
"id": "d-public-start",
"area": "sharing",
From 7317bb28e2d0f0fc2b3e00b86b65b4e8d322e7fe Mon Sep 17 00:00:00 2001
From: Ruben van der Linde
Date: Wed, 7 Oct 2026 21:17:25 +0200
Subject: [PATCH 23/60] docs(openspec): competitor cells on spec-round rows
read unknown, not unchecked
---
openspec/parity/capabilities.json | 40 +++++++++++++++----------------
1 file changed, 20 insertions(+), 20 deletions(-)
diff --git a/openspec/parity/capabilities.json b/openspec/parity/capabilities.json
index 3feeb6e31..7bf7316d4 100644
--- a/openspec/parity/capabilities.json
+++ b/openspec/parity/capabilities.json
@@ -1264,11 +1264,11 @@
"area": "dashboards",
"name": "A new personal dashboard starts with a useful set of widgets instead of an empty grid.",
"launchpad": "yes",
- "workspace-365": "unchecked",
- "microsoft-viva": "unchecked",
- "nextcloud-dashboard": "unchecked",
- "homarr": "unchecked",
- "dashy": "unchecked",
+ "workspace-365": "unknown",
+ "microsoft-viva": "unknown",
+ "nextcloud-dashboard": "unknown",
+ "homarr": "unknown",
+ "dashy": "unknown",
"built": {
"state": "built",
"evidence": "lib/Service/DashboardService.php:3085",
@@ -5445,11 +5445,11 @@
"area": "widgets",
"name": "See what needs your attention today from all your apps in one ranked list.",
"launchpad": "yes",
- "workspace-365": "unchecked",
- "microsoft-viva": "unchecked",
- "nextcloud-dashboard": "unchecked",
- "homarr": "unchecked",
- "dashy": "unchecked",
+ "workspace-365": "unknown",
+ "microsoft-viva": "unknown",
+ "nextcloud-dashboard": "unknown",
+ "homarr": "unknown",
+ "dashy": "unknown",
"built": {
"state": "built",
"evidence": "lib/Controller/AttentionController.php:39; src/constants/widgetRegistry.js:176",
@@ -5464,11 +5464,11 @@
"area": "widgets",
"name": "Put a navigation menu with nested links on a dashboard.",
"launchpad": "yes",
- "workspace-365": "unchecked",
- "microsoft-viva": "unchecked",
- "nextcloud-dashboard": "unchecked",
- "homarr": "unchecked",
- "dashy": "unchecked",
+ "workspace-365": "unknown",
+ "microsoft-viva": "unknown",
+ "nextcloud-dashboard": "unknown",
+ "homarr": "unknown",
+ "dashy": "unknown",
"built": {
"state": "built",
"evidence": "lib/widget-types.json:28; src/components/WidgetRenderer.vue:10",
@@ -5483,11 +5483,11 @@
"area": "widgets",
"name": "Show a compact grid of icon shortcuts in one widget.",
"launchpad": "yes",
- "workspace-365": "unchecked",
- "microsoft-viva": "unchecked",
- "nextcloud-dashboard": "unchecked",
- "homarr": "unchecked",
- "dashy": "unchecked",
+ "workspace-365": "unknown",
+ "microsoft-viva": "unknown",
+ "nextcloud-dashboard": "unknown",
+ "homarr": "unknown",
+ "dashy": "unknown",
"built": {
"state": "built",
"evidence": "lib/widget-types.json:33; src/components/WidgetRenderer.vue:9",
From b5c34f938d4bced04c59da3c881e0bef5dd26a6a Mon Sep 17 00:00:00 2001
From: Ruben van der Linde
Date: Wed, 7 Oct 2026 22:15:27 +0200
Subject: [PATCH 24/60] docs(parity): re-rate build-keyboard from code, the
move panel change is delivered
---
openspec/parity/capabilities.json | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/openspec/parity/capabilities.json b/openspec/parity/capabilities.json
index 7bf7316d4..802b8e78a 100644
--- a/openspec/parity/capabilities.json
+++ b/openspec/parity/capabilities.json
@@ -604,9 +604,10 @@
"dashy": "no",
"built": {
"state": "built",
- "evidence": "src/modals/WidgetMovePanel.vue:27; src/components/Widgets/WidgetContextMenu.vue:27",
+ "evidence": "nextcloud-vue CnDashboardGrid.vue:35-47 (main 0f1c0ffbb) makes every grid item in edit mode a focusable named group (role group, aria-label, tabindex 0 while keyboardRepositioning, default true) and turns Enter or Space into the contextmenu event a right-click sends; src/views/Views.vue:290 catches it on the placement and opens WidgetContextMenu; src/components/Widgets/WidgetContextMenu.vue:22-27 offers Move, which src/views/Views.vue:420 and :444 route to src/modals/WidgetMovePanel.vue; WidgetMovePanel.vue:286-310 moves with the arrow keys, resizes with Shift and the arrow keys, saves on Enter and cancels on Escape, through nudgePlacement (src/composables/useGridManager.js:248), the collision model the drag path uses; Views.vue:1964 handleMoveSave persists it. Tests: src/modals/__tests__/WidgetMovePanel.spec.js, tests/e2e/keyboard-widget-repositioning.spec.ts (drives the page with the keyboard only, checks the position after reload)",
"owner": "ConductionNL/launchpad",
- "change": "openspec/changes/keyboard-accessible-widget-repositioning"
+ "change": "openspec/changes/keyboard-accessible-widget-repositioning",
+ "readOn": "2026-10-07"
},
"reachedOn": "Workspace page, widget context menu, Move",
"provider": "launchpad",
@@ -620,7 +621,8 @@
"dashy": "source read at 4.7.0: searched src for keydown, arrow, move with keyboard: src/utils/ArrowKeyNavigation.js:1-6 only moves focus between tiles in search results; reordering is pointer drag only (src/directives/DragSort.js:24-35 forceFallback). Items can be copied or moved to another section through the context menu (src/assets/locales/en.json:320 'Copy or Move'), widgets and sections cannot",
"workspace-365": "not checked: W365 user guide and FAQ for keyboard move or resize of tiles, not found",
"microsoft-viva": "not checked: support.microsoft.com c4f573dd screen reader guide covers touch gestures to reorder cards only; no keyboard move or resize found"
- }
+ },
+ "note": "Re-rated 2026-10-07 from code: built, yes, unchanged. The change keyboard-accessible-widget-repositioning still shows its 11 tasks unticked, but every one is delivered: tasks 1-3 (focusable, named grid item; Enter opens the menu) by nextcloud-vue CnDashboardGrid instead of the retired src/components/DashboardGrid.vue the tasks name, tasks 4-9 in launchpad, tasks 10-11 in the Playwright spec. The change is ready to tick and archive. Not checked live in a browser in this round."
},
{
"id": "build-columns",
From 6e62eed83f141b140430ccdc07993ea6df10821f Mon Sep 17 00:00:00 2001
From: Ruben van der Linde
Date: Wed, 7 Oct 2026 22:16:55 +0200
Subject: [PATCH 25/60] docs(openspec): specs for the action matrix and the
first-visit tour, link standalone install
---
openspec/parity/capabilities.json | 11 +-
openspec/specs/action-authorization/spec.md | 105 ++++++++++++++++++++
openspec/specs/runtime-shell/spec.md | 23 +++++
3 files changed, 134 insertions(+), 5 deletions(-)
create mode 100644 openspec/specs/action-authorization/spec.md
diff --git a/openspec/parity/capabilities.json b/openspec/parity/capabilities.json
index 802b8e78a..5aeace919 100644
--- a/openspec/parity/capabilities.json
+++ b/openspec/parity/capabilities.json
@@ -2094,9 +2094,10 @@
"dashy": "partial",
"built": {
"state": "built",
- "evidence": "lib/widget-types.json:12; src/services/graphqlClient.js:16",
+ "evidence": "src/constants/widgetRegistry.js:116-120 routes the chart and stats-block types to src/components/Widgets/Renderers/ChartHost.vue and StatsBlockHost.vue, thin adapters onto nextcloud-vue's CnChartWidget and CnStatsBlockWidget, which fetch their figures from an OpenRegister dataSource; stat, gauge, delta and table are nextcloud-vue registry types LaunchPad passes through unchanged (lib/widget-types.json:12-38). Renderers, configuration forms and data are nextcloud-vue's and OpenRegister's (nextcloud-vue src/components/CnWidgetGrid/registerDashboardWidgets.js:81-127, main 0f1c0ffbb); src/services/graphqlClient.js, cited before, serves the spend-analytics widget, not these",
"owner": "ConductionNL/openregister",
- "spec": "none"
+ "spec": "none",
+ "readOn": "2026-10-07"
},
"reachedOn": "Workspace page, Add widget, Chart, Stat or Table, only with OpenRegister installed",
"provider": "openregister",
@@ -2777,7 +2778,7 @@
"state": "built",
"evidence": "src/components/admin/tabs/RolesPermissionsTab.vue:15; lib/Service/ActionAuthService.php:95",
"owner": "ConductionNL/launchpad",
- "spec": "none"
+ "spec": "openspec/specs/action-authorization"
},
"reachedOn": "Admin settings, Roles and permissions tab, action matrix",
"provider": "launchpad",
@@ -4556,7 +4557,7 @@
"state": "built",
"evidence": "appinfo/info.xml:75",
"owner": "ConductionNL/launchpad",
- "spec": "none"
+ "spec": "openspec/specs/runtime-or-consumption"
},
"reachedOn": "the app installs and boots with no OpenRegister; OpenRegister-backed widgets are feature-detected",
"provider": "launchpad",
@@ -4801,7 +4802,7 @@
"state": "built",
"owner": "ConductionNL/nextcloud-vue",
"evidence": "src/manifest.json:475",
- "spec": "none"
+ "spec": "openspec/specs/runtime-shell"
},
"reachedOn": "Workspace page on first visit, 'Getting started' tour, and again from the help menu",
"evidence": {
diff --git a/openspec/specs/action-authorization/spec.md b/openspec/specs/action-authorization/spec.md
new file mode 100644
index 000000000..fe9dba76b
--- /dev/null
+++ b/openspec/specs/action-authorization/spec.md
@@ -0,0 +1,105 @@
+---
+capability: action-authorization
+status: done
+cross-links:
+ - openspec/architecture/adr-023-action-authorization.md
+ - openspec/specs/admin-roles/spec.md
+ - openspec/specs/permissions/spec.md
+---
+
+# Action Authorization Specification
+
+## Purpose
+
+An administrator decides in one grid which Nextcloud groups may take each LaunchPad action: list dashboards, publish one, react, see analytics, edit the navigation, and so on. Every controller method names its action and asks one service whether the caller may take it. This is action RBAC as ADR-023 defines it. Which objects a user may touch stays with PermissionService and the admin roles spec; this spec only answers "may this user call this endpoint at all".
+
+Written on 2026-10-07 after the fact, from the code on `development`. It describes what ships today and covers parity row `t-action-matrix`.
+
+Code:
+- `lib/Service/ActionAuthService.php`: `requireAction()` (line 117), `getAllowedGroups()` (171), `getMatrix()` (187), `setMatrix()` (234).
+- `lib/Controller/ActionMatrixController.php`: `getMatrix()` (73) and `setMatrix()` (111), routed at `GET` and `PUT /api/admin/action-matrix` (`appinfo/routes.php:440-441`).
+- `lib/actions.seed.json`: 82 actions and their default groups.
+- `lib/Repair/InitializeActions.php` (run, line 71) and `lib/Repair/ApplyActionBaseline.php` (run, line 127), both registered as install and upgrade steps in `appinfo/info.xml`.
+- `src/components/admin/ActionAuthMatrix.vue`, mounted by `src/components/admin/tabs/RolesPermissionsTab.vue:15` in the Roles and permissions tab of the admin settings (`src/components/admin/AdminSettings.vue:246`).
+
+## Requirements
+
+### Requirement: Every action endpoint asks the action service (REQ-ACT-001)
+
+A controller method that performs a LaunchPad action MUST call `ActionAuthService::requireAction()` with the caller and a dot-separated action name (for example `dashboard-reaction.add-reaction`) before it does any work. The service SHALL refuse with an `OCSForbiddenException` when the caller may not take the action. Editing the matrix itself is not an action: it is guarded by `#[AuthorizedAdminSetting(LaunchPadAdmin::class)]` at the route.
+
+@e2e exclude Backend authorization; asserted per controller in tests/Unit/Controller (for example DashboardReactionApiControllerTest.php). No page to drive.
+
+#### Scenario: A member outside the allowed groups is refused
+- GIVEN the matrix entry for `analytics.top-dashboards` is `["admin"]`
+- AND Pieter is not an administrator
+- WHEN Pieter's session calls the endpoint behind that action
+- THEN the service MUST throw `OCSForbiddenException` with the message "Action 'analytics.top-dashboards' requires admin rights"
+- AND the controller MUST NOT run the action
+
+#### Scenario: A member of an allowed group passes
+- GIVEN the matrix entry for `dashboard.publish` is `["admin", "redactie"]`
+- AND Pieter is a member of the group `redactie`
+- WHEN Pieter's session calls the endpoint behind that action
+- THEN `requireAction()` MUST return without throwing
+
+### Requirement: How an action resolves (REQ-ACT-002)
+
+The service SHALL resolve an action in this order. An administrator always passes. An entry holding the sentinel `@all` passes every signed-in user. An entry that is empty or holds only `admin` refuses everyone else. Otherwise the caller passes when one of their groups, read through `AdminTemplateService::getUserGroupIdsFor()`, is in the entry. An action missing from the matrix MUST resolve to `["admin"]`, so a new action is closed until an administrator opens it.
+
+@e2e exclude Pure service logic; covered by tests/Unit/Service/ActionAuthBaselineTest.php.
+
+#### Scenario: The all-users sentinel wins over an admin-only reading
+- GIVEN the entry for `dashboard.list` is `["admin", "@all"]`
+- WHEN any signed-in user calls an endpoint behind `dashboard.list`
+- THEN the call MUST pass
+
+#### Scenario: An unknown action is admin-only
+- GIVEN the matrix has no entry for `example.new-action`
+- WHEN a user who is not an administrator calls the endpoint behind it
+- THEN the service MUST refuse the call
+
+#### Scenario: An unreadable matrix denies by default
+- GIVEN the stored `actions` app config value is not valid JSON
+- WHEN the service reads the matrix
+- THEN it MUST treat the matrix as empty
+- AND every action MUST resolve to `["admin"]`
+
+### Requirement: The administrator edits the matrix in one grid (REQ-ACT-003)
+
+The Roles and permissions tab of the LaunchPad admin settings MUST show an "Action authorization" table with one row per action and one column per group: first `admin`, then "All logged-in users" for `@all`, then every Nextcloud group. The action list SHALL be the union of the stored matrix and the seed file, so an action no one has configured yet still has a row. Each cell is a checkbox. The `admin` column MUST be ticked and disabled. "Save action matrix" MUST send the whole matrix with `PUT /api/admin/action-matrix`, always keeping `admin` in each entry, and confirm with "Action matrix saved." or report "Failed to save the action matrix.".
+
+@e2e exclude Written after the fact; no Playwright spec drives this table yet. Adding one belongs to a build change.
+
+#### Scenario: An administrator opens the publish action to a group
+- GIVEN Fatima is an administrator on the Roles and permissions tab
+- WHEN she ticks the `redactie` column on the `dashboard.publish` row and presses "Save action matrix"
+- THEN the request MUST carry `dashboard.publish: ["admin", "redactie"]`
+- AND the table MUST redraw from the matrix the server returns
+- AND the message "Action matrix saved." MUST appear
+
+#### Scenario: The admin column cannot be unticked
+- GIVEN the action table is shown
+- THEN every cell in the `admin` column MUST be ticked and disabled
+
+#### Scenario: Only administrators reach the matrix endpoints
+- GIVEN Pieter is not a LaunchPad administrator
+- WHEN his session calls `GET` or `PUT /api/admin/action-matrix`
+- THEN Nextcloud MUST refuse the request before the controller runs
+
+### Requirement: A fresh install is usable and an upgrade keeps the administrator's choices (REQ-ACT-004)
+
+On install, when the stored matrix is empty, the `InitializeActions` repair step MUST write the actions from `lib/actions.seed.json`, which grants the ordinary end-user actions to `@all` and keeps administrative actions at `["admin"]`. When the matrix already has entries it MUST leave them alone. The `ApplyActionBaseline` step SHALL, once per baseline version, widen only entries that still hold the untouched `["admin"]` default or are missing, and MUST leave every entry an administrator changed.
+
+@e2e exclude Install and upgrade repair steps; covered by tests/Unit/Repair/ApplyActionBaselineTest.php and tests/Unit/Service/ActionSeedCoverageTest.php.
+
+#### Scenario: Fresh install
+- GIVEN LaunchPad is installed on an instance with no stored action matrix
+- WHEN the install repair steps run
+- THEN the matrix MUST equal the seed file's actions
+- AND a non-admin user MUST be able to list their dashboards
+
+#### Scenario: An administrator's narrowing survives the upgrade
+- GIVEN an administrator set `dashboard.list` to `["admin", "medewerkers"]`
+- WHEN a later version runs `ApplyActionBaseline`
+- THEN the entry for `dashboard.list` MUST still be `["admin", "medewerkers"]`
diff --git a/openspec/specs/runtime-shell/spec.md b/openspec/specs/runtime-shell/spec.md
index 244bfbf53..53f0c44d8 100644
--- a/openspec/specs/runtime-shell/spec.md
+++ b/openspec/specs/runtime-shell/spec.md
@@ -234,3 +234,26 @@ When the option is off, or the initial state does not carry the key, nothing cha
- THEN every entry the panel would have shown MUST have a destination in the menu, and every router destination MUST be a page the app declares
@e2e openspec/specs/runtime-shell/spec.md#req-shell-009
+
+### Requirement: REQ-SHELL-010 First-visit tour
+
+LaunchPad MUST declare one guided tour in `src/manifest.json` (`walkthrough`, lines 470-525), and the shell MUST hand that manifest to the shared library's `CnAppRoot` (`src/App.vue:16-17`), which draws the tour with `CnWalkthrough`. The tour SHALL be `launchpad:getting-started`, titled "Getting started", triggered on a user's first visit, with three steps in this order: "Welcome to Launchpad" centred on the Workspace page, "Open Dashboards from the menu" pointing at the `dashboards` menu item, and a step pointing at the Flows menu item that the user may skip. Completion MUST be stored per user under `walkthrough_completed_version` against the declared tour `version` (1), so raising the version shows the tour again. Drawing, dimming, skipping and reopening the tour belong to the library; this requirement covers only what LaunchPad declares.
+
+Written on 2026-10-07 after the fact, from the code on `development`; covers parity row `d-tour`. The tour does not visit the admin settings.
+
+@e2e exclude The tour is drawn by nextcloud-vue's CnWalkthrough from a manifest block; no LaunchPad Playwright spec drives it. Written after the fact.
+
+#### Scenario: A first visit starts the tour
+- GIVEN Pieter has never completed the LaunchPad tour
+- WHEN he opens LaunchPad
+- THEN the "Welcome to Launchpad" step MUST open in the centre of the Workspace page
+
+#### Scenario: Opening Dashboards advances the tour
+- GIVEN the tour shows the step "Open Dashboards from the menu"
+- WHEN Pieter clicks Dashboards in the menu and the Workspace route matches
+- THEN the tour MUST move on to the Flows step
+
+#### Scenario: A finished tour stays closed
+- GIVEN Pieter completed tour version 1
+- WHEN he opens LaunchPad again
+- THEN the tour MUST NOT start
From 208bd9c3724f912a3ab21d48b228675382af49e1 Mon Sep 17 00:00:00 2001
From: Ruben van der Linde
Date: Wed, 7 Oct 2026 23:29:42 +0200
Subject: [PATCH 26/60] docs(openspec): archive pass, changes the code has
delivered
Archives beta-surface-alignment, store-plane-dashboard-sharing,
wizard-dataset-card-load and keyboard-accessible-widget-repositioning (ticked
from code: the grid half landed in nextcloud-vue CnDashboardGrid).
---
.../.openspec.yaml | 0
.../proposal.md | 0
.../specs/beta-alignment/spec.md | 0
.../tasks.md | 0
.../.openspec.yaml | 0
.../proposal.md | 0
.../specs/grid-layout/spec.md | 0
.../tasks.md | 33 ++-
.../design.md | 0
.../proposal.md | 0
.../specs/dashboard-store/spec.md | 2 +-
.../tasks.md | 0
.../proposal.md | 0
.../specs/first-time-setup/spec.md | 0
.../tasks.md | 0
openspec/parity/capabilities.json | 2 +-
openspec/specs/beta-alignment/spec.md | 47 +++
openspec/specs/dashboard-store/spec.md | 274 ++++++++++++++++++
openspec/specs/first-time-setup/spec.md | 54 ++++
openspec/specs/grid-layout/spec.md | 70 +++++
20 files changed, 469 insertions(+), 13 deletions(-)
rename openspec/changes/{beta-surface-alignment => archive/2026-10-07-beta-surface-alignment}/.openspec.yaml (100%)
rename openspec/changes/{beta-surface-alignment => archive/2026-10-07-beta-surface-alignment}/proposal.md (100%)
rename openspec/changes/{beta-surface-alignment => archive/2026-10-07-beta-surface-alignment}/specs/beta-alignment/spec.md (100%)
rename openspec/changes/{beta-surface-alignment => archive/2026-10-07-beta-surface-alignment}/tasks.md (100%)
rename openspec/changes/{keyboard-accessible-widget-repositioning => archive/2026-10-07-keyboard-accessible-widget-repositioning}/.openspec.yaml (100%)
rename openspec/changes/{keyboard-accessible-widget-repositioning => archive/2026-10-07-keyboard-accessible-widget-repositioning}/proposal.md (100%)
rename openspec/changes/{keyboard-accessible-widget-repositioning => archive/2026-10-07-keyboard-accessible-widget-repositioning}/specs/grid-layout/spec.md (100%)
rename openspec/changes/{keyboard-accessible-widget-repositioning => archive/2026-10-07-keyboard-accessible-widget-repositioning}/tasks.md (59%)
rename openspec/changes/{store-plane-dashboard-sharing => archive/2026-10-07-store-plane-dashboard-sharing}/design.md (100%)
rename openspec/changes/{store-plane-dashboard-sharing => archive/2026-10-07-store-plane-dashboard-sharing}/proposal.md (100%)
rename openspec/changes/{store-plane-dashboard-sharing => archive/2026-10-07-store-plane-dashboard-sharing}/specs/dashboard-store/spec.md (99%)
rename openspec/changes/{store-plane-dashboard-sharing => archive/2026-10-07-store-plane-dashboard-sharing}/tasks.md (100%)
rename openspec/changes/{wizard-dataset-card-load => archive/2026-10-07-wizard-dataset-card-load}/proposal.md (100%)
rename openspec/changes/{wizard-dataset-card-load => archive/2026-10-07-wizard-dataset-card-load}/specs/first-time-setup/spec.md (100%)
rename openspec/changes/{wizard-dataset-card-load => archive/2026-10-07-wizard-dataset-card-load}/tasks.md (100%)
create mode 100644 openspec/specs/beta-alignment/spec.md
create mode 100644 openspec/specs/dashboard-store/spec.md
create mode 100644 openspec/specs/first-time-setup/spec.md
diff --git a/openspec/changes/beta-surface-alignment/.openspec.yaml b/openspec/changes/archive/2026-10-07-beta-surface-alignment/.openspec.yaml
similarity index 100%
rename from openspec/changes/beta-surface-alignment/.openspec.yaml
rename to openspec/changes/archive/2026-10-07-beta-surface-alignment/.openspec.yaml
diff --git a/openspec/changes/beta-surface-alignment/proposal.md b/openspec/changes/archive/2026-10-07-beta-surface-alignment/proposal.md
similarity index 100%
rename from openspec/changes/beta-surface-alignment/proposal.md
rename to openspec/changes/archive/2026-10-07-beta-surface-alignment/proposal.md
diff --git a/openspec/changes/beta-surface-alignment/specs/beta-alignment/spec.md b/openspec/changes/archive/2026-10-07-beta-surface-alignment/specs/beta-alignment/spec.md
similarity index 100%
rename from openspec/changes/beta-surface-alignment/specs/beta-alignment/spec.md
rename to openspec/changes/archive/2026-10-07-beta-surface-alignment/specs/beta-alignment/spec.md
diff --git a/openspec/changes/beta-surface-alignment/tasks.md b/openspec/changes/archive/2026-10-07-beta-surface-alignment/tasks.md
similarity index 100%
rename from openspec/changes/beta-surface-alignment/tasks.md
rename to openspec/changes/archive/2026-10-07-beta-surface-alignment/tasks.md
diff --git a/openspec/changes/keyboard-accessible-widget-repositioning/.openspec.yaml b/openspec/changes/archive/2026-10-07-keyboard-accessible-widget-repositioning/.openspec.yaml
similarity index 100%
rename from openspec/changes/keyboard-accessible-widget-repositioning/.openspec.yaml
rename to openspec/changes/archive/2026-10-07-keyboard-accessible-widget-repositioning/.openspec.yaml
diff --git a/openspec/changes/keyboard-accessible-widget-repositioning/proposal.md b/openspec/changes/archive/2026-10-07-keyboard-accessible-widget-repositioning/proposal.md
similarity index 100%
rename from openspec/changes/keyboard-accessible-widget-repositioning/proposal.md
rename to openspec/changes/archive/2026-10-07-keyboard-accessible-widget-repositioning/proposal.md
diff --git a/openspec/changes/keyboard-accessible-widget-repositioning/specs/grid-layout/spec.md b/openspec/changes/archive/2026-10-07-keyboard-accessible-widget-repositioning/specs/grid-layout/spec.md
similarity index 100%
rename from openspec/changes/keyboard-accessible-widget-repositioning/specs/grid-layout/spec.md
rename to openspec/changes/archive/2026-10-07-keyboard-accessible-widget-repositioning/specs/grid-layout/spec.md
diff --git a/openspec/changes/keyboard-accessible-widget-repositioning/tasks.md b/openspec/changes/archive/2026-10-07-keyboard-accessible-widget-repositioning/tasks.md
similarity index 59%
rename from openspec/changes/keyboard-accessible-widget-repositioning/tasks.md
rename to openspec/changes/archive/2026-10-07-keyboard-accessible-widget-repositioning/tasks.md
index c32c15de8..feba36b4c 100644
--- a/openspec/changes/keyboard-accessible-widget-repositioning/tasks.md
+++ b/openspec/changes/archive/2026-10-07-keyboard-accessible-widget-repositioning/tasks.md
@@ -1,15 +1,26 @@
# Tasks — keyboard-accessible-widget-repositioning
+> Archive pass 2026-10-07: every task ticked from code. `src/components/DashboardGrid.vue` was deleted when the grid moved to nextcloud-vue's `CnDashboardGrid`, so tasks 1 to 3 landed there (nextcloud-vue `development`):
+> - Task 1: `src/components/CnDashboardGrid/CnDashboardGrid.vue:35-38` (`role="group"`, `:aria-label`, `:tabindex`).
+> - Task 2: `CnDashboardGrid.vue:1055` (`.grid-stack-item:focus-visible`).
+> - Task 3: `CnDashboardGrid.vue:633` (Enter and Space) and `:845-867` (`item-activate`, then a synthesised `contextmenu` that launchpad's `@contextmenu` handler at `src/views/Views.vue:290` opens the menu from).
+> - Task 4: `src/components/Widgets/WidgetContextMenu.vue:26` (Move) and `:110` (`move` emit).
+> - Task 5: `src/modals/WidgetMovePanel.vue` (in `src/modals/`, per the modal-isolation rule).
+> - Task 6: `src/composables/useGridManager.js:248` (`nudgePlacement`).
+> - Task 7: `src/views/Views.vue:420` and `:444-450`.
+> - Task 8: `src/modals/__tests__/WidgetMovePanel.spec.js`. Task 9: `src/components/Widgets/__tests__/WidgetContextMenu.spec.js:59`.
+> - Tasks 10 and 11: `tests/e2e/keyboard-widget-repositioning.spec.ts` (keyboard-only move, focus trap), plus nextcloud-vue `tests/a11y/CnDashboardGrid.a11y.spec.js` and `tests/components/CnDashboardGridKeyboard.spec.js`.
+
## Grid item accessibility baseline
-- [ ] Task 1: In `src/components/DashboardGrid.vue`, add `role="group"`,
+- [x] Task 1: In `src/components/DashboardGrid.vue`, add `role="group"`,
`:aria-label="getPlacementAriaLabel(placement)"` (new computed helper
returning e.g. `Widget: {title}` or `Tile: {title}`), and `tabindex="0"`
to the `.grid-stack-item` element (currently lines 9-20).
-- [ ] Task 2: Add `:focus-visible` CSS to `.grid-stack-item` using NC
+- [x] Task 2: Add `:focus-visible` CSS to `.grid-stack-item` using NC
variables (`var(--color-primary-element)` outline), matching the
existing visual language of the component's other interactive states.
-- [ ] Task 3: Wire `keydown.enter` and `keydown.space` on the grid item (in
+- [x] Task 3: Wire `keydown.enter` and `keydown.space` on the grid item (in
edit mode only) to open the same context-menu / move-panel flow that
right-click already triggers, calling the existing `onItemContextMenu`
handler with a synthesized position (e.g. centered on the focused
@@ -17,11 +28,11 @@
## Move action
-- [ ] Task 4: In `src/components/Widgets/WidgetContextMenu.vue`, add a new
+- [x] Task 4: In `src/components/Widgets/WidgetContextMenu.vue`, add a new
`role="menuitem"` button "Move" between the existing Edit (line 13) and
Remove (line 29) buttons, with a new `onMove()` handler emitting a new
`move` event (add `'move'` to the `emits` array at line 88) then `close`.
-- [ ] Task 5: Create `src/components/Widgets/WidgetMovePanel.vue` — a
+- [x] Task 5: Create `src/components/Widgets/WidgetMovePanel.vue` — a
focus-trapped panel/modal (reuse `NcModal` or a lightweight popover
matching `WidgetContextMenu`'s positioning approach) that:
- Displays the widget's current `gridX/gridY/gridWidth/gridHeight`.
@@ -33,29 +44,29 @@
`Escape` to cancel (emit `close` with no change).
- Shows a live text readout of the pending position/size for screen
readers (`aria-live="polite"`).
-- [ ] Task 6: In `src/composables/useGridManager.js`, extract a pure
+- [x] Task 6: In `src/composables/useGridManager.js`, extract a pure
helper (e.g. `nudgePlacement(placement, direction, allPlacements)`) that
computes the candidate new rect and reuses the same collision/pushing
logic `placeNewWidget()` (lines 232+) already implements, so keyboard
moves get the same collision-avoidance behaviour as drag moves.
-- [ ] Task 7: Wire `DashboardGrid.vue`'s `move` event (from the context
+- [x] Task 7: Wire `DashboardGrid.vue`'s `move` event (from the context
menu) to open `WidgetMovePanel`, and on `save` call the existing
placement-update path (the same one GridStack's `change` event already
uses) with the panel's resulting rect.
## Tests
-- [ ] Task 8: Add a Vitest test for `WidgetMovePanel.vue` asserting arrow
+- [x] Task 8: Add a Vitest test for `WidgetMovePanel.vue` asserting arrow
keys nudge the displayed position, `Shift+Arrow` resizes, `Enter` emits
`save` with the expected rect, and `Escape` emits `close` with no `save`.
-- [ ] Task 9: Add a Vitest test for `WidgetContextMenu.vue` asserting the
+- [x] Task 9: Add a Vitest test for `WidgetContextMenu.vue` asserting the
new Move button emits `move` then `close`.
-- [ ] Task 10: Add a Playwright e2e test (per `feedback_playwright-ui-only-newman-api.md`
+- [x] Task 10: Add a Playwright e2e test (per `feedback_playwright-ui-only-newman-api.md`
— UI-driven, not API-direct) that: focuses a grid item via `Tab`, opens
the move panel via `Enter`, nudges it right twice via arrow keys, confirms
via `Enter`, and asserts the widget's rendered grid position changed by
the expected offset without any pointer/mouse events being dispatched.
-- [ ] Task 11: Add an axe-core (or equivalent) accessibility check to the
+- [x] Task 11: Add an axe-core (or equivalent) accessibility check to the
existing Playwright/Vitest suite asserting each grid item has a
discoverable accessible name and that the move panel traps focus
correctly (no focus leak to the canvas behind it while open).
diff --git a/openspec/changes/store-plane-dashboard-sharing/design.md b/openspec/changes/archive/2026-10-07-store-plane-dashboard-sharing/design.md
similarity index 100%
rename from openspec/changes/store-plane-dashboard-sharing/design.md
rename to openspec/changes/archive/2026-10-07-store-plane-dashboard-sharing/design.md
diff --git a/openspec/changes/store-plane-dashboard-sharing/proposal.md b/openspec/changes/archive/2026-10-07-store-plane-dashboard-sharing/proposal.md
similarity index 100%
rename from openspec/changes/store-plane-dashboard-sharing/proposal.md
rename to openspec/changes/archive/2026-10-07-store-plane-dashboard-sharing/proposal.md
diff --git a/openspec/changes/store-plane-dashboard-sharing/specs/dashboard-store/spec.md b/openspec/changes/archive/2026-10-07-store-plane-dashboard-sharing/specs/dashboard-store/spec.md
similarity index 99%
rename from openspec/changes/store-plane-dashboard-sharing/specs/dashboard-store/spec.md
rename to openspec/changes/archive/2026-10-07-store-plane-dashboard-sharing/specs/dashboard-store/spec.md
index e8da25dc2..8f14239a3 100644
--- a/openspec/changes/store-plane-dashboard-sharing/specs/dashboard-store/spec.md
+++ b/openspec/changes/archive/2026-10-07-store-plane-dashboard-sharing/specs/dashboard-store/spec.md
@@ -27,7 +27,7 @@ provisions one instance. Faking the registry inside the suite would test the
fake. `StoreServiceTest` covers the install path against the engine's real
signatures.
-## NEW Requirements
+## ADDED Requirements
### Requirement: REQ-STORE-001 The store routes MUST exist wherever the store page is declared
diff --git a/openspec/changes/store-plane-dashboard-sharing/tasks.md b/openspec/changes/archive/2026-10-07-store-plane-dashboard-sharing/tasks.md
similarity index 100%
rename from openspec/changes/store-plane-dashboard-sharing/tasks.md
rename to openspec/changes/archive/2026-10-07-store-plane-dashboard-sharing/tasks.md
diff --git a/openspec/changes/wizard-dataset-card-load/proposal.md b/openspec/changes/archive/2026-10-07-wizard-dataset-card-load/proposal.md
similarity index 100%
rename from openspec/changes/wizard-dataset-card-load/proposal.md
rename to openspec/changes/archive/2026-10-07-wizard-dataset-card-load/proposal.md
diff --git a/openspec/changes/wizard-dataset-card-load/specs/first-time-setup/spec.md b/openspec/changes/archive/2026-10-07-wizard-dataset-card-load/specs/first-time-setup/spec.md
similarity index 100%
rename from openspec/changes/wizard-dataset-card-load/specs/first-time-setup/spec.md
rename to openspec/changes/archive/2026-10-07-wizard-dataset-card-load/specs/first-time-setup/spec.md
diff --git a/openspec/changes/wizard-dataset-card-load/tasks.md b/openspec/changes/archive/2026-10-07-wizard-dataset-card-load/tasks.md
similarity index 100%
rename from openspec/changes/wizard-dataset-card-load/tasks.md
rename to openspec/changes/archive/2026-10-07-wizard-dataset-card-load/tasks.md
diff --git a/openspec/parity/capabilities.json b/openspec/parity/capabilities.json
index 5aeace919..ebd9e9fad 100644
--- a/openspec/parity/capabilities.json
+++ b/openspec/parity/capabilities.json
@@ -606,7 +606,7 @@
"state": "built",
"evidence": "nextcloud-vue CnDashboardGrid.vue:35-47 (main 0f1c0ffbb) makes every grid item in edit mode a focusable named group (role group, aria-label, tabindex 0 while keyboardRepositioning, default true) and turns Enter or Space into the contextmenu event a right-click sends; src/views/Views.vue:290 catches it on the placement and opens WidgetContextMenu; src/components/Widgets/WidgetContextMenu.vue:22-27 offers Move, which src/views/Views.vue:420 and :444 route to src/modals/WidgetMovePanel.vue; WidgetMovePanel.vue:286-310 moves with the arrow keys, resizes with Shift and the arrow keys, saves on Enter and cancels on Escape, through nudgePlacement (src/composables/useGridManager.js:248), the collision model the drag path uses; Views.vue:1964 handleMoveSave persists it. Tests: src/modals/__tests__/WidgetMovePanel.spec.js, tests/e2e/keyboard-widget-repositioning.spec.ts (drives the page with the keyboard only, checks the position after reload)",
"owner": "ConductionNL/launchpad",
- "change": "openspec/changes/keyboard-accessible-widget-repositioning",
+ "change": "openspec/changes/archive/2026-10-07-keyboard-accessible-widget-repositioning",
"readOn": "2026-10-07"
},
"reachedOn": "Workspace page, widget context menu, Move",
diff --git a/openspec/specs/beta-alignment/spec.md b/openspec/specs/beta-alignment/spec.md
new file mode 100644
index 000000000..e7c905900
--- /dev/null
+++ b/openspec/specs/beta-alignment/spec.md
@@ -0,0 +1,47 @@
+# beta-alignment Specification
+
+## Purpose
+LaunchPad is described in four places: the app metadata in `appinfo/info.xml`, the pages in `src/manifest.json`, the product page on conduction.nl and the documentation. This capability keeps those four in step: they use the same feature vocabulary, the licence tag matches the licence text, and a translated product page lives at the same relative path as the page it translates.
+
+## Requirements
+
+### Requirement: The four public surfaces SHALL agree on feature vocabulary
+`appinfo/info.xml`, the shipped feature set in `src/manifest.json` / `lib/`,
+the product page (`conduction-website/src/pages/apps/launchpad.mdx` + its
+Dutch translation), and `docs/` SHALL describe the same canonical feature
+list using the same feature names, and SHALL NOT assert a capability that
+is not present in `lib/` or `src/` at HEAD.
+
+#### Scenario: A feature bullet on the product page names a real capability
+- **WHEN** the product page or docs describe a LaunchPad capability
+- **THEN** that capability SHALL be traceable to a concrete file in `lib/`
+ or `src/` (a controller, service, widget renderer, or registry entry)
+
+#### Scenario: info.xml does not declare an install-time dependency the architecture forbids
+- **WHEN** `docs/architecture.md` documents a "MUST NOT" install-time
+ dependency rule for an app (e.g. OpenRegister)
+- **THEN** `appinfo/info.xml`'s `` block SHALL NOT list that
+ app
+
+### Requirement: The license tag SHALL match the declared license text
+`appinfo/info.xml`'s `` element and its top-of-file SPDX header
+SHALL match the license asserted in both `` CDATA blocks
+("Free and open source under the EUPL-1.2 license") and every PHP file's
+`@license` docblock tag.
+
+#### Scenario: licence tag matches description text
+- **WHEN** `info.xml`'s description states a license
+- **THEN** the `` element SHALL declare the same license
+
+### Requirement: A translated product page SHALL live at the same relative path as its source page
+Docusaurus i18n resolves a translated page by matching the source page's
+file path under the locale's content directory. A translation SHALL NOT be
+left at a stale pre-rename filename after the English source page is
+renamed.
+
+#### Scenario: English product page is renamed
+- **WHEN** `conduction-website/src/pages/apps/.mdx` is renamed to
+ `.mdx`
+- **THEN** every locale's translation under
+ `i18n//docusaurus-plugin-content-pages/apps/` SHALL be renamed to
+ the same `.mdx`, and no stale `.mdx` file SHALL remain
diff --git a/openspec/specs/dashboard-store/spec.md b/openspec/specs/dashboard-store/spec.md
new file mode 100644
index 000000000..cdc359448
--- /dev/null
+++ b/openspec/specs/dashboard-store/spec.md
@@ -0,0 +1,274 @@
+# dashboard-store Specification
+
+## Purpose
+An administrator can connect LaunchPad to a registry, another OpenRegister instance that publishes dashboard templates, and install a template from the store page as a new dashboard. OpenRegister's store plane owns discovery (the address guard, the token transport and the outcome it reports); LaunchPad owns the install, which goes through the existing dashboard importer and always creates a dashboard, never replaces one. The registry token is write-only, and the store degrades to a not-configured state when OpenRegister is absent.
+
+## Requirements
+
+### Requirement: REQ-STORE-001 The store routes MUST exist wherever the store page is declared
+
+LaunchPad's manifest declares a `type: "store"` page, which renders `CnStorePage`,
+which calls `GET /apps/launchpad/api/store/items` on mount and
+`POST /apps/launchpad/api/store/items/{slug}/install` on an install click.
+`appinfo/routes.php` SHALL declare both, and the `{slug}` route SHALL constrain
+the slug to `[a-z0-9][a-z0-9-]*[a-z0-9]` so a malformed slug fails at the router
+rather than reaching the registry URL.
+
+#### Scenario: The declared page can reach its endpoint
+
+- **GIVEN** `src/manifest.json` declares a page of type `store`
+- **WHEN** the route table is read
+- **THEN** it MUST contain `store#search` at `/api/store/items`
+- **AND** it MUST contain `store#install` at `/api/store/items/{slug}/install`
+
+@e2e exclude route-table shape: asserted by reading appinfo/routes.php in tests/Unit/Support/StoreWiringTest.php; the browser consequence is the next scenario
+
+#### Scenario: An unconfigured store shows the not-configured state
+
+- **GIVEN** no `registry_url` is configured
+- **WHEN** an administrator opens the Store page
+- **THEN** `GET /api/store/items` MUST answer 200 with outcome `not_configured`
+- **AND** the page MUST show its not-configured note
+- **AND** it MUST NOT show the unreachable note or an empty result grid
+
+---
+
+### Requirement: REQ-STORE-002 An absent OpenRegister MUST degrade, never fatal
+
+`GenericStoreService` is resolved optionally, exactly as LaunchPad already
+resolves the AppHost observability collaborators. When OpenRegister is disabled or
+absent the collaborator is null. `search()` MUST then return outcome
+`not_configured` with an empty card list, and `install()` MUST refuse, and neither
+MUST touch an `OCA\OpenRegister\…` symbol.
+
+An engine that is present but unconfigured behaves the same way, because
+`GenericStoreService::isConfigured()` reports false for an empty `registry_url`
+and makes no network call.
+
+#### Scenario: No OpenRegister yields not_configured
+
+- **GIVEN** the store service holds a null discovery client
+- **WHEN** `search()` is called
+- **THEN** the outcome MUST be `not_configured` and the card list MUST be empty
+
+@e2e exclude requires an instance WITHOUT OpenRegister, and CI installs it; asserted in tests/Unit/Service/StoreServiceTest.php
+
+#### Scenario: No OpenRegister refuses an install rather than half-running one
+
+- **GIVEN** the store service holds a null discovery client
+- **WHEN** `install()` is called for any slug
+- **THEN** it MUST report failure and MUST NOT call `ImportService`
+
+@e2e exclude requires an instance WITHOUT OpenRegister, and CI installs it; asserted in tests/Unit/Service/StoreServiceTest.php
+
+---
+
+### Requirement: REQ-STORE-003 The engine's outcome MUST reach the caller unchanged
+
+The plane distinguishes `store_unreachable` from `store_invalid_response` so a
+misconfigured registry does not read as an offline one. LaunchPad SHALL pass the
+outcome through verbatim and MUST NOT collapse the two, and MUST NOT substitute
+its own. Upstream error detail MUST NOT reach the caller; the engine already keeps
+it server-side.
+
+#### Scenario: An invalid response is not reported as unreachable
+
+- **GIVEN** the engine returns outcome `store_invalid_response`
+- **WHEN** the service returns
+- **THEN** the outcome MUST be `store_invalid_response`
+
+@e2e exclude needs a registry that answers malformed JSON, and CI has no registry; asserted in tests/Unit/Service/StoreServiceTest.php
+
+---
+
+### Requirement: REQ-STORE-004 Installing MUST require an administrator
+
+An install writes dashboards into the instance from a third-party server. The
+install endpoint SHALL carry `#[AuthorizedAdminSetting(LaunchPadAdmin::class)]`,
+the posture LaunchPad already uses for its other administrative endpoints.
+
+Search SHALL carry `#[NoAdminRequired]`, because browsing a registry addresses no
+object of this instance. It forwards a query to an external registry and returns
+normalised cards, so there is no local identifier to guess and no IDOR to create.
+
+#### Scenario: Every store route declares a posture
+
+- **GIVEN** the store controller
+- **WHEN** its public methods are read
+- **THEN** `search` MUST carry `#[NoAdminRequired]`
+- **AND** `install` MUST carry `#[AuthorizedAdminSetting]`
+
+@e2e exclude attribute presence: asserted by reflection in tests/Unit/Controller/StoreControllerTest.php; the two scenarios below prove the attributes hold at runtime
+
+#### Scenario: A non-admin is refused the registry config
+
+- **GIVEN** a signed-in account that is not an administrator, holding a valid request token
+- **WHEN** it reads or writes `/api/store/config`
+- **THEN** both requests MUST answer 403
+- **AND** the stored registry MUST be unchanged afterwards
+
+#### Scenario: A non-admin is refused an install
+
+- **GIVEN** the same account
+- **WHEN** it posts to `/api/store/items/{slug}/install`
+- **THEN** the request MUST answer 403
+
+---
+
+### Requirement: REQ-STORE-005 An install MUST reuse ImportService, not reimplement it
+
+The resolved payload SHALL be materialised into a `launchpad-export-v1` ZIP in a
+temporary directory and handed to `ImportService::import()`. LaunchPad MUST NOT
+gain a second code path that turns a dashboard payload into rows.
+
+The materialised archive MUST carry a `manifest.json` with `schemaVersion` equal
+to the importer's supported version and a `scope`, and one
+`dashboards/{uuid}.json` entry per dashboard, because that is what
+`validateZipStructure()` requires and what `ExportService` writes.
+
+The temporary archive MUST be removed whether the import succeeds or throws.
+
+#### Scenario: The payload reaches the existing importer
+
+- **GIVEN** a resolved item carrying one dashboard payload
+- **WHEN** it is installed
+- **THEN** `ImportService::import()` MUST be called exactly once
+- **AND** the path it receives MUST be an existing ZIP containing `manifest.json`
+
+@e2e exclude needs a second OpenRegister publishing a template, and CI has one instance; asserted in tests/Unit/Service/StoreServiceTest.php
+
+#### Scenario: An install with no registry is refused with a reason
+
+- **GIVEN** no `registry_url` is configured
+- **WHEN** an administrator posts an install for any slug
+- **THEN** the request MUST answer 400 with `success` false and a message
+- **AND** it MUST NOT answer 500
+
+#### Scenario: A payload with no dashboards is refused before any import
+
+- **GIVEN** a resolved item whose dashboards list is empty or absent
+- **WHEN** it is installed
+- **THEN** the install MUST fail and `ImportService::import()` MUST NOT be called
+
+@e2e exclude needs a registry serving a malformed item, and CI has no registry; asserted in tests/Unit/Service/StoreServiceTest.php
+
+#### Scenario: The temporary archive does not survive a failing import
+
+- **GIVEN** `ImportService::import()` throws
+- **WHEN** the install returns
+- **THEN** no temporary archive from that install MUST remain on disk
+
+@e2e exclude a temp-directory effect with no browser surface; asserted in tests/Unit/Service/StoreServiceTest.php
+
+---
+
+### Requirement: REQ-STORE-006 A store install MUST create dashboards, never replace them
+
+The install SHALL call `ImportService::import()` with `preserveUuids` false. A
+template published by a foreign instance carries that instance's UUIDs, and one of
+them can collide with a live local dashboard. Re-mapping makes the install
+additive.
+
+This mirrors the plane's own rule that an install creates rather than replaces,
+reached by a different route: the identity that would do the damage is the
+dashboard UUID rather than an object `uuid`.
+
+#### Scenario: A colliding UUID still creates
+
+- **GIVEN** a resolved item whose dashboard UUID matches a local dashboard
+- **WHEN** it is installed
+- **THEN** `import()` MUST be called with `preserveUuids` false
+
+@e2e exclude needs a second OpenRegister publishing a template, and CI has one instance; asserted in tests/Unit/Service/StoreServiceTest.php
+
+---
+
+### Requirement: REQ-STORE-007 The registry token MUST NOT be readable back
+
+`registry_url`, `registry_token` and `registry_register` live in `IAppConfig`
+under `launchpad`, because that is where `GenericStoreService` reads them.
+LaunchPad's own admin settings write to a different store, so the change adds an
+admin-gated endpoint for these three keys.
+
+The read endpoint SHALL return the URL and the register, and for the token SHALL
+return only whether one is set. The token value MUST NOT appear in any response.
+
+#### Scenario: Reading the config never returns the token
+
+- **GIVEN** `registry_token` holds a value
+- **WHEN** the config is read
+- **THEN** the response MUST report that a token is set
+- **AND** the response MUST NOT contain the token value
+
+#### Scenario: An empty token submission clears rather than blanks around
+
+- **GIVEN** a stored token
+- **WHEN** the config is written with an empty token
+- **THEN** the stored token MUST be cleared
+
+@e2e exclude the clear-versus-keep distinction is a request-payload property the browser cannot see; asserted in tests/Unit/Service/StoreServiceTest.php and src/components/admin/__tests__/DashboardRegistrySettings.spec.js
+
+---
+
+### Requirement: REQ-STORE-008 The manifest MUST NOT declare a store block
+
+`src/manifest.json` MUST NOT carry a `store` key. That block exists to configure
+OpenRegister's `GenericStoreController`, which LaunchPad does not use, so a block
+here would declare a schema and card fields that nothing reads while
+`StoreService` declares the ones that are used.
+
+A non-empty `types` list is the specific harm, because it selects the engine's
+federated configuration path. The block on `development` declared
+`openregister.configset` and `openregister.flows`, which trade registers, schemas
+and flows. Had the routes existed, the store page would have offered
+configuration sets to somebody looking for a dashboard.
+
+#### Scenario: No store block is declared
+
+- **GIVEN** `src/manifest.json`
+- **WHEN** it is decoded
+- **THEN** it MUST NOT contain a `store` key
+
+@e2e exclude a source-file property; asserted in tests/Unit/Support/StoreWiringTest.php
+
+#### Scenario: The store page still declares itself
+
+- **GIVEN** the same manifest
+- **WHEN** its pages are read
+- **THEN** exactly one page MUST carry `type` of `store`
+
+@e2e exclude a source-file property; asserted in tests/Unit/Support/StoreWiringTest.php
+
+---
+
+### Requirement: REQ-STORE-009 An administrator MUST be able to connect a registry without a shell
+
+The three registry keys live in `IAppConfig`, which LaunchPad's own admin
+settings do not write. So Beheer ▸ Sharing SHALL carry a form for
+`registry_url`, `registry_register` and the token, backed by
+`/api/store/config`.
+
+The token field SHALL be write-only. It MUST start empty on every load, and
+the page MUST say whether a token is set without showing it. Saving with an
+empty token field MUST leave the stored token alone; removing it is a
+separate action.
+
+#### Scenario: An administrator saves a registry and reads it back
+
+- **GIVEN** an administrator on Beheer ▸ Sharing
+- **WHEN** they enter a registry URL, a register and a token, and save
+- **THEN** after a reload the URL and register fields MUST show what they entered
+- **AND** the page MUST say a token is set
+
+#### Scenario: The token is never shown back
+
+- **GIVEN** a stored token
+- **WHEN** the form loads
+- **THEN** the token field MUST be empty
+- **AND** neither the page nor the config response MUST contain the token
+
+#### Scenario: The saved registry is the one the store reads
+
+- **GIVEN** an administrator saved a registry URL whose host does not resolve
+- **WHEN** they open the Store page
+- **THEN** the page MUST show the unreachable note rather than the not-configured one
diff --git a/openspec/specs/first-time-setup/spec.md b/openspec/specs/first-time-setup/spec.md
new file mode 100644
index 000000000..c73d3bf7c
--- /dev/null
+++ b/openspec/specs/first-time-setup/spec.md
@@ -0,0 +1,54 @@
+# first-time-setup Specification
+
+## Purpose
+The first-time setup wizard gets a new LaunchPad installation to a working state. The operator picks an example dataset on a card and loads it from that card, and the setup status reports every step the manifest declares. This capability defines those steps and what the server accepts and refuses while running them.
+
+## Requirements
+
+### Requirement: Each example data card loads itself
+
+The `demo-data` setup step MUST be a cards choice step with `loadAction: load-demo-data`. The setup wizard MUST NOT carry a separate run-action step that loads the picked dataset.
+
+#### Scenario: The operator loads a dataset from its card
+
+- GIVEN the setup wizard shows the example data cards
+- WHEN the operator presses Load on a card
+- THEN the wizard posts `{ "dataset": }` to `/api/setup/action/load-demo-data`
+- AND the server loads that dataset
+- AND the server records the dataset as the pick only after the load succeeds
+- @e2e exclude the card and its spinner are CnSetupWizard UI, tested in nextcloud-vue; the posted body is covered by tests/Unit/Controller/SetupControllerTest.php
+
+#### Scenario: An unknown dataset is refused
+
+- GIVEN a dataset id that no card offers
+- WHEN it is posted to `/api/setup/action/load-demo-data`
+- THEN the server answers 400 with `success: false`
+- AND nothing is loaded or stored
+- @e2e tests/e2e/spec-coverage/demo-data-setup-step.spec.ts
+
+#### Scenario: A call without a body keeps working
+
+- GIVEN a dataset was stored through `/api/setup/config`
+- WHEN `/api/setup/action/load-demo-data` is called without a body
+- THEN the stored dataset is loaded
+- @e2e tests/e2e/spec-coverage/demo-data-setup-step.spec.ts
+
+#### Scenario: A failed load leaves the step open
+
+- GIVEN the load of the posted dataset fails
+- WHEN the server answers
+- THEN the answer carries `success: false`
+- AND no pick or decision is stored
+- @e2e exclude needs a load that fails on a live instance; covered by tests/Unit/Controller/SetupControllerTest.php
+
+### Requirement: Setup status reports every manifest step
+
+`GET /api/setup/status` MUST report a `done` state for every step id in `manifest.setup.steps`.
+
+#### Scenario: The status ids match the manifest
+
+- GIVEN the LaunchPad manifest
+- WHEN an administrator reads `/api/setup/status`
+- THEN `steps` holds an entry for every manifest step id
+- AND the retired load step is not reported
+- @e2e tests/e2e/spec-coverage/demo-data-setup-step.spec.ts
diff --git a/openspec/specs/grid-layout/spec.md b/openspec/specs/grid-layout/spec.md
index 031c5bd2f..b6ec3eede 100644
--- a/openspec/specs/grid-layout/spec.md
+++ b/openspec/specs/grid-layout/spec.md
@@ -477,6 +477,76 @@ All "add widget" code paths (toolbar dropdown, keyboard shortcut, drag-from-pick
- **THEN** it MUST call `placeNewWidget(spec)` exported from `useGridManager.js`
- **AND** MUST NOT call `grid.addWidget(...)` directly
+### Requirement: REQ-GRID-KBD-001 Grid items expose an accessible role and name
+
+Every rendered grid item MUST carry `role="group"`, a computed
+`aria-label` describing the widget (e.g. "Widget: {title}"), and
+`tabindex="0"` so it is reachable via sequential keyboard navigation
+(`Tab`/`Shift+Tab`) and announced by assistive technology as a distinct,
+named unit.
+
+#### Scenario: Grid item is keyboard-focusable
+
+- **GIVEN** a dashboard with 3 widget placements in edit mode
+- **WHEN** a keyboard user presses `Tab` repeatedly from the canvas
+- **THEN** focus MUST land on each grid item in turn
+- **AND** each focused item MUST have a visible focus indicator
+
+#### Scenario: Screen reader announces the widget
+
+- **GIVEN** a grid item wrapping a widget titled "Team Announcements"
+- **WHEN** a screen reader user focuses the item
+- **THEN** it MUST announce a group with the accessible name "Widget:
+ Team Announcements" (or the tile-equivalent label for tile placements)
+
+### Requirement: REQ-GRID-KBD-002 Keyboard-operable move and resize
+
+Any user MUST be able to reposition or resize a focused widget placement
+using only the keyboard, producing the same `gridX`/`gridY`/`gridWidth`/
+`gridHeight` outcome a mouse drag would produce, including respecting the
+existing minimum size (`gs-min-w`/`gs-min-h` = 2) and existing collision/
+push behaviour.
+
+#### Scenario: Opening the move panel via keyboard
+
+- **GIVEN** a grid item has keyboard focus
+- **WHEN** the user presses `Enter` or `Space`
+- **THEN** the same panel/menu reachable via right-click MUST open
+- **AND** a "Move" action MUST be present in that panel/menu
+
+#### Scenario: Nudging position with arrow keys
+
+- **GIVEN** the move panel is open for a widget at `gridX=2, gridY=3`
+- **WHEN** the user presses `ArrowRight`
+- **THEN** the pending position MUST become `gridX=3, gridY=3`
+- **AND** the change MUST NOT be persisted until the user confirms
+
+#### Scenario: Resizing with Shift+arrow keys
+
+- **GIVEN** the move panel is open for a widget at `gridWidth=3,
+ gridHeight=2`
+- **WHEN** the user presses `Shift+ArrowRight`
+- **THEN** the pending size MUST become `gridWidth=4, gridHeight=2`
+- **AND** the size MUST NOT shrink below the existing 2-cell minimum in
+ either dimension
+
+#### Scenario: Confirming and cancelling
+
+- **GIVEN** the move panel is open with a pending position/size change
+- **WHEN** the user presses `Enter`
+- **THEN** the placement MUST be updated via the same persistence path
+ drag-and-drop already uses
+- **WHEN** instead the user presses `Escape`
+- **THEN** no change MUST be persisted and the panel MUST close
+
+#### Scenario: Collisions are resolved the same way as drag
+
+- **GIVEN** a keyboard-driven move would overlap an existing placement
+- **WHEN** the user confirms the move
+- **THEN** the system MUST apply the same collision/push resolution
+ `placeNewWidget()` already applies for drag-and-drop moves — no widget
+ MUST silently overlap another after confirmation
+
## Non-Functional Requirements
- **Performance**: Grid initialization MUST complete within 500ms for dashboards with up to 30 widget placements. Drag and resize interactions MUST maintain 60fps with no visible lag.
From 97f56ea2bb98dd1bf3126cb87930ee0c5482a745 Mon Sep 17 00:00:00 2001
From: Ruben van der Linde
Date: Thu, 8 Oct 2026 15:17:28 +0200
Subject: [PATCH 27/60] chore(parity): one feature list, every capability under
one feature
---
openspec/parity/capabilities.json | 681 ++++++++++++++++++++++++------
1 file changed, 561 insertions(+), 120 deletions(-)
diff --git a/openspec/parity/capabilities.json b/openspec/parity/capabilities.json
index ebd9e9fad..4527287c9 100644
--- a/openspec/parity/capabilities.json
+++ b/openspec/parity/capabilities.json
@@ -394,6 +394,408 @@
"name_nl": "Identiteit, apparaten en uiterlijk"
}
],
+ "features": [
+ {
+ "slug": "grid-layout",
+ "title": "Drag-and-drop grid",
+ "title_nl": "Sleep-en-neerzet raster",
+ "area": "building",
+ "overlay": "grid-layout",
+ "specs": [
+ "admin-settings",
+ "grid-layout",
+ "widgets"
+ ]
+ },
+ {
+ "slug": "dashboard-versioning",
+ "title": "Versioning and rollback",
+ "title_nl": "Versiebeheer en terugdraaien",
+ "area": "building",
+ "overlay": "dashboard-versioning",
+ "specs": []
+ },
+ {
+ "slug": "permissions",
+ "title": "Permission tiers",
+ "title_nl": "Rechtenniveaus",
+ "area": "rollout",
+ "overlay": "permissions",
+ "specs": [
+ "permissions"
+ ]
+ },
+ {
+ "slug": "dashboard-export-import",
+ "title": "Export and import",
+ "title_nl": "Exporteren en importeren",
+ "area": "rollout",
+ "overlay": "dashboard-export-import",
+ "specs": [
+ "dashboard-export-import"
+ ]
+ },
+ {
+ "slug": "dashboard-switcher",
+ "title": "Multiple dashboards",
+ "title_nl": "Meerdere dashboards",
+ "area": "dashboards",
+ "overlay": "dashboard-switcher",
+ "specs": [
+ "dashboard-deeplinking",
+ "dashboard-icons",
+ "dashboards",
+ "default-widget-bundle",
+ "runtime-shell"
+ ]
+ },
+ {
+ "slug": "tiles",
+ "title": "Shortcut tiles",
+ "title_nl": "Snelkoppelingstegels",
+ "area": "launcher",
+ "overlay": "tiles",
+ "specs": [
+ "launcher-bookmark-import",
+ "launcher-tile-internal-address",
+ "launcher-tile-open-behaviour",
+ "launcher-tile-sorting",
+ "tiles"
+ ]
+ },
+ {
+ "slug": "app-catalogue-and-launch",
+ "title": "App catalogue and launching any kind of app",
+ "title_nl": "App-catalogus en elk soort app starten",
+ "area": "launcher",
+ "overlay": null,
+ "specs": [
+ "launcher-app-catalogue",
+ "launcher-discover-server-services",
+ "launcher-tile-launch-types",
+ "launcher-tile-sorting"
+ ]
+ },
+ {
+ "slug": "organisation-navigation",
+ "title": "Organisation menu and footer",
+ "title_nl": "Organisatiemenu en voettekst",
+ "area": "launcher",
+ "overlay": null,
+ "specs": []
+ },
+ {
+ "slug": "nc-dashboard-widget-proxy",
+ "title": "Nextcloud widgets",
+ "title_nl": "Nextcloud-widgets",
+ "area": "widgets",
+ "overlay": "nc-dashboard-widget-proxy",
+ "specs": [
+ "attention-feed"
+ ]
+ },
+ {
+ "slug": "content-widgets",
+ "title": "Content widgets for text, media, news and files",
+ "title_nl": "Inhoudswidgets voor tekst, media, nieuws en bestanden",
+ "area": "widgets",
+ "overlay": null,
+ "specs": [
+ "widgets"
+ ]
+ },
+ {
+ "slug": "people-widget",
+ "title": "Colleagues, birthdays and expertise",
+ "title_nl": "Collega's, verjaardagen en expertise",
+ "area": "widgets",
+ "overlay": null,
+ "specs": [
+ "people-widget"
+ ]
+ },
+ {
+ "slug": "iframe-embed-widget",
+ "title": "Embedded web pages and reports",
+ "title_nl": "Ingebedde webpagina's en rapporten",
+ "area": "widgets",
+ "overlay": null,
+ "specs": [
+ "adopt-connection-registry"
+ ]
+ },
+ {
+ "slug": "runtime-or-consumption",
+ "title": "Live business data",
+ "title_nl": "Live bedrijfsdata",
+ "area": "integrations",
+ "overlay": "runtime-or-consumption",
+ "specs": [
+ "adopt-connection-registry",
+ "background-job-feed-refresh",
+ "integrations-live-tile-lists",
+ "integrations-office-tiles"
+ ]
+ },
+ {
+ "slug": "kpi-cards",
+ "title": "KPI cards",
+ "title_nl": "KPI-kaarten",
+ "area": "widgets",
+ "overlay": "kpi-cards",
+ "specs": []
+ },
+ {
+ "slug": "launchpad-spend-analytics-widget",
+ "title": "Spend analytics card",
+ "title_nl": "Uitgavenanalyse-kaart",
+ "area": "widgets",
+ "overlay": "launchpad-spend-analytics-widget",
+ "specs": []
+ },
+ {
+ "slug": "conditional-visibility",
+ "title": "Conditional visibility",
+ "title_nl": "Voorwaardelijke zichtbaarheid",
+ "area": "targeting",
+ "overlay": "conditional-visibility",
+ "specs": [
+ "conditional-visibility"
+ ]
+ },
+ {
+ "slug": "admin-roles",
+ "title": "Role delegation",
+ "title_nl": "Roldelegatie",
+ "area": "targeting",
+ "overlay": "admin-roles",
+ "specs": [
+ "action-authorization",
+ "admin-roles",
+ "role-feature-permissions"
+ ]
+ },
+ {
+ "slug": "admin-templates",
+ "title": "Admin templates",
+ "title_nl": "Beheersjablonen",
+ "area": "rollout",
+ "overlay": "admin-templates",
+ "specs": [
+ "admin-templates",
+ "dashboards"
+ ]
+ },
+ {
+ "slug": "setup-wizard",
+ "title": "Setup wizard, demo content and guided tour",
+ "title_nl": "Installatiewizard, demo-inhoud en rondleiding",
+ "area": "rollout",
+ "overlay": null,
+ "specs": [
+ "runtime-shell"
+ ]
+ },
+ {
+ "slug": "dashboard-quota-limits",
+ "title": "Dashboard quotas",
+ "title_nl": "Dashboardquota",
+ "area": "rollout",
+ "overlay": "dashboard-quota-limits",
+ "specs": []
+ },
+ {
+ "slug": "confluence-html-import",
+ "title": "Confluence import",
+ "title_nl": "Confluence-import",
+ "area": "rollout",
+ "overlay": "confluence-html-import",
+ "specs": []
+ },
+ {
+ "slug": "dashboard-sharing",
+ "title": "Sharing dashboards with people and groups",
+ "title_nl": "Dashboards delen met personen en groepen",
+ "area": "sharing",
+ "overlay": null,
+ "specs": [
+ "dashboard-sharing"
+ ]
+ },
+ {
+ "slug": "dashboard-public-share",
+ "title": "Public share links",
+ "title_nl": "Openbare deellinks",
+ "area": "sharing",
+ "overlay": "dashboard-public-share",
+ "specs": [
+ "sharing-public-start-page"
+ ]
+ },
+ {
+ "slug": "dashboard-kiosk-mode",
+ "title": "Kiosk and signage mode",
+ "title_nl": "Kiosk- en narrowcastmodus",
+ "area": "sharing",
+ "overlay": "dashboard-kiosk-mode",
+ "specs": []
+ },
+ {
+ "slug": "news-and-announcements",
+ "title": "News and announcements",
+ "title_nl": "Nieuws en mededelingen",
+ "area": "engagement",
+ "overlay": null,
+ "specs": [
+ "engagement-activity-digest",
+ "engagement-announcements"
+ ]
+ },
+ {
+ "slug": "acknowledgements-and-reactions",
+ "title": "Read confirmations, reactions and comments",
+ "title_nl": "Leesbevestigingen, reacties en opmerkingen",
+ "area": "engagement",
+ "overlay": null,
+ "specs": []
+ },
+ {
+ "slug": "communities-events-and-knowledge",
+ "title": "Communities, events and knowledge base",
+ "title_nl": "Community's, evenementen en kennisbank",
+ "area": "engagement",
+ "overlay": null,
+ "specs": [
+ "engagement-communities"
+ ]
+ },
+ {
+ "slug": "activity-feed-integration",
+ "title": "Activity feed",
+ "title_nl": "Activiteitenoverzicht",
+ "area": "engagement",
+ "overlay": "activity-feed-integration",
+ "specs": []
+ },
+ {
+ "slug": "nc-unified-search-integration",
+ "title": "Unified search",
+ "title_nl": "Geintegreerd zoeken",
+ "area": "search-ai",
+ "overlay": "nc-unified-search-integration",
+ "specs": [
+ "search-ai-prefix-shortcuts"
+ ]
+ },
+ {
+ "slug": "launchpad-ai-dashboard-assistant",
+ "title": "AI assistant",
+ "title_nl": "AI-assistent",
+ "area": "search-ai",
+ "overlay": "launchpad-ai-dashboard-assistant",
+ "specs": [
+ "mcp-tool-surface"
+ ]
+ },
+ {
+ "slug": "dashboard-view-analytics",
+ "title": "Usage analytics",
+ "title_nl": "Gebruiksanalyse",
+ "area": "governance",
+ "overlay": null,
+ "specs": []
+ },
+ {
+ "slug": "prometheus-metrics",
+ "title": "Prometheus metrics",
+ "title_nl": "Prometheus-metrieken",
+ "area": "governance",
+ "overlay": "prometheus-metrics",
+ "specs": [
+ "prometheus-metrics"
+ ]
+ },
+ {
+ "slug": "launchpad-compliance-audit-panel",
+ "title": "Audit panel",
+ "title_nl": "Auditpaneel",
+ "area": "governance",
+ "overlay": "launchpad-compliance-audit-panel",
+ "specs": []
+ },
+ {
+ "slug": "orphaned-data-cleanup",
+ "title": "Data cleanup",
+ "title_nl": "Data-opschoning",
+ "area": "sharing",
+ "overlay": "orphaned-data-cleanup",
+ "specs": [
+ "dashboard-sharing"
+ ]
+ },
+ {
+ "slug": "admin-tooling",
+ "title": "Bulk changes, command line and REST API",
+ "title_nl": "Bulkwijzigingen, commandoregel en REST-API",
+ "area": "governance",
+ "overlay": null,
+ "specs": [
+ "governance-rest-api-reference"
+ ]
+ },
+ {
+ "slug": "launchpad-enterprise-security-access",
+ "title": "SSO and access posture",
+ "title_nl": "SSO en toegangsstatus",
+ "area": "platform",
+ "overlay": "launchpad-enterprise-security-access",
+ "specs": []
+ },
+ {
+ "slug": "theming-and-languages",
+ "title": "House style, themes and languages",
+ "title_nl": "Huisstijl, thema's en talen",
+ "area": "platform",
+ "overlay": null,
+ "specs": [
+ "platform-dashboard-theme-and-css"
+ ]
+ },
+ {
+ "slug": "launchpad-mobile-remote-access",
+ "title": "Mobile access",
+ "title_nl": "Mobiele toegang",
+ "area": "platform",
+ "overlay": "launchpad-mobile-remote-access",
+ "specs": []
+ },
+ {
+ "slug": "self-hosting",
+ "title": "Self-hosting on your own Nextcloud",
+ "title_nl": "Zelf hosten op je eigen Nextcloud",
+ "area": "platform",
+ "overlay": null,
+ "specs": [
+ "runtime-or-consumption"
+ ]
+ },
+ {
+ "slug": "groupfolder-storage-backend",
+ "title": "GroupFolder storage (optional)",
+ "title_nl": "GroupFolder-opslag (optioneel)",
+ "area": "governance",
+ "overlay": "groupfolder-storage-backend",
+ "specs": []
+ },
+ {
+ "slug": "launchpad-meeting-calendar-actions",
+ "title": "Meeting actions",
+ "title_nl": "Vergaderacties",
+ "area": "widgets",
+ "overlay": "launchpad-meeting-calendar-actions",
+ "specs": []
+ }
+ ],
"providers": [
{
"key": "launchpad",
@@ -438,7 +840,8 @@
"homarr": "unknown",
"dashy": "unknown",
"question": "src/manifest.d/connection-registry.json shows the Integrations page only when appInstalled 'integriq'. If integriq's appinfo id has not moved from openconnector yet, the check matches nothing and the page silently never appears. Settle by reading integriq's info.xml id on development and loading the page.",
- "seen": "src/manifest.d/connection-registry.json:16"
+ "seen": "src/manifest.d/connection-registry.json:16",
+ "feature": "runtime-or-consumption"
},
{
"id": "q-charts-live",
@@ -451,7 +854,8 @@
"homarr": "unknown",
"dashy": "unknown",
"question": "The widgets call OpenRegister GraphQL. The repo shows the call, not that an installed OpenRegister answers it with the aggregations the widget expects. Drive it with OpenRegister installed.",
- "seen": "src/services/graphqlClient.js:16"
+ "seen": "src/services/graphqlClient.js:16",
+ "feature": "kpi-cards"
},
{
"id": "q-registry-live",
@@ -464,7 +868,8 @@
"homarr": "unknown",
"dashy": "unknown",
"question": "Discovery is OpenRegister's GenericStoreService and install is LaunchPad's ImportService. Neither half was run against a live registry.",
- "seen": "lib/Controller/StoreController.php:134"
+ "seen": "lib/Controller/StoreController.php:134",
+ "feature": "dashboard-export-import"
},
{
"id": "q-groupfolder",
@@ -477,7 +882,8 @@
"homarr": "unknown",
"dashy": "unknown",
"question": "openspec/features.overlay.json lists groupfolder-storage-backend as beta, while the open change retire-groupfolder-storage-backend says the migration command moved nothing and proposes retiring it. Decide whether this is a capability or decided-no.",
- "seen": "openspec/changes/retire-groupfolder-storage-backend/proposal.md:33"
+ "seen": "openspec/changes/retire-groupfolder-storage-backend/proposal.md:33",
+ "feature": "groupfolder-storage-backend"
},
{
"id": "q-comments",
@@ -490,7 +896,8 @@
"homarr": "unknown",
"dashy": "unknown",
"question": "lib/Notification/Notifier.php renders a mentioned_in_comment subject, but this reading found no comment screen and no code that raises the notification. Either a sibling or the platform comments app produces it, or the handler is dead.",
- "seen": "lib/Notification/Notifier.php:119"
+ "seen": "lib/Notification/Notifier.php:119",
+ "feature": "acknowledgements-and-reactions"
},
{
"id": "q-sibling-widgets",
@@ -503,7 +910,8 @@
"homarr": "unknown",
"dashy": "unknown",
"question": "competitor-parity-2026-09 names C-reporting-22 as covered by group dashboards plus role feature permissions, with dossiq contributing widgets. Whether any sibling registers a Nextcloud widget that the nc-widget proxy then scopes per role was not read.",
- "seen": "openspec/changes/competitor-parity-2026-09/proposal.md:37"
+ "seen": "openspec/changes/competitor-parity-2026-09/proposal.md:37",
+ "feature": "nc-dashboard-widget-proxy"
},
{
"id": "q-person-activity",
@@ -516,7 +924,8 @@
"homarr": "unknown",
"dashy": "unknown",
"question": "Candidates C-reporting-13 and C-reporting-19 in the dashboards-and-who-may-see-them change. The change itself says it needs a stated purpose before it is a feature, because in a gemeente it is a works-council matter. Not a rating until that is decided.",
- "seen": "openspec/changes/competitor-parity-2026-09/proposal.md:65"
+ "seen": "openspec/changes/competitor-parity-2026-09/proposal.md:65",
+ "feature": "launchpad-compliance-audit-panel"
},
{
"id": "q-drive-nc-dashboard",
@@ -528,7 +937,8 @@
"nextcloud-dashboard": "unknown",
"homarr": "unknown",
"dashy": "unknown",
- "question": "The built-in Dashboard's column rests on 14 feature rows captured in April. It runs on every dev instance, so it is the cheapest column to drive: resize, outside feeds, background and per-group defaults can all be settled in one session."
+ "question": "The built-in Dashboard's column rests on 14 feature rows captured in April. It runs on every dev instance, so it is the cheapest column to drive: resize, outside feeds, background and per-group defaults can all be settled in one session.",
+ "feature": "grid-layout"
}
],
"capabilities": [
@@ -825,7 +1235,7 @@
"provider": "launchpad",
"providerHow": "read-from-code",
"feature": "grid-layout",
- "featureConfidence": "medium",
+ "featureConfidence": "high",
"origin": "own-code",
"evidence": {
"nextcloud-dashboard": "source read at v35.0.1: apps/dashboard/src/DashboardApp.vue:463 a newly ticked widget is appended to the end of the layout list, and the flex-wrap list (:587-596) cannot overlap cards by construction",
@@ -854,7 +1264,7 @@
"reachedOn": "nothing: /api/dashboards/{uuid}/lock has no caller anywhere in src/",
"provider": "launchpad",
"providerHow": "read-from-code",
- "feature": "dashboard-locking",
+ "feature": "grid-layout",
"featureConfidence": "high",
"origin": "own-code",
"note": "The lock service and its four routes exist; no page acquires or shows a lock, so a user cannot reach it.",
@@ -1040,7 +1450,7 @@
"reachedOn": "Workspace page, dashboard menu, Dashboard configuration, General tab",
"provider": "launchpad",
"providerHow": "read-from-code",
- "feature": "dashboard-icons",
+ "feature": "dashboard-switcher",
"featureConfidence": "high",
"origin": "own-code",
"evidence": {
@@ -1100,7 +1510,7 @@
"reachedOn": "/apps/launchpad/dashboards/{id} and /apps/launchpad/{path}",
"provider": "launchpad",
"providerHow": "read-from-code",
- "feature": "dashboard-deeplinking",
+ "feature": "dashboard-switcher",
"featureConfidence": "high",
"origin": "own-code",
"evidence": {
@@ -1161,7 +1571,7 @@
"provider": "launchpad",
"providerHow": "read-from-code",
"feature": "dashboard-switcher",
- "featureConfidence": "medium",
+ "featureConfidence": "high",
"origin": "own-code",
"evidence": {
"nextcloud-dashboard": "source read at v35.0.1: apps/dashboard/lib/Controller/DashboardController.php:52 single page, id-app-navigation null (:78) so no side navigation; searched apps/dashboard for parent, child, tree: none",
@@ -1220,8 +1630,8 @@
"reachedOn": "nothing: /api/dashboards/{uuid}/translations and /resolved have no caller in src/",
"provider": "launchpad",
"providerHow": "read-from-code",
- "feature": "dashboard-language-content",
- "featureConfidence": "medium",
+ "feature": "dashboard-switcher",
+ "featureConfidence": "high",
"origin": "own-code",
"evidence": {
"nextcloud-dashboard": "source read at v35.0.1: apps/dashboard/src/DashboardApp.vue:236-258 greetings and the widget titles from each app (e.g. apps/files/lib/Dashboard/FavoriteWidget.php:50) are translated with the reader's own language; apps/dashboard/l10n holds 56 languages. Note: there is no author-written dashboard content to translate",
@@ -1250,8 +1660,8 @@
"reachedOn": "nothing: the store actions fetchMetadataFields and updateDashboardMetadata have no component caller",
"provider": "launchpad",
"providerHow": "read-from-code",
- "feature": "dashboard-metadata-fields",
- "featureConfidence": "medium",
+ "feature": "dashboard-switcher",
+ "featureConfidence": "high",
"origin": "own-code",
"evidence": {
"nextcloud-dashboard": "source read at v35.0.1: apps/dashboard/lib/Service/DashboardService.php:33-38 a dashboard is only a list of widget ids; searched apps/dashboard for owner, department, metadata: none",
@@ -1278,7 +1688,8 @@
"spec": "openspec/specs/default-widget-bundle"
},
"reachedOn": "Workspace page, after creating a new personal dashboard",
- "source": "spec-round-2026-10-07"
+ "source": "spec-round-2026-10-07",
+ "feature": "dashboard-switcher"
},
{
"id": "tile-pin",
@@ -1397,7 +1808,7 @@
"provider": "launchpad",
"providerHow": "read-from-code",
"feature": "tiles",
- "featureConfidence": "medium",
+ "featureConfidence": "high",
"origin": "own-code"
},
{
@@ -1420,7 +1831,7 @@
"provider": "nextcloud-vue",
"providerHow": "read-from-code",
"feature": "tiles",
- "featureConfidence": "medium",
+ "featureConfidence": "high",
"origin": "own-code",
"evidence": {
"nextcloud-dashboard": "source read at v35.0.1: searched apps/dashboard/src and lib for bookmark, link list: none; the Bookmarks app is not in core/shipped.json",
@@ -1449,8 +1860,8 @@
"reachedOn": "clicks are recorded from the Workspace page; nothing shows them: getAnalyticsTopTiles and the tile CSV export have no caller in src/",
"provider": "launchpad",
"providerHow": "read-from-code",
- "feature": "prometheus-metrics",
- "featureConfidence": "low",
+ "feature": "dashboard-view-analytics",
+ "featureConfidence": "medium",
"origin": "own-code",
"evidence": {
"nextcloud-dashboard": "source read at v35.0.1: searched apps/dashboard for click, track, count, analytics: none; the page records nothing but the layout (apps/dashboard/lib/Controller/DashboardApiController.php:202-206)",
@@ -1485,6 +1896,7 @@
},
"provider": "launchpad",
"providerHow": "research-then-code",
+ "feature": "app-catalogue-and-launch",
"origin": "competitor",
"note": "Specified 2026-09-27 in openspec/changes/launcher-tile-sorting (openspec pass)."
},
@@ -1513,6 +1925,7 @@
},
"provider": "launchpad",
"providerHow": "research-then-code",
+ "feature": "app-catalogue-and-launch",
"origin": "competitor",
"note": "Specified 2026-09-27 in openspec/changes/launcher-tile-launch-types (openspec pass)."
},
@@ -1541,6 +1954,7 @@
},
"provider": "launchpad",
"providerHow": "research-then-code",
+ "feature": "app-catalogue-and-launch",
"origin": "competitor",
"note": "Specified 2026-09-27 in openspec/changes/launcher-tile-launch-types (openspec pass)."
},
@@ -1569,6 +1983,7 @@
},
"provider": "launchpad",
"providerHow": "research-then-code",
+ "feature": "app-catalogue-and-launch",
"origin": "competitor",
"note": "Specified 2026-09-27 in openspec/changes/launcher-tile-launch-types (openspec pass)."
},
@@ -1597,6 +2012,7 @@
},
"provider": "launchpad",
"providerHow": "research-then-code",
+ "feature": "app-catalogue-and-launch",
"origin": "competitor",
"note": "Specified 2026-09-27 in openspec/changes/launcher-app-catalogue (openspec pass)."
},
@@ -1625,6 +2041,7 @@
},
"provider": "launchpad",
"providerHow": "research-then-code",
+ "feature": "app-catalogue-and-launch",
"origin": "competitor",
"note": "Specified 2026-09-27 in openspec/changes/launcher-discover-server-services (openspec pass)."
},
@@ -1647,7 +2064,7 @@
"reachedOn": "Workspace page, navigation panel; edited on Admin settings, Organisation navigation tab",
"provider": "launchpad",
"providerHow": "read-from-code",
- "feature": "navigation-editor-org",
+ "feature": "organisation-navigation",
"featureConfidence": "high",
"origin": "own-code",
"evidence": {
@@ -1677,7 +2094,7 @@
"reachedOn": "Workspace page, Add widget, Text",
"provider": "nextcloud-vue",
"providerHow": "read-from-code",
- "feature": "text-display-widget",
+ "feature": "content-widgets",
"featureConfidence": "high",
"origin": "own-code",
"evidence": {
@@ -1707,8 +2124,8 @@
"reachedOn": "Workspace page, Add widget, Image",
"provider": "launchpad",
"providerHow": "read-from-code",
- "feature": "image-widget",
- "featureConfidence": "medium",
+ "feature": "content-widgets",
+ "featureConfidence": "high",
"origin": "own-code",
"evidence": {
"nextcloud-dashboard": "source read at v35.0.1: searched apps/dashboard for image widget, upload: none; the only image on the page is the theming background (apps/dashboard/src/DashboardApp.vue:569-575)",
@@ -1744,7 +2161,7 @@
},
"provider": "nextcloud-vue",
"providerHow": "read-from-code",
- "feature": "video-widget",
+ "feature": "content-widgets",
"featureConfidence": "high",
"origin": "own-code"
},
@@ -1804,7 +2221,7 @@
},
"provider": "launchpad",
"providerHow": "read-from-code",
- "feature": "news-widget",
+ "feature": "content-widgets",
"featureConfidence": "high",
"origin": "own-code"
},
@@ -1834,7 +2251,7 @@
},
"provider": "launchpad",
"providerHow": "read-from-code",
- "feature": "clock-weather-widgets",
+ "feature": "content-widgets",
"featureConfidence": "high",
"origin": "own-code"
},
@@ -1864,7 +2281,7 @@
},
"provider": "launchpad",
"providerHow": "read-from-code",
- "feature": "clock-weather-widgets",
+ "feature": "content-widgets",
"featureConfidence": "high",
"origin": "own-code"
},
@@ -1894,7 +2311,7 @@
},
"provider": "launchpad",
"providerHow": "read-from-code",
- "feature": "calendar-widget",
+ "feature": "content-widgets",
"featureConfidence": "high",
"origin": "own-code"
},
@@ -1924,7 +2341,7 @@
},
"provider": "launchpad",
"providerHow": "read-from-code",
- "feature": "files-widget",
+ "feature": "content-widgets",
"featureConfidence": "high",
"origin": "own-code"
},
@@ -1985,7 +2402,7 @@
"provider": "launchpad",
"providerHow": "read-from-code",
"feature": "people-widget",
- "featureConfidence": "medium",
+ "featureConfidence": "high",
"origin": "own-code"
},
{
@@ -2011,7 +2428,7 @@
"reachedOn": "Workspace page, Add widget, Header, Label or Divider",
"provider": "nextcloud-vue",
"providerHow": "read-from-code",
- "feature": "header-widget",
+ "feature": "content-widgets",
"featureConfidence": "high",
"origin": "own-code",
"evidence": {
@@ -2041,7 +2458,7 @@
"reachedOn": "Workspace page, Add widget, Link button",
"provider": "launchpad",
"providerHow": "read-from-code",
- "feature": "link-button-widget",
+ "feature": "content-widgets",
"featureConfidence": "high",
"origin": "own-code",
"evidence": {
@@ -2078,7 +2495,7 @@
},
"provider": "launchpad",
"providerHow": "read-from-code",
- "feature": "live-data-tile-widget",
+ "feature": "runtime-or-consumption",
"featureConfidence": "high",
"origin": "own-code"
},
@@ -2103,7 +2520,7 @@
"provider": "openregister",
"providerHow": "read-from-code",
"feature": "kpi-cards",
- "featureConfidence": "medium",
+ "featureConfidence": "high",
"origin": "own-code",
"note": "The widgets read OpenRegister over GraphQL. LaunchPad is OR-free by ADR, so without OpenRegister these widgets have no data source; this reading did not run one against a live register.",
"evidence": {
@@ -2141,7 +2558,7 @@
"provider": "launchpad",
"providerHow": "research-then-code",
"feature": "iframe-embed-widget",
- "featureConfidence": "low",
+ "featureConfidence": "high",
"origin": "competitor",
"note": "Only as a generic embedded page; no report picker and no row-level security hand-off."
},
@@ -2165,7 +2582,7 @@
"provider": "openregister",
"providerHow": "read-from-code",
"feature": "launchpad-spend-analytics-widget",
- "featureConfidence": "medium",
+ "featureConfidence": "high",
"origin": "own-code",
"note": "Marked beta in openspec/features.overlay.json; depends on sibling apps holding the data in OpenRegister.",
"evidence": {
@@ -2202,8 +2619,8 @@
},
"provider": "launchpad",
"providerHow": "research-then-code",
- "feature": "live-data-tile-widget",
- "featureConfidence": "low",
+ "feature": "runtime-or-consumption",
+ "featureConfidence": "high",
"origin": "competitor",
"note": "A live tile reads one value from a URL. There is no connector, authentication handling or data mapping as the competitor describes. Corrected 2026-09-27 (openspec pass): a connector mode exists; it calls Integriq's DashboardDatasourceService::resolve(), which applies the source's stored authentication (lib/Service/LiveTileService.php:380). What is missing is mapping more than one field and choosing how to show it. Specified 2026-09-27 in openspec/changes/integrations-live-tile-lists (openspec pass)."
},
@@ -2231,6 +2648,7 @@
},
"provider": "launchpad",
"providerHow": "research-then-code",
+ "feature": "runtime-or-consumption",
"origin": "competitor",
"note": "Decided no 2026-09-28 (build-all decision pass): Single competitor, no demand: Workspace 365 rated yes; no tender, feature request or roadmap row. Outside the core area (integrations). Reversible: reopen the row if demand appears."
},
@@ -2258,6 +2676,7 @@
},
"provider": "launchpad",
"providerHow": "research-then-code",
+ "feature": "runtime-or-consumption",
"origin": "competitor",
"note": "Decided no 2026-09-28 (build-all decision pass): Single competitor, no demand: Workspace 365 rated yes; no tender, feature request or roadmap row. Outside the core area (integrations). Reversible: reopen the row if demand appears."
},
@@ -2288,7 +2707,7 @@
"provider": "nextcloud",
"providerHow": "read-from-code",
"feature": "nc-dashboard-widget-proxy",
- "featureConfidence": "medium",
+ "featureConfidence": "high",
"origin": "own-code"
},
{
@@ -2318,7 +2737,7 @@
"provider": "nextcloud",
"providerHow": "read-from-code",
"feature": "nc-dashboard-widget-proxy",
- "featureConfidence": "medium",
+ "featureConfidence": "high",
"origin": "own-code"
},
{
@@ -2346,6 +2765,7 @@
},
"provider": "launchpad",
"providerHow": "research-then-code",
+ "feature": "runtime-or-consumption",
"origin": "competitor",
"note": "Specified 2026-09-27 in openspec/changes/integrations-office-tiles (openspec pass)."
},
@@ -2375,6 +2795,7 @@
},
"provider": "launchpad",
"providerHow": "research-then-code",
+ "feature": "runtime-or-consumption",
"origin": "competitor",
"note": "Corrected 2026-09-27 (openspec pass): the news widget reads RSS and Atom feeds from any allowed host (lib/Service/NewsWidgetService.php:133) and the embed widget frames an allowed page (lib/Service/IframeService.php:9), the same half Homarr and Dashy are rated partial for; the other intranet's documents are missing. Specified 2026-09-27 in openspec/changes/integrations-live-tile-lists (openspec pass)."
},
@@ -2398,7 +2819,7 @@
"provider": "integriq",
"providerHow": "read-from-code",
"feature": "runtime-or-consumption",
- "featureConfidence": "medium",
+ "featureConfidence": "high",
"origin": "own-code",
"note": "LaunchPad declares its six connections; the list and its status are integriq app_connection objects, so without integriq nothing shows them. Set building 2026-09-27 (openspec pass, coordinator): the integriq lane covers it in ConductionNL/integriq openspec/changes/connection-registry.",
"evidence": {
@@ -2548,7 +2969,7 @@
"reachedOn": "Workspace page, Visibility rules editor",
"provider": "launchpad",
"providerHow": "read-from-code",
- "feature": "conditional-visibility-editor",
+ "feature": "conditional-visibility",
"featureConfidence": "high",
"origin": "own-code",
"evidence": {
@@ -2704,6 +3125,7 @@
},
"provider": "launchpad",
"providerHow": "research-then-code",
+ "feature": "conditional-visibility",
"origin": "competitor",
"note": "Decided no 2026-09-28 (build-all decision pass): Single competitor, no demand: Workspace 365 rated yes; no tender, feature request or roadmap row. Outside the core area (targeting). Reversible: reopen the row if demand appears."
},
@@ -2731,6 +3153,7 @@
},
"provider": "launchpad",
"providerHow": "research-then-code",
+ "feature": "conditional-visibility",
"origin": "competitor",
"note": "Decided no 2026-09-28 (build-all decision pass): Single competitor, no demand: Workspace 365 rated yes; no tender, feature request or roadmap row. Outside the core area (targeting). Reversible: reopen the row if demand appears."
},
@@ -2753,7 +3176,7 @@
"reachedOn": "Admin settings, Sharing tab, group order",
"provider": "launchpad",
"providerHow": "read-from-code",
- "feature": "dashboards",
+ "feature": "admin-templates",
"featureConfidence": "medium",
"origin": "own-code",
"evidence": {
@@ -2910,8 +3333,8 @@
},
"provider": "launchpad",
"providerHow": "read-from-code",
- "feature": "dashboards",
- "featureConfidence": "high",
+ "feature": "admin-templates",
+ "featureConfidence": "medium",
"origin": "own-code"
},
{
@@ -2963,7 +3386,7 @@
"reachedOn": "Admin settings, Demo data tab; also occ launchpad demo-showcases install",
"provider": "launchpad",
"providerHow": "read-from-code",
- "feature": "demo-data-showcases",
+ "feature": "setup-wizard",
"featureConfidence": "high",
"origin": "own-code",
"evidence": {
@@ -3001,7 +3424,7 @@
"provider": "launchpad",
"providerHow": "read-from-code",
"feature": "admin-roles",
- "featureConfidence": "medium",
+ "featureConfidence": "high",
"origin": "own-code",
"note": "Roles are assigned in the setup wizard; this reading found no other screen that lists or edits them."
},
@@ -3024,7 +3447,7 @@
"reachedOn": "Admin settings, Operations tab",
"provider": "launchpad",
"providerHow": "read-from-code",
- "feature": "dashboard-bulk-operations",
+ "feature": "admin-tooling",
"featureConfidence": "high",
"origin": "own-code",
"evidence": {
@@ -3089,6 +3512,7 @@
},
"provider": "launchpad",
"providerHow": "research-then-code",
+ "feature": "confluence-html-import",
"origin": "competitor",
"note": "Decided no 2026-09-28 (build-all decision pass): No competitor rated yes; no tender, feature request or roadmap row. Outside the core area (rollout). Reversible: reopen the row if demand appears."
},
@@ -3142,7 +3566,7 @@
"provider": "openregister",
"providerHow": "read-from-code",
"feature": "dashboard-export-import",
- "featureConfidence": "medium",
+ "featureConfidence": "high",
"origin": "own-code",
"note": "Discovery runs in OpenRegister's GenericStoreService; install is LaunchPad's ImportService. Not run against a live registry in this reading. Specified in ConductionNL/openregister openspec/changes/store-over-federated-config (openregister lane, OpenSpec pass 2026-09-27).",
"evidence": {
@@ -3413,7 +3837,7 @@
"reachedOn": "nothing: the store actions publishDashboard and scheduleDashboard have no component caller",
"provider": "launchpad",
"providerHow": "read-from-code",
- "feature": "dashboards",
+ "feature": "dashboard-kiosk-mode",
"featureConfidence": "medium",
"origin": "own-code",
"evidence": {
@@ -3475,7 +3899,7 @@
"provider": "launchpad",
"providerHow": "read-from-code",
"feature": "orphaned-data-cleanup",
- "featureConfidence": "medium",
+ "featureConfidence": "high",
"origin": "own-code",
"evidence": {
"nextcloud-dashboard": "source read at v35.0.1: lib/private/User/User.php:315 deleting a user removes all their user config, including the dashboard layout; there is nothing shared to hand over and no transfer (searched apps/dashboard for owner, transfer: none)",
@@ -3509,6 +3933,7 @@
},
"provider": "launchpad",
"providerHow": "research-then-code",
+ "feature": "news-and-announcements",
"origin": "competitor",
"note": "Decided no 2026-09-28 (build-all decision pass): Single competitor, no demand: Microsoft Viva Connections rated yes; no tender, feature request or roadmap row. Outside the core area (sharing). Reversible: reopen the row if demand appears."
},
@@ -3531,7 +3956,7 @@
"reachedOn": "the prompt renders on the Workspace page, but no form sets requiresAcknowledgement; only PUT /api/widgets/{placementId} does",
"provider": "launchpad",
"providerHow": "read-from-code",
- "feature": "dashboard-acknowledgements",
+ "feature": "acknowledgements-and-reactions",
"featureConfidence": "high",
"origin": "own-code",
"evidence": {
@@ -3562,7 +3987,7 @@
"reachedOn": "Workspace page, Read receipts button",
"provider": "launchpad",
"providerHow": "read-from-code",
- "feature": "dashboard-acknowledgements",
+ "feature": "acknowledgements-and-reactions",
"featureConfidence": "high",
"origin": "own-code",
"evidence": {
@@ -3592,7 +4017,7 @@
"reachedOn": "nothing: the store actions addReaction and fetchReactionsSummary have no component caller",
"provider": "launchpad",
"providerHow": "read-from-code",
- "feature": "dashboard-reactions",
+ "feature": "acknowledgements-and-reactions",
"featureConfidence": "high",
"origin": "own-code",
"evidence": {
@@ -3629,6 +4054,7 @@
},
"provider": "launchpad",
"providerHow": "research-then-code",
+ "feature": "news-and-announcements",
"origin": "competitor",
"note": "Specified 2026-09-27 in openspec/changes/engagement-announcements (openspec pass)."
},
@@ -3656,6 +4082,7 @@
},
"provider": "launchpad",
"providerHow": "research-then-code",
+ "feature": "communities-events-and-knowledge",
"origin": "competitor",
"note": "Decided no 2026-09-28 (build-all decision pass): Single competitor, no demand: Workspace 365 rated yes; no tender, feature request or roadmap row. Outside the core area (engagement). Reversible: reopen the row if demand appears."
},
@@ -3683,6 +4110,7 @@
},
"provider": "launchpad",
"providerHow": "research-then-code",
+ "feature": "communities-events-and-knowledge",
"origin": "competitor",
"note": "Decided no 2026-09-28 (build-all decision pass): Single competitor, no demand: Workspace 365 rated yes; no tender, feature request or roadmap row. Outside the core area (engagement). Reversible: reopen the row if demand appears."
},
@@ -3711,6 +4139,7 @@
},
"provider": "launchpad",
"providerHow": "research-then-code",
+ "feature": "communities-events-and-knowledge",
"origin": "competitor",
"note": "Specified 2026-09-27 in openspec/changes/engagement-communities (openspec pass)."
},
@@ -3739,6 +4168,7 @@
},
"provider": "launchpad",
"providerHow": "research-then-code",
+ "feature": "news-and-announcements",
"origin": "competitor",
"note": "Specified 2026-09-27 in openspec/changes/engagement-activity-digest (openspec pass)."
},
@@ -3768,7 +4198,7 @@
"provider": "nextcloud",
"providerHow": "research-then-code",
"feature": "activity-feed-integration",
- "featureConfidence": "low",
+ "featureConfidence": "high",
"origin": "competitor",
"note": "LaunchPad writes its own events to the Nextcloud activity stream; gathering approvals and news from outside systems is not there. Decided no on 2026-09-27 (openspec pass): Owed to nextcloud/server: the personal activity stream is the Nextcloud activity app; LaunchPad already writes its events into it. Platform-provided, outside the fleet. Corrections round 8 (2026-09-28): LaunchPad writes only 2 of its 14 declared events into the activity stream, not all of them as the note above says, launchpad#713; rating kept because the row rates one personal stream of approvals, news and meetings from several systems, which the Nextcloud activity app provides and which this gap does not change."
},
@@ -3798,8 +4228,8 @@
},
"provider": "launchpad",
"providerHow": "research-then-code",
- "feature": "conditional-visibility",
- "featureConfidence": "low",
+ "feature": "news-and-announcements",
+ "featureConfidence": "medium",
"origin": "competitor",
"note": "Possible by hand with a date-limited text widget; there is no notice type. Specified 2026-09-27 in openspec/changes/engagement-announcements (openspec pass)."
},
@@ -3859,7 +4289,7 @@
"provider": "nextcloud",
"providerHow": "research-then-code",
"feature": "nc-unified-search-integration",
- "featureConfidence": "low",
+ "featureConfidence": "high",
"origin": "competitor",
"note": "Nextcloud unified search covers installed Nextcloud apps; outside systems appear only if an app provides a search provider. Decided no on 2026-09-27 (openspec pass): Owed to nextcloud/server: unified search across mail, files and connected systems is Nextcloud server's unified search; outside systems join it through their own search providers. Platform-provided, outside the fleet."
},
@@ -3891,7 +4321,7 @@
"provider": "launchpad",
"providerHow": "research-then-code",
"feature": "launchpad-ai-dashboard-assistant",
- "featureConfidence": "medium",
+ "featureConfidence": "high",
"origin": "competitor"
},
{
@@ -3945,8 +4375,8 @@
"note": "corrections round 9 (2026-09-28): built.change names openspec/changes/mcp-tool-surface, open on development at 9d5535a with 0 of 28 tasks checked, whose proposal specifies a LaunchpadToolProvider with 14 read and 35 write MCP tools over dashboards, widgets, tiles and sharing; lib/Mcp does not exist yet, so the rating stays no; source: the corrections-9 list of specified rows that named no change.",
"provider": "launchpad",
"providerHow": "read-from-code",
- "feature": "mcp-tool-surface",
- "featureConfidence": "medium",
+ "feature": "launchpad-ai-dashboard-assistant",
+ "featureConfidence": "high",
"origin": "own-code",
"evidence": {
"nextcloud-dashboard": "source read at v35.0.1: apps/dashboard/openapi.json documents OCS endpoints to list widgets and read or set the layout (apps/dashboard/lib/Controller/DashboardApiController.php:137,188,202), usable by any client with an app password; there is no agent or MCP tool definition in apps/dashboard",
@@ -3980,6 +4410,7 @@
},
"provider": "launchpad",
"providerHow": "research-then-code",
+ "feature": "launchpad-ai-dashboard-assistant",
"origin": "competitor",
"note": "Decided no 2026-09-28 (build-all decision pass): Single competitor, no demand: Workspace 365 rated yes; no tender, feature request or roadmap row. Outside the core area (search-ai). Reversible: reopen the row if demand appears."
},
@@ -4007,6 +4438,7 @@
},
"provider": "launchpad",
"providerHow": "research-then-code",
+ "feature": "launchpad-ai-dashboard-assistant",
"origin": "competitor",
"note": "Decided no 2026-09-28 (build-all decision pass): Single competitor, no demand: Workspace 365 rated yes; no tender, feature request or roadmap row. Outside the core area (search-ai). Reversible: reopen the row if demand appears."
},
@@ -4155,8 +4587,8 @@
},
"provider": "nextcloud",
"providerHow": "research-then-code",
- "feature": "activity-feed-integration",
- "featureConfidence": "low",
+ "feature": "launchpad-compliance-audit-panel",
+ "featureConfidence": "medium",
"origin": "competitor",
"note": "Bulk operations, template resyncs and acknowledgements write activity events; that is an activity stream, not a compliance audit log. Decided no on 2026-09-27 (openspec pass): Owed to nextcloud/server: the change log for review is Nextcloud's admin_audit app. Platform-provided, outside the fleet."
},
@@ -4180,7 +4612,7 @@
"provider": "nextcloud",
"providerHow": "read-from-code",
"feature": "activity-feed-integration",
- "featureConfidence": "medium",
+ "featureConfidence": "high",
"origin": "own-code",
"evidence": {
"nextcloud-dashboard": "source read at v35.0.1: searched apps/dashboard for activity, IManager publish: none; layout saves (apps/dashboard/lib/Controller/DashboardApiController.php:202-206) emit no activity",
@@ -4240,7 +4672,7 @@
"reachedOn": "occ launchpad commands",
"provider": "launchpad",
"providerHow": "read-from-code",
- "feature": "cli-commands",
+ "feature": "admin-tooling",
"featureConfidence": "high",
"origin": "own-code",
"evidence": {
@@ -4270,7 +4702,7 @@
"reachedOn": "the footer renders on the Workspace page; /api/admin/footer-settings has no caller in src/, so no screen edits it",
"provider": "launchpad",
"providerHow": "read-from-code",
- "feature": "footer-customization",
+ "feature": "organisation-navigation",
"featureConfidence": "high",
"origin": "own-code",
"evidence": {
@@ -4304,7 +4736,7 @@
"provider": "launchpad",
"providerHow": "read-from-code",
"feature": "iframe-embed-widget",
- "featureConfidence": "medium",
+ "featureConfidence": "high",
"origin": "own-code",
"evidence": {
"nextcloud-dashboard": "source read at v35.0.1: searched apps/dashboard for allowlist, domain, csp: the only policy is geolocation for self (apps/dashboard/lib/Controller/DashboardController.php:82-85); widgets are server-side app code, and weather data is fetched by the server (apps/dashboard/src/DashboardApp.vue:118)",
@@ -4333,7 +4765,7 @@
"reachedOn": "every icon upload through /api/resources",
"provider": "launchpad",
"providerHow": "read-from-code",
- "feature": "resource-uploads",
+ "feature": "tiles",
"featureConfidence": "high",
"origin": "own-code",
"evidence": {
@@ -4370,6 +4802,7 @@
},
"provider": "nextcloud",
"providerHow": "read-from-code",
+ "feature": "launchpad-enterprise-security-access",
"origin": "own-code"
},
{
@@ -4428,6 +4861,7 @@
},
"provider": "nextcloud",
"providerHow": "read-from-code",
+ "feature": "self-hosting",
"origin": "own-code"
},
{
@@ -4456,6 +4890,7 @@
},
"provider": "nextcloud",
"providerHow": "read-from-code",
+ "feature": "theming-and-languages",
"origin": "own-code"
},
{
@@ -4483,6 +4918,7 @@
},
"provider": "launchpad",
"providerHow": "research-then-code",
+ "feature": "theming-and-languages",
"origin": "competitor",
"note": "Specified 2026-09-27 in openspec/changes/platform-dashboard-theme-and-css (openspec pass)."
},
@@ -4510,6 +4946,7 @@
},
"provider": "launchpad",
"providerHow": "research-then-code",
+ "feature": "theming-and-languages",
"origin": "competitor",
"note": "Decided no 2026-09-28 (build-all decision pass): Single competitor, no demand: Nextcloud Dashboard (built-in) rated yes; no tender, feature request or roadmap row. Outside the core area (platform). Reversible: reopen the row if demand appears."
},
@@ -4539,8 +4976,8 @@
},
"provider": "launchpad",
"providerHow": "read-from-code",
- "feature": "dashboards",
- "featureConfidence": "low",
+ "feature": "theming-and-languages",
+ "featureConfidence": "medium",
"origin": "own-code"
},
{
@@ -4562,8 +4999,8 @@
"reachedOn": "the app installs and boots with no OpenRegister; OpenRegister-backed widgets are feature-detected",
"provider": "launchpad",
"providerHow": "read-from-code",
- "feature": "runtime-or-consumption",
- "featureConfidence": "high",
+ "feature": "self-hosting",
+ "featureConfidence": "medium",
"origin": "own-code",
"evidence": {
"nextcloud-dashboard": "source read at v35.0.1: core/shipped.json lists dashboard in shippedApps and defaultEnabled; apps/dashboard/appinfo/info.xml:25 depends only on Nextcloud 35",
@@ -4597,6 +5034,7 @@
},
"provider": "launchpad",
"providerHow": "research-then-code",
+ "feature": "self-hosting",
"origin": "competitor",
"note": "Decided no 2026-09-28 (build-all decision pass): Single competitor, no demand: Workspace 365 rated yes; no tender, feature request or roadmap row. Outside the core area (platform). Reversible: reopen the row if demand appears."
},
@@ -4627,7 +5065,7 @@
"provider": "launchpad",
"providerHow": "read-from-code",
"feature": "grid-layout",
- "featureConfidence": "medium",
+ "featureConfidence": "high",
"origin": "featureRequest",
"originUrl": "https://github.com/nextcloud/server/issues/24658",
"note": "A placed widget stays clickable in the picker (WidgetPickerModal.vue:36, :165 isPlaced only styles it) and the placement table has no unique index on dashboard and widget (PlacementTableBuilder.php:239-248)."
@@ -4656,8 +5094,8 @@
},
"provider": "launchpad",
"providerHow": "read-from-code",
- "feature": null,
- "featureConfidence": "low",
+ "feature": "nc-dashboard-widget-proxy",
+ "featureConfidence": "medium",
"origin": "featureRequest",
"originUrl": "https://github.com/nextcloud/server/issues/22897",
"note": "Searched src and lib for circles, teams and ITeamManager: only a 'People & teams' label in src/dialogs/CreateGroupDashboardModal.vue:152, which picks a group dashboard template. No widget shows Nextcloud Teams or their shared resources. Decided no 2026-09-28 (build-all decision pass): No competitor rated yes (Workspace 365, Microsoft Viva Connections partial); a feature request row but the rule needs demand plus one competitor yes. Outside the core area (widgets). Reversible: reopen the row if demand appears."
@@ -4687,7 +5125,7 @@
"provider": "launchpad",
"providerHow": "read-from-code",
"feature": "dashboard-export-import",
- "featureConfidence": "medium",
+ "featureConfidence": "high",
"origin": "featureRequest",
"originUrl": "https://github.com/nextcloud/server/issues/57423",
"note": "Searched lib for IMigrator and UserMigration: LaunchPad registers no user_migration migrator, so a moved account arrives without its dashboards. Admin export and import of dashboards exists (row r-export), which is a different path. Decided no 2026-09-28 (build-all decision pass): No competitor rated yes (Homarr, Dashy partial); a feature request row but the rule needs demand plus one competitor yes. Outside the core area (rollout). Reversible: reopen the row if demand appears."
@@ -4719,7 +5157,7 @@
"provider": "launchpad",
"providerHow": "read-from-code",
"feature": "tiles",
- "featureConfidence": "medium",
+ "featureConfidence": "high",
"origin": "featureRequest",
"originUrl": "https://github.com/nextcloud/server/issues/51332",
"note": "The target is fixed by link type: web-address tiles always open in a new tab and app tiles in the same tab (TileWidget.vue:41). Nobody can choose it per tile, per dashboard or per user. Also mined from https://github.com/homarr-labs/homarr/issues/3778. Specified 2026-09-27 in openspec/changes/launcher-tile-open-behaviour (openspec pass)."
@@ -4750,7 +5188,7 @@
},
"provider": "launchpad",
"providerHow": "read-from-code",
- "feature": "nc-dashboard-widget-proxy",
+ "feature": "content-widgets",
"featureConfidence": "medium",
"origin": "changelog",
"originUrl": "https://github.com/nextcloud/server/pull/62624",
@@ -4782,7 +5220,7 @@
},
"provider": "nextcloud",
"providerHow": "read-from-code",
- "feature": "conditional-visibility",
+ "feature": "launchpad-enterprise-security-access",
"featureConfidence": "medium",
"origin": "changelog",
"originUrl": "https://github.com/homarr-labs/homarr/releases/tag/v1.66.1",
@@ -4814,8 +5252,8 @@
},
"provider": "nextcloud-vue",
"providerHow": "read-from-code",
- "feature": null,
- "featureConfidence": "low",
+ "feature": "setup-wizard",
+ "featureConfidence": "medium",
"origin": "changelog",
"originUrl": "https://github.com/homarr-labs/homarr/releases/tag/v1.64.0",
"note": "The manifest declares a first-visit walkthrough (manifest.json:471-500, trigger first-visit, per-user completion key) that the shared shell renders: nextcloud-vue CnAppRoot.vue:464 mounts CnWalkthrough from manifest.walkthrough (nextcloud-vue development d239cf54), and App.vue:16 mounts CnAppRoot."
@@ -4846,8 +5284,8 @@
},
"provider": "nextcloud",
"providerHow": "read-from-code",
- "feature": null,
- "featureConfidence": "low",
+ "feature": "launchpad-mobile-remote-access",
+ "featureConfidence": "medium",
"origin": "changelog",
"originUrl": "https://github.com/homarr-labs/homarr/releases/tag/v1.61.0",
"note": "The web app manifest is Nextcloud's: nextcloud/server v35.0.1 apps/theming/lib/Controller/ThemingController.php:459-510 serves a per-app manifest with start_url and display standalone (route /apps/theming/manifest/{app}). LaunchPad adds nothing of its own; the cited LaunchPad line is its use of the theme variables the installed app inherits."
@@ -4878,8 +5316,8 @@
},
"provider": "launchpad",
"providerHow": "read-from-code",
- "feature": null,
- "featureConfidence": "low",
+ "feature": "admin-tooling",
+ "featureConfidence": "medium",
"origin": "changelog",
"originUrl": "https://github.com/homarr-labs/homarr/releases/tag/v1.50.0",
"note": "LaunchPad exposes about 180 JSON routes (appinfo/routes.php) that work with a Nextcloud app password, but ships no OpenAPI file or public API reference (searched the repo for openapi: only the OpenRegister register files). Usable, not documented. Also mined from https://github.com/Lissy93/dashy/releases/tag/4.4.0. Specified 2026-09-27 in openspec/changes/governance-rest-api-reference (openspec pass)."
@@ -4910,8 +5348,8 @@
},
"provider": "launchpad",
"providerHow": "read-from-code",
- "feature": "live-data-tile-widget",
- "featureConfidence": "medium",
+ "feature": "runtime-or-consumption",
+ "featureConfidence": "high",
"origin": "changelog",
"originUrl": "https://github.com/homarr-labs/homarr/releases/tag/v1.48.0",
"note": "Every outbound fetch goes through Nextcloud's IClientService (LiveTileService.php:455, NewsWidgetService.php:135, CalendarWidgetService.php:119, WeatherService, HealthPingService, IframeService.php:94, FeedRefreshService), which applies the server's proxy setting from config.php. Searched lib for curl_, file_get_contents and new Client: none."
@@ -4940,8 +5378,8 @@
},
"provider": "nextcloud",
"providerHow": "read-from-code",
- "feature": null,
- "featureConfidence": "low",
+ "feature": "launchpad-enterprise-security-access",
+ "featureConfidence": "medium",
"origin": "featureRequest",
"originUrl": "https://github.com/homarr-labs/homarr/issues/5154",
"note": "not checked: single logout is the Nextcloud login app's job (user_oidc or user_saml), which was not read in this pass; LaunchPad has no logout code of its own (searched src and lib for logout and end_session). Decided no on 2026-09-27 (openspec pass): Owed to nextcloud/server: single logout is done by the Nextcloud login apps (user_oidc, user_saml), not by LaunchPad. Platform-provided, outside the fleet."
@@ -4972,7 +5410,7 @@
"provider": "launchpad",
"providerHow": "read-from-code",
"feature": "tiles",
- "featureConfidence": "medium",
+ "featureConfidence": "high",
"origin": "featureRequest",
"originUrl": "https://github.com/homarr-labs/homarr/issues/1014",
"note": "Searched src and lib for bookmark import, netscape and html import: no importer for browser bookmark files; tiles are made one at a time or arrive through dashboard import (row r-export). Specified 2026-09-27 in openspec/changes/launcher-bookmark-import (openspec pass)."
@@ -5003,8 +5441,8 @@
},
"provider": "nextcloud-vue",
"providerHow": "read-from-code",
- "feature": "text-display-widget",
- "featureConfidence": "medium",
+ "feature": "content-widgets",
+ "featureConfidence": "high",
"origin": "featureRequest",
"originUrl": "https://github.com/Lissy93/dashy/issues/636",
"note": "A text widget on your own dashboard can hold notes (widget-types.json:37, rendered by nextcloud-vue), but it is edited in dashboard edit mode, not a quick notepad you type into in place. Specified in ConductionNL/nextcloud-vue openspec/changes/dashboard-notepad-widget (nextcloud-vue lane, OpenSpec pass 2026-09-27)."
@@ -5035,7 +5473,7 @@
"provider": "launchpad",
"providerHow": "read-from-code",
"feature": "tiles",
- "featureConfidence": "medium",
+ "featureConfidence": "high",
"origin": "changelog",
"originUrl": "https://github.com/Lissy93/dashy/releases/tag/4.4.0",
"note": "A tile holds one address (TileWidget.vue:41 and :158 split only Nextcloud-internal paths from external URLs); searched src and lib for internal, external and network-aware URLs: nothing picks an address by the network you are on. Specified 2026-09-27 in openspec/changes/launcher-tile-internal-address (openspec pass)."
@@ -5066,7 +5504,7 @@
},
"provider": "launchpad",
"providerHow": "read-from-code",
- "feature": "dashboards",
+ "feature": "theming-and-languages",
"featureConfidence": "medium",
"origin": "changelog",
"originUrl": "https://github.com/Lissy93/dashy/releases/tag/4.0.0",
@@ -5098,7 +5536,7 @@
},
"provider": "launchpad",
"providerHow": "read-from-code",
- "feature": "iframe-embed-widget",
+ "feature": "tiles",
"featureConfidence": "medium",
"origin": "changelog",
"originUrl": "https://github.com/Lissy93/dashy/releases/tag/3.2.0",
@@ -5129,8 +5567,8 @@
},
"provider": "launchpad",
"providerHow": "read-from-code",
- "feature": null,
- "featureConfidence": "low",
+ "feature": "nc-unified-search-integration",
+ "featureConfidence": "medium",
"origin": "changelog",
"originUrl": "https://github.com/Lissy93/dashy/releases/tag/3.2.0",
"note": "The search widget filters tiles and falls back to one configured web search per widget (SearchWidget.vue:178 onSearchFallback, SearchWidgetForm.vue:41); searched for prefix, bang and engine routing: none. Specified 2026-09-27 in openspec/changes/search-ai-prefix-shortcuts (openspec pass)."
@@ -5161,8 +5599,8 @@
},
"provider": "nextcloud",
"providerHow": "read-from-code",
- "feature": null,
- "featureConfidence": "low",
+ "feature": "theming-and-languages",
+ "featureConfidence": "medium",
"origin": "changelog",
"originUrl": "https://github.com/Lissy93/dashy/releases/tag/3.2.0",
"note": "LaunchPad draws with Nextcloud's theme variables (WorkspaceApp.vue:480, :500), and Nextcloud's theming app switches them by the device's colour scheme through media-query themes (nextcloud/server v35.0.1 apps/theming/lib/Service/ThemeInjectionService.php:39-48). Also mined from https://www.microsoft.com/releasecommunications/api/v1/m365."
@@ -5193,7 +5631,7 @@
"provider": "launchpad",
"providerHow": "read-from-code",
"feature": "tiles",
- "featureConfidence": "medium",
+ "featureConfidence": "high",
"origin": "changelog",
"originUrl": "https://github.com/Lissy93/dashy/releases/tag/4.6.0",
"note": "Tiles sit where they were placed on the grid; searched TileWidget, the links and quick links widgets for sortBy, sortOrder and sortMode: none. Only the people widget has a sort (PeopleWidget.vue:120). Specified 2026-09-27 in openspec/changes/launcher-tile-sorting (openspec pass)."
@@ -5222,8 +5660,8 @@
},
"provider": "launchpad",
"providerHow": "read-from-code",
- "feature": null,
- "featureConfidence": "low",
+ "feature": "launchpad-ai-dashboard-assistant",
+ "featureConfidence": "medium",
"origin": "changelog",
"originUrl": "https://support.workspace365.net/en/articles/747342-release-notes-workspace-365-v4-45",
"note": "Searched src and lib for assistant, TextProcessing and ITaskProcessing: no writing help. LaunchPad has no announcement editor to attach one to (row e-announcements is no). Decided no 2026-09-28 (build-all decision pass): Single competitor, no demand: Workspace 365 rated yes; no tender, feature request or roadmap row. Outside the core area (search-ai). Reversible: reopen the row if demand appears."
@@ -5252,7 +5690,7 @@
},
"provider": "launchpad",
"providerHow": "read-from-code",
- "feature": "calendar-widget",
+ "feature": "launchpad-meeting-calendar-actions",
"featureConfidence": "medium",
"origin": "changelog",
"originUrl": "https://support.workspace365.net/en/articles/747342-release-notes-workspace-365-v4-45",
@@ -5285,7 +5723,7 @@
"provider": "launchpad",
"providerHow": "read-from-code",
"feature": "people-widget",
- "featureConfidence": "medium",
+ "featureConfidence": "high",
"origin": "changelog",
"originUrl": "https://support.workspace365.net/en/articles/766599-release-notes-workspace-365-v4-46",
"note": "The people widget shows the standard Nextcloud profile fields (PeopleWidgetService.php:91-103: phone, address, organisation, role, headline, biography, pronouns, birthdate), which a login app can fill from the identity provider. No custom fields such as office location or cost centre. Specified 2026-09-27 in openspec/changes/widgets-people-expertise-and-fields (openspec pass)."
@@ -5315,7 +5753,7 @@
},
"provider": "launchpad",
"providerHow": "read-from-code",
- "feature": "news-widget",
+ "feature": "news-and-announcements",
"featureConfidence": "medium",
"origin": "roadmap",
"originUrl": "https://portal.productboard.com/iqfsnhkpzih6grzjwagif2ek/tabs/3-launched",
@@ -5345,8 +5783,8 @@
},
"provider": "launchpad",
"providerHow": "read-from-code",
- "feature": null,
- "featureConfidence": "low",
+ "feature": "communities-events-and-knowledge",
+ "featureConfidence": "medium",
"origin": "roadmap",
"originUrl": "https://portal.productboard.com/iqfsnhkpzih6grzjwagif2ek/tabs/4-planned",
"note": "LaunchPad runs no internal events (row e-events is no); the calendar widget only reads calendars. Decided no 2026-09-28 (build-all decision pass): No competitor rated yes (Nextcloud Dashboard (built-in), Homarr, Dashy partial); a roadmap row but the rule needs demand plus one competitor yes. Outside the core area (engagement). Reversible: reopen the row if demand appears."
@@ -5375,8 +5813,8 @@
},
"provider": "launchpad",
"providerHow": "read-from-code",
- "feature": null,
- "featureConfidence": "low",
+ "feature": "communities-events-and-knowledge",
+ "featureConfidence": "medium",
"origin": "roadmap",
"originUrl": "https://portal.productboard.com/iqfsnhkpzih6grzjwagif2ek/tabs/4-planned",
"note": "Searched src and lib for review reminders, owner reminders and content expiry: none. Dashboards have a publication status (DashboardTableBuilder.php:435) but nobody is reminded to review them. Decided no 2026-09-28 (build-all decision pass): No competitor rated yes; a roadmap row but the rule needs demand plus one competitor yes. Outside the core area (governance). Reversible: reopen the row if demand appears."
@@ -5405,8 +5843,8 @@
},
"provider": "launchpad",
"providerHow": "read-from-code",
- "feature": null,
- "featureConfidence": "low",
+ "feature": "kpi-cards",
+ "featureConfidence": "medium",
"origin": "roadmap",
"originUrl": "https://www.microsoft.com/releasecommunications/api/v1/m365",
"note": "Charts are configured by hand over OpenRegister data (row w-charts); the AI dashboard assistant spec exists (openspec/specs/launchpad-ai-dashboard-assistant) but no page reaches it (row a-place-widget is no). Decided no 2026-09-28 (build-all decision pass): No competitor rated yes (Microsoft Viva Connections, Homarr partial); a roadmap row but the rule needs demand plus one competitor yes. Outside the core area (widgets). Reversible: reopen the row if demand appears."
@@ -5438,7 +5876,7 @@
"provider": "dossiq",
"providerHow": "read-from-code",
"feature": "nc-dashboard-widget-proxy",
- "featureConfidence": "medium",
+ "featureConfidence": "high",
"origin": "tender",
"originUrl": "https://www.tenderned.nl/aankondigingen/overzicht/296963",
"note": "LaunchPad has no work queue of its own; it can show a case app's Nextcloud widget. dossiq registers 'My tasks' and 'Cases overview' widgets (ConductionNL/dossiq development 00869dd7 lib/AppInfo/Registrar/AppHostRegistrar.php:126-132, lib/Dashboard/MyTasksWidget.php:42 a legacy IWidget) and LaunchPad's bridge catches OCA.Dashboard.register for such widgets (widgetBridge.js:29-62). Whether they render end to end is pending q-sibling-widgets. Also mined from https://www.tenderned.nl/aankondigingen/overzicht/237144, https://www.tenderned.nl/aankondigingen/overzicht/416075. Tender text: Noorderzijlvest 2023: 'Startscherm/medewerkersportaal met doorklik naar werkvoorraad'; Nationale ombudsman 2021: 'persoonlijk dashboard ... Overzicht lopende (eigen) zaken'; Súdwest-Fryslân 2026: dashboard with 'alle openstaande signaleringen, actiepunten, issues' at a glance, set up by the user with widgets. Specified 2026-09-27 (openspec pass, coordinator) in ConductionNL/dossiq openspec/changes/widgets-my-open-cases-on-the-start-page."
@@ -5460,7 +5898,8 @@
"spec": "openspec/specs/attention-feed"
},
"reachedOn": "Workspace page, Add widget, First today",
- "source": "spec-round-2026-10-07"
+ "source": "spec-round-2026-10-07",
+ "feature": "nc-dashboard-widget-proxy"
},
{
"id": "w-menu",
@@ -5479,7 +5918,8 @@
"change": "openspec/changes/archive/2026-05-03-menu-widget"
},
"reachedOn": "Workspace page, Add widget, Menu",
- "source": "spec-round-2026-10-07"
+ "source": "spec-round-2026-10-07",
+ "feature": "organisation-navigation"
},
{
"id": "w-quicklinks",
@@ -5498,7 +5938,8 @@
"change": "openspec/changes/archive/2026-05-03-quicklinks-widget"
},
"reachedOn": "Workspace page, Add widget, Quick links",
- "source": "spec-round-2026-10-07"
+ "source": "spec-round-2026-10-07",
+ "feature": "tiles"
},
{
"id": "d-public-start",
@@ -5527,7 +5968,7 @@
"provider": "launchpad",
"providerHow": "read-from-code",
"feature": "dashboard-public-share",
- "featureConfidence": "medium",
+ "featureConfidence": "high",
"origin": "tender",
"originUrl": "https://www.publicprocurement.be/nl/publication-workspaces/204ca8ae-6191-43b9-ac3b-c943f91d1d2d",
"note": "A dashboard can be published read-only on a public link (routes.php:189-212, PublicShareController.php:248 #[PublicPage]), but signed-out visitors at the Nextcloud address still get the login page; nothing makes a public dashboard the start page for them. Tender text: VVSG Blink 2025: 'Er is een publieke startpagina die kan aangepast worden en die verschilt van de inlogpagina'. Specified 2026-09-27 in openspec/changes/sharing-public-start-page (openspec pass)."
@@ -5559,7 +6000,7 @@
"provider": "launchpad",
"providerHow": "read-from-code",
"feature": "people-widget",
- "featureConfidence": "medium",
+ "featureConfidence": "high",
"origin": "tender",
"originUrl": "https://www.tenderned.nl/aankondigingen/overzicht/418890",
"note": "The people widget filters only by group (PeopleWidgetService.php:462-470 extractGroupFilter) and shows biography and headline but cannot be searched by them; searched for skills, expertise and tags: none. Tender text: Gemeente Noordwijk 2026: 'Onze persoonlijke trefwoorden (profiel tags) op ons intranet-profiel zijn volledig en actueel' so KCC colleagues can find them. Specified 2026-09-27 in openspec/changes/widgets-people-expertise-and-fields (openspec pass)."
From 4e747448d5a5578e46c6542060e635a42eb972bf Mon Sep 17 00:00:00 2001
From: Ruben van der Linde
Date: Thu, 8 Oct 2026 16:23:48 +0200
Subject: [PATCH 28/60] chore(parity): every spec under a feature, every
capability on a screen
---
openspec/parity/capabilities.json | 1248 ++++++++++++++++++++++++++---
1 file changed, 1130 insertions(+), 118 deletions(-)
diff --git a/openspec/parity/capabilities.json b/openspec/parity/capabilities.json
index 4527287c9..eeb0f93ac 100644
--- a/openspec/parity/capabilities.json
+++ b/openspec/parity/capabilities.json
@@ -404,7 +404,9 @@
"specs": [
"admin-settings",
"grid-layout",
- "widgets"
+ "widgets",
+ "container-widget",
+ "dashboard-locking"
]
},
{
@@ -413,7 +415,9 @@
"title_nl": "Versiebeheer en terugdraaien",
"area": "building",
"overlay": "dashboard-versioning",
- "specs": []
+ "specs": [
+ "dashboard-versioning"
+ ]
},
{
"slug": "permissions",
@@ -432,7 +436,8 @@
"area": "rollout",
"overlay": "dashboard-export-import",
"specs": [
- "dashboard-export-import"
+ "dashboard-export-import",
+ "dashboard-store"
]
},
{
@@ -446,7 +451,11 @@
"dashboard-icons",
"dashboards",
"default-widget-bundle",
- "runtime-shell"
+ "runtime-shell",
+ "dashboard-language-content",
+ "dashboard-metadata-fields",
+ "dashboard-switcher",
+ "effective-default-marker"
]
},
{
@@ -460,7 +469,11 @@
"launcher-tile-internal-address",
"launcher-tile-open-behaviour",
"launcher-tile-sorting",
- "tiles"
+ "tiles",
+ "links-widget",
+ "quicklinks-widget",
+ "service-health-ping",
+ "tile-quick-search"
]
},
{
@@ -482,7 +495,11 @@
"title_nl": "Organisatiemenu en voettekst",
"area": "launcher",
"overlay": null,
- "specs": []
+ "specs": [
+ "footer-customization",
+ "menu-widget",
+ "navigation-editor-org"
+ ]
},
{
"slug": "nc-dashboard-widget-proxy",
@@ -491,7 +508,9 @@
"area": "widgets",
"overlay": "nc-dashboard-widget-proxy",
"specs": [
- "attention-feed"
+ "attention-feed",
+ "legacy-widget-bridge",
+ "nc-dashboard-widget-proxy"
]
},
{
@@ -501,7 +520,19 @@
"area": "widgets",
"overlay": null,
"specs": [
- "widgets"
+ "widgets",
+ "calendar-widget",
+ "clock-weather-widgets",
+ "divider-widget",
+ "files-widget",
+ "header-widget",
+ "image-widget",
+ "label-widget",
+ "launchpad-file-access-widget",
+ "link-button-widget",
+ "news-widget",
+ "text-display-widget",
+ "video-widget"
]
},
{
@@ -521,7 +552,8 @@
"area": "widgets",
"overlay": null,
"specs": [
- "adopt-connection-registry"
+ "adopt-connection-registry",
+ "iframe-embed-widget"
]
},
{
@@ -534,7 +566,8 @@
"adopt-connection-registry",
"background-job-feed-refresh",
"integrations-live-tile-lists",
- "integrations-office-tiles"
+ "integrations-office-tiles",
+ "live-data-tile-widget"
]
},
{
@@ -551,7 +584,9 @@
"title_nl": "Uitgavenanalyse-kaart",
"area": "widgets",
"overlay": "launchpad-spend-analytics-widget",
- "specs": []
+ "specs": [
+ "launchpad-spend-analytics-widget"
+ ]
},
{
"slug": "conditional-visibility",
@@ -560,7 +595,8 @@
"area": "targeting",
"overlay": "conditional-visibility",
"specs": [
- "conditional-visibility"
+ "conditional-visibility",
+ "conditional-visibility-editor"
]
},
{
@@ -593,7 +629,10 @@
"area": "rollout",
"overlay": null,
"specs": [
- "runtime-shell"
+ "runtime-shell",
+ "demo-data-showcases",
+ "first-time-setup",
+ "setup-wizard"
]
},
{
@@ -602,7 +641,9 @@
"title_nl": "Dashboardquota",
"area": "rollout",
"overlay": "dashboard-quota-limits",
- "specs": []
+ "specs": [
+ "dashboard-quota-limits"
+ ]
},
{
"slug": "confluence-html-import",
@@ -610,7 +651,9 @@
"title_nl": "Confluence-import",
"area": "rollout",
"overlay": "confluence-html-import",
- "specs": []
+ "specs": [
+ "confluence-html-import"
+ ]
},
{
"slug": "dashboard-sharing",
@@ -629,7 +672,8 @@
"area": "sharing",
"overlay": "dashboard-public-share",
"specs": [
- "sharing-public-start-page"
+ "sharing-public-start-page",
+ "dashboard-public-share"
]
},
{
@@ -638,7 +682,9 @@
"title_nl": "Kiosk- en narrowcastmodus",
"area": "sharing",
"overlay": "dashboard-kiosk-mode",
- "specs": []
+ "specs": [
+ "dashboard-kiosk-mode"
+ ]
},
{
"slug": "news-and-announcements",
@@ -657,7 +703,10 @@
"title_nl": "Leesbevestigingen, reacties en opmerkingen",
"area": "engagement",
"overlay": null,
- "specs": []
+ "specs": [
+ "dashboard-acknowledgements",
+ "dashboard-reactions"
+ ]
},
{
"slug": "communities-events-and-knowledge",
@@ -675,7 +724,9 @@
"title_nl": "Activiteitenoverzicht",
"area": "engagement",
"overlay": "activity-feed-integration",
- "specs": []
+ "specs": [
+ "activity-feed-integration"
+ ]
},
{
"slug": "nc-unified-search-integration",
@@ -684,7 +735,8 @@
"area": "search-ai",
"overlay": "nc-unified-search-integration",
"specs": [
- "search-ai-prefix-shortcuts"
+ "search-ai-prefix-shortcuts",
+ "nc-unified-search-integration"
]
},
{
@@ -694,7 +746,8 @@
"area": "search-ai",
"overlay": "launchpad-ai-dashboard-assistant",
"specs": [
- "mcp-tool-surface"
+ "mcp-tool-surface",
+ "launchpad-ai-dashboard-assistant"
]
},
{
@@ -703,7 +756,9 @@
"title_nl": "Gebruiksanalyse",
"area": "governance",
"overlay": null,
- "specs": []
+ "specs": [
+ "dashboard-view-analytics"
+ ]
},
{
"slug": "prometheus-metrics",
@@ -721,7 +776,9 @@
"title_nl": "Auditpaneel",
"area": "governance",
"overlay": "launchpad-compliance-audit-panel",
- "specs": []
+ "specs": [
+ "launchpad-compliance-audit-panel"
+ ]
},
{
"slug": "orphaned-data-cleanup",
@@ -730,7 +787,8 @@
"area": "sharing",
"overlay": "orphaned-data-cleanup",
"specs": [
- "dashboard-sharing"
+ "dashboard-sharing",
+ "orphaned-data-cleanup"
]
},
{
@@ -740,7 +798,9 @@
"area": "governance",
"overlay": null,
"specs": [
- "governance-rest-api-reference"
+ "governance-rest-api-reference",
+ "cli-commands",
+ "dashboard-bulk-operations"
]
},
{
@@ -749,7 +809,9 @@
"title_nl": "SSO en toegangsstatus",
"area": "platform",
"overlay": "launchpad-enterprise-security-access",
- "specs": []
+ "specs": [
+ "launchpad-enterprise-security-access"
+ ]
},
{
"slug": "theming-and-languages",
@@ -767,7 +829,9 @@
"title_nl": "Mobiele toegang",
"area": "platform",
"overlay": "launchpad-mobile-remote-access",
- "specs": []
+ "specs": [
+ "launchpad-mobile-remote-access"
+ ]
},
{
"slug": "self-hosting",
@@ -776,7 +840,8 @@
"area": "platform",
"overlay": null,
"specs": [
- "runtime-or-consumption"
+ "runtime-or-consumption",
+ "launchpad-adopt-or-abstractions"
]
},
{
@@ -785,7 +850,9 @@
"title_nl": "GroupFolder-opslag (optioneel)",
"area": "governance",
"overlay": "groupfolder-storage-backend",
- "specs": []
+ "specs": [
+ "groupfolder-storage-backend"
+ ]
},
{
"slug": "launchpad-meeting-calendar-actions",
@@ -793,9 +860,19 @@
"title_nl": "Vergaderacties",
"area": "widgets",
"overlay": "launchpad-meeting-calendar-actions",
- "specs": []
+ "specs": [
+ "launchpad-meeting-calendar-actions"
+ ]
}
],
+ "internalSpecs": [
+ "beta-alignment",
+ "dashboard-cascade-events",
+ "infrastructure-helpers",
+ "initial-state-contract",
+ "license-header-consistency",
+ "resource-uploads"
+ ],
"providers": [
{
"key": "launchpad",
@@ -970,7 +1047,11 @@
"providerHow": "read-from-code",
"feature": "grid-layout",
"featureConfidence": "high",
- "origin": "own-code"
+ "origin": "own-code",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpBewerken"
+ }
},
{
"id": "build-resize",
@@ -1000,7 +1081,11 @@
"providerHow": "read-from-code",
"feature": "grid-layout",
"featureConfidence": "high",
- "origin": "own-code"
+ "origin": "own-code",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpBewerken"
+ }
},
{
"id": "build-keyboard",
@@ -1032,7 +1117,11 @@
"workspace-365": "not checked: W365 user guide and FAQ for keyboard move or resize of tiles, not found",
"microsoft-viva": "not checked: support.microsoft.com c4f573dd screen reader guide covers touch gestures to reorder cards only; no keyboard move or resize found"
},
- "note": "Re-rated 2026-10-07 from code: built, yes, unchanged. The change keyboard-accessible-widget-repositioning still shows its 11 tasks unticked, but every one is delivered: tasks 1-3 (focusable, named grid item; Enter opens the menu) by nextcloud-vue CnDashboardGrid instead of the retired src/components/DashboardGrid.vue the tasks name, tasks 4-9 in launchpad, tasks 10-11 in the Playwright spec. The change is ready to tick and archive. Not checked live in a browser in this round."
+ "note": "Re-rated 2026-10-07 from code: built, yes, unchanged. The change keyboard-accessible-widget-repositioning still shows its 11 tasks unticked, but every one is delivered: tasks 1-3 (focusable, named grid item; Enter opens the menu) by nextcloud-vue CnDashboardGrid instead of the retired src/components/DashboardGrid.vue the tasks name, tasks 4-9 in launchpad, tasks 10-11 in the Playwright spec. The change is ready to tick and archive. Not checked live in a browser in this round.",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpBewerken"
+ }
},
{
"id": "build-columns",
@@ -1063,6 +1152,10 @@
"dashy": "source read at 4.7.0: src/utils/config/ConfigSchema.json:421-427 appConfig.colCount 1 to 8, applied in src/views/Home.vue:138 and :277-284; editable in the Edit App Config form (src/components/InteractiveEditor/EditAppConfig.vue:21 schema form) or the YAML editor",
"workspace-365": "https://support.workspace365.net/en/articles/175608-faq-for-admins admins can set shared tile group size to '2 columns (Enlarge) or 1 column (Shrink)'; a grid column count for the whole page is not described (read 2026-09-26)",
"microsoft-viva": "not checked: create-dashboard and card designer pages for a column setting; cards are medium or large only"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpBewerken"
}
},
{
@@ -1093,7 +1186,11 @@
"providerHow": "read-from-code",
"feature": "grid-layout",
"featureConfidence": "high",
- "origin": "own-code"
+ "origin": "own-code",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpMobiel"
+ }
},
{
"id": "build-container",
@@ -1123,7 +1220,11 @@
"providerHow": "read-from-code",
"feature": "grid-layout",
"featureConfidence": "high",
- "origin": "own-code"
+ "origin": "own-code",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpManagement"
+ }
},
{
"id": "build-style",
@@ -1153,6 +1254,10 @@
"dashy": "source read at 4.7.0: a widget has only a label (src/components/InteractiveEditor/EditWidget.vue:19-23); colour and custom CSS are set per section (ConfigSchema.json:954-963, applied src/components/LinkItems/Collapsable.vue:89-90) and per item (color, backgroundColor ConfigSchema.json:1401-1410), not per widget",
"workspace-365": "https://support.workspace365.net/en/articles/483000-shortcut-tile 'Choose an icon' and 'the default color, header color, primary color, or a custom color'; group titles renamable; borders not described (read 2026-09-26)",
"microsoft-viva": "https://learn.microsoft.com/en-us/sharepoint/homesites/create-dashboard 'edit properties like the label, icon, image'; colours and borders per card not described (read 2026-09-26)"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpWidgetInstellen"
}
},
{
@@ -1183,6 +1288,10 @@
"dashy": "source read at 4.7.0: src/components/InteractiveEditor/EditWidget.vue:14-18 the widget type is a free-text input ('Widget Type', en.json:385); the registry in src/components/Widgets/WidgetBase.vue:53-148 is not offered as a list; ConfigSchema.json:1476-1480 type has no enum, so the YAML editor cannot complete it either",
"workspace-365": "https://support.workspace365.net/en/articles/175363-full-guide-for-user 'Click on Edit and then + Add tiles'; https://support.workspace365.net/en/articles/483000-shortcut-tile shortcut apps in the App store 'with searchable tags' (read 2026-09-26)",
"microsoft-viva": "https://learn.microsoft.com/en-us/sharepoint/homesites/available-dashboard-cards '+ Add a card' opens the dashboard toolbox with 'Suggested cards' and 'Add more cards'; a search box over the toolbox is not described (read 2026-09-26)"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpWidgetKiezen"
}
},
{
@@ -1213,7 +1322,11 @@
"providerHow": "read-from-code",
"feature": "nc-dashboard-widget-proxy",
"featureConfidence": "high",
- "origin": "own-code"
+ "origin": "own-code",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpWidgetKiezen"
+ }
},
{
"id": "build-no-overlap",
@@ -1243,6 +1356,10 @@
"dashy": "source read at 4.7.0: sections flow in a CSS grid (src/views/Home.vue:287 grid-template-columns repeat(var(--col-count))) and widgets in a list (Section.vue:60-63); there is no free positioning, so a new section or widget always lands in the next free slot",
"workspace-365": "https://support.workspace365.net/en/articles/175608-faq-for-admins the system 'can automatically reorder tiles within shared groups to minimize gaps based on tile size' (read 2026-09-26)",
"microsoft-viva": "not checked: create-dashboard for placement rules; the targeting page only advises rearranging cards to avoid gaps"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpBewerken"
}
},
{
@@ -1274,6 +1391,10 @@
"dashy": "source read at 4.7.0: services/endpoints/save-config.js:36-90 writes conf.yml whole with only a timestamped backup; searched services for etag, if-match, lock, conflict: none. The last save wins",
"workspace-365": "not checked: W365 spaces and FAQ articles for concurrent edit locking",
"microsoft-viva": "not checked: create-dashboard and edit pages for co-authoring or locking of the dashboard"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpBewerken"
}
},
{
@@ -1304,7 +1425,11 @@
"providerHow": "read-from-code",
"feature": "permissions",
"featureConfidence": "high",
- "origin": "own-code"
+ "origin": "own-code",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpIntranet"
+ }
},
{
"id": "build-config-file",
@@ -1335,7 +1460,11 @@
"feature": "dashboard-export-import",
"featureConfidence": "high",
"origin": "own-code",
- "note": "Import takes a ZIP of the JSON export. That moves a dashboard between servers; it is not a hand-authored configuration workflow."
+ "note": "Import takes a ZIP of the JSON export. That moves a dashboard between servers; it is not a hand-authored configuration workflow.",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpBeheerOnderhoud"
+ }
},
{
"id": "build-rollback",
@@ -1366,6 +1495,10 @@
"dashy": "source read at 4.7.0: services/endpoints/save-config.js:61-75 every save copies the old conf.yml to user-data/config-backups/-.backup.yml; searched src for backup restore: no UI lists or restores them, an admin copies a file back on the server",
"workspace-365": "not checked: W365 spaces, FAQ and audit logging articles for restoring a previous layout",
"microsoft-viva": "https://learn.microsoft.com/en-us/sharepoint/homesites/create-dashboard 'The dashboard details contain settings for your dashboard, page versioning'; https://learn.microsoft.com/en-us/sharepoint/homesites/create-multilingual-dashboard 'view the version history of the default dashboard'; restoring a version is not described (read 2026-09-26)"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpVersies"
}
},
{
@@ -1396,7 +1529,11 @@
"providerHow": "read-from-code",
"feature": "dashboard-switcher",
"featureConfidence": "high",
- "origin": "own-code"
+ "origin": "own-code",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpWerkplek"
+ }
},
{
"id": "dash-default",
@@ -1426,6 +1563,13 @@
"dashy": "source read at 4.7.0: the root conf.yml always opens first; src/router.js:48-52 startingView picks the view (home, minimal, workspace) not a page; searched ConfigSchema.json pages for default: no flag, and users cannot choose their own",
"workspace-365": "https://support.workspace365.net/en/articles/703640-using-and-organising-spaces-in-workspace-365 admins can 'let users start on the Personal space or on a Shared space' by dragging it 'to the top position' (read 2026-09-26)",
"microsoft-viva": "https://www.microsoft.com/releasecommunications/api/v1/m365 item 567887 admins 'manage home site priority order' across multiple home sites; a user cannot pick their own default (read 2026-09-26)"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": [
+ "LpWerkplek",
+ "LpDashboardInstellingen"
+ ]
}
},
{
@@ -1459,6 +1603,10 @@
"dashy": "source read at 4.7.0: ConfigSchema.json:151-221 pageInfo title, description, logo, favicon per config file; src/components/InteractiveEditor/EditPageInfo.vue form ('Edit Page Info', en.json:339); per-page pageInfo merged at src/store.js:535-539",
"workspace-365": "not checked: W365 spaces article for space description or icon",
"microsoft-viva": "https://learn.microsoft.com/en-us/sharepoint/homesites/create-dashboard Dashboard details: 'Change Thumbnail' and 'Description ... to be shown in search results'; https://learn.microsoft.com/en-us/sharepoint/homesites/create-multilingual-dashboard translated dashboard name (read 2026-09-26)"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpDashboardInstellingen"
}
},
{
@@ -1489,6 +1637,10 @@
"dashy": "source read at 4.7.0: searched src and services for duplicate, clone, copy page: only item 'Copy or Move' (en.json:320) and section name duplicate checks (EditSection.vue:92); no way to copy a page",
"workspace-365": "not checked: W365 spaces and user guide for copying a space",
"microsoft-viva": "not checked: create-dashboard and user customise pages for copying a dashboard"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpWerkplek"
}
},
{
@@ -1519,6 +1671,10 @@
"dashy": "source read at 4.7.0: src/router.js:55-67 canonical /home/:page and /home/:page/:section routes, also for minimal and workspace views; deep links resolve the page's config (src/store.js:500-505)",
"workspace-365": "not checked: W365 spaces article for deep links to a space",
"microsoft-viva": "https://learn.microsoft.com/en-us/sharepoint/homesites/create-multilingual-dashboard 'Copy the link to the dashboard under Properties > Name' and 'Paste the link in a browser' (read 2026-09-26)"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpWerkplek"
}
},
{
@@ -1549,7 +1705,11 @@
"providerHow": "read-from-code",
"feature": "dashboard-sharing",
"featureConfidence": "high",
- "origin": "own-code"
+ "origin": "own-code",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpWerkplek"
+ }
},
{
"id": "dash-tree",
@@ -1579,6 +1739,13 @@
"dashy": "source read at 4.7.0: src/utils/config/ConfigHelpers.js:14-24 pages is root-owned and stripped from sub-pages, and EditModeSaveMenu.vue:49-55 disables Edit Pages on a sub-page, so pages are one flat list",
"workspace-365": "not checked: W365 spaces article for nested spaces",
"microsoft-viva": "not checked: overview and edit pages for parent and child dashboards"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": [
+ "LpWerkplek",
+ "LpDashboardInstellingen"
+ ]
}
},
{
@@ -1609,6 +1776,10 @@
"dashy": "source read at 4.7.0: src/views/Home.vue:40-49 an empty config shows 'No Data Configured' (en.json:9); no offer or wizard to create a first dashboard; the Add Section launcher only appears once in edit mode (Home.vue:37)",
"workspace-365": "not checked: W365 user guide for a first-dashboard prompt",
"microsoft-viva": "https://learn.microsoft.com/en-us/sharepoint/homesites/create-dashboard 'You're asked to select your primary audience to generate a set of default dashboard cards' at first setup (editor, not end user) (read 2026-09-26)"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpEersteKeer"
}
},
{
@@ -1639,6 +1810,10 @@
"dashy": "source read at 4.7.0: 33 UI locales exist (src/assets/locales), but config text (section names, item titles, pageInfo) has no per-language variants: ConfigSchema.json:151-221 and :907-1115 single strings, additionalProperties false",
"workspace-365": "not checked: W365 FAQ and spaces for translated space content",
"microsoft-viva": "https://learn.microsoft.com/en-us/sharepoint/homesites/create-multilingual-dashboard translators make per-language copies; 'The Dashboard web part will display in the users preferred language' (read 2026-09-26)"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpIntranet"
}
},
{
@@ -1669,6 +1844,10 @@
"dashy": "source read at 4.7.0: ConfigSchema.json:151-221 pageInfo allows only title, description, navLinks, footer, logo, favicon, color with additionalProperties false; pages entries only name, path, displayData (:29-45)",
"workspace-365": "https://support.workspace365.net/en/articles/703640-using-and-organising-spaces-in-workspace-365 admins configure 'who owns the Shared space'; other facts such as department not described (read 2026-09-26)",
"microsoft-viva": "not checked: create-dashboard for custom dashboard properties"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpDashboardInstellingen"
}
},
{
@@ -1689,7 +1868,11 @@
},
"reachedOn": "Workspace page, after creating a new personal dashboard",
"source": "spec-round-2026-10-07",
- "feature": "dashboard-switcher"
+ "feature": "dashboard-switcher",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpEersteKeer"
+ }
},
{
"id": "tile-pin",
@@ -1719,7 +1902,14 @@
"providerHow": "read-from-code",
"feature": "tiles",
"featureConfidence": "high",
- "origin": "own-code"
+ "origin": "own-code",
+ "screen": {
+ "canvas": "design-system",
+ "board": [
+ "LpWerkplek",
+ "LpTegelBewerken"
+ ]
+ }
},
{
"id": "tile-health",
@@ -1749,7 +1939,14 @@
"providerHow": "read-from-code",
"feature": "tiles",
"featureConfidence": "high",
- "origin": "own-code"
+ "origin": "own-code",
+ "screen": {
+ "canvas": "design-system",
+ "board": [
+ "LpWerkplek",
+ "LpTegelBewerken"
+ ]
+ }
},
{
"id": "tile-filter",
@@ -1779,7 +1976,11 @@
"providerHow": "read-from-code",
"feature": "tiles",
"featureConfidence": "high",
- "origin": "own-code"
+ "origin": "own-code",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpWerkplek"
+ }
},
{
"id": "tile-web-search",
@@ -1809,7 +2010,11 @@
"providerHow": "read-from-code",
"feature": "tiles",
"featureConfidence": "high",
- "origin": "own-code"
+ "origin": "own-code",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpWerkplek"
+ }
},
{
"id": "tile-bookmarks",
@@ -1839,6 +2044,10 @@
"dashy": "source read at 4.7.0: a section is a named group of link items (src/components/LinkItems/Section.vue:22-58); sub-item groups via ConfigSchema.json:1416 subItems (SubItemGroup.vue)",
"workspace-365": "https://support.workspace365.net/en/articles/175363-full-guide-for-user Documents tile offers 'Bookmarks' and tiles can be grouped in named tile groups; a dedicated grouped-link list is not described (read 2026-09-26)",
"microsoft-viva": "https://learn.microsoft.com/en-us/sharepoint/homesites/available-dashboard-cards Quick links card 'Provide list of relevant links or files', up to 20 links; https://support.microsoft.com/office/3da30f39-684a-4bde-bb81-2e1407d59b52 users add their own Quick Links card (read 2026-09-26)"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpWerkplek"
}
},
{
@@ -1869,6 +2078,10 @@
"dashy": "source read at 4.7.0: src/mixins/ItemMixin.js:385-387 click counts are kept only in each browser's localStorage for sorting; searched services for analytics, track, clicks: nothing is sent to the server or reported",
"workspace-365": "https://support.workspace365.net/en/articles/175636-analytics-of-a-workspace-365-environment 'Apps: usage per app'; not broken down per space (read 2026-09-26)",
"microsoft-viva": "https://learn.microsoft.com/en-us/sharepoint/homesites/sharepoint-app-in-teams-analytics 'Engaged users by dashboard card ... how often each user engages with a card', per experience (read 2026-09-26)"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpBeheerAnalyse"
}
},
{
@@ -1898,7 +2111,11 @@
"providerHow": "research-then-code",
"feature": "app-catalogue-and-launch",
"origin": "competitor",
- "note": "Specified 2026-09-27 in openspec/changes/launcher-tile-sorting (openspec pass)."
+ "note": "Specified 2026-09-27 in openspec/changes/launcher-tile-sorting (openspec pass).",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpWerkplek"
+ }
},
{
"id": "tile-legacy-apps",
@@ -1927,7 +2144,11 @@
"providerHow": "research-then-code",
"feature": "app-catalogue-and-launch",
"origin": "competitor",
- "note": "Specified 2026-09-27 in openspec/changes/launcher-tile-launch-types (openspec pass)."
+ "note": "Specified 2026-09-27 in openspec/changes/launcher-tile-launch-types (openspec pass).",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpTegelBewerken"
+ }
},
{
"id": "tile-local-apps",
@@ -1956,7 +2177,11 @@
"providerHow": "research-then-code",
"feature": "app-catalogue-and-launch",
"origin": "competitor",
- "note": "Specified 2026-09-27 in openspec/changes/launcher-tile-launch-types (openspec pass)."
+ "note": "Specified 2026-09-27 in openspec/changes/launcher-tile-launch-types (openspec pass).",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpTegelBewerken"
+ }
},
{
"id": "tile-sso-app",
@@ -1985,7 +2210,11 @@
"providerHow": "research-then-code",
"feature": "app-catalogue-and-launch",
"origin": "competitor",
- "note": "Specified 2026-09-27 in openspec/changes/launcher-tile-launch-types (openspec pass)."
+ "note": "Specified 2026-09-27 in openspec/changes/launcher-tile-launch-types (openspec pass).",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpTegelBewerken"
+ }
},
{
"id": "tile-catalogue",
@@ -2014,7 +2243,11 @@
"providerHow": "research-then-code",
"feature": "app-catalogue-and-launch",
"origin": "competitor",
- "note": "Specified 2026-09-27 in openspec/changes/launcher-app-catalogue (openspec pass)."
+ "note": "Specified 2026-09-27 in openspec/changes/launcher-app-catalogue (openspec pass).",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpTegelToevoegen"
+ }
},
{
"id": "tile-discover-containers",
@@ -2043,7 +2276,11 @@
"providerHow": "research-then-code",
"feature": "app-catalogue-and-launch",
"origin": "competitor",
- "note": "Specified 2026-09-27 in openspec/changes/launcher-discover-server-services (openspec pass)."
+ "note": "Specified 2026-09-27 in openspec/changes/launcher-discover-server-services (openspec pass).",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpTegelToevoegen"
+ }
},
{
"id": "tile-org-nav",
@@ -2073,6 +2310,13 @@
"dashy": "source read at 4.7.0: ConfigSchema.json:164-198 pageInfo.navLinks (up to 6) shown in the header by src/components/PageStrcture/Nav.vue:14-31 beside the page links; root pageInfo is merged into every sub-page (src/store.js:161)",
"workspace-365": "https://support.workspace365.net/en/articles/175363-full-guide-for-user 'Through the navigation menu on the left side of you digital workplace, navigate easily'; the menu lists spaces and apps rather than curated org links (read 2026-09-26)",
"microsoft-viva": "https://learn.microsoft.com/en-us/sharepoint/homesites/use-audience-targeting-sharepoint-app-in-teams 'Resources are the navigation links that display beneath the dashboard', audience targetable (read 2026-09-26)"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": [
+ "LpWerkplek",
+ "LpBeheerNavigatie"
+ ]
}
},
{
@@ -2103,6 +2347,10 @@
"dashy": "source read at 4.7.0: src/components/Widgets/EmbedWidget.vue:14-30 embed widget renders raw HTML, CSS and script from options (or htmlSrc); formatted text is possible only as hand-written HTML, there is no text editor",
"workspace-365": "https://support.workspace365.net/en/articles/480271-text-editor-text-content 'bold, italic, lists, tables, and hyperlinks' as a tile or a full space; headings not mentioned (read 2026-09-26)",
"microsoft-viva": "https://learn.microsoft.com/en-us/sharepoint/homesites/use-card-designer templates 'Heading' and 'Description'; richer layout only by Adaptive Card JSON in a quick view (read 2026-09-26)"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpIntranet"
}
},
{
@@ -2133,6 +2381,10 @@
"dashy": "source read at 4.7.0: src/components/Widgets/ImageWidget.vue:18-43 image widget shows an image from imagePath with optional size and refresh; searched src for upload and type=file: none, so linked only",
"workspace-365": "not checked: W365 tile articles for an image tile",
"microsoft-viva": "https://learn.microsoft.com/en-us/sharepoint/homesites/use-card-designer 'Image: Create a card with a heading and image', upload or pick from site or web (read 2026-09-26)"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpIntranet"
}
},
{
@@ -2163,7 +2415,11 @@
"providerHow": "read-from-code",
"feature": "content-widgets",
"featureConfidence": "high",
- "origin": "own-code"
+ "origin": "own-code",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpIntranet"
+ }
},
{
"id": "w-embed",
@@ -2193,7 +2449,11 @@
"providerHow": "read-from-code",
"feature": "iframe-embed-widget",
"featureConfidence": "high",
- "origin": "own-code"
+ "origin": "own-code",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpIntranet"
+ }
},
{
"id": "w-news",
@@ -2223,7 +2483,11 @@
"providerHow": "read-from-code",
"feature": "content-widgets",
"featureConfidence": "high",
- "origin": "own-code"
+ "origin": "own-code",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpIntranet"
+ }
},
{
"id": "w-weather",
@@ -2253,7 +2517,11 @@
"providerHow": "read-from-code",
"feature": "content-widgets",
"featureConfidence": "high",
- "origin": "own-code"
+ "origin": "own-code",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpWerkplek"
+ }
},
{
"id": "w-clock",
@@ -2283,7 +2551,11 @@
"providerHow": "read-from-code",
"feature": "content-widgets",
"featureConfidence": "high",
- "origin": "own-code"
+ "origin": "own-code",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpWerkplek"
+ }
},
{
"id": "w-calendar",
@@ -2313,7 +2585,14 @@
"providerHow": "read-from-code",
"feature": "content-widgets",
"featureConfidence": "high",
- "origin": "own-code"
+ "origin": "own-code",
+ "screen": {
+ "canvas": "design-system",
+ "board": [
+ "LpStart",
+ "LpWerkplek"
+ ]
+ }
},
{
"id": "w-files",
@@ -2343,7 +2622,11 @@
"providerHow": "read-from-code",
"feature": "content-widgets",
"featureConfidence": "high",
- "origin": "own-code"
+ "origin": "own-code",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpWerkplek"
+ }
},
{
"id": "w-people",
@@ -2373,7 +2656,11 @@
"providerHow": "read-from-code",
"feature": "people-widget",
"featureConfidence": "high",
- "origin": "own-code"
+ "origin": "own-code",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpIntranet"
+ }
},
{
"id": "w-birthdays",
@@ -2403,7 +2690,11 @@
"providerHow": "read-from-code",
"feature": "people-widget",
"featureConfidence": "high",
- "origin": "own-code"
+ "origin": "own-code",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpIntranet"
+ }
},
{
"id": "w-structure",
@@ -2437,6 +2728,10 @@
"dashy": "source read at 4.7.0: every section has a name and icon shown as a heading (src/components/LinkItems/Section.vue:3-4 via Collapsable), sections can be collapsed; there is no free label or divider widget",
"workspace-365": "https://support.workspace365.net/en/articles/175363-full-guide-for-user 'Change the name of Personal tile group by clicking on the title'; group titles act as headings, no divider (read 2026-09-26)",
"microsoft-viva": "not checked: available-dashboard-cards for heading or divider cards"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpIntranet"
}
},
{
@@ -2467,6 +2762,10 @@
"dashy": "source read at 4.7.0: items only open or copy a URL (ConfigSchema.json:1293-1307); searched src for webhook, action button: none; open request #383 'Action Buttons to execute web hooks'",
"workspace-365": "https://support.workspace365.net/en/articles/483000-shortcut-tile 'call, send an email, or open an external website directly'; https://support.workspace365.net/en/articles/175363-full-guide-for-user email tile 'compose new emails from the live tile' (read 2026-09-26)",
"microsoft-viva": "https://learn.microsoft.com/en-us/sharepoint/homesites/available-dashboard-cards Assigned tasks card 'Add tasks' button; Approvals 'create new approvals'; https://learn.microsoft.com/en-us/sharepoint/homesites/use-card-designer primary and secondary buttons (read 2026-09-26)"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpWerkplek"
}
},
{
@@ -2497,7 +2796,14 @@
"providerHow": "read-from-code",
"feature": "runtime-or-consumption",
"featureConfidence": "high",
- "origin": "own-code"
+ "origin": "own-code",
+ "screen": {
+ "canvas": "design-system",
+ "board": [
+ "LpManagement",
+ "LpWidgetInstellen"
+ ]
+ }
},
{
"id": "w-charts",
@@ -2529,6 +2835,10 @@
"dashy": "source read at 4.7.0: no store of your own data; custom-api (CustomApi.vue:1-14, list or block display :58-59) shows counts and values from any JSON endpoint but no charts or tables; charts exist only inside fixed widgets such as crypto-price-chart (src/components/Charts)",
"workspace-365": "not checked: W365 docs for charts over own data (Power BI aside)",
"microsoft-viva": "not checked: available-dashboard-cards for chart cards"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpManagement"
}
},
{
@@ -2560,7 +2870,11 @@
"feature": "iframe-embed-widget",
"featureConfidence": "high",
"origin": "competitor",
- "note": "Only as a generic embedded page; no report picker and no row-level security hand-off."
+ "note": "Only as a generic embedded page; no report picker and no row-level security hand-off.",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpManagement"
+ }
},
{
"id": "w-spend",
@@ -2591,6 +2905,10 @@
"dashy": "source read at 4.7.0: searched src/components/Widgets for finance, spend, budget, invoice: only crypto and wallet balance widgets (WalletBalance.vue), nothing over finance or case apps",
"workspace-365": "not checked: W365 integration list for finance figures",
"microsoft-viva": "not checked: available-dashboard-cards for finance cards"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpManagement"
}
},
{
@@ -2622,7 +2940,11 @@
"feature": "runtime-or-consumption",
"featureConfidence": "high",
"origin": "competitor",
- "note": "A live tile reads one value from a URL. There is no connector, authentication handling or data mapping as the competitor describes. Corrected 2026-09-27 (openspec pass): a connector mode exists; it calls Integriq's DashboardDatasourceService::resolve(), which applies the source's stored authentication (lib/Service/LiveTileService.php:380). What is missing is mapping more than one field and choosing how to show it. Specified 2026-09-27 in openspec/changes/integrations-live-tile-lists (openspec pass)."
+ "note": "A live tile reads one value from a URL. There is no connector, authentication handling or data mapping as the competitor describes. Corrected 2026-09-27 (openspec pass): a connector mode exists; it calls Integriq's DashboardDatasourceService::resolve(), which applies the source's stored authentication (lib/Service/LiveTileService.php:380). What is missing is mapping more than one field and choosing how to show it. Specified 2026-09-27 in openspec/changes/integrations-live-tile-lists (openspec pass).",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpWidgetInstellen"
+ }
},
{
"id": "int-itsm",
@@ -2708,7 +3030,11 @@
"providerHow": "read-from-code",
"feature": "nc-dashboard-widget-proxy",
"featureConfidence": "high",
- "origin": "own-code"
+ "origin": "own-code",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpWerkplek"
+ }
},
{
"id": "int-tasks",
@@ -2738,7 +3064,11 @@
"providerHow": "read-from-code",
"feature": "nc-dashboard-widget-proxy",
"featureConfidence": "high",
- "origin": "own-code"
+ "origin": "own-code",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpStart"
+ }
},
{
"id": "int-office-tiles",
@@ -2767,7 +3097,11 @@
"providerHow": "research-then-code",
"feature": "runtime-or-consumption",
"origin": "competitor",
- "note": "Specified 2026-09-27 in openspec/changes/integrations-office-tiles (openspec pass)."
+ "note": "Specified 2026-09-27 in openspec/changes/integrations-office-tiles (openspec pass).",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpTegelToevoegen"
+ }
},
{
"id": "int-intranet-content",
@@ -2797,7 +3131,11 @@
"providerHow": "research-then-code",
"feature": "runtime-or-consumption",
"origin": "competitor",
- "note": "Corrected 2026-09-27 (openspec pass): the news widget reads RSS and Atom feeds from any allowed host (lib/Service/NewsWidgetService.php:133) and the embed widget frames an allowed page (lib/Service/IframeService.php:9), the same half Homarr and Dashy are rated partial for; the other intranet's documents are missing. Specified 2026-09-27 in openspec/changes/integrations-live-tile-lists (openspec pass)."
+ "note": "Corrected 2026-09-27 (openspec pass): the news widget reads RSS and Atom feeds from any allowed host (lib/Service/NewsWidgetService.php:133) and the embed widget frames an allowed page (lib/Service/IframeService.php:9), the same half Homarr and Dashy are rated partial for; the other intranet's documents are missing. Specified 2026-09-27 in openspec/changes/integrations-live-tile-lists (openspec pass).",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpIntranet"
+ }
},
{
"id": "int-connections-status",
@@ -2828,6 +3166,10 @@
"dashy": "source read at 4.7.0: each widget shows its own fetch error (WidgetBase.vue, ignoreErrors ConfigSchema.json:1511); searched src and services for connections overview, last success: no list of the outside sources the dashboard uses",
"workspace-365": "not checked: W365 integration builder guide for a connection health overview",
"microsoft-viva": "not checked: learn.microsoft.com dashboard pages for connection status"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpManagement"
}
},
{
@@ -2858,7 +3200,11 @@
"providerHow": "read-from-code",
"feature": "conditional-visibility",
"featureConfidence": "high",
- "origin": "own-code"
+ "origin": "own-code",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpZichtbaarheid"
+ }
},
{
"id": "t-hours",
@@ -2888,6 +3234,10 @@
"dashy": "source read at 4.7.0: src/utils/IsVisibleToUser.js:79-83 checks only users, guests, groups and roles; searched src for hour, weekday, schedule: none",
"workspace-365": "not checked: W365 conditional access and app settings for time windows",
"microsoft-viva": "not checked: audience targeting page for time-of-day rules"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpZichtbaarheid"
}
},
{
@@ -2918,6 +3268,10 @@
"dashy": "source read at 4.7.0: src/utils/IsVisibleToUser.js:79-83 has no date rule; searched src for startDate, endDate, expires in displayData: none",
"workspace-365": "not checked: W365 tile settings for date windows (announcements have end dates, tiles not described)",
"microsoft-viva": "https://learn.microsoft.com/en-us/viva/connections/announcements-viva-connections announcements (not cards) 'schedule a future send date' and an end date 'up to two weeks from posting' (read 2026-09-26)"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpZichtbaarheid"
}
},
{
@@ -2948,7 +3302,11 @@
"providerHow": "read-from-code",
"feature": "conditional-visibility",
"featureConfidence": "high",
- "origin": "own-code"
+ "origin": "own-code",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpZichtbaarheid"
+ }
},
{
"id": "t-preview",
@@ -2978,6 +3336,10 @@
"dashy": "source read at 4.7.0: searched src for preview as, impersonate, view as: none; the only preview is of an edited config (en.json config-editor.preview-button)",
"workspace-365": "not checked: W365 permission and space articles for preview-as-user",
"microsoft-viva": "https://learn.microsoft.com/en-us/sharepoint/homesites/use-audience-targeting-sharepoint-app-in-teams 'Open the Select audiences to preview as drop-down list' (read 2026-09-26)"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpZichtbaarheid"
}
},
{
@@ -3008,6 +3370,10 @@
"dashy": "source read at 4.7.0: searched src/components for a list of visibility rules: none; rules live in each section's displayData in the YAML",
"workspace-365": "not checked: W365 admin articles for an overview of targeted tiles ('permissions viewer' is about users and groups)",
"microsoft-viva": "https://learn.microsoft.com/en-us/sharepoint/homesites/use-audience-targeting-sharepoint-app-in-teams 'An icon appears on the card indicating audience targeting is applied'; no central overview (read 2026-09-26)"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpBeheerAnalyse"
}
},
{
@@ -3038,6 +3404,13 @@
"dashy": "source read at 4.7.0: src/store.js:224-248 permissions are three booleans (write to disk, save locally, view config); nothing limits widget types per group",
"workspace-365": "https://support.workspace365.net/en/articles/175398-understand-and-manage-user-permissions per-group permission 'Edit your own workplace, create personal tile groups and add tiles' (all or nothing, not per kind) (read 2026-09-26)",
"microsoft-viva": "https://learn.microsoft.com/en-us/sharepoint/homesites/available-dashboard-cards partner cards need App Catalog Admin approval before they appear in the toolbox; tenant-wide, not per group (read 2026-09-26)"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": [
+ "LpBeheerRollen",
+ "LpWidgetKiezen"
+ ]
}
},
{
@@ -3068,7 +3441,11 @@
"providerHow": "read-from-code",
"feature": "admin-roles",
"featureConfidence": "high",
- "origin": "own-code"
+ "origin": "own-code",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpBeheerRollen"
+ }
},
{
"id": "t-compulsory",
@@ -3099,7 +3476,11 @@
"feature": "permissions",
"featureConfidence": "high",
"origin": "own-code",
- "note": "The server refuses removal only on add-only dashboards; a user with full permission can remove a compulsory widget (PermissionService::canRemoveWidget returns true for full)."
+ "note": "The server refuses removal only on add-only dashboards; a user with full permission can remove a compulsory widget (PermissionService::canRemoveWidget returns true for full).",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpIntranet"
+ }
},
{
"id": "t-device",
@@ -3185,6 +3566,10 @@
"dashy": "source read at 4.7.0: there is one shared config, not one per group; searched src for priority, precedence: none",
"workspace-365": "https://support.workspace365.net/en/articles/175398-understand-and-manage-user-permissions 'Allow combined with Deny results in Deny' for permissions; no priority among group spaces described (read 2026-09-26)",
"microsoft-viva": "https://www.microsoft.com/releasecommunications/api/v1/m365 roadmap 567887 admins 'configure audience targeting, manage home site priority order' across home sites (read 2026-09-26)"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpBeheerDelen"
}
},
{
@@ -3215,6 +3600,10 @@
"dashy": "source read at 4.7.0: src/store.js:224-248 only admin versus non-admin plus three global flags; no per-role action grid",
"workspace-365": "https://support.workspace365.net/en/articles/175398-understand-and-manage-user-permissions permissions set 'per group or to all users' via Default permissions and Group management (read 2026-09-26)",
"microsoft-viva": "https://learn.microsoft.com/en-us/sharepoint/homesites/edit-sharepoint-app-in-teams fixed Owner, Member, Visitor table; no custom grid (read 2026-09-26)"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpBeheerRollen"
}
},
{
@@ -3245,7 +3634,11 @@
"providerHow": "read-from-code",
"feature": "admin-templates",
"featureConfidence": "high",
- "origin": "own-code"
+ "origin": "own-code",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpBeheerSjablonen"
+ }
},
{
"id": "r-permission-levels",
@@ -3275,7 +3668,14 @@
"providerHow": "read-from-code",
"feature": "permissions",
"featureConfidence": "high",
- "origin": "own-code"
+ "origin": "own-code",
+ "screen": {
+ "canvas": "design-system",
+ "board": [
+ "LpDelen",
+ "LpIntranet"
+ ]
+ }
},
{
"id": "r-resync",
@@ -3305,6 +3705,10 @@
"dashy": "source read at 4.7.0: a personal 'Save Locally' copy replaces the shared sections (src/store.js:125-128) and never receives template changes; searched src for merge template, resync: none",
"workspace-365": "https://support.workspace365.net/en/articles/703640-using-and-organising-spaces-in-workspace-365 template can be pushed to current users but 'the order of tile groups may look different'; no merge choice (read 2026-09-26)",
"microsoft-viva": "https://learn.microsoft.com/en-us/sharepoint/homesites/create-dashboard 'Any updates or changes to the dashboard by an owner ... will override the user's personal changes'; no merge choice (read 2026-09-26)"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpBeheerSjablonen"
}
},
{
@@ -3335,7 +3739,11 @@
"providerHow": "read-from-code",
"feature": "admin-templates",
"featureConfidence": "medium",
- "origin": "own-code"
+ "origin": "own-code",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpWerkplek"
+ }
},
{
"id": "r-wizard",
@@ -3365,6 +3773,10 @@
"dashy": "source read at 4.7.0: searched src/components and src/views for setup, wizard, onboard: none; first run shows the sample conf.yml",
"workspace-365": "not checked: W365 get-started collection for a guided in-product setup (setup is partner-led)",
"microsoft-viva": "https://learn.microsoft.com/en-us/sharepoint/homesites/sharepoint-app-in-teams-overview 'quick guide to set up'; first edit asks for a primary audience to generate default cards (https://learn.microsoft.com/en-us/sharepoint/homesites/create-dashboard) (read 2026-09-26)"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpInstellen"
}
},
{
@@ -3395,6 +3807,10 @@
"dashy": "source read at 4.7.0: user-data/conf.yml ships one sample page with 'Getting Started' links and an image widget; no installable set of example dashboards",
"workspace-365": "https://support.workspace365.net/en/articles/175367 'Creating a demo environment' for partners; example dashboards inside a customer environment not described (read 2026-09-26)",
"microsoft-viva": "not checked: setup pages for demo dashboards"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpInstellen"
}
},
{
@@ -3426,7 +3842,11 @@
"feature": "admin-roles",
"featureConfidence": "high",
"origin": "own-code",
- "note": "Roles are assigned in the setup wizard; this reading found no other screen that lists or edits them."
+ "note": "Roles are assigned in the setup wizard; this reading found no other screen that lists or edits them.",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpBeheerRollen"
+ }
},
{
"id": "r-bulk",
@@ -3456,6 +3876,10 @@
"dashy": "source read at 4.7.0: pages are edited one config file at a time (src/components/InteractiveEditor/EditMultiPages.vue); searched src for bulk, select all: none",
"workspace-365": "not checked: W365 admin articles for bulk actions on spaces",
"microsoft-viva": "not checked: dashboard pages for bulk actions"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpBeheerOnderhoud"
}
},
{
@@ -3486,7 +3910,11 @@
"providerHow": "read-from-code",
"feature": "dashboard-export-import",
"featureConfidence": "high",
- "origin": "own-code"
+ "origin": "own-code",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpBeheerOnderhoud"
+ }
},
{
"id": "r-migrate-from",
@@ -3544,6 +3972,10 @@
"dashy": "source read at 4.7.0: searched src and services for confluence: none",
"workspace-365": "https://portal.productboard.com/iqfsnhkpzih6grzjwagif2ek/tabs/5-ecosystem 'Confluence - Content Search' in global search; not as dashboard content (read 2026-09-26)",
"microsoft-viva": "not checked: available-dashboard-cards for Confluence"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpBeheerOnderhoud"
}
},
{
@@ -3575,6 +4007,13 @@
"dashy": "source read at 4.7.0: a config published at a URL can be previewed and applied with ?config= (RemoteConfigLoader.vue:62-70, en.json:127-128 'Load Remote Config?') and a page path can be a remote URL (src/store.js:507); there is no shared registry to browse",
"workspace-365": "not checked: W365; 'Pre-built integrations store' is only planned on the productboard",
"microsoft-viva": "https://learn.microsoft.com/en-us/sharepoint/homesites/edit-sharepoint-app-in-teams ACE solution gallery on AppSource offers cards; whole dashboards are not shared through it (read 2026-09-26)"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": [
+ "LpStore",
+ "LpBeheerDelen"
+ ]
}
},
{
@@ -3605,6 +4044,10 @@
"dashy": "source read at 4.7.0: ConfigSchema.json limits are fixed schema bounds (navLinks maxItems 6, :166); searched src for quota, per-user limit: none",
"workspace-365": "not checked: W365 admin articles for limits on spaces or tiles",
"microsoft-viva": "https://learn.microsoft.com/en-us/sharepoint/homesites/create-dashboard 'You can set the maximum number of cards you want to display'; ~20 cards advised; no per-person dashboard quota (read 2026-09-26)"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpBeheerAlgemeen"
}
},
{
@@ -3635,6 +4078,10 @@
"dashy": "source read at 4.7.0: ConfigSchema.json:829-846 preventLocalSave and disableConfigurationForNonAdmin switch off personal edits (src/store.js:233-247)",
"workspace-365": "https://support.workspace365.net/en/articles/703640-using-and-organising-spaces-in-workspace-365 admins 'prevent users from customizing their Personal space' via the permission 'Edit own workspace' (read 2026-09-26)",
"microsoft-viva": "https://learn.microsoft.com/en-us/sharepoint/homesites/create-dashboard 'Disabling user customization resets all user dashboard views to what the organization created' (read 2026-09-26)"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpBeheerAlgemeen"
}
},
{
@@ -3665,7 +4112,11 @@
"providerHow": "read-from-code",
"feature": "dashboard-sharing",
"featureConfidence": "high",
- "origin": "own-code"
+ "origin": "own-code",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpDelen"
+ }
},
{
"id": "s-public",
@@ -3695,6 +4146,13 @@
"dashy": "source read at 4.7.0: without auth the whole instance is open, and with auth enableGuestAccess (ConfigSchema.json:606-611, src/utils/auth/Auth.js:120-123) gives read-only guest viewing, trimmed per page, section or item by hideForGuests (:64, :1033); this is instance-wide guest mode, not a link per dashboard",
"workspace-365": "not checked: W365 spaces articles for public links",
"microsoft-viva": "not checked: dashboard pages for anonymous links"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": [
+ "LpDelen",
+ "LpPubliek"
+ ]
}
},
{
@@ -3725,6 +4183,10 @@
"dashy": "source read at 4.7.0: searched src and services for link password, expiry: none; only instance-wide HTTP basic auth (ENABLE_HTTP_AUTH, services/app.js) with no end date",
"workspace-365": "not checked: W365 for password protected public links",
"microsoft-viva": "not checked: dashboard pages for protected links"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpDelen"
}
},
{
@@ -3755,6 +4217,10 @@
"dashy": "source read at 4.7.0: src/utils/auth/Auth.js:138-176 checks built-in credentials in the browser; searched services for rate limit, throttle, lockout: none",
"workspace-365": "not checked: W365 security articles for public link throttling",
"microsoft-viva": "not checked: dashboard pages for link throttling"
+ },
+ "screen": {
+ "board": null,
+ "reason": "Het vertragen van geraden wachtwoorden gebeurt op de server; een bezoeker merkt alleen een trager antwoord."
}
},
{
@@ -3785,6 +4251,10 @@
"dashy": "source read at 4.7.0: searched src and services for notification of shares: none (only LocalConfigWarning.vue, a local-config notice)",
"workspace-365": "not checked: W365 activity feed for space-shared notifications",
"microsoft-viva": "not checked: news notifications page covers news, not dashboard shares"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpWerkplek"
}
},
{
@@ -3816,7 +4286,11 @@
"feature": "dashboard-kiosk-mode",
"featureConfidence": "high",
"origin": "own-code",
- "note": "openspec/features.overlay.json advertises dashboard-kiosk-mode as stable (\"You rotate dashboards on a lobby screen unattended\")."
+ "note": "openspec/features.overlay.json advertises dashboard-kiosk-mode as stable (\"You rotate dashboards on a lobby screen unattended\").",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpKiosk"
+ }
},
{
"id": "s-schedule",
@@ -3847,7 +4321,11 @@
"workspace-365": "not checked: W365 spaces for scheduled go-live",
"microsoft-viva": "not checked: dashboard publish is manual; no schedule found"
},
- "note": "Decided no 2026-09-28 (build-all decision pass): No competitor rated yes; no tender, feature request or roadmap row. Outside the core area (sharing). Reversible: reopen the row if demand appears. The code already built stays in the tree; it has no screen (see the pull request)."
+ "note": "Decided no 2026-09-28 (build-all decision pass): No competitor rated yes; no tender, feature request or roadmap row. Outside the core area (sharing). Reversible: reopen the row if demand appears. The code already built stays in the tree; it has no screen (see the pull request).",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpDelen"
+ }
},
{
"id": "s-policy",
@@ -3877,6 +4355,10 @@
"dashy": "source read at 4.7.0: no sharing exists to govern; permissions are three global flags (src/store.js:224-248)",
"workspace-365": "not checked: W365 permissions for a sharing policy",
"microsoft-viva": "not checked: dashboard pages for sharing policy"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpBeheerDelen"
}
},
{
@@ -3907,6 +4389,10 @@
"dashy": "source read at 4.7.0: dashboards are not owned by users; searched services for user deletion or transfer: none",
"workspace-365": "not checked: W365 user management for ownership transfer",
"microsoft-viva": "not checked: dashboard pages for ownership transfer"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpBeheerOnderhoud"
}
},
{
@@ -3966,7 +4452,14 @@
"workspace-365": "https://support.workspace365.net/en/articles/175643-best-practices 'Must-read' items use 'the pop-up function'; https://portal.productboard.com/iqfsnhkpzih6grzjwagif2ek/tabs/3-launched launched 'Read / unread hub items'; an explicit confirm button not described (read 2026-09-26)",
"microsoft-viva": "not checked: announcements page offers 'Allow users to dismiss', no read confirmation found"
},
- "note": "Decided no 2026-09-28 (build-all decision pass): No competitor rated yes (Workspace 365 partial); no tender, feature request or roadmap row. Outside the core area (engagement). Reversible: reopen the row if demand appears. The code already built stays in the tree; it has no screen (see the pull request)."
+ "note": "Decided no 2026-09-28 (build-all decision pass): No competitor rated yes (Workspace 365 partial); no tender, feature request or roadmap row. Outside the core area (engagement). Reversible: reopen the row if demand appears. The code already built stays in the tree; it has no screen (see the pull request).",
+ "screen": {
+ "canvas": "design-system",
+ "board": [
+ "LpIntranet",
+ "LpAankondiging"
+ ]
+ }
},
{
"id": "e-ack-report",
@@ -3996,6 +4489,10 @@
"dashy": "source read at 4.7.0: no acknowledgements exist (searched src and services for acknowledge, export csv of readers: none)",
"workspace-365": "https://portal.productboard.com/iqfsnhkpzih6grzjwagif2ek/tabs/3-launched launched 'See who has read an announcement (insights)'; spreadsheet export not described for readers (read 2026-09-26)",
"microsoft-viva": "not checked: announcements and analytics pages for per-user read lists (analytics are aggregated)"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpLeesbevestigingen"
}
},
{
@@ -4027,7 +4524,11 @@
"workspace-365": "https://support.workspace365.net/en/articles/284826-create-announcements emoji reactions 'like, love, sad, celebrate, laugh, thanks' on announcements, not on a space (read 2026-09-26)",
"microsoft-viva": "https://learn.microsoft.com/en-us/sharepoint/homesites/sharepoint-app-in-teams-news-notifications 'Likes' on news posts; not on the dashboard (read 2026-09-26)"
},
- "note": "Decided no 2026-09-28 (build-all decision pass): No competitor rated yes (Workspace 365, Microsoft Viva Connections partial); no tender, feature request or roadmap row. Outside the core area (engagement). Reversible: reopen the row if demand appears. The code already built stays in the tree; it has no screen (see the pull request)."
+ "note": "Decided no 2026-09-28 (build-all decision pass): No competitor rated yes (Workspace 365, Microsoft Viva Connections partial); no tender, feature request or roadmap row. Outside the core area (engagement). Reversible: reopen the row if demand appears. The code already built stays in the tree; it has no screen (see the pull request).",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpIntranet"
+ }
},
{
"id": "e-announcements",
@@ -4056,7 +4557,14 @@
"providerHow": "research-then-code",
"feature": "news-and-announcements",
"origin": "competitor",
- "note": "Specified 2026-09-27 in openspec/changes/engagement-announcements (openspec pass)."
+ "note": "Specified 2026-09-27 in openspec/changes/engagement-announcements (openspec pass).",
+ "screen": {
+ "canvas": "design-system",
+ "board": [
+ "LpIntranet",
+ "LpAankondiging"
+ ]
+ }
},
{
"id": "e-events",
@@ -4141,7 +4649,11 @@
"providerHow": "research-then-code",
"feature": "communities-events-and-knowledge",
"origin": "competitor",
- "note": "Specified 2026-09-27 in openspec/changes/engagement-communities (openspec pass)."
+ "note": "Specified 2026-09-27 in openspec/changes/engagement-communities (openspec pass).",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpIntranet"
+ }
},
{
"id": "e-digest",
@@ -4170,7 +4682,11 @@
"providerHow": "research-then-code",
"feature": "news-and-announcements",
"origin": "competitor",
- "note": "Specified 2026-09-27 in openspec/changes/engagement-activity-digest (openspec pass)."
+ "note": "Specified 2026-09-27 in openspec/changes/engagement-activity-digest (openspec pass).",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpPersoonlijk"
+ }
},
{
"id": "e-activity-stream",
@@ -4231,7 +4747,14 @@
"feature": "news-and-announcements",
"featureConfidence": "medium",
"origin": "competitor",
- "note": "Possible by hand with a date-limited text widget; there is no notice type. Specified 2026-09-27 in openspec/changes/engagement-announcements (openspec pass)."
+ "note": "Possible by hand with a date-limited text widget; there is no notice type. Specified 2026-09-27 in openspec/changes/engagement-announcements (openspec pass).",
+ "screen": {
+ "canvas": "design-system",
+ "board": [
+ "LpIntranet",
+ "LpAankondiging"
+ ]
+ }
},
{
"id": "a-find-dashboard",
@@ -4261,6 +4784,10 @@
"dashy": "source read at 4.7.0: Dashy is a standalone app with its own search over items (src/utils/Search.js:15-21), not pages; there is no Nextcloud search provider (no appinfo or PHP in the tree)",
"workspace-365": "not checked: W365 global search categories do not list spaces",
"microsoft-viva": "https://learn.microsoft.com/en-us/sharepoint/homesites/create-dashboard dashboard thumbnail and description 'to be shown in search results' (read 2026-09-26)"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpWerkplek"
}
},
{
@@ -4322,7 +4849,11 @@
"providerHow": "research-then-code",
"feature": "launchpad-ai-dashboard-assistant",
"featureConfidence": "high",
- "origin": "competitor"
+ "origin": "competitor",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpWerkplek"
+ }
},
{
"id": "a-place-widget",
@@ -4353,7 +4884,11 @@
"workspace-365": "not checked: Kelly FAQ for placing tiles",
"microsoft-viva": "not checked: dashboard pages for AI card placement; roadmap 558441 'add webparts' by prompt is for SharePoint pages and In development"
},
- "note": "Specified in openspec/changes/mcp-tool-surface (named 2026-09-27, openspec pass): the scenario \"Add the calendar widget to my dashboard\" resolves a plain request to launchpad.addWidget through hermiq. The features overlay entry (openspec/features.overlay.json:231) had pointed at the assistant widget spec, which places nothing."
+ "note": "Specified in openspec/changes/mcp-tool-surface (named 2026-09-27, openspec pass): the scenario \"Add the calendar widget to my dashboard\" resolves a plain request to launchpad.addWidget through hermiq. The features overlay entry (openspec/features.overlay.json:231) had pointed at the assistant widget spec, which places nothing.",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpWidgetKiezen"
+ }
},
{
"id": "a-agent-tools",
@@ -4384,6 +4919,10 @@
"dashy": "source read at 4.7.0: services/endpoints/api/openapi.yml:75-330 opt-in REST API (ENABLE_API, API_TOKEN) to list, read, replace and patch configs, sections and items; no MCP or other agent tool interface (searched src, services, docs for mcp: none)",
"workspace-365": "not checked: W365 API docs for an agent tool interface",
"microsoft-viva": "not checked: dashboard pages for agent tools"
+ },
+ "screen": {
+ "board": null,
+ "reason": "Een koppelvlak voor AI-agenten (MCP), zonder eigen scherm."
}
},
{
@@ -4470,6 +5009,10 @@
"dashy": "source read at 4.7.0: searched services for view counts, analytics: none; usage counts stay in the browser (src/mixins/ItemMixin.js:385)",
"workspace-365": "https://support.workspace365.net/en/articles/175636-analytics-of-a-workspace-365-environment 'usage of the Hub', 'usage per app', 'active users'; per space and export not described (read 2026-09-26)",
"microsoft-viva": "https://learn.microsoft.com/en-us/sharepoint/homesites/sharepoint-app-in-teams-analytics total views, engaged users per card; 'Download report ... Excel spreadsheet' (read 2026-09-26)"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpBeheerAnalyse"
}
},
{
@@ -4500,6 +5043,13 @@
"dashy": "source read at 4.7.0: there is no server-side view tracking to opt out of (searched services for track, analytics: none); only local click counts that can be reset by clearing site data",
"workspace-365": "not checked: W365 analytics article on opt-out or retention",
"microsoft-viva": "https://learn.microsoft.com/en-us/sharepoint/homesites/sharepoint-app-in-teams-analytics data 'aggregated and can't be tracked to an individual user'; tenant can disable analytics; no per-person opt-out (read 2026-09-26)"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": [
+ "LpPersoonlijk",
+ "LpBeheerAnalyse"
+ ]
}
},
{
@@ -4530,6 +5080,10 @@
"dashy": "source read at 4.7.0: services/app.js:229-235 GET healthz returns status, uptime, version; yarn health-check (services/healthcheck.js); the config menu has a hidden Debug Info tab with error log and environment (src/components/Configuration/ConfigContainer.vue:40, :76); no admin health page",
"workspace-365": "not checked: W365 admin articles for a health page",
"microsoft-viva": "not checked: dashboard pages for health checks"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpBeheerOnderhoud"
}
},
{
@@ -4560,6 +5114,10 @@
"dashy": "source read at 4.7.0: searched services and src for prometheus, metrics endpoint: none (UptimeKuma.vue:35 only reads another tool's metrics)",
"workspace-365": "not checked: W365 docs for Prometheus",
"microsoft-viva": "not checked: dashboard pages for Prometheus"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpBeheerOnderhoud"
}
},
{
@@ -4621,7 +5179,11 @@
"workspace-365": "not checked: W365 activity feed for space changes",
"microsoft-viva": "not checked: dashboard pages for change activity"
},
- "note": "Own rating yes to partial (corrections round 8, 2026-09-28): only 2 of the 14 declared dashboard event types are ever published, so ordinary edits, shares and publishes never reach the activity stream, launchpad#713."
+ "note": "Own rating yes to partial (corrections round 8, 2026-09-28): only 2 of the 14 declared dashboard event types are ever published, so ordinary edits, shares and publishes never reach the activity stream, launchpad#713.",
+ "screen": {
+ "board": null,
+ "reason": "Wijzigingen verschijnen in de Activiteit-app van Nextcloud, niet op een scherm van Launchpad."
+ }
},
{
"id": "g-cleanup",
@@ -4651,6 +5213,10 @@
"dashy": "source read at 4.7.0: there is no per-user server data to clean; personal changes live in each browser (src/store.js:96-130); searched services for user deletion: none",
"workspace-365": "not checked: W365 user management for cleanup",
"microsoft-viva": "not checked: dashboard pages for cleanup"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpBeheerOnderhoud"
}
},
{
@@ -4681,6 +5247,10 @@
"dashy": "source read at 4.7.0: package.json scripts validate-config (services/utils/config-validator.js) and health-check; config files can be listed, read, replaced and deleted over the REST API with curl (openapi.yml:75-128); no dedicated dashboard CLI",
"workspace-365": "not checked: W365 'Create & Delete environment script' covers environments, not spaces",
"microsoft-viva": "not checked: PowerShell pages for dashboard commands (only analytics toggle found)"
+ },
+ "screen": {
+ "board": null,
+ "reason": "Commando's op de server (occ), zonder scherm."
}
},
{
@@ -4711,6 +5281,13 @@
"dashy": "source read at 4.7.0: ConfigSchema.json:199-203 pageInfo.footer with sanitised inline HTML, rendered on every page by src/components/PageStrcture/Footer.vue:2",
"workspace-365": "not checked: W365 branding for a footer",
"microsoft-viva": "not checked: edit page for a dashboard footer"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": [
+ "LpWerkplek",
+ "LpBeheerNavigatie"
+ ]
}
},
{
@@ -4744,6 +5321,10 @@
"dashy": "source read at 4.7.0: services/utils/request.js:14-52 the proxy only blocks cloud metadata hosts (escape hatch DANGEROUSLY_DISABLE_PROXY_RESTRICTIONS); the iframe and embed widgets load any site; no allow-list",
"workspace-365": "not checked: https://support.workspace365.net/en/articles/175489-content-security-policy-csp allow-list governs where Workspace itself may be embedded, not what tiles fetch",
"microsoft-viva": "not checked: dashboard pages for an allow-list"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpBeheerAlgemeen"
}
},
{
@@ -4774,6 +5355,10 @@
"dashy": "source read at 4.7.0: there is no icon upload (searched src for upload, type=file: none); remote SVG icons are shown as img (src/components/LinkItems/ItemIcon.vue:110), which keeps scripts from running, but nothing cleans them",
"workspace-365": "not checked: W365 branding for icon sanitising",
"microsoft-viva": "not checked: dashboard pages for icon sanitising"
+ },
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpTegelBewerken"
}
},
{
@@ -4803,7 +5388,11 @@
"provider": "nextcloud",
"providerHow": "read-from-code",
"feature": "launchpad-enterprise-security-access",
- "origin": "own-code"
+ "origin": "own-code",
+ "screen": {
+ "board": null,
+ "reason": "Aanmelden loopt via Nextcloud en de identiteitsprovider van de organisatie, niet via een scherm van Launchpad."
+ }
},
{
"id": "p-mobile-app",
@@ -4862,7 +5451,11 @@
"provider": "nextcloud",
"providerHow": "read-from-code",
"feature": "self-hosting",
- "origin": "own-code"
+ "origin": "own-code",
+ "screen": {
+ "board": null,
+ "reason": "Hosting op eigen servers, geen scherm."
+ }
},
{
"id": "p-theme",
@@ -4891,7 +5484,11 @@
"provider": "nextcloud",
"providerHow": "read-from-code",
"feature": "theming-and-languages",
- "origin": "own-code"
+ "origin": "own-code",
+ "screen": {
+ "board": null,
+ "reason": "De huisstijl komt uit Nextcloud en thematiq; elk Launchpad-scherm volgt die, er is geen eigen scherm voor."
+ }
},
{
"id": "p-custom-css",
@@ -4920,7 +5517,11 @@
"providerHow": "research-then-code",
"feature": "theming-and-languages",
"origin": "competitor",
- "note": "Specified 2026-09-27 in openspec/changes/platform-dashboard-theme-and-css (openspec pass)."
+ "note": "Specified 2026-09-27 in openspec/changes/platform-dashboard-theme-and-css (openspec pass).",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpBeheerAlgemeen"
+ }
},
{
"id": "p-background",
@@ -4978,7 +5579,11 @@
"providerHow": "read-from-code",
"feature": "theming-and-languages",
"featureConfidence": "medium",
- "origin": "own-code"
+ "origin": "own-code",
+ "screen": {
+ "board": null,
+ "reason": "De taal komt uit de persoonlijke instellingen van Nextcloud; elk scherm volgt die."
+ }
},
{
"id": "p-standalone",
@@ -5008,6 +5613,10 @@
"dashy": "source read at 4.7.0: Dashy is its own Node and Vue app (server.js, Dockerfile); there is no Nextcloud app (no appinfo/info.xml or PHP in the tree), so it cannot be installed on Nextcloud",
"workspace-365": "https://support.workspace365.net/en/articles/175370-workspace-365-configuration-possibilities every version needs SharePoint and Exchange (cloud or on-premises) (read 2026-09-26)",
"microsoft-viva": "https://learn.microsoft.com/en-us/sharepoint/homesites/sharepoint-app-in-teams-overview 'built on the SharePoint home site technology' and Teams; cannot run without them (read 2026-09-26)"
+ },
+ "screen": {
+ "board": null,
+ "reason": "Installatie op een kale Nextcloud, geen scherm."
}
},
{
@@ -5068,7 +5677,11 @@
"featureConfidence": "high",
"origin": "featureRequest",
"originUrl": "https://github.com/nextcloud/server/issues/24658",
- "note": "A placed widget stays clickable in the picker (WidgetPickerModal.vue:36, :165 isPlaced only styles it) and the placement table has no unique index on dashboard and widget (PlacementTableBuilder.php:239-248)."
+ "note": "A placed widget stays clickable in the picker (WidgetPickerModal.vue:36, :165 isPlaced only styles it) and the placement table has no unique index on dashboard and widget (PlacementTableBuilder.php:239-248).",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpBewerken"
+ }
},
{
"id": "d-teams",
@@ -5160,7 +5773,11 @@
"featureConfidence": "high",
"origin": "featureRequest",
"originUrl": "https://github.com/nextcloud/server/issues/51332",
- "note": "The target is fixed by link type: web-address tiles always open in a new tab and app tiles in the same tab (TileWidget.vue:41). Nobody can choose it per tile, per dashboard or per user. Also mined from https://github.com/homarr-labs/homarr/issues/3778. Specified 2026-09-27 in openspec/changes/launcher-tile-open-behaviour (openspec pass)."
+ "note": "The target is fixed by link type: web-address tiles always open in a new tab and app tiles in the same tab (TileWidget.vue:41). Nobody can choose it per tile, per dashboard or per user. Also mined from https://github.com/homarr-labs/homarr/issues/3778. Specified 2026-09-27 in openspec/changes/launcher-tile-open-behaviour (openspec pass).",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpTegelBewerken"
+ }
},
{
"id": "d-auto-refresh",
@@ -5192,7 +5809,14 @@
"featureConfidence": "medium",
"origin": "changelog",
"originUrl": "https://github.com/nextcloud/server/pull/62624",
- "note": "Nextcloud widgets that declare reloadInterval are refreshed on a timer (WidgetRenderer.vue:442-447, :536 setupAutoRefresh); live value tiles refresh on their own interval (row w-live-value)."
+ "note": "Nextcloud widgets that declare reloadInterval are refreshed on a timer (WidgetRenderer.vue:442-447, :536 setupAutoRefresh); live value tiles refresh on their own interval (row w-live-value).",
+ "screen": {
+ "canvas": "design-system",
+ "board": [
+ "LpManagement",
+ "LpWidgetInstellen"
+ ]
+ }
},
{
"id": "d-idp-groups",
@@ -5224,7 +5848,11 @@
"featureConfidence": "medium",
"origin": "changelog",
"originUrl": "https://github.com/homarr-labs/homarr/releases/tag/v1.66.1",
- "note": "LaunchPad targets Nextcloud groups (RuleEvaluatorService.php:125, the same path as row t-groups), and the Nextcloud login apps (user_oidc, user_saml) provision those groups from the identity provider's claims at sign-in. LaunchPad itself holds no group sync; the platform does."
+ "note": "LaunchPad targets Nextcloud groups (RuleEvaluatorService.php:125, the same path as row t-groups), and the Nextcloud login apps (user_oidc, user_saml) provision those groups from the identity provider's claims at sign-in. LaunchPad itself holds no group sync; the platform does.",
+ "screen": {
+ "board": null,
+ "reason": "Groepslidmaatschap komt bij het aanmelden van de identiteitsprovider, zonder scherm."
+ }
},
{
"id": "d-tour",
@@ -5256,7 +5884,11 @@
"featureConfidence": "medium",
"origin": "changelog",
"originUrl": "https://github.com/homarr-labs/homarr/releases/tag/v1.64.0",
- "note": "The manifest declares a first-visit walkthrough (manifest.json:471-500, trigger first-visit, per-user completion key) that the shared shell renders: nextcloud-vue CnAppRoot.vue:464 mounts CnWalkthrough from manifest.walkthrough (nextcloud-vue development d239cf54), and App.vue:16 mounts CnAppRoot."
+ "note": "The manifest declares a first-visit walkthrough (manifest.json:471-500, trigger first-visit, per-user completion key) that the shared shell renders: nextcloud-vue CnAppRoot.vue:464 mounts CnWalkthrough from manifest.walkthrough (nextcloud-vue development d239cf54), and App.vue:16 mounts CnAppRoot.",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpEersteKeer"
+ }
},
{
"id": "d-pwa",
@@ -5288,7 +5920,11 @@
"featureConfidence": "medium",
"origin": "changelog",
"originUrl": "https://github.com/homarr-labs/homarr/releases/tag/v1.61.0",
- "note": "The web app manifest is Nextcloud's: nextcloud/server v35.0.1 apps/theming/lib/Controller/ThemingController.php:459-510 serves a per-app manifest with start_url and display standalone (route /apps/theming/manifest/{app}). LaunchPad adds nothing of its own; the cited LaunchPad line is its use of the theme variables the installed app inherits."
+ "note": "The web app manifest is Nextcloud's: nextcloud/server v35.0.1 apps/theming/lib/Controller/ThemingController.php:459-510 serves a per-app manifest with start_url and display standalone (route /apps/theming/manifest/{app}). LaunchPad adds nothing of its own; the cited LaunchPad line is its use of the theme variables the installed app inherits.",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpMobiel"
+ }
},
{
"id": "d-rest-api",
@@ -5320,7 +5956,11 @@
"featureConfidence": "medium",
"origin": "changelog",
"originUrl": "https://github.com/homarr-labs/homarr/releases/tag/v1.50.0",
- "note": "LaunchPad exposes about 180 JSON routes (appinfo/routes.php) that work with a Nextcloud app password, but ships no OpenAPI file or public API reference (searched the repo for openapi: only the OpenRegister register files). Usable, not documented. Also mined from https://github.com/Lissy93/dashy/releases/tag/4.4.0. Specified 2026-09-27 in openspec/changes/governance-rest-api-reference (openspec pass)."
+ "note": "LaunchPad exposes about 180 JSON routes (appinfo/routes.php) that work with a Nextcloud app password, but ships no OpenAPI file or public API reference (searched the repo for openapi: only the OpenRegister register files). Usable, not documented. Also mined from https://github.com/Lissy93/dashy/releases/tag/4.4.0. Specified 2026-09-27 in openspec/changes/governance-rest-api-reference (openspec pass).",
+ "screen": {
+ "board": null,
+ "reason": "Een REST-API voor andere systemen, zonder scherm."
+ }
},
{
"id": "d-proxy",
@@ -5352,7 +5992,11 @@
"featureConfidence": "high",
"origin": "changelog",
"originUrl": "https://github.com/homarr-labs/homarr/releases/tag/v1.48.0",
- "note": "Every outbound fetch goes through Nextcloud's IClientService (LiveTileService.php:455, NewsWidgetService.php:135, CalendarWidgetService.php:119, WeatherService, HealthPingService, IframeService.php:94, FeedRefreshService), which applies the server's proxy setting from config.php. Searched lib for curl_, file_get_contents and new Client: none."
+ "note": "Every outbound fetch goes through Nextcloud's IClientService (LiveTileService.php:455, NewsWidgetService.php:135, CalendarWidgetService.php:119, WeatherService, HealthPingService, IframeService.php:94, FeedRefreshService), which applies the server's proxy setting from config.php. Searched lib for curl_, file_get_contents and new Client: none.",
+ "screen": {
+ "board": null,
+ "reason": "Uitgaande verzoeken gebruiken de proxy-instelling van de server, zonder scherm."
+ }
},
{
"id": "d-single-logout",
@@ -5413,7 +6057,11 @@
"featureConfidence": "high",
"origin": "featureRequest",
"originUrl": "https://github.com/homarr-labs/homarr/issues/1014",
- "note": "Searched src and lib for bookmark import, netscape and html import: no importer for browser bookmark files; tiles are made one at a time or arrive through dashboard import (row r-export). Specified 2026-09-27 in openspec/changes/launcher-bookmark-import (openspec pass)."
+ "note": "Searched src and lib for bookmark import, netscape and html import: no importer for browser bookmark files; tiles are made one at a time or arrive through dashboard import (row r-export). Specified 2026-09-27 in openspec/changes/launcher-bookmark-import (openspec pass).",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpTegelToevoegen"
+ }
},
{
"id": "d-notes",
@@ -5445,7 +6093,11 @@
"featureConfidence": "high",
"origin": "featureRequest",
"originUrl": "https://github.com/Lissy93/dashy/issues/636",
- "note": "A text widget on your own dashboard can hold notes (widget-types.json:37, rendered by nextcloud-vue), but it is edited in dashboard edit mode, not a quick notepad you type into in place. Specified in ConductionNL/nextcloud-vue openspec/changes/dashboard-notepad-widget (nextcloud-vue lane, OpenSpec pass 2026-09-27)."
+ "note": "A text widget on your own dashboard can hold notes (widget-types.json:37, rendered by nextcloud-vue), but it is edited in dashboard edit mode, not a quick notepad you type into in place. Specified in ConductionNL/nextcloud-vue openspec/changes/dashboard-notepad-widget (nextcloud-vue lane, OpenSpec pass 2026-09-27).",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpWerkplek"
+ }
},
{
"id": "d-internal-url",
@@ -5476,7 +6128,11 @@
"featureConfidence": "high",
"origin": "changelog",
"originUrl": "https://github.com/Lissy93/dashy/releases/tag/4.4.0",
- "note": "A tile holds one address (TileWidget.vue:41 and :158 split only Nextcloud-internal paths from external URLs); searched src and lib for internal, external and network-aware URLs: nothing picks an address by the network you are on. Specified 2026-09-27 in openspec/changes/launcher-tile-internal-address (openspec pass)."
+ "note": "A tile holds one address (TileWidget.vue:41 and :158 split only Nextcloud-internal paths from external URLs); searched src and lib for internal, external and network-aware URLs: nothing picks an address by the network you are on. Specified 2026-09-27 in openspec/changes/launcher-tile-internal-address (openspec pass).",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpTegelBewerken"
+ }
},
{
"id": "d-theme-per-dashboard",
@@ -5508,7 +6164,11 @@
"featureConfidence": "medium",
"origin": "changelog",
"originUrl": "https://github.com/Lissy93/dashy/releases/tag/4.0.0",
- "note": "Each dashboard carries its own grid_columns (DashboardTableBuilder.php:173) and has per-language content variants in the backend (lib/Db/DashboardTranslation.php:6, no page reaches them, row dash-languages), but there is no theme, colour or background per dashboard (dashboard columns at DashboardTableBuilder.php:75-255). Specified 2026-09-27 in openspec/changes/platform-dashboard-theme-and-css (openspec pass)."
+ "note": "Each dashboard carries its own grid_columns (DashboardTableBuilder.php:173) and has per-language content variants in the backend (lib/Db/DashboardTranslation.php:6, no page reaches them, row dash-languages), but there is no theme, colour or background per dashboard (dashboard columns at DashboardTableBuilder.php:75-255). Specified 2026-09-27 in openspec/changes/platform-dashboard-theme-and-css (openspec pass).",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpDashboardInstellingen"
+ }
},
{
"id": "d-open-inside",
@@ -5540,7 +6200,11 @@
"featureConfidence": "medium",
"origin": "changelog",
"originUrl": "https://github.com/Lissy93/dashy/releases/tag/3.2.0",
- "note": "An embed widget shows another site inside the dashboard (IframeWidget.vue:38, host allow-list, row w-embed), but a tile cannot open its app in a side panel next to the tiles; tiles navigate away or open a new tab (TileWidget.vue:41). Specified 2026-09-27 in openspec/changes/launcher-tile-open-behaviour (openspec pass)."
+ "note": "An embed widget shows another site inside the dashboard (IframeWidget.vue:38, host allow-list, row w-embed), but a tile cannot open its app in a side panel next to the tiles; tiles navigate away or open a new tab (TileWidget.vue:41). Specified 2026-09-27 in openspec/changes/launcher-tile-open-behaviour (openspec pass).",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpTegelBewerken"
+ }
},
{
"id": "d-search-prefix",
@@ -5571,7 +6235,11 @@
"featureConfidence": "medium",
"origin": "changelog",
"originUrl": "https://github.com/Lissy93/dashy/releases/tag/3.2.0",
- "note": "The search widget filters tiles and falls back to one configured web search per widget (SearchWidget.vue:178 onSearchFallback, SearchWidgetForm.vue:41); searched for prefix, bang and engine routing: none. Specified 2026-09-27 in openspec/changes/search-ai-prefix-shortcuts (openspec pass)."
+ "note": "The search widget filters tiles and falls back to one configured web search per widget (SearchWidget.vue:178 onSearchFallback, SearchWidgetForm.vue:41); searched for prefix, bang and engine routing: none. Specified 2026-09-27 in openspec/changes/search-ai-prefix-shortcuts (openspec pass).",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpWerkplek"
+ }
},
{
"id": "d-dark-mode",
@@ -5603,7 +6271,11 @@
"featureConfidence": "medium",
"origin": "changelog",
"originUrl": "https://github.com/Lissy93/dashy/releases/tag/3.2.0",
- "note": "LaunchPad draws with Nextcloud's theme variables (WorkspaceApp.vue:480, :500), and Nextcloud's theming app switches them by the device's colour scheme through media-query themes (nextcloud/server v35.0.1 apps/theming/lib/Service/ThemeInjectionService.php:39-48). Also mined from https://www.microsoft.com/releasecommunications/api/v1/m365."
+ "note": "LaunchPad draws with Nextcloud's theme variables (WorkspaceApp.vue:480, :500), and Nextcloud's theming app switches them by the device's colour scheme through media-query themes (nextcloud/server v35.0.1 apps/theming/lib/Service/ThemeInjectionService.php:39-48). Also mined from https://www.microsoft.com/releasecommunications/api/v1/m365.",
+ "screen": {
+ "board": null,
+ "reason": "Licht of donker volgt het apparaat en de Nextcloud-instelling; elk scherm volgt die."
+ }
},
{
"id": "d-tile-sort",
@@ -5634,7 +6306,11 @@
"featureConfidence": "high",
"origin": "changelog",
"originUrl": "https://github.com/Lissy93/dashy/releases/tag/4.6.0",
- "note": "Tiles sit where they were placed on the grid; searched TileWidget, the links and quick links widgets for sortBy, sortOrder and sortMode: none. Only the people widget has a sort (PeopleWidget.vue:120). Specified 2026-09-27 in openspec/changes/launcher-tile-sorting (openspec pass)."
+ "note": "Tiles sit where they were placed on the grid; searched TileWidget, the links and quick links widgets for sortBy, sortOrder and sortMode: none. Only the people widget has a sort (PeopleWidget.vue:120). Specified 2026-09-27 in openspec/changes/launcher-tile-sorting (openspec pass).",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpTegelBewerken"
+ }
},
{
"id": "d-ai-write",
@@ -5726,7 +6402,11 @@
"featureConfidence": "high",
"origin": "changelog",
"originUrl": "https://support.workspace365.net/en/articles/766599-release-notes-workspace-365-v4-46",
- "note": "The people widget shows the standard Nextcloud profile fields (PeopleWidgetService.php:91-103: phone, address, organisation, role, headline, biography, pronouns, birthdate), which a login app can fill from the identity provider. No custom fields such as office location or cost centre. Specified 2026-09-27 in openspec/changes/widgets-people-expertise-and-fields (openspec pass)."
+ "note": "The people widget shows the standard Nextcloud profile fields (PeopleWidgetService.php:91-103: phone, address, organisation, role, headline, biography, pronouns, birthdate), which a login app can fill from the identity provider. No custom fields such as office location or cost centre. Specified 2026-09-27 in openspec/changes/widgets-people-expertise-and-fields (openspec pass).",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpIntranet"
+ }
},
{
"id": "d-news-preview",
@@ -5757,7 +6437,11 @@
"featureConfidence": "medium",
"origin": "roadmap",
"originUrl": "https://portal.productboard.com/iqfsnhkpzih6grzjwagif2ek/tabs/3-launched",
- "note": "LaunchPad reads outside news feeds (row w-news) and has no news authoring (row e-announcements is no), so there is nothing to preview before publishing. Specified 2026-09-27 in openspec/changes/engagement-announcements (openspec pass)."
+ "note": "LaunchPad reads outside news feeds (row w-news) and has no news authoring (row e-announcements is no), so there is nothing to preview before publishing. Specified 2026-09-27 in openspec/changes/engagement-announcements (openspec pass).",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpAankondiging"
+ }
},
{
"id": "d-recurring-events",
@@ -5879,7 +6563,11 @@
"featureConfidence": "high",
"origin": "tender",
"originUrl": "https://www.tenderned.nl/aankondigingen/overzicht/296963",
- "note": "LaunchPad has no work queue of its own; it can show a case app's Nextcloud widget. dossiq registers 'My tasks' and 'Cases overview' widgets (ConductionNL/dossiq development 00869dd7 lib/AppInfo/Registrar/AppHostRegistrar.php:126-132, lib/Dashboard/MyTasksWidget.php:42 a legacy IWidget) and LaunchPad's bridge catches OCA.Dashboard.register for such widgets (widgetBridge.js:29-62). Whether they render end to end is pending q-sibling-widgets. Also mined from https://www.tenderned.nl/aankondigingen/overzicht/237144, https://www.tenderned.nl/aankondigingen/overzicht/416075. Tender text: Noorderzijlvest 2023: 'Startscherm/medewerkersportaal met doorklik naar werkvoorraad'; Nationale ombudsman 2021: 'persoonlijk dashboard ... Overzicht lopende (eigen) zaken'; Súdwest-Fryslân 2026: dashboard with 'alle openstaande signaleringen, actiepunten, issues' at a glance, set up by the user with widgets. Specified 2026-09-27 (openspec pass, coordinator) in ConductionNL/dossiq openspec/changes/widgets-my-open-cases-on-the-start-page."
+ "note": "LaunchPad has no work queue of its own; it can show a case app's Nextcloud widget. dossiq registers 'My tasks' and 'Cases overview' widgets (ConductionNL/dossiq development 00869dd7 lib/AppInfo/Registrar/AppHostRegistrar.php:126-132, lib/Dashboard/MyTasksWidget.php:42 a legacy IWidget) and LaunchPad's bridge catches OCA.Dashboard.register for such widgets (widgetBridge.js:29-62). Whether they render end to end is pending q-sibling-widgets. Also mined from https://www.tenderned.nl/aankondigingen/overzicht/237144, https://www.tenderned.nl/aankondigingen/overzicht/416075. Tender text: Noorderzijlvest 2023: 'Startscherm/medewerkersportaal met doorklik naar werkvoorraad'; Nationale ombudsman 2021: 'persoonlijk dashboard ... Overzicht lopende (eigen) zaken'; Súdwest-Fryslân 2026: dashboard with 'alle openstaande signaleringen, actiepunten, issues' at a glance, set up by the user with widgets. Specified 2026-09-27 (openspec pass, coordinator) in ConductionNL/dossiq openspec/changes/widgets-my-open-cases-on-the-start-page.",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpStart"
+ }
},
{
"id": "w-attention",
@@ -5899,7 +6587,11 @@
},
"reachedOn": "Workspace page, Add widget, First today",
"source": "spec-round-2026-10-07",
- "feature": "nc-dashboard-widget-proxy"
+ "feature": "nc-dashboard-widget-proxy",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpStart"
+ }
},
{
"id": "w-menu",
@@ -5919,7 +6611,11 @@
},
"reachedOn": "Workspace page, Add widget, Menu",
"source": "spec-round-2026-10-07",
- "feature": "organisation-navigation"
+ "feature": "organisation-navigation",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpIntranet"
+ }
},
{
"id": "w-quicklinks",
@@ -5939,7 +6635,11 @@
},
"reachedOn": "Workspace page, Add widget, Quick links",
"source": "spec-round-2026-10-07",
- "feature": "tiles"
+ "feature": "tiles",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpWerkplek"
+ }
},
{
"id": "d-public-start",
@@ -5971,7 +6671,11 @@
"featureConfidence": "high",
"origin": "tender",
"originUrl": "https://www.publicprocurement.be/nl/publication-workspaces/204ca8ae-6191-43b9-ac3b-c943f91d1d2d",
- "note": "A dashboard can be published read-only on a public link (routes.php:189-212, PublicShareController.php:248 #[PublicPage]), but signed-out visitors at the Nextcloud address still get the login page; nothing makes a public dashboard the start page for them. Tender text: VVSG Blink 2025: 'Er is een publieke startpagina die kan aangepast worden en die verschilt van de inlogpagina'. Specified 2026-09-27 in openspec/changes/sharing-public-start-page (openspec pass)."
+ "note": "A dashboard can be published read-only on a public link (routes.php:189-212, PublicShareController.php:248 #[PublicPage]), but signed-out visitors at the Nextcloud address still get the login page; nothing makes a public dashboard the start page for them. Tender text: VVSG Blink 2025: 'Er is een publieke startpagina die kan aangepast worden en die verschilt van de inlogpagina'. Specified 2026-09-27 in openspec/changes/sharing-public-start-page (openspec pass).",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpPubliek"
+ }
},
{
"id": "d-find-expert",
@@ -6003,7 +6707,315 @@
"featureConfidence": "high",
"origin": "tender",
"originUrl": "https://www.tenderned.nl/aankondigingen/overzicht/418890",
- "note": "The people widget filters only by group (PeopleWidgetService.php:462-470 extractGroupFilter) and shows biography and headline but cannot be searched by them; searched for skills, expertise and tags: none. Tender text: Gemeente Noordwijk 2026: 'Onze persoonlijke trefwoorden (profiel tags) op ons intranet-profiel zijn volledig en actueel' so KCC colleagues can find them. Specified 2026-09-27 in openspec/changes/widgets-people-expertise-and-fields (openspec pass)."
+ "note": "The people widget filters only by group (PeopleWidgetService.php:462-470 extractGroupFilter) and shows biography and headline but cannot be searched by them; searched for skills, expertise and tags: none. Tender text: Gemeente Noordwijk 2026: 'Onze persoonlijke trefwoorden (profiel tags) op ons intranet-profiel zijn volledig en actueel' so KCC colleagues can find them. Specified 2026-09-27 in openspec/changes/widgets-people-expertise-and-fields (openspec pass).",
+ "screen": {
+ "canvas": "design-system",
+ "board": "LpIntranet"
+ }
+ }
+ ],
+ "specScreens": {
+ "activity-feed-integration": {
+ "board": null,
+ "reason": "Gebeurtenissen verschijnen in de Activiteit-app van Nextcloud, niet op een scherm van Launchpad."
+ },
+ "beta-alignment": {
+ "board": null,
+ "reason": "Interne afstemming van app-informatie, manifest en documentatie."
+ },
+ "calendar-widget": {
+ "board": [
+ "LpStart",
+ "LpWerkplek"
+ ]
+ },
+ "cli-commands": {
+ "board": null,
+ "reason": "Commando's op de server (occ), zonder scherm."
+ },
+ "clock-weather-widgets": {
+ "board": [
+ "LpWerkplek"
+ ]
+ },
+ "conditional-visibility-editor": {
+ "board": [
+ "LpZichtbaarheid"
+ ]
+ },
+ "confluence-html-import": {
+ "board": [
+ "LpBeheerOnderhoud"
+ ]
+ },
+ "container-widget": {
+ "board": [
+ "LpManagement"
+ ]
+ },
+ "dashboard-acknowledgements": {
+ "board": [
+ "LpIntranet",
+ "LpLeesbevestigingen"
+ ]
+ },
+ "dashboard-bulk-operations": {
+ "board": [
+ "LpBeheerOnderhoud"
+ ]
+ },
+ "dashboard-cascade-events": {
+ "board": null,
+ "reason": "Opruimen van afhankelijke gegevens bij het verwijderen van een dashboard gebeurt op de achtergrond."
+ },
+ "dashboard-kiosk-mode": {
+ "board": [
+ "LpKiosk"
+ ]
+ },
+ "dashboard-language-content": {
+ "board": [
+ "LpIntranet"
+ ]
+ },
+ "dashboard-locking": {
+ "board": [
+ "LpBewerken"
+ ]
+ },
+ "dashboard-metadata-fields": {
+ "board": [
+ "LpDashboardInstellingen"
+ ]
+ },
+ "dashboard-public-share": {
+ "board": [
+ "LpDelen",
+ "LpPubliek"
+ ]
+ },
+ "dashboard-quota-limits": {
+ "board": [
+ "LpBeheerAlgemeen"
+ ]
+ },
+ "dashboard-reactions": {
+ "board": [
+ "LpIntranet"
+ ]
+ },
+ "dashboard-store": {
+ "board": [
+ "LpStore"
+ ]
+ },
+ "dashboard-switcher": {
+ "board": [
+ "LpWerkplek"
+ ]
+ },
+ "dashboard-versioning": {
+ "board": [
+ "LpVersies"
+ ]
+ },
+ "dashboard-view-analytics": {
+ "board": [
+ "LpBeheerAnalyse"
+ ]
+ },
+ "demo-data-showcases": {
+ "board": [
+ "LpInstellen"
+ ]
+ },
+ "divider-widget": {
+ "board": [
+ "LpIntranet"
+ ]
+ },
+ "effective-default-marker": {
+ "board": [
+ "LpWerkplek"
+ ]
+ },
+ "files-widget": {
+ "board": [
+ "LpWerkplek"
+ ]
+ },
+ "first-time-setup": {
+ "board": [
+ "LpInstellen"
+ ]
+ },
+ "footer-customization": {
+ "board": [
+ "LpWerkplek",
+ "LpBeheerNavigatie"
+ ]
+ },
+ "groupfolder-storage-backend": {
+ "board": null,
+ "reason": "Ingetrokken: deze opslag is nooit in de app aangesloten."
+ },
+ "header-widget": {
+ "board": [
+ "LpIntranet"
+ ]
+ },
+ "iframe-embed-widget": {
+ "board": [
+ "LpIntranet"
+ ]
+ },
+ "image-widget": {
+ "board": [
+ "LpIntranet"
+ ]
+ },
+ "infrastructure-helpers": {
+ "board": null,
+ "reason": "Interne hulpklassen zonder scherm."
+ },
+ "initial-state-contract": {
+ "board": null,
+ "reason": "Interne afspraak over de gegevens die de server aan de pagina meegeeft."
+ },
+ "label-widget": {
+ "board": [
+ "LpIntranet"
+ ]
+ },
+ "launchpad-adopt-or-abstractions": {
+ "board": null,
+ "reason": "Zorgt dat Launchpad zonder OpenRegister draait; een architectuurafspraak zonder eigen scherm."
+ },
+ "launchpad-ai-dashboard-assistant": {
+ "board": [
+ "LpWerkplek"
+ ]
+ },
+ "launchpad-compliance-audit-panel": {
+ "board": [
+ "LpManagement"
+ ]
+ },
+ "launchpad-enterprise-security-access": {
+ "board": [
+ "LpManagement"
+ ]
+ },
+ "launchpad-file-access-widget": {
+ "board": [
+ "LpWerkplek"
+ ]
+ },
+ "launchpad-meeting-calendar-actions": {
+ "board": [
+ "LpWerkplek"
+ ]
+ },
+ "launchpad-mobile-remote-access": {
+ "board": [
+ "LpMobiel"
+ ]
+ },
+ "launchpad-spend-analytics-widget": {
+ "board": [
+ "LpManagement"
+ ]
+ },
+ "legacy-widget-bridge": {
+ "board": [
+ "LpWidgetKiezen",
+ "LpBeheerOnderhoud"
+ ]
+ },
+ "license-header-consistency": {
+ "board": null,
+ "reason": "Licentiekoppen in de broncode, zonder scherm."
+ },
+ "link-button-widget": {
+ "board": [
+ "LpWerkplek"
+ ]
+ },
+ "links-widget": {
+ "board": [
+ "LpWerkplek"
+ ]
+ },
+ "live-data-tile-widget": {
+ "board": [
+ "LpManagement",
+ "LpWidgetInstellen"
+ ]
+ },
+ "menu-widget": {
+ "board": [
+ "LpIntranet"
+ ]
+ },
+ "navigation-editor-org": {
+ "board": [
+ "LpBeheerNavigatie"
+ ]
+ },
+ "nc-dashboard-widget-proxy": {
+ "board": [
+ "LpWidgetKiezen"
+ ]
+ },
+ "nc-unified-search-integration": {
+ "board": [
+ "LpWerkplek"
+ ]
+ },
+ "news-widget": {
+ "board": [
+ "LpIntranet"
+ ]
+ },
+ "orphaned-data-cleanup": {
+ "board": [
+ "LpBeheerOnderhoud"
+ ]
+ },
+ "quicklinks-widget": {
+ "board": [
+ "LpWerkplek"
+ ]
+ },
+ "resource-uploads": {
+ "board": null,
+ "reason": "Interne bestands-API voor pictogrammen en afbeeldingen; het uploaden zelf staat op de schermen van die widgets."
+ },
+ "service-health-ping": {
+ "board": [
+ "LpWerkplek",
+ "LpTegelBewerken"
+ ]
+ },
+ "setup-wizard": {
+ "board": [
+ "LpInstellen"
+ ]
+ },
+ "text-display-widget": {
+ "board": [
+ "LpIntranet"
+ ]
+ },
+ "tile-quick-search": {
+ "board": [
+ "LpWerkplek"
+ ]
+ },
+ "video-widget": {
+ "board": [
+ "LpIntranet"
+ ]
}
- ]
+ }
}
From d6e6322a2734d872c725bab24cc03401853477a8 Mon Sep 17 00:00:00 2001
From: Ruben van der Linde
Date: Thu, 8 Oct 2026 16:51:42 +0200
Subject: [PATCH 29/60] chore(parity): place dashboard-icons on its screens
---
openspec/parity/capabilities.json | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/openspec/parity/capabilities.json b/openspec/parity/capabilities.json
index eeb0f93ac..a11e81d4d 100644
--- a/openspec/parity/capabilities.json
+++ b/openspec/parity/capabilities.json
@@ -6768,6 +6768,12 @@
"board": null,
"reason": "Opruimen van afhankelijke gegevens bij het verwijderen van een dashboard gebeurt op de achtergrond."
},
+ "dashboard-icons": {
+ "board": [
+ "LpDashboardInstellingen",
+ "LpWerkplek"
+ ]
+ },
"dashboard-kiosk-mode": {
"board": [
"LpKiosk"
From d493d7ee655eda6f241ea3facade257cc150bbc7 Mon Sep 17 00:00:00 2001
From: Ruben van der Linde
Date: Fri, 9 Oct 2026 06:56:32 +0200
Subject: [PATCH 30/60] docs(grid-layout): new widgets append below the layout,
the spec now says so
Archives grid-bottom-append-placement: REQ-GRID-006 describes the shipped
bottom-append rule and the push-down scenarios are retired.
---
.../design.md | 0
.../proposal.md | 0
.../specs/grid-layout/spec.md | 14 +++++
.../tasks.md | 2 +-
openspec/specs/grid-layout/spec.md | 60 +++++++++++--------
5 files changed, 51 insertions(+), 25 deletions(-)
rename openspec/changes/{grid-bottom-append-placement => archive/2026-10-09-grid-bottom-append-placement}/design.md (100%)
rename openspec/changes/{grid-bottom-append-placement => archive/2026-10-09-grid-bottom-append-placement}/proposal.md (100%)
rename openspec/changes/{grid-bottom-append-placement => archive/2026-10-09-grid-bottom-append-placement}/specs/grid-layout/spec.md (80%)
rename openspec/changes/{grid-bottom-append-placement => archive/2026-10-09-grid-bottom-append-placement}/tasks.md (97%)
diff --git a/openspec/changes/grid-bottom-append-placement/design.md b/openspec/changes/archive/2026-10-09-grid-bottom-append-placement/design.md
similarity index 100%
rename from openspec/changes/grid-bottom-append-placement/design.md
rename to openspec/changes/archive/2026-10-09-grid-bottom-append-placement/design.md
diff --git a/openspec/changes/grid-bottom-append-placement/proposal.md b/openspec/changes/archive/2026-10-09-grid-bottom-append-placement/proposal.md
similarity index 100%
rename from openspec/changes/grid-bottom-append-placement/proposal.md
rename to openspec/changes/archive/2026-10-09-grid-bottom-append-placement/proposal.md
diff --git a/openspec/changes/grid-bottom-append-placement/specs/grid-layout/spec.md b/openspec/changes/archive/2026-10-09-grid-bottom-append-placement/specs/grid-layout/spec.md
similarity index 80%
rename from openspec/changes/grid-bottom-append-placement/specs/grid-layout/spec.md
rename to openspec/changes/archive/2026-10-09-grid-bottom-append-placement/specs/grid-layout/spec.md
index 25ddd4a72..ccc1a0655 100644
--- a/openspec/changes/grid-bottom-append-placement/specs/grid-layout/spec.md
+++ b/openspec/changes/archive/2026-10-09-grid-bottom-append-placement/specs/grid-layout/spec.md
@@ -62,3 +62,17 @@ This requirement deliberately replaces the former "GridStack auto-position + top
- **WHEN** the placement completes
- **THEN** the new widget MUST be persisted via the standard placement-update API (REQ-WDG-008 batch update or per-placement PUT)
- **AND** because no existing widget is moved, only the new widget's position needs to be written
+
+#### Scenario: Add widget to partially filled grid (incremental render)
+- GIVEN the grid has widgets occupying rows 0-2 in columns 0-8
+- AND columns 8-11 in row 0 are empty
+- WHEN the user adds a new widget with gridWidth 4 and gridHeight 2
+- THEN `syncGridItems()` MUST detect the new placement and call `grid.makeWidget()` on the next tick
+- AND GridStack MUST render the widget at the position computed by `placeNewWidget` (auto-position or fallback)
+- NOTE: With `float: true`, auto-placement behavior may differ from non-float mode.
+
+#### Scenario: Remove widget syncs grid
+- GIVEN widget placement id 10 is removed from the placements array
+- WHEN the placements watcher triggers `syncGridItems()`
+- THEN `syncGridItems()` MUST find the orphaned grid node and call `grid.removeWidget()` with `removeDOM: false`
+- AND the grid MUST update its layout accordingly
diff --git a/openspec/changes/grid-bottom-append-placement/tasks.md b/openspec/changes/archive/2026-10-09-grid-bottom-append-placement/tasks.md
similarity index 97%
rename from openspec/changes/grid-bottom-append-placement/tasks.md
rename to openspec/changes/archive/2026-10-09-grid-bottom-append-placement/tasks.md
index 92d99dc0b..56f4e52da 100644
--- a/openspec/changes/grid-bottom-append-placement/tasks.md
+++ b/openspec/changes/archive/2026-10-09-grid-bottom-append-placement/tasks.md
@@ -5,7 +5,7 @@
- [x] Task 1: Amend REQ-GRID-006 requirement text in `openspec/specs/grid-layout/spec.md` from "auto-position + top-left/push-down fallback" to the bottom-append rule (`x = 0`, `y = max(gridY + gridHeight)`, existing widgets never moved, empty dashboard → `(0,0)`) — captured in this change's `specs/grid-layout/spec.md` delta
- [x] Task 2: Rewrite the REQ-GRID-006 scenarios — remove "Push-down fallback when grid is full at top" and "Pushed widgets remain within their column lane"; replace "Auto-position into empty space" with bottom-append scenarios; update "Default size on omitted dimensions" and "Persistence after placement" to the no-push expectations (delta restates the full scenario set)
- [x] Task 3: Reconcile the stale doc comments in `src/stores/dashboard.js` (`addWidgetToDashboard` and `addTileToDashboard`) that still describe "try autoPosition, fall back to top-left + push down" so they describe bottom-append; logic untouched
-- [ ] Task 4 (apply time): Update the "Current Implementation Status" traceability line for REQ-GRID-006 in `openspec/specs/grid-layout/spec.md` (line ~496) from "autoPosition primary path and the top-left + push-down fallback … Push-down side effects flow through `updatePlacements`" to the bottom-append description
+- [x] Task 4 (apply time): Update the "Current Implementation Status" traceability line for REQ-GRID-006 in `openspec/specs/grid-layout/spec.md` (line ~496) from "autoPosition primary path and the top-left + push-down fallback … Push-down side effects flow through `updatePlacements`" to the bottom-append description
- [x] Task 5: Confirm the existing Vitest suite (`src/composables/__tests__/useGridManager.spec.js`, `describe('REQ-GRID-014 placeNewWidget() — always appends at the bottom')`) already encodes the new expectations and passes — no test change required
- [x] Task 6: Confirm REQ-GRID-014 (single placement authority) and its grep guard are unaffected — `grid.addWidget` still appears only in `useGridManager.js` and its test file
diff --git a/openspec/specs/grid-layout/spec.md b/openspec/specs/grid-layout/spec.md
index b6ec3eede..cc9f2b076 100644
--- a/openspec/specs/grid-layout/spec.md
+++ b/openspec/specs/grid-layout/spec.md
@@ -217,50 +217,62 @@ Grid position changes MUST be communicated to the parent component for API persi
- THEN each grid item's `id` MUST be matched to a placement's `id` via string comparison
- AND the placement's gridX, gridY, gridWidth, gridHeight MUST be updated from the grid item's x, y, w, h values
-### Requirement: Widget Auto-Layout — collision placement algorithm (REQ-GRID-006)
+### Requirement: REQ-GRID-006 Widget Auto-Layout (bottom-append placement)
-When a new widget is added to an existing dashboard, the system MUST place it without overlapping any existing widget and without leaving it outside the visible grid region. The algorithm MUST be:
-@e2e exclude auto-layout collision algorithm tests composable internal math — Vitest unit scope; browser outcome covered by REQ-GRID-001 init test
+@e2e exclude bottom-append placement is composable math; Vitest unit scope (src/composables/__tests__/useGridManager.spec.js), browser outcome covered by REQ-GRID-001 init test
-1. **Try GridStack auto-position** — call `grid.addWidget({x: 0, y: 0, w: newW, h: newH, autoPosition: true, ...})`. GridStack scans for the first empty rectangle that fits and uses it.
-2. **Fallback: top-left with push-down** — if step 1 fails (GridStack returns no slot, or the picked slot is below `viewportRows`), place the new widget at `(x: 0, y: 0)` with size `(newW, newH)` AND for every existing widget whose rectangle overlaps `[0..newW] × [0..newH]`, set its `gridY` to `newH` (pushing it just below the new one). Existing widgets that do not overlap MUST NOT be moved.
+When a new widget is added to a dashboard, the system MUST place it in a fresh row directly below all existing widgets, without overlapping any existing widget and without moving any existing widget. The algorithm MUST be:
-Default size when the caller omits `w`/`h` MUST be `w=4, h=4`. Position writes MUST trigger the persistence path of REQ-GRID-005.
+1. **Compute the bottom edge** — for every existing placement compute `gridY + gridHeight`; the new widget's `y` MUST be the maximum of these values (the lowest occupied bottom edge). A placement missing `gridY` contributes `0`; a placement missing `gridHeight` contributes height `1`.
+2. **Anchor to the left** — the new widget's `x` MUST be `0`.
+3. **Empty dashboard** — when there are no existing placements, the new widget MUST be placed at `(x: 0, y: 0)`.
-#### Scenario: Auto-position into empty space
+Existing widgets MUST NOT be moved — there is no push-down — and the helper's `pushed` array MUST always be empty. Default size when the caller omits `w`/`h` MUST be `w=4, h=4`. Position writes MUST trigger the persistence path of REQ-GRID-005.
+
+This requirement deliberately replaces the former "GridStack auto-position + top-left/push-down fallback" rule, which reordered the user's existing layout on every add once the visible rows filled up. The shipped `placeNewWidget(spec, placements, options?)` helper implements bottom-append; this requirement is the authoritative description of that behaviour.
+
+#### Scenario: Append below the only existing widget
- **GIVEN** a dashboard with one widget at `(x:0, y:0, w:6, h:4)`
- **WHEN** a new 4×4 widget is added
-- **THEN** GridStack MUST place it at `(x:6, y:0)` (the empty right-half)
+- **THEN** it MUST be placed at `(x:0, y:4)` (a fresh row below the existing widget)
+- **AND** the existing widget MUST NOT be moved
+- **AND** the returned `pushed` array MUST be empty
+
+#### Scenario: Append below the lowest widget regardless of array order
+
+- **GIVEN** a dashboard with widgets at `(x:0, y:5, w:6, h:3)` (bottom edge `8`) and `(x:6, y:0, w:6, h:2)` (bottom edge `2`)
+- **WHEN** a new 6×3 widget is added
+- **THEN** it MUST be placed at `(x:0, y:8)` (below the lowest bottom edge, not below the last array entry)
- **AND** no existing widget MUST be moved
-#### Scenario: Push-down fallback when grid is full at top
+#### Scenario: First widget on an empty dashboard
-- **GIVEN** a dashboard with widgets occupying the entire `[0..12] × [0..4]` region
+- **GIVEN** a dashboard with no widgets
- **WHEN** a new 4×4 widget is added
-- **THEN** it MUST be placed at `(x:0, y:0, w:4, h:4)`
-- **AND** every previously-overlapping widget MUST have `gridY = 4` (just below the new one)
-- **AND** non-overlapping widgets (already at `y >= 4`) MUST NOT have their `gridY` changed
+- **THEN** it MUST be placed at `(x:0, y:0)`
+
+#### Scenario: Existing widgets are never pushed when the top region is full
+
+- **GIVEN** a dashboard whose `[0..12] × [0..4]` region is fully occupied
+- **WHEN** a new 4×4 widget is added
+- **THEN** it MUST be placed at `(x:0, y:4)`, below the occupied region
+- **AND** every existing widget MUST retain its original `gridX`, `gridY`, `gridWidth`, and `gridHeight`
+- **AND** the returned `pushed` array MUST be empty
#### Scenario: Default size on omitted dimensions
- **GIVEN** a caller invokes `placeNewWidget({type: 'text'})` with no `w` or `h`
- **WHEN** the placement runs
- **THEN** the placement MUST use `w=4, h=4`
+- **AND** the `y` coordinate MUST still be the lowest occupied bottom edge (`0` on an empty dashboard)
#### Scenario: Persistence after placement
-- **GIVEN** a new widget has been placed (via either step 1 or step 2)
+- **GIVEN** a new widget has been placed
- **WHEN** the placement completes
-- **THEN** the new widget AND any pushed-down widgets MUST be persisted via the standard placement-update API (REQ-WDG-008 batch update or per-placement PUT)
-- **AND** a single network round-trip SHOULD be used for the batch (debounce 300 ms per the design rule in `openspec/config.yaml`)
-
-#### Scenario: Pushed widgets remain within their column lane
-
-- **GIVEN** existing widget at `(x:8, y:0, w:4, h:2)` overlaps a new 6×3 widget being placed at top-left
-- **WHEN** the push-down fallback runs
-- **THEN** the existing widget's `gridX` and `gridW` MUST remain `8` and `4` respectively
-- **AND** only `gridY` MUST be increased to `3`
+- **THEN** the new widget MUST be persisted via the standard placement-update API (REQ-WDG-008 batch update or per-placement PUT)
+- **AND** because no existing widget is moved, only the new widget's position needs to be written
#### Scenario: Add widget to partially filled grid (incremental render)
- GIVEN the grid has widgets occupying rows 0-2 in columns 0-8
@@ -563,7 +575,7 @@ push behaviour.
- REQ-GRID-003 (Resize by Edge Dragging): Resize controlled via `disableResize: !this.editMode`. Min sizes via `gs-min-w="2"`, `gs-min-h="2"`.
- REQ-GRID-004 (View Mode vs Edit Mode): `editMode` prop controls grid state.
- REQ-GRID-005 (Position Persistence): `handleGridChange()` emits `update:placements` on every change event.
-- REQ-GRID-006 (Widget Auto-Layout — collision placement algorithm): `placeNewWidget()` in `src/composables/useGridManager.js` computes the position via the autoPosition primary path and the top-left + push-down fallback; `syncGridItems()` then renders the new placement via `grid.makeWidget()`. Push-down side effects flow through the existing `updatePlacements` batch path.
+- REQ-GRID-006 (Widget Auto-Layout — collision placement algorithm): `placeNewWidget()` in `src/composables/useGridManager.js` appends the new widget at the bottom: `x = 0`, `y = max(gridY + gridHeight)` over the existing placements, `(0, 0)` on an empty dashboard, and no existing widget moves (`pushed` is always empty); `syncGridItems()` then renders the new placement via `grid.makeWidget()`.
- REQ-GRID-009 (Tile vs Widget Rendering): `isTilePlacement()`, `getTileData()` handle rendering distinction.
- REQ-GRID-010 (Grid Styling): CSS applied via scoped styles with deep selectors.
- REQ-GRID-011 (Grid Synchronization): `placements` watcher triggers `syncGridItems()`.
From 024d6221cef7afddacdbe16c25e77fd291be9f87 Mon Sep 17 00:00:00 2001
From: Ruben van der Linde
Date: Fri, 9 Oct 2026 06:57:07 +0200
Subject: [PATCH 31/60] docs(openspec): archive
retire-groupfolder-storage-backend, the wizard step stays a follow-up
---
.../.openspec.yaml | 0
.../proposal.md | 0
.../2026-10-09-retire-groupfolder-storage-backend}/tasks.md | 6 +++++-
3 files changed, 5 insertions(+), 1 deletion(-)
rename openspec/changes/{retire-groupfolder-storage-backend => archive/2026-10-09-retire-groupfolder-storage-backend}/.openspec.yaml (100%)
rename openspec/changes/{retire-groupfolder-storage-backend => archive/2026-10-09-retire-groupfolder-storage-backend}/proposal.md (100%)
rename openspec/changes/{retire-groupfolder-storage-backend => archive/2026-10-09-retire-groupfolder-storage-backend}/tasks.md (82%)
diff --git a/openspec/changes/retire-groupfolder-storage-backend/.openspec.yaml b/openspec/changes/archive/2026-10-09-retire-groupfolder-storage-backend/.openspec.yaml
similarity index 100%
rename from openspec/changes/retire-groupfolder-storage-backend/.openspec.yaml
rename to openspec/changes/archive/2026-10-09-retire-groupfolder-storage-backend/.openspec.yaml
diff --git a/openspec/changes/retire-groupfolder-storage-backend/proposal.md b/openspec/changes/archive/2026-10-09-retire-groupfolder-storage-backend/proposal.md
similarity index 100%
rename from openspec/changes/retire-groupfolder-storage-backend/proposal.md
rename to openspec/changes/archive/2026-10-09-retire-groupfolder-storage-backend/proposal.md
diff --git a/openspec/changes/retire-groupfolder-storage-backend/tasks.md b/openspec/changes/archive/2026-10-09-retire-groupfolder-storage-backend/tasks.md
similarity index 82%
rename from openspec/changes/retire-groupfolder-storage-backend/tasks.md
rename to openspec/changes/archive/2026-10-09-retire-groupfolder-storage-backend/tasks.md
index 1c9fceb8c..4a2e83b7b 100644
--- a/openspec/changes/retire-groupfolder-storage-backend/tasks.md
+++ b/openspec/changes/archive/2026-10-09-retire-groupfolder-storage-backend/tasks.md
@@ -9,7 +9,11 @@
- [x] Task 5: Remove the PHPUnit classes that covered the removed tree — `DashboardContentStorageFactoryTest`, `DashboardContentStorageExceptionTest`, `DbContentStorageTest`, `GroupFolderContentStorageTest`, `MigrateStorageToGroupFolderTest`
- [x] Task 6: Mark `openspec/specs/groupfolder-storage-backend/spec.md` **withdrawn**, recording what was removed, what was deliberately kept, and why. This also retires the spec's 16 `@e2e exclude` markers, whose stated reasons cited `tests/Unit/Service/DashboardContentStorage/DbContentStorageTest.php` — a test file this change deletes, which would have left sixteen exemptions resting on a class that no longer exists
- [x] Task 7: Correct the phantom `- [x]` on Task 5 of the archived change at `openspec/changes/archive/2026-06-14-groupfolder-storage-backend/tasks.md`, because the project record asserted a wiring that did not exist
-- [ ] Task 8: **Follow-up, tracked on launchpad#87 — NOT in this change.** Decide whether setup-wizard step 2 (`launchpad.content_storage`, `AdminSettingKey::CONTENT_STORAGE`, `SetupWizardService::{getContentStorage,setContentStorage,hasGroupfolderApp}`, `AdminController::setWizardStorage` and its route) also retires. It writes a setting nothing now reads. Note that `tools/spec-annotations-allowlist.txt` carries entries for those three `SetupWizardService` methods and for `AdminController::setWizardStorage`
+
+
+## Follow-up (not a task of this change)
+
+- **Follow-up, tracked on launchpad#87 — NOT in this change.** Decide whether setup-wizard step 2 (`launchpad.content_storage`, `AdminSettingKey::CONTENT_STORAGE`, `SetupWizardService::{getContentStorage,setContentStorage,hasGroupfolderApp}`, `AdminController::setWizardStorage` and its route) also retires. It writes a setting nothing now reads. Note that `tools/spec-annotations-allowlist.txt` carries entries for those three `SetupWizardService` methods and for `AdminController::setWizardStorage` Open as Q-launchpad-1 (2026-10-09).
## Verification
From f7b9192dcefb20a087b6dac464af6d5d108bfafd Mon Sep 17 00:00:00 2001
From: Ruben van der Linde
Date: Fri, 9 Oct 2026 22:00:10 +0200
Subject: [PATCH 32/60] test(e2e): read the dashboard from its singular route
and the items envelope (#824)
The personal-layer spec fetched /api/dashboards/{id}, which has no route (show
is /api/dashboard/{id}), and read the visible list from data, while the
controller answers with items.
---
tests/e2e/api-direct/personal-dashboard-layer.api.spec.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/tests/e2e/api-direct/personal-dashboard-layer.api.spec.ts b/tests/e2e/api-direct/personal-dashboard-layer.api.spec.ts
index fefe3730c..82955138b 100644
--- a/tests/e2e/api-direct/personal-dashboard-layer.api.spec.ts
+++ b/tests/e2e/api-direct/personal-dashboard-layer.api.spec.ts
@@ -66,7 +66,7 @@ async function anyVisibleDashboard(api: APIRequestContext): Promise<{
const res = await api.get(`${API}/dashboards/visible`)
expect(res.ok(), 'the visible-dashboards route must answer').toBeTruthy()
const body = (await res.json()) as Record
- const list = (body.data ?? body.dashboards ?? body) as Array<
+ const list = (body.items ?? body.data ?? body.dashboards ?? body) as Array<
Record
>
expect(
@@ -79,7 +79,7 @@ async function anyVisibleDashboard(api: APIRequestContext): Promise<{
)
expect(Number.isFinite(id) && id > 0).toBeTruthy()
- const one = await api.get(`${API}/dashboards/${id}`)
+ const one = await api.get(`${API}/dashboard/${id}`)
expect(one.ok()).toBeTruthy()
const envelope = (await one.json()) as Record
const payload = (envelope.data ?? envelope) as Record
From daa7a9208ed2b02f47cd107fca82bbc0a8a0e004 Mon Sep 17 00:00:00 2001
From: Ruben van der Linde
Date: Fri, 9 Oct 2026 22:20:52 +0200
Subject: [PATCH 33/60] wip: saved by the coordinator after the 9 Oct afternoon
session stop (unverified)
---
appinfo/routes.php | 1 -
l10n/be.js | 13 +-
l10n/be.json | 13 +-
l10n/bg.js | 13 +-
l10n/bg.json | 13 +-
l10n/bs.js | 13 +-
l10n/bs.json | 13 +-
l10n/ca.js | 13 +-
l10n/ca.json | 13 +-
l10n/cs.js | 13 +-
l10n/cs.json | 13 +-
l10n/da.js | 13 +-
l10n/da.json | 13 +-
l10n/de.js | 13 +-
l10n/de.json | 13 +-
l10n/el.js | 13 +-
l10n/el.json | 13 +-
l10n/en.js | 13 +-
l10n/en.json | 13 +-
l10n/es.js | 13 +-
l10n/es.json | 13 +-
l10n/et.js | 13 +-
l10n/et.json | 13 +-
l10n/fi.js | 13 +-
l10n/fi.json | 13 +-
l10n/fr.js | 13 +-
l10n/fr.json | 13 +-
l10n/ga.js | 13 +-
l10n/ga.json | 13 +-
l10n/hr.js | 13 +-
l10n/hr.json | 13 +-
l10n/hu.js | 13 +-
l10n/hu.json | 13 +-
l10n/is.js | 13 +-
l10n/is.json | 13 +-
l10n/it.js | 13 +-
l10n/it.json | 13 +-
l10n/lb.js | 13 +-
l10n/lb.json | 13 +-
l10n/lt.js | 13 +-
l10n/lt.json | 13 +-
l10n/lv.js | 13 +-
l10n/lv.json | 13 +-
l10n/mk.js | 13 +-
l10n/mk.json | 13 +-
l10n/mt.js | 13 +-
l10n/mt.json | 13 +-
l10n/nb.js | 13 +-
l10n/nb.json | 13 +-
l10n/nl.js | 13 +-
l10n/nl.json | 13 +-
l10n/pl.js | 13 +-
l10n/pl.json | 13 +-
l10n/pt.js | 13 +-
l10n/pt.json | 13 +-
l10n/rm.js | 13 +-
l10n/rm.json | 13 +-
l10n/ro.js | 13 +-
l10n/ro.json | 13 +-
l10n/ru.js | 13 +-
l10n/ru.json | 13 +-
l10n/sk.js | 13 +-
l10n/sk.json | 13 +-
l10n/sl.js | 13 +-
l10n/sl.json | 13 +-
l10n/sq.js | 13 +-
l10n/sq.json | 13 +-
l10n/sr.js | 13 +-
l10n/sr.json | 13 +-
l10n/sv.js | 13 +-
l10n/sv.json | 13 +-
l10n/tr.js | 13 +-
l10n/tr.json | 13 +-
l10n/uk.js | 13 +-
l10n/uk.json | 13 +-
lib/Command/SetupCommand.php | 51 +---
lib/Controller/AdminController.php | 57 -----
lib/Db/AdminSetting.php | 9 -
lib/Db/AdminSettingKey.php | 3 +-
lib/Service/AdminSettingsService.php | 32 +--
lib/Service/SetupWizardService.php | 102 +-------
openspec/specs/admin-settings/spec.md | 14 +-
.../specs/groupfolder-storage-backend/spec.md | 14 +-
openspec/specs/setup-wizard/spec.md | 238 ++++++++----------
src/components/admin/AdminSettings.vue | 2 +-
src/modals/SetupWizardModal.vue | 132 ++--------
src/modals/__tests__/SetupWizardModal.spec.js | 57 +----
src/services/api.js | 11 -
.../AdminControllerSetupWizardTest.php | 59 +----
tests/Unit/Db/AdminSettingKeyTest.php | 13 +-
.../Unit/Service/AdminSettingsServiceTest.php | 5 +-
tests/Unit/Service/SetupWizardServiceTest.php | 115 ++++-----
.../launchpad.postman_collection.json | 42 ----
tools/spec-annotations-allowlist.txt | 4 -
94 files changed, 462 insertions(+), 1461 deletions(-)
diff --git a/appinfo/routes.php b/appinfo/routes.php
index 47f794178..422e04141 100644
--- a/appinfo/routes.php
+++ b/appinfo/routes.php
@@ -480,7 +480,6 @@
// Step 2 choice immediately on `Next`.
['name' => 'admin#getWizardState', 'url' => '/api/admin/setup-wizard/state', 'verb' => 'GET'],
['name' => 'admin#completeWizard', 'url' => '/api/admin/setup-wizard/complete', 'verb' => 'POST'],
- ['name' => 'admin#setWizardStorage', 'url' => '/api/admin/setup-wizard/storage', 'verb' => 'POST'],
// Confluence HTML export importer (REQ-CFLI-001..012). Admin-only
// via runtime `IGroupManager::isAdmin` check inside the
diff --git a/l10n/be.js b/l10n/be.js
index acb4eb5f7..ee30e0d02 100644
--- a/l10n/be.js
+++ b/l10n/be.js
@@ -926,14 +926,6 @@ OC.L10N.register(
"LaunchPad setup wizard" : "Майстар наладкі LaunchPad",
"Step {n} / {total}" : "Крок {n} / {total}",
"Welcome" : "Вітаем",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings." : "Наладзьце ваш экзэмпляр LaunchPad са сховішчам, парадкам груп, дэманстрацыйнымі данымі, ролямі адміністратараў і наладамі ніжняга калонтытула.",
- "Storage backend" : "Бэкенд сховішча",
- "Choose how LaunchPad stores dashboard content." : "Выберыце, як LaunchPad захоўвае змесціва панэляў.",
- "Database (default)" : "База даных (прадвызначана)",
- "Store dashboard content in the LaunchPad database table." : "Захоўваць змесціва панэляў у табліцы базы даных LaunchPad.",
- "GroupFolder (recommended for org use)" : "GroupFolder (рэкамендуецца для арганізацыйнага выкарыстання)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access." : "Захоўваць змесціва панэляў у Nextcloud GroupFolders для сумеснага доступу.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option." : "Праграма GroupFolder не ўсталявана. Усталюйце 'Nextcloud GroupFolders', каб выкарыстоўваць гэты варыянт.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace." : "Выберыце парадак, у якім групы Nextcloud з'яўляюцца, калі LaunchPad накіроўвае карыстальнікаў да працоўнай прасторы.",
"Demo data" : "Дэманстрацыйныя даныя",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only." : "Дэманстрацыі дэманстрацыйных даных з'явяцца тут, як толькі будзе выпушчана магчымасць demo-data-showcases. Пакуль што гэты крок толькі інфармацыйны.",
@@ -952,7 +944,6 @@ OC.L10N.register(
"Finish" : "Завяршыць",
"Run setup wizard" : "Запусціць майстар наладкі",
"Run setup wizard again" : "Запусціць майстар наладкі зноў",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles." : "Запусціце ваш інтранэт: выберыце сховішча, наладзьце групы, усталюйце дэманстрацыйныя даныя і настройце ролі адміністратараў.",
"Container" : "Кантэйнер",
"Padding" : "Водступ",
"Title (optional)" : "Загаловак (неабавязкова)",
@@ -1354,6 +1345,8 @@ OC.L10N.register(
"Personal settings" : "Personal settings",
"Store" : "Store",
"Reports" : "Reports",
- "Flows" : "Flows"
+ "Flows" : "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings." : "Наладзьце ваш экзэмпляр LaunchPad з парадкам груп, дэманстрацыйнымі данымі, ролямі адміністратараў і наладамі ніжняга калонтытула.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles." : "Запусціце ваш інтранэт: наладзьце групы, усталюйце дэманстрацыйныя даныя і настройце ролі адміністратараў."
},
"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);");
diff --git a/l10n/be.json b/l10n/be.json
index 77714ff6e..27d641c36 100644
--- a/l10n/be.json
+++ b/l10n/be.json
@@ -928,14 +928,6 @@
"LaunchPad setup wizard": "Майстар наладкі LaunchPad",
"Step {n} / {total}": "Крок {n} / {total}",
"Welcome": "Вітаем",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings.": "Наладзьце ваш экзэмпляр LaunchPad са сховішчам, парадкам груп, дэманстрацыйнымі данымі, ролямі адміністратараў і наладамі ніжняга калонтытула.",
- "Storage backend": "Бэкенд сховішча",
- "Choose how LaunchPad stores dashboard content.": "Выберыце, як LaunchPad захоўвае змесціва панэляў.",
- "Database (default)": "База даных (прадвызначана)",
- "Store dashboard content in the LaunchPad database table.": "Захоўваць змесціва панэляў у табліцы базы даных LaunchPad.",
- "GroupFolder (recommended for org use)": "GroupFolder (рэкамендуецца для арганізацыйнага выкарыстання)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access.": "Захоўваць змесціва панэляў у Nextcloud GroupFolders для сумеснага доступу.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option.": "Праграма GroupFolder не ўсталявана. Усталюйце 'Nextcloud GroupFolders', каб выкарыстоўваць гэты варыянт.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace.": "Выберыце парадак, у якім групы Nextcloud з'яўляюцца, калі LaunchPad накіроўвае карыстальнікаў да працоўнай прасторы.",
"Demo data": "Дэманстрацыйныя даныя",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only.": "Дэманстрацыі дэманстрацыйных даных з'явяцца тут, як толькі будзе выпушчана магчымасць demo-data-showcases. Пакуль што гэты крок толькі інфармацыйны.",
@@ -954,7 +946,6 @@
"Finish": "Завяршыць",
"Run setup wizard": "Запусціць майстар наладкі",
"Run setup wizard again": "Запусціць майстар наладкі зноў",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles.": "Запусціце ваш інтранэт: выберыце сховішча, наладзьце групы, усталюйце дэманстрацыйныя даныя і настройце ролі адміністратараў.",
"Container": "Кантэйнер",
"Padding": "Водступ",
"Title (optional)": "Загаловак (неабавязкова)",
@@ -1331,6 +1322,8 @@
"Personal settings": "Personal settings",
"Store": "Store",
"Reports": "Reports",
- "Flows": "Flows"
+ "Flows": "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings.": "Наладзьце ваш экзэмпляр LaunchPad з парадкам груп, дэманстрацыйнымі данымі, ролямі адміністратараў і наладамі ніжняга калонтытула.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles.": "Запусціце ваш інтранэт: наладзьце групы, усталюйце дэманстрацыйныя даныя і настройце ролі адміністратараў."
}
}
diff --git a/l10n/bg.js b/l10n/bg.js
index 87b74012b..79f9bb33c 100644
--- a/l10n/bg.js
+++ b/l10n/bg.js
@@ -926,14 +926,6 @@ OC.L10N.register(
"LaunchPad setup wizard" : "Помощник за настройка на LaunchPad",
"Step {n} / {total}" : "Стъпка {n} / {total}",
"Welcome" : "Добре дошли",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings." : "Конфигурирайте вашия екземпляр на LaunchPad със съхранение, подреждане на групи, демонстрационни данни, администраторски роли и настройки на долния колонтитул.",
- "Storage backend" : "Бекенд за съхранение",
- "Choose how LaunchPad stores dashboard content." : "Изберете как LaunchPad съхранява съдържанието на таблата.",
- "Database (default)" : "База данни (по подразбиране)",
- "Store dashboard content in the LaunchPad database table." : "Съхранявайте съдържанието на таблата в таблицата на базата данни на LaunchPad.",
- "GroupFolder (recommended for org use)" : "GroupFolder (препоръчително за организационна употреба)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access." : "Съхранявайте съдържанието на таблата в Nextcloud GroupFolders за съвместен достъп.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option." : "Приложението GroupFolder не е инсталирано. Инсталирайте „Nextcloud GroupFolders“, за да използвате тази опция.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace." : "Изберете реда, в който се появяват Nextcloud групите, когато LaunchPad насочва потребителите към работно пространство.",
"Demo data" : "Демонстрационни данни",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only." : "Демонстрациите на демонстрационни данни ще се появят тук, след като функцията demo-data-showcases бъде издадена. Засега тази стъпка е само информативна.",
@@ -952,7 +944,6 @@ OC.L10N.register(
"Finish" : "Завършване",
"Run setup wizard" : "Стартиране на помощника за настройка",
"Run setup wizard again" : "Повторно стартиране на помощника за настройка",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles." : "Стартирайте вашия интранет: изберете съхранение, конфигурирайте групи, инсталирайте демонстрационни данни и настройте администраторски роли.",
"Container" : "Контейнер",
"Padding" : "Отстъп",
"Title (optional)" : "Заглавие (незадължително)",
@@ -1363,7 +1354,9 @@ OC.L10N.register(
"Personal settings" : "Personal settings",
"Store" : "Store",
"Reports" : "Reports",
- "Flows" : "Flows"
+ "Flows" : "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings." : "Конфигурирайте вашата инстанция на LaunchPad с подреждане на групи, демонстрационни данни, администраторски роли и настройки на долния колонтитул.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles." : "Започнете с вашия интранет: конфигурирайте групи, инсталирайте демонстрационни данни и настройте администраторски роли."
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/bg.json b/l10n/bg.json
index 3da51a67f..0f1166234 100644
--- a/l10n/bg.json
+++ b/l10n/bg.json
@@ -927,14 +927,6 @@
"LaunchPad setup wizard": "Помощник за настройка на LaunchPad",
"Step {n} / {total}": "Стъпка {n} / {total}",
"Welcome": "Добре дошли",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings.": "Конфигурирайте вашата инстанция на LaunchPad с хранилище, подреждане на групи, демонстрационни данни, администраторски роли и настройки на долния колонтитул.",
- "Storage backend": "Бекенд за съхранение",
- "Choose how LaunchPad stores dashboard content.": "Изберете как LaunchPad съхранява съдържанието на таблата.",
- "Database (default)": "База данни (по подразбиране)",
- "Store dashboard content in the LaunchPad database table.": "Съхранявайте съдържанието на таблата в таблицата на базата данни на LaunchPad.",
- "GroupFolder (recommended for org use)": "GroupFolder (препоръчва се за организационна употреба)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access.": "Съхранявайте съдържанието на таблата в GroupFolders на Nextcloud за съвместен достъп.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option.": "Приложението GroupFolder не е инсталирано. Инсталирайте „Nextcloud GroupFolders“, за да използвате тази опция.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace.": "Изберете реда, в който се появяват групите на Nextcloud, когато LaunchPad насочва потребителите към работно пространство.",
"Demo data": "Демонстрационни данни",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only.": "Демонстрационните данни ще се появят тук, след като бъде пусната възможността demo-data-showcases. Засега тази стъпка е само информативна.",
@@ -953,7 +945,6 @@
"Finish": "Завършване",
"Run setup wizard": "Стартиране на помощника за настройка",
"Run setup wizard again": "Повторно стартиране на помощника за настройка",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles.": "Започнете с вашия интранет: изберете хранилище, конфигурирайте групи, инсталирайте демонстрационни данни и настройте администраторски роли.",
"Container": "Контейнер",
"Padding": "Вътрешен отстъп",
"Title (optional)": "Заглавие (незадължително)",
@@ -1330,6 +1321,8 @@
"Personal settings": "Personal settings",
"Store": "Store",
"Reports": "Reports",
- "Flows": "Flows"
+ "Flows": "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings.": "Конфигурирайте вашата инстанция на LaunchPad с подреждане на групи, демонстрационни данни, администраторски роли и настройки на долния колонтитул.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles.": "Започнете с вашия интранет: конфигурирайте групи, инсталирайте демонстрационни данни и настройте администраторски роли."
}
}
diff --git a/l10n/bs.js b/l10n/bs.js
index cb3bc1f36..cf5b9fb59 100644
--- a/l10n/bs.js
+++ b/l10n/bs.js
@@ -926,14 +926,6 @@ OC.L10N.register(
"LaunchPad setup wizard" : "LaunchPad čarobnjak za postavljanje",
"Step {n} / {total}" : "Korak {n} / {total}",
"Welcome" : "Dobrodošli",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings." : "Konfigurišite svoju LaunchPad instancu s pohranom, redoslijedom grupa, demo podacima, administratorskim ulogama i postavkama podnožja.",
- "Storage backend" : "Pozadinski sistem pohrane",
- "Choose how LaunchPad stores dashboard content." : "Odaberite kako LaunchPad pohranjuje sadržaj kontrolne table.",
- "Database (default)" : "Baza podataka (zadano)",
- "Store dashboard content in the LaunchPad database table." : "Pohrani sadržaj kontrolne table u LaunchPad tabelu baze podataka.",
- "GroupFolder (recommended for org use)" : "GroupFolder (preporučeno za organizacijsku upotrebu)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access." : "Pohrani sadržaj kontrolne table u Nextcloud GroupFolders za saradnički pristup.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option." : "Aplikacija GroupFolder nije instalirana. Instalirajte 'Nextcloud GroupFolders' da biste koristili ovu opciju.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace." : "Odaberite redoslijed kojim se Nextcloud grupe pojavljuju kada LaunchPad usmjerava korisnike u radni prostor.",
"Demo data" : "Demo podaci",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only." : "Prezentacije demo podataka pojavit će se ovdje kada se isporuči mogućnost demo-data-showcases. Za sada je ovaj korak samo informativan.",
@@ -952,7 +944,6 @@ OC.L10N.register(
"Finish" : "Završi",
"Run setup wizard" : "Pokreni čarobnjak za postavljanje",
"Run setup wizard again" : "Ponovo pokreni čarobnjak za postavljanje",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles." : "Pokrenite svoj intranet: odaberite pohranu, konfigurišite grupe, instalirajte demo podatke i postavite administratorske uloge.",
"Container" : "Kontejner",
"Padding" : "Razmak",
"Title (optional)" : "Naslov (opcionalno)",
@@ -1363,7 +1354,9 @@ OC.L10N.register(
"Personal settings" : "Personal settings",
"Store" : "Store",
"Reports" : "Reports",
- "Flows" : "Flows"
+ "Flows" : "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings." : "Konfigurišite svoju LaunchPad instancu s redoslijedom grupa, demo podacima, administratorskim ulogama i postavkama podnožja.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles." : "Pokrenite svoj intranet: konfigurišite grupe, instalirajte demo podatke i postavite administratorske uloge."
},
"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"
);
diff --git a/l10n/bs.json b/l10n/bs.json
index ab5bdc588..ce96c3957 100644
--- a/l10n/bs.json
+++ b/l10n/bs.json
@@ -928,14 +928,6 @@
"LaunchPad setup wizard": "LaunchPad čarobnjak za postavljanje",
"Step {n} / {total}": "Korak {n} / {total}",
"Welcome": "Dobrodošli",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings.": "Konfigurišite svoju LaunchPad instancu sa pohranom, redoslijedom grupa, demo podacima, administratorskim ulogama i postavkama podnožja.",
- "Storage backend": "Pozadinski sistem za pohranu",
- "Choose how LaunchPad stores dashboard content.": "Odaberite kako LaunchPad pohranjuje sadržaj kontrolne ploče.",
- "Database (default)": "Baza podataka (zadano)",
- "Store dashboard content in the LaunchPad database table.": "Pohrani sadržaj kontrolne ploče u LaunchPad tabelu baze podataka.",
- "GroupFolder (recommended for org use)": "GroupFolder (preporučeno za organizacijsku upotrebu)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access.": "Pohrani sadržaj kontrolne ploče u Nextcloud GroupFolders za kolaborativni pristup.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option.": "Aplikacija GroupFolder nije instalirana. Instalirajte 'Nextcloud GroupFolders' da biste koristili ovu opciju.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace.": "Odaberite redoslijed kojim se Nextcloud grupe pojavljuju kada LaunchPad usmjerava korisnike na radni prostor.",
"Demo data": "Demo podaci",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only.": "Prikazi demo podataka pojavit će se ovdje kada se isporuči mogućnost demo-data-showcases. Za sada je ovaj korak samo informativni.",
@@ -954,7 +946,6 @@
"Finish": "Završi",
"Run setup wizard": "Pokreni čarobnjak za postavljanje",
"Run setup wizard again": "Ponovo pokreni čarobnjak za postavljanje",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles.": "Pokrenite svoj intranet: odaberite pohranu, konfigurišite grupe, instalirajte demo podatke i postavite administratorske uloge.",
"Container": "Kontejner",
"Padding": "Razmak",
"Title (optional)": "Naslov (opcionalno)",
@@ -1331,6 +1322,8 @@
"Personal settings": "Personal settings",
"Store": "Store",
"Reports": "Reports",
- "Flows": "Flows"
+ "Flows": "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings.": "Konfigurišite svoju LaunchPad instancu s redoslijedom grupa, demo podacima, administratorskim ulogama i postavkama podnožja.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles.": "Pokrenite svoj intranet: konfigurišite grupe, instalirajte demo podatke i postavite administratorske uloge."
}
}
diff --git a/l10n/ca.js b/l10n/ca.js
index 8d3c9d34f..a898dd3c5 100644
--- a/l10n/ca.js
+++ b/l10n/ca.js
@@ -926,14 +926,6 @@ OC.L10N.register(
"LaunchPad setup wizard" : "Assistent de configuració de LaunchPad",
"Step {n} / {total}" : "Pas {n} / {total}",
"Welcome" : "Us donem la benvinguda",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings." : "Configureu la vostra instància de LaunchPad amb emmagatzematge, ordenació de grups, dades de demostració, rols d'administrador i configuració del peu de pàgina.",
- "Storage backend" : "Servei d'emmagatzematge",
- "Choose how LaunchPad stores dashboard content." : "Trieu com emmagatzema LaunchPad el contingut dels taulers.",
- "Database (default)" : "Base de dades (predeterminat)",
- "Store dashboard content in the LaunchPad database table." : "Emmagatzema el contingut dels taulers a la taula de base de dades de LaunchPad.",
- "GroupFolder (recommended for org use)" : "GroupFolder (recomanat per a ús organitzatiu)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access." : "Emmagatzema el contingut dels taulers a les GroupFolders de Nextcloud per a un accés col·laboratiu.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option." : "L'aplicació GroupFolder no està instal·lada. Instal·leu «Nextcloud GroupFolders» per utilitzar aquesta opció.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace." : "Trieu l'ordre en què apareixen els grups de Nextcloud quan LaunchPad dirigeix els usuaris a un espai de treball.",
"Demo data" : "Dades de demostració",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only." : "Les mostres de dades de demostració apareixeran aquí un cop s'enviï la funcionalitat demo-data-showcases. De moment, aquest pas només és informatiu.",
@@ -952,7 +944,6 @@ OC.L10N.register(
"Finish" : "Finalitza",
"Run setup wizard" : "Executa l'assistent de configuració",
"Run setup wizard again" : "Torna a executar l'assistent de configuració",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles." : "Poseu en marxa la vostra intranet: trieu l'emmagatzematge, configureu els grups, instal·leu dades de demostració i configureu els rols d'administrador.",
"Container" : "Contenidor",
"Padding" : "Encoixinat",
"Title (optional)" : "Títol (opcional)",
@@ -1363,7 +1354,9 @@ OC.L10N.register(
"Personal settings" : "Personal settings",
"Store" : "Store",
"Reports" : "Reports",
- "Flows" : "Flows"
+ "Flows" : "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings." : "Configureu la vostra instància de LaunchPad amb ordenació de grups, dades de demostració, rols d'administrador i configuració del peu de pàgina.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles." : "Poseu en marxa la vostra intranet: configureu els grups, instal·leu dades de demostració i configureu els rols d'administrador."
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/ca.json b/l10n/ca.json
index 27156682f..ee6b0e496 100644
--- a/l10n/ca.json
+++ b/l10n/ca.json
@@ -927,14 +927,6 @@
"LaunchPad setup wizard": "Auxiliar de configuració de LaunchPad",
"Step {n} / {total}": "Pas {n} / {total}",
"Welcome": "Us donem la benvinguda",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings.": "Configureu la vostra instància de LaunchPad amb emmagatzematge, ordenació de grups, dades de demostració, rols d'administrador i configuració del peu de pàgina.",
- "Storage backend": "Rerefons d'emmagatzematge",
- "Choose how LaunchPad stores dashboard content.": "Trieu com emmagatzema LaunchPad el contingut dels taulers.",
- "Database (default)": "Base de dades (predeterminat)",
- "Store dashboard content in the LaunchPad database table.": "Emmagatzema el contingut dels taulers a la taula de la base de dades de LaunchPad.",
- "GroupFolder (recommended for org use)": "GroupFolder (recomanat per a ús organitzatiu)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access.": "Emmagatzema el contingut dels taulers a les GroupFolders de Nextcloud per a un accés col·laboratiu.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option.": "L'aplicació GroupFolder no està instal·lada. Instal·leu «Nextcloud GroupFolders» per utilitzar aquesta opció.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace.": "Trieu l'ordre en què apareixen els grups de Nextcloud quan LaunchPad encamina els usuaris cap a un espai de treball.",
"Demo data": "Dades de demostració",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only.": "Les mostres de dades de demostració apareixeran aquí un cop s'incorpori la funcionalitat demo-data-showcases. De moment, aquest pas és només informatiu.",
@@ -953,7 +945,6 @@
"Finish": "Finalitza",
"Run setup wizard": "Executa l'auxiliar de configuració",
"Run setup wizard again": "Torna a executar l'auxiliar de configuració",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles.": "Poseu en marxa la vostra intranet: trieu l'emmagatzematge, configureu els grups, instal·leu dades de demostració i configureu els rols d'administrador.",
"Container": "Contenidor",
"Padding": "Encoixinat",
"Title (optional)": "Títol (opcional)",
@@ -1330,6 +1321,8 @@
"Personal settings": "Personal settings",
"Store": "Store",
"Reports": "Reports",
- "Flows": "Flows"
+ "Flows": "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings.": "Configureu la vostra instància de LaunchPad amb ordenació de grups, dades de demostració, rols d'administrador i configuració del peu de pàgina.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles.": "Poseu en marxa la vostra intranet: configureu els grups, instal·leu dades de demostració i configureu els rols d'administrador."
}
}
diff --git a/l10n/cs.js b/l10n/cs.js
index 8cabbb3d3..ceb7bc35a 100644
--- a/l10n/cs.js
+++ b/l10n/cs.js
@@ -926,14 +926,6 @@ OC.L10N.register(
"LaunchPad setup wizard" : "Průvodce nastavením LaunchPad",
"Step {n} / {total}" : "Krok {n} / {total}",
"Welcome" : "Vítejte",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings." : "Nastavte svou instanci LaunchPad pomocí úložiště, řazení skupin, demo dat, rolí správců a nastavení zápatí.",
- "Storage backend" : "Backend úložiště",
- "Choose how LaunchPad stores dashboard content." : "Vyberte, jak LaunchPad ukládá obsah nástěnek.",
- "Database (default)" : "Databáze (výchozí)",
- "Store dashboard content in the LaunchPad database table." : "Ukládat obsah nástěnek v databázové tabulce LaunchPad.",
- "GroupFolder (recommended for org use)" : "GroupFolder (doporučeno pro firemní použití)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access." : "Ukládat obsah nástěnek v GroupFolders Nextcloud pro společný přístup.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option." : "Aplikace GroupFolder není nainstalována. Pro použití této možnosti nainstalujte „Nextcloud GroupFolders“.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace." : "Vyberte pořadí, ve kterém se skupiny Nextcloud zobrazují, když LaunchPad směruje uživatele do pracovního prostoru.",
"Demo data" : "Demo data",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only." : "Ukázky demo dat se zde zobrazí, jakmile bude dostupná funkce demo-data-showcases. Prozatím je tento krok pouze informativní.",
@@ -952,7 +944,6 @@ OC.L10N.register(
"Finish" : "Dokončit",
"Run setup wizard" : "Spustit průvodce nastavením",
"Run setup wizard again" : "Spustit průvodce nastavením znovu",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles." : "Začněte se svým intranetem: vyberte úložiště, nastavte skupiny, nainstalujte demo data a nastavte role správců.",
"Container" : "Kontejner",
"Padding" : "Vnitřní okraj",
"Title (optional)" : "Název (volitelné)",
@@ -1363,7 +1354,9 @@ OC.L10N.register(
"Personal settings" : "Personal settings",
"Store" : "Store",
"Reports" : "Reports",
- "Flows" : "Flows"
+ "Flows" : "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings." : "Nakonfigurujte svou instanci LaunchPad pomocí řazení skupin, demo dat, rolí správce a nastavení zápatí.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles." : "Začněte se svým intranetem: nakonfigurujte skupiny, nainstalujte demo data a nastavte role správce."
},
"nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;"
);
diff --git a/l10n/cs.json b/l10n/cs.json
index 5ee2db728..91ad4b5ab 100644
--- a/l10n/cs.json
+++ b/l10n/cs.json
@@ -928,14 +928,6 @@
"LaunchPad setup wizard": "Průvodce nastavením LaunchPad",
"Step {n} / {total}": "Krok {n} / {total}",
"Welcome": "Vítejte",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings.": "Nakonfigurujte svou instanci LaunchPad pomocí úložiště, řazení skupin, demo dat, rolí správce a nastavení zápatí.",
- "Storage backend": "Backend úložiště",
- "Choose how LaunchPad stores dashboard content.": "Vyberte, jak LaunchPad ukládá obsah nástěnek.",
- "Database (default)": "Databáze (výchozí)",
- "Store dashboard content in the LaunchPad database table.": "Ukládat obsah nástěnek do databázové tabulky LaunchPad.",
- "GroupFolder (recommended for org use)": "GroupFolder (doporučeno pro organizační použití)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access.": "Ukládat obsah nástěnek do Nextcloud GroupFolders pro spolupráci.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option.": "Aplikace GroupFolder není nainstalována. Pro použití této možnosti nainstalujte „Nextcloud GroupFolders“.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace.": "Vyberte pořadí, ve kterém se zobrazují skupiny Nextcloud, když LaunchPad směruje uživatele do pracovního prostoru.",
"Demo data": "Demo data",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only.": "Ukázky demo dat se zde zobrazí, jakmile bude dodána funkce demo-data-showcases. Prozatím je tento krok pouze informativní.",
@@ -954,7 +946,6 @@
"Finish": "Dokončit",
"Run setup wizard": "Spustit průvodce nastavením",
"Run setup wizard again": "Spustit průvodce nastavením znovu",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles.": "Začněte se svým intranetem: vyberte úložiště, nakonfigurujte skupiny, nainstalujte demo data a nastavte role správce.",
"Container": "Kontejner",
"Padding": "Odsazení",
"Title (optional)": "Název (volitelné)",
@@ -1331,6 +1322,8 @@
"Personal settings": "Personal settings",
"Store": "Store",
"Reports": "Reports",
- "Flows": "Flows"
+ "Flows": "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings.": "Nakonfigurujte svou instanci LaunchPad pomocí řazení skupin, demo dat, rolí správce a nastavení zápatí.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles.": "Začněte se svým intranetem: nakonfigurujte skupiny, nainstalujte demo data a nastavte role správce."
}
}
diff --git a/l10n/da.js b/l10n/da.js
index 2f800d597..94cb9b9fd 100644
--- a/l10n/da.js
+++ b/l10n/da.js
@@ -926,14 +926,6 @@ OC.L10N.register(
"LaunchPad setup wizard" : "LaunchPad-opsætningsguide",
"Step {n} / {total}" : "Trin {n} / {total}",
"Welcome" : "Velkommen",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings." : "Konfigurer din LaunchPad-instans med lagring, grupperækkefølge, demodata, administratorroller og sidefodsindstillinger.",
- "Storage backend" : "Lagringsbackend",
- "Choose how LaunchPad stores dashboard content." : "Vælg, hvordan LaunchPad gemmer dashboard-indhold.",
- "Database (default)" : "Database (standard)",
- "Store dashboard content in the LaunchPad database table." : "Gem dashboard-indhold i LaunchPad-databasetabellen.",
- "GroupFolder (recommended for org use)" : "GroupFolder (anbefales til organisationsbrug)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access." : "Gem dashboard-indhold i Nextcloud GroupFolders for samarbejdsadgang.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option." : "GroupFolder-appen er ikke installeret. Installer 'Nextcloud GroupFolders' for at bruge denne mulighed.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace." : "Vælg den rækkefølge, Nextcloud-grupper vises i, når LaunchPad dirigerer brugere til et arbejdsområde.",
"Demo data" : "Demodata",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only." : "Demodata-fremvisningerne vil vises her, når demo-data-showcases-funktionen leveres. Indtil videre er dette trin kun til orientering.",
@@ -952,7 +944,6 @@ OC.L10N.register(
"Finish" : "Afslut",
"Run setup wizard" : "Kør opsætningsguide",
"Run setup wizard again" : "Kør opsætningsguide igen",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles." : "Kom i gang med dit intranet: vælg lagring, konfigurer grupper, installer demodata, og opsæt administratorroller.",
"Container" : "Container",
"Padding" : "Indvendig margen",
"Title (optional)" : "Titel (valgfri)",
@@ -1363,7 +1354,9 @@ OC.L10N.register(
"Personal settings" : "Personal settings",
"Store" : "Store",
"Reports" : "Reports",
- "Flows" : "Flows"
+ "Flows" : "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings." : "Konfigurer din LaunchPad-instans med grupperækkefølge, demodata, administratorroller og footer-indstillinger.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles." : "Kom i gang med dit intranet: konfigurer grupper, installer demodata, og opsæt administratorroller."
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/da.json b/l10n/da.json
index 7cf891549..d713e2651 100644
--- a/l10n/da.json
+++ b/l10n/da.json
@@ -927,14 +927,6 @@
"LaunchPad setup wizard": "LaunchPad-opsætningsguide",
"Step {n} / {total}": "Trin {n} / {total}",
"Welcome": "Velkommen",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings.": "Konfigurer din LaunchPad-instans med lagring, grupperækkefølge, demodata, administratorroller og footer-indstillinger.",
- "Storage backend": "Lagringsbackend",
- "Choose how LaunchPad stores dashboard content.": "Vælg, hvordan LaunchPad gemmer dashboard-indhold.",
- "Database (default)": "Database (standard)",
- "Store dashboard content in the LaunchPad database table.": "Gem dashboard-indhold i LaunchPad-databasetabellen.",
- "GroupFolder (recommended for org use)": "GroupFolder (anbefales til organisationsbrug)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access.": "Gem dashboard-indhold i Nextcloud GroupFolders til samarbejdsadgang.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option.": "GroupFolder-appen er ikke installeret. Installer 'Nextcloud GroupFolders' for at bruge denne mulighed.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace.": "Vælg den rækkefølge, Nextcloud-grupper vises i, når LaunchPad dirigerer brugere til et arbejdsområde.",
"Demo data": "Demodata",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only.": "Demodata-showcasene vises her, når demo-data-showcases-funktionen leveres. Indtil videre er dette trin kun informativt.",
@@ -953,7 +945,6 @@
"Finish": "Afslut",
"Run setup wizard": "Kør opsætningsguide",
"Run setup wizard again": "Kør opsætningsguide igen",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles.": "Kom i gang med dit intranet: vælg lagring, konfigurer grupper, installer demodata, og opsæt administratorroller.",
"Container": "Container",
"Padding": "Udfyldning",
"Title (optional)": "Titel (valgfri)",
@@ -1330,6 +1321,8 @@
"Personal settings": "Personal settings",
"Store": "Store",
"Reports": "Reports",
- "Flows": "Flows"
+ "Flows": "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings.": "Konfigurer din LaunchPad-instans med grupperækkefølge, demodata, administratorroller og footer-indstillinger.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles.": "Kom i gang med dit intranet: konfigurer grupper, installer demodata, og opsæt administratorroller."
}
}
diff --git a/l10n/de.js b/l10n/de.js
index ce48ea0c1..f2e87ac21 100644
--- a/l10n/de.js
+++ b/l10n/de.js
@@ -926,14 +926,6 @@ OC.L10N.register(
"LaunchPad setup wizard" : "LaunchPad-Einrichtungsassistent",
"Step {n} / {total}" : "Schritt {n} / {total}",
"Welcome" : "Willkommen",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings." : "Konfigurieren Sie Ihre LaunchPad-Instanz mit Speicher, Gruppenreihenfolge, Demodaten, Administratorrollen und Fußzeilen-Einstellungen.",
- "Storage backend" : "Speicher-Backend",
- "Choose how LaunchPad stores dashboard content." : "Wählen Sie, wie LaunchPad Dashboard-Inhalte speichert.",
- "Database (default)" : "Datenbank (Standard)",
- "Store dashboard content in the LaunchPad database table." : "Dashboard-Inhalte in der LaunchPad-Datenbanktabelle speichern.",
- "GroupFolder (recommended for org use)" : "GroupFolder (empfohlen für Organisationsnutzung)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access." : "Dashboard-Inhalte in Nextcloud GroupFolders für kollaborativen Zugriff speichern.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option." : "Die GroupFolder-App ist nicht installiert. Installieren Sie 'Nextcloud GroupFolders', um diese Option zu verwenden.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace." : "Wählen Sie die Reihenfolge, in der Nextcloud-Gruppen erscheinen, wenn LaunchPad Benutzer zu einem Arbeitsbereich leitet.",
"Demo data" : "Demodaten",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only." : "Die Demodaten-Schaukästen erscheinen hier, sobald die Funktion demo-data-showcases verfügbar ist. Vorerst dient dieser Schritt nur zur Information.",
@@ -952,7 +944,6 @@ OC.L10N.register(
"Finish" : "Fertigstellen",
"Run setup wizard" : "Einrichtungsassistent ausführen",
"Run setup wizard again" : "Einrichtungsassistent erneut ausführen",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles." : "Bringen Sie Ihr Intranet in Gang: Wählen Sie Speicher, konfigurieren Sie Gruppen, installieren Sie Demodaten und richten Sie Administratorrollen ein.",
"Container" : "Container",
"Padding" : "Innenabstand",
"Title (optional)" : "Titel (optional)",
@@ -1363,7 +1354,9 @@ OC.L10N.register(
"Personal settings" : "Personal settings",
"Store" : "Store",
"Reports" : "Reports",
- "Flows" : "Flows"
+ "Flows" : "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings." : "Konfigurieren Sie Ihre LaunchPad-Instanz mit Gruppenreihenfolge, Demodaten, Administratorrollen und Fußzeileneinstellungen.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles." : "Bringen Sie Ihr Intranet in Gang: Konfigurieren Sie Gruppen, installieren Sie Demodaten und richten Sie Administratorrollen ein."
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/de.json b/l10n/de.json
index 7bd079f06..6eb63301d 100644
--- a/l10n/de.json
+++ b/l10n/de.json
@@ -927,14 +927,6 @@
"LaunchPad setup wizard": "LaunchPad-Einrichtungsassistent",
"Step {n} / {total}": "Schritt {n} / {total}",
"Welcome": "Willkommen",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings.": "Konfigurieren Sie Ihre LaunchPad-Instanz mit Speicher, Gruppenreihenfolge, Demodaten, Administratorrollen und Fußzeileneinstellungen.",
- "Storage backend": "Speicher-Backend",
- "Choose how LaunchPad stores dashboard content.": "Wählen Sie, wie LaunchPad Dashboard-Inhalte speichert.",
- "Database (default)": "Datenbank (Standard)",
- "Store dashboard content in the LaunchPad database table.": "Dashboard-Inhalte in der LaunchPad-Datenbanktabelle speichern.",
- "GroupFolder (recommended for org use)": "GroupFolder (empfohlen für Organisationsnutzung)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access.": "Dashboard-Inhalte in Nextcloud-GroupFolders für gemeinschaftlichen Zugriff speichern.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option.": "Die GroupFolder-App ist nicht installiert. Installieren Sie 'Nextcloud GroupFolders', um diese Option zu nutzen.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace.": "Wählen Sie die Reihenfolge, in der Nextcloud-Gruppen erscheinen, wenn LaunchPad Benutzer zu einem Arbeitsbereich leitet.",
"Demo data": "Demodaten",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only.": "Die Demodaten-Beispiele erscheinen hier, sobald die Funktion demo-data-showcases verfügbar ist. Vorerst dient dieser Schritt nur zur Information.",
@@ -953,7 +945,6 @@
"Finish": "Fertigstellen",
"Run setup wizard": "Einrichtungsassistent ausführen",
"Run setup wizard again": "Einrichtungsassistent erneut ausführen",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles.": "Bringen Sie Ihr Intranet in Gang: Wählen Sie Speicher, konfigurieren Sie Gruppen, installieren Sie Demodaten und richten Sie Administratorrollen ein.",
"Container": "Container",
"Padding": "Innenabstand",
"Title (optional)": "Titel (optional)",
@@ -1330,6 +1321,8 @@
"Personal settings": "Personal settings",
"Store": "Store",
"Reports": "Reports",
- "Flows": "Flows"
+ "Flows": "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings.": "Konfigurieren Sie Ihre LaunchPad-Instanz mit Gruppenreihenfolge, Demodaten, Administratorrollen und Fußzeileneinstellungen.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles.": "Bringen Sie Ihr Intranet in Gang: Konfigurieren Sie Gruppen, installieren Sie Demodaten und richten Sie Administratorrollen ein."
}
}
diff --git a/l10n/el.js b/l10n/el.js
index 085475e25..523ef339d 100644
--- a/l10n/el.js
+++ b/l10n/el.js
@@ -926,14 +926,6 @@ OC.L10N.register(
"LaunchPad setup wizard" : "Οδηγός εγκατάστασης LaunchPad",
"Step {n} / {total}" : "Βήμα {n} / {total}",
"Welcome" : "Καλώς ήρθατε",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings." : "Διαμορφώστε την εγκατάσταση LaunchPad σας με αποθήκευση, σειρά ομάδων, δεδομένα επίδειξης, ρόλους διαχειριστή και ρυθμίσεις υποσέλιδου.",
- "Storage backend" : "Backend αποθήκευσης",
- "Choose how LaunchPad stores dashboard content." : "Επιλέξτε πώς το LaunchPad αποθηκεύει το περιεχόμενο των πινάκων.",
- "Database (default)" : "Βάση δεδομένων (προεπιλογή)",
- "Store dashboard content in the LaunchPad database table." : "Αποθήκευση περιεχομένου πίνακα στον πίνακα βάσης δεδομένων του LaunchPad.",
- "GroupFolder (recommended for org use)" : "GroupFolder (συνιστάται για χρήση οργανισμού)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access." : "Αποθήκευση περιεχομένου πίνακα στα Nextcloud GroupFolders για συνεργατική πρόσβαση.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option." : "Η εφαρμογή GroupFolder δεν είναι εγκατεστημένη. Εγκαταστήστε το 'Nextcloud GroupFolders' για να χρησιμοποιήσετε αυτήν την επιλογή.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace." : "Επιλέξτε τη σειρά με την οποία εμφανίζονται οι ομάδες Nextcloud όταν το LaunchPad δρομολογεί χρήστες σε έναν χώρο εργασίας.",
"Demo data" : "Δεδομένα επίδειξης",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only." : "Οι επιδείξεις δεδομένων επίδειξης θα εμφανιστούν εδώ μόλις κυκλοφορήσει η δυνατότητα demo-data-showcases. Προς το παρόν αυτό το βήμα είναι μόνο ενημερωτικό.",
@@ -952,7 +944,6 @@ OC.L10N.register(
"Finish" : "Τέλος",
"Run setup wizard" : "Εκτέλεση οδηγού εγκατάστασης",
"Run setup wizard again" : "Εκτέλεση οδηγού εγκατάστασης ξανά",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles." : "Ξεκινήστε το intranet σας: επιλέξτε αποθήκευση, διαμορφώστε ομάδες, εγκαταστήστε δεδομένα επίδειξης και ρυθμίστε ρόλους διαχειριστή.",
"Container" : "Κοντέινερ",
"Padding" : "Εσωτερικό περιθώριο",
"Title (optional)" : "Τίτλος (προαιρετικό)",
@@ -1363,7 +1354,9 @@ OC.L10N.register(
"Personal settings" : "Personal settings",
"Store" : "Store",
"Reports" : "Reports",
- "Flows" : "Flows"
+ "Flows" : "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings." : "Διαμορφώστε το παράδειγμα του LaunchPad με σειρά ομάδων, δεδομένα επίδειξης, ρόλους διαχειριστή και ρυθμίσεις υποσέλιδου.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles." : "Ξεκινήστε το intranet σας: διαμορφώστε ομάδες, εγκαταστήστε δεδομένα επίδειξης και ρυθμίστε ρόλους διαχειριστή."
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/el.json b/l10n/el.json
index b7bd82a87..d9ac82fc6 100644
--- a/l10n/el.json
+++ b/l10n/el.json
@@ -927,14 +927,6 @@
"LaunchPad setup wizard": "Οδηγός εγκατάστασης LaunchPad",
"Step {n} / {total}": "Βήμα {n} / {total}",
"Welcome": "Καλώς ήρθατε",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings.": "Διαμορφώστε το παράδειγμα του LaunchPad με αποθηκευτικό χώρο, σειρά ομάδων, δεδομένα επίδειξης, ρόλους διαχειριστή και ρυθμίσεις υποσέλιδου.",
- "Storage backend": "Σύστημα αποθήκευσης",
- "Choose how LaunchPad stores dashboard content.": "Επιλέξτε πώς το LaunchPad αποθηκεύει το περιεχόμενο των πινάκων.",
- "Database (default)": "Βάση δεδομένων (προεπιλογή)",
- "Store dashboard content in the LaunchPad database table.": "Αποθήκευση του περιεχομένου των πινάκων στον πίνακα βάσης δεδομένων του LaunchPad.",
- "GroupFolder (recommended for org use)": "GroupFolder (συνιστάται για χρήση από οργανισμό)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access.": "Αποθήκευση του περιεχομένου των πινάκων στα GroupFolders του Nextcloud για συνεργατική πρόσβαση.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option.": "Η εφαρμογή GroupFolder δεν είναι εγκατεστημένη. Εγκαταστήστε το 'Nextcloud GroupFolders' για να χρησιμοποιήσετε αυτήν την επιλογή.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace.": "Επιλέξτε τη σειρά με την οποία εμφανίζονται οι ομάδες Nextcloud όταν το LaunchPad κατευθύνει τους χρήστες σε έναν χώρο εργασίας.",
"Demo data": "Δεδομένα επίδειξης",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only.": "Οι παρουσιάσεις δεδομένων επίδειξης θα εμφανιστούν εδώ μόλις κυκλοφορήσει η δυνατότητα demo-data-showcases. Προς το παρόν αυτό το βήμα είναι μόνο ενημερωτικό.",
@@ -953,7 +945,6 @@
"Finish": "Τέλος",
"Run setup wizard": "Εκτέλεση οδηγού εγκατάστασης",
"Run setup wizard again": "Εκτέλεση οδηγού εγκατάστασης ξανά",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles.": "Ξεκινήστε το intranet σας: επιλέξτε αποθηκευτικό χώρο, διαμορφώστε ομάδες, εγκαταστήστε δεδομένα επίδειξης και ρυθμίστε ρόλους διαχειριστή.",
"Container": "Δοχείο",
"Padding": "Εσωτερικό περιθώριο",
"Title (optional)": "Τίτλος (προαιρετικό)",
@@ -1330,6 +1321,8 @@
"Personal settings": "Personal settings",
"Store": "Store",
"Reports": "Reports",
- "Flows": "Flows"
+ "Flows": "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings.": "Διαμορφώστε το παράδειγμα του LaunchPad με σειρά ομάδων, δεδομένα επίδειξης, ρόλους διαχειριστή και ρυθμίσεις υποσέλιδου.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles.": "Ξεκινήστε το intranet σας: διαμορφώστε ομάδες, εγκαταστήστε δεδομένα επίδειξης και ρυθμίστε ρόλους διαχειριστή."
}
}
diff --git a/l10n/en.js b/l10n/en.js
index ea036701f..ec967294d 100644
--- a/l10n/en.js
+++ b/l10n/en.js
@@ -860,14 +860,6 @@ OC.L10N.register(
"LaunchPad setup wizard" : "LaunchPad setup wizard",
"Step {n} / {total}" : "Step {n} / {total}",
"Welcome" : "Welcome",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings." : "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings.",
- "Storage backend" : "Storage backend",
- "Choose how LaunchPad stores dashboard content." : "Choose how LaunchPad stores dashboard content.",
- "Database (default)" : "Database (default)",
- "Store dashboard content in the LaunchPad database table." : "Store dashboard content in the LaunchPad database table.",
- "GroupFolder (recommended for org use)" : "GroupFolder (recommended for org use)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access." : "Store dashboard content in Nextcloud GroupFolders for collaborative access.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option." : "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace." : "Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace.",
"Demo data" : "Demo data",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only." : "The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only.",
@@ -886,7 +878,6 @@ OC.L10N.register(
"Finish" : "Finish",
"Run setup wizard" : "Run setup wizard",
"Run setup wizard again" : "Run setup wizard again",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles." : "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles.",
"Container" : "Container",
"Padding" : "Padding",
"Title (optional)" : "Title (optional)",
@@ -1343,7 +1334,9 @@ OC.L10N.register(
"Personal settings" : "Personal settings",
"Store" : "Store",
"Reports" : "Reports",
- "Flows" : "Flows"
+ "Flows" : "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings." : "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles." : "Get your intranet started: configure groups, install demo data, and set up admin roles."
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/en.json b/l10n/en.json
index 4fb4149c9..d52b53240 100644
--- a/l10n/en.json
+++ b/l10n/en.json
@@ -857,14 +857,6 @@
"LaunchPad setup wizard": "LaunchPad setup wizard",
"Step {n} / {total}": "Step {n} / {total}",
"Welcome": "Welcome",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings.": "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings.",
- "Storage backend": "Storage backend",
- "Choose how LaunchPad stores dashboard content.": "Choose how LaunchPad stores dashboard content.",
- "Database (default)": "Database (default)",
- "Store dashboard content in the LaunchPad database table.": "Store dashboard content in the LaunchPad database table.",
- "GroupFolder (recommended for org use)": "GroupFolder (recommended for org use)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access.": "Store dashboard content in Nextcloud GroupFolders for collaborative access.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option.": "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace.": "Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace.",
"Demo data": "Demo data",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only.": "The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only.",
@@ -883,7 +875,6 @@
"Finish": "Finish",
"Run setup wizard": "Run setup wizard",
"Run setup wizard again": "Run setup wizard again",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles.": "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles.",
"Container": "Container",
"Padding": "Padding",
"Title (optional)": "Title (optional)",
@@ -1308,6 +1299,8 @@
"Personal settings": "Personal settings",
"Store": "Store",
"Reports": "Reports",
- "Flows": "Flows"
+ "Flows": "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings.": "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles.": "Get your intranet started: configure groups, install demo data, and set up admin roles."
}
}
diff --git a/l10n/es.js b/l10n/es.js
index 9960fc610..ce4a8e659 100644
--- a/l10n/es.js
+++ b/l10n/es.js
@@ -926,14 +926,6 @@ OC.L10N.register(
"LaunchPad setup wizard" : "Asistente de configuración de LaunchPad",
"Step {n} / {total}" : "Paso {n} / {total}",
"Welcome" : "Bienvenido",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings." : "Configure su instancia de LaunchPad con almacenamiento, ordenación de grupos, datos de demostración, roles de administrador y ajustes del pie de página.",
- "Storage backend" : "Backend de almacenamiento",
- "Choose how LaunchPad stores dashboard content." : "Elija cómo almacena LaunchPad el contenido de los paneles.",
- "Database (default)" : "Base de datos (predeterminado)",
- "Store dashboard content in the LaunchPad database table." : "Almacenar el contenido de los paneles en la tabla de la base de datos de LaunchPad.",
- "GroupFolder (recommended for org use)" : "GroupFolder (recomendado para uso organizativo)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access." : "Almacenar el contenido de los paneles en Nextcloud GroupFolders para el acceso colaborativo.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option." : "La aplicación GroupFolder no está instalada. Instale 'Nextcloud GroupFolders' para usar esta opción.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace." : "Elija el orden en que aparecen los grupos de Nextcloud cuando LaunchPad dirige a los usuarios a un espacio de trabajo.",
"Demo data" : "Datos de demostración",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only." : "Las muestras de datos de demostración aparecerán aquí una vez que se incorpore la funcionalidad demo-data-showcases. Por ahora, este paso es solo informativo.",
@@ -952,7 +944,6 @@ OC.L10N.register(
"Finish" : "Finalizar",
"Run setup wizard" : "Ejecutar el asistente de configuración",
"Run setup wizard again" : "Ejecutar de nuevo el asistente de configuración",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles." : "Ponga en marcha su intranet: elija el almacenamiento, configure los grupos, instale datos de demostración y configure los roles de administrador.",
"Container" : "Contenedor",
"Padding" : "Relleno",
"Title (optional)" : "Título (opcional)",
@@ -1363,7 +1354,9 @@ OC.L10N.register(
"Personal settings" : "Personal settings",
"Store" : "Store",
"Reports" : "Reports",
- "Flows" : "Flows"
+ "Flows" : "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings." : "Configure su instancia de LaunchPad con ordenación de grupos, datos de demostración, roles de administrador y ajustes del pie de página.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles." : "Ponga en marcha su intranet: configure grupos, instale datos de demostración y configure roles de administrador."
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/es.json b/l10n/es.json
index 2cb682b7f..b52ab8794 100644
--- a/l10n/es.json
+++ b/l10n/es.json
@@ -927,14 +927,6 @@
"LaunchPad setup wizard": "Asistente de configuración de LaunchPad",
"Step {n} / {total}": "Paso {n} / {total}",
"Welcome": "Bienvenido",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings.": "Configure su instancia de LaunchPad con almacenamiento, ordenación de grupos, datos de demostración, roles de administrador y ajustes del pie de página.",
- "Storage backend": "Backend de almacenamiento",
- "Choose how LaunchPad stores dashboard content.": "Elija cómo almacena LaunchPad el contenido de los paneles.",
- "Database (default)": "Base de datos (predeterminado)",
- "Store dashboard content in the LaunchPad database table.": "Almacenar el contenido de los paneles en la tabla de base de datos de LaunchPad.",
- "GroupFolder (recommended for org use)": "GroupFolder (recomendado para uso organizativo)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access.": "Almacenar el contenido de los paneles en GroupFolders de Nextcloud para el acceso colaborativo.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option.": "La aplicación GroupFolder no está instalada. Instale «Nextcloud GroupFolders» para usar esta opción.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace.": "Elija el orden en que aparecen los grupos de Nextcloud cuando LaunchPad dirige a los usuarios a un espacio de trabajo.",
"Demo data": "Datos de demostración",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only.": "Las muestras de datos de demostración aparecerán aquí una vez que se distribuya la funcionalidad demo-data-showcases. Por ahora, este paso es solo informativo.",
@@ -953,7 +945,6 @@
"Finish": "Finalizar",
"Run setup wizard": "Ejecutar el asistente de configuración",
"Run setup wizard again": "Ejecutar de nuevo el asistente de configuración",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles.": "Ponga en marcha su intranet: elija el almacenamiento, configure grupos, instale datos de demostración y configure roles de administrador.",
"Container": "Contenedor",
"Padding": "Relleno",
"Title (optional)": "Título (opcional)",
@@ -1330,6 +1321,8 @@
"Personal settings": "Personal settings",
"Store": "Store",
"Reports": "Reports",
- "Flows": "Flows"
+ "Flows": "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings.": "Configure su instancia de LaunchPad con ordenación de grupos, datos de demostración, roles de administrador y ajustes del pie de página.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles.": "Ponga en marcha su intranet: configure grupos, instale datos de demostración y configure roles de administrador."
}
}
diff --git a/l10n/et.js b/l10n/et.js
index 8063f77bc..295aa6d2d 100644
--- a/l10n/et.js
+++ b/l10n/et.js
@@ -926,14 +926,6 @@ OC.L10N.register(
"LaunchPad setup wizard" : "LaunchPadi seadistusviisard",
"Step {n} / {total}" : "Samm {n} / {total}",
"Welcome" : "Tere tulemast",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings." : "Seadistage oma LaunchPadi instants salvestusruumi, gruppide järjestuse, demoandmete, administraatori rollide ja jaluse seadetega.",
- "Storage backend" : "Salvestamise taustsüsteem",
- "Choose how LaunchPad stores dashboard content." : "Valige, kuidas LaunchPad töölaua sisu salvestab.",
- "Database (default)" : "Andmebaas (vaikimisi)",
- "Store dashboard content in the LaunchPad database table." : "Salvesta töölaua sisu LaunchPadi andmebaasi tabelisse.",
- "GroupFolder (recommended for org use)" : "GroupFolder (soovitatav organisatsiooni kasutuseks)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access." : "Salvesta töölaua sisu Nextcloudi GroupFolderitesse koostöös juurdepääsuks.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option." : "GroupFolderi rakendus pole paigaldatud. Selle valiku kasutamiseks paigaldage 'Nextcloud GroupFolders'.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace." : "Valige järjekord, milles Nextcloudi grupid kuvatakse, kui LaunchPad suunab kasutajaid tööruumi.",
"Demo data" : "Demoandmed",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only." : "Demoandmete näidised ilmuvad siia, kui demo-data-showcases võimekus tarnitakse. Praegu on see samm ainult informatiivne.",
@@ -952,7 +944,6 @@ OC.L10N.register(
"Finish" : "Lõpeta",
"Run setup wizard" : "Käivita seadistusviisard",
"Run setup wizard again" : "Käivita seadistusviisard uuesti",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles." : "Alustage oma sisevõrku: valige salvestusruum, seadistage grupid, paigaldage demoandmed ja seadistage administraatori rollid.",
"Container" : "Konteiner",
"Padding" : "Polster",
"Title (optional)" : "Pealkiri (valikuline)",
@@ -1363,7 +1354,9 @@ OC.L10N.register(
"Personal settings" : "Personal settings",
"Store" : "Store",
"Reports" : "Reports",
- "Flows" : "Flows"
+ "Flows" : "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings." : "Seadista oma LaunchPadi eksemplar gruppide järjestuse, näidisandmete, administraatori rollide ja jaluse seadetega.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles." : "Käivita oma siseveeb: seadista grupid, paigalda näidisandmed ja seadista administraatori rollid."
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/et.json b/l10n/et.json
index 97d295ded..0de48d0df 100644
--- a/l10n/et.json
+++ b/l10n/et.json
@@ -927,14 +927,6 @@
"LaunchPad setup wizard": "LaunchPadi seadistusviisard",
"Step {n} / {total}": "Samm {n} / {total}",
"Welcome": "Tere tulemast",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings.": "Seadista oma LaunchPadi eksemplar salvestusruumi, gruppide järjestuse, näidisandmete, administraatori rollide ja jaluse seadetega.",
- "Storage backend": "Salvestuse taustasüsteem",
- "Choose how LaunchPad stores dashboard content.": "Vali, kuidas LaunchPad töölaua sisu salvestab.",
- "Database (default)": "Andmebaas (vaikimisi)",
- "Store dashboard content in the LaunchPad database table.": "Salvesta töölaua sisu LaunchPadi andmebaasi tabelisse.",
- "GroupFolder (recommended for org use)": "GroupFolder (soovitatav organisatsiooni kasutuseks)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access.": "Salvesta töölaua sisu Nextcloudi GroupFolderitesse koostöö juurdepääsuks.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option.": "GroupFolderi rakendus pole paigaldatud. Selle valiku kasutamiseks paigalda 'Nextcloud GroupFolders'.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace.": "Vali järjekord, milles Nextcloudi grupid kuvatakse, kui LaunchPad suunab kasutajad tööruumi.",
"Demo data": "Näidisandmed",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only.": "Näidisandmete esitlused ilmuvad siia, kui demo-data-showcases võimekus on saadaval. Praegu on see samm üksnes informatiivne.",
@@ -953,7 +945,6 @@
"Finish": "Lõpeta",
"Run setup wizard": "Käivita seadistusviisard",
"Run setup wizard again": "Käivita seadistusviisard uuesti",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles.": "Käivita oma siseveeb: vali salvestusruum, seadista grupid, paigalda näidisandmed ja seadista administraatori rollid.",
"Container": "Konteiner",
"Padding": "Polster",
"Title (optional)": "Pealkiri (valikuline)",
@@ -1330,6 +1321,8 @@
"Personal settings": "Personal settings",
"Store": "Store",
"Reports": "Reports",
- "Flows": "Flows"
+ "Flows": "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings.": "Seadista oma LaunchPadi eksemplar gruppide järjestuse, näidisandmete, administraatori rollide ja jaluse seadetega.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles.": "Käivita oma siseveeb: seadista grupid, paigalda näidisandmed ja seadista administraatori rollid."
}
}
diff --git a/l10n/fi.js b/l10n/fi.js
index 414148c9c..ea70603e7 100644
--- a/l10n/fi.js
+++ b/l10n/fi.js
@@ -926,14 +926,6 @@ OC.L10N.register(
"LaunchPad setup wizard" : "LaunchPad-määritysopas",
"Step {n} / {total}" : "Vaihe {n} / {total}",
"Welcome" : "Tervetuloa",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings." : "Määritä LaunchPad-instanssisi tallennustilan, ryhmien järjestyksen, esittelydatan, ylläpitäjäroolien ja alatunnisteen asetusten kanssa.",
- "Storage backend" : "Tallennuksen taustajärjestelmä",
- "Choose how LaunchPad stores dashboard content." : "Valitse, miten LaunchPad tallentaa kojelautasisällön.",
- "Database (default)" : "Tietokanta (oletus)",
- "Store dashboard content in the LaunchPad database table." : "Tallenna kojelautasisältö LaunchPadin tietokantatauluun.",
- "GroupFolder (recommended for org use)" : "GroupFolder (suositeltu organisaatiokäyttöön)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access." : "Tallenna kojelautasisältö Nextcloud GroupFolders -kansioihin yhteistä käyttöä varten.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option." : "GroupFolder-sovellusta ei ole asennettu. Asenna 'Nextcloud GroupFolders' käyttääksesi tätä vaihtoehtoa.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace." : "Valitse järjestys, jossa Nextcloud-ryhmät näkyvät, kun LaunchPad ohjaa käyttäjiä työtilaan.",
"Demo data" : "Esittelydata",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only." : "Esittelydatan näytteet ilmestyvät tähän, kun demo-data-showcases-ominaisuus julkaistaan. Toistaiseksi tämä vaihe on vain informatiivinen.",
@@ -952,7 +944,6 @@ OC.L10N.register(
"Finish" : "Valmis",
"Run setup wizard" : "Suorita määritysopas",
"Run setup wizard again" : "Suorita määritysopas uudelleen",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles." : "Käynnistä intranetisi: valitse tallennustila, määritä ryhmät, asenna esittelydata ja määritä ylläpitäjäroolit.",
"Container" : "Säiliö",
"Padding" : "Täyte",
"Title (optional)" : "Otsikko (valinnainen)",
@@ -1363,7 +1354,9 @@ OC.L10N.register(
"Personal settings" : "Personal settings",
"Store" : "Store",
"Reports" : "Reports",
- "Flows" : "Flows"
+ "Flows" : "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings." : "Määritä LaunchPad-ilmentymäsi ryhmäjärjestyksen, esittelytietojen, ylläpitäjän roolien ja alatunnisteasetusten avulla.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles." : "Käynnistä intranetisi: määritä ryhmät, asenna esittelytiedot ja määritä ylläpitäjän roolit."
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/fi.json b/l10n/fi.json
index 3c3009d99..65552c093 100644
--- a/l10n/fi.json
+++ b/l10n/fi.json
@@ -927,14 +927,6 @@
"LaunchPad setup wizard": "LaunchPadin asennustoiminto",
"Step {n} / {total}": "Vaihe {n} / {total}",
"Welcome": "Tervetuloa",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings.": "Määritä LaunchPad-ilmentymäsi tallennustilan, ryhmäjärjestyksen, esittelytietojen, ylläpitäjän roolien ja alatunnisteasetusten avulla.",
- "Storage backend": "Tallennustaustajärjestelmä",
- "Choose how LaunchPad stores dashboard content.": "Valitse, miten LaunchPad tallentaa koontinäytön sisällön.",
- "Database (default)": "Tietokanta (oletus)",
- "Store dashboard content in the LaunchPad database table.": "Tallenna koontinäytön sisältö LaunchPadin tietokantatauluun.",
- "GroupFolder (recommended for org use)": "GroupFolder (suositeltu organisaatiokäyttöön)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access.": "Tallenna koontinäytön sisältö Nextcloud GroupFolders -kansioihin yhteistyökäyttöä varten.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option.": "GroupFolder-sovellusta ei ole asennettu. Asenna 'Nextcloud GroupFolders' käyttääksesi tätä vaihtoehtoa.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace.": "Valitse järjestys, jossa Nextcloud-ryhmät näkyvät, kun LaunchPad ohjaa käyttäjät työtilaan.",
"Demo data": "Esittelytiedot",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only.": "Esittelytietojen esittelyt näkyvät tässä, kun demo-data-showcases-ominaisuus julkaistaan. Toistaiseksi tämä vaihe on vain tiedoksi.",
@@ -953,7 +945,6 @@
"Finish": "Valmis",
"Run setup wizard": "Suorita asennustoiminto",
"Run setup wizard again": "Suorita asennustoiminto uudelleen",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles.": "Käynnistä intranetisi: valitse tallennustila, määritä ryhmät, asenna esittelytiedot ja määritä ylläpitäjän roolit.",
"Container": "Säiliö",
"Padding": "Täyte",
"Title (optional)": "Otsikko (valinnainen)",
@@ -1330,6 +1321,8 @@
"Personal settings": "Personal settings",
"Store": "Store",
"Reports": "Reports",
- "Flows": "Flows"
+ "Flows": "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings.": "Määritä LaunchPad-ilmentymäsi ryhmäjärjestyksen, esittelytietojen, ylläpitäjän roolien ja alatunnisteasetusten avulla.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles.": "Käynnistä intranetisi: määritä ryhmät, asenna esittelytiedot ja määritä ylläpitäjän roolit."
}
}
diff --git a/l10n/fr.js b/l10n/fr.js
index c041d7711..cbbe9f10a 100644
--- a/l10n/fr.js
+++ b/l10n/fr.js
@@ -926,14 +926,6 @@ OC.L10N.register(
"LaunchPad setup wizard" : "Assistant de configuration de LaunchPad",
"Step {n} / {total}" : "Étape {n} / {total}",
"Welcome" : "Bienvenue",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings." : "Configurez votre instance LaunchPad avec le stockage, l'ordre des groupes, les données de démonstration, les rôles d'administration et les réglages du pied de page.",
- "Storage backend" : "Backend de stockage",
- "Choose how LaunchPad stores dashboard content." : "Choisissez comment LaunchPad stocke le contenu des tableaux de bord.",
- "Database (default)" : "Base de données (par défaut)",
- "Store dashboard content in the LaunchPad database table." : "Stocker le contenu des tableaux de bord dans la table de base de données de LaunchPad.",
- "GroupFolder (recommended for org use)" : "GroupFolder (recommandé pour un usage en organisation)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access." : "Stocker le contenu des tableaux de bord dans les GroupFolders Nextcloud pour un accès collaboratif.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option." : "L'application GroupFolder n'est pas installée. Installez « Nextcloud GroupFolders » pour utiliser cette option.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace." : "Choisissez l'ordre d'apparition des groupes Nextcloud lorsque LaunchPad dirige les utilisateurs vers un espace de travail.",
"Demo data" : "Données de démonstration",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only." : "Les vitrines de données de démonstration apparaîtront ici une fois que la capacité demo-data-showcases sera livrée. Pour l'instant, cette étape est purement informative.",
@@ -952,7 +944,6 @@ OC.L10N.register(
"Finish" : "Terminer",
"Run setup wizard" : "Lancer l'assistant de configuration",
"Run setup wizard again" : "Relancer l'assistant de configuration",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles." : "Lancez votre intranet : choisissez le stockage, configurez les groupes, installez des données de démonstration et configurez les rôles d'administration.",
"Container" : "Conteneur",
"Padding" : "Marge intérieure",
"Title (optional)" : "Titre (facultatif)",
@@ -1363,7 +1354,9 @@ OC.L10N.register(
"Personal settings" : "Personal settings",
"Store" : "Store",
"Reports" : "Reports",
- "Flows" : "Flows"
+ "Flows" : "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings." : "Configurez votre instance LaunchPad avec l'ordre des groupes, les données de démonstration, les rôles d'administration et les paramètres du pied de page.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles." : "Démarrez votre intranet : configurez les groupes, installez les données de démonstration et configurez les rôles d'administration."
},
"nplurals=2; plural=(n > 1);"
);
diff --git a/l10n/fr.json b/l10n/fr.json
index f446a22a9..f463d1346 100644
--- a/l10n/fr.json
+++ b/l10n/fr.json
@@ -927,14 +927,6 @@
"LaunchPad setup wizard": "Assistant de configuration LaunchPad",
"Step {n} / {total}": "Étape {n} / {total}",
"Welcome": "Bienvenue",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings.": "Configurez votre instance LaunchPad avec le stockage, l'ordre des groupes, les données de démonstration, les rôles d'administration et les paramètres du pied de page.",
- "Storage backend": "Backend de stockage",
- "Choose how LaunchPad stores dashboard content.": "Choisissez comment LaunchPad stocke le contenu des tableaux de bord.",
- "Database (default)": "Base de données (par défaut)",
- "Store dashboard content in the LaunchPad database table.": "Stocker le contenu des tableaux de bord dans la table de base de données LaunchPad.",
- "GroupFolder (recommended for org use)": "GroupFolder (recommandé pour un usage organisationnel)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access.": "Stocker le contenu des tableaux de bord dans les GroupFolders Nextcloud pour un accès collaboratif.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option.": "L'application GroupFolder n'est pas installée. Installez « Nextcloud GroupFolders » pour utiliser cette option.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace.": "Choisissez l'ordre dans lequel les groupes Nextcloud apparaissent lorsque LaunchPad dirige les utilisateurs vers un espace de travail.",
"Demo data": "Données de démonstration",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only.": "Les vitrines de données de démonstration apparaîtront ici une fois que la fonctionnalité demo-data-showcases sera disponible. Pour l'instant, cette étape est uniquement informative.",
@@ -953,7 +945,6 @@
"Finish": "Terminer",
"Run setup wizard": "Lancer l'assistant de configuration",
"Run setup wizard again": "Relancer l'assistant de configuration",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles.": "Démarrez votre intranet : choisissez le stockage, configurez les groupes, installez les données de démonstration et configurez les rôles d'administration.",
"Container": "Conteneur",
"Padding": "Marge intérieure",
"Title (optional)": "Titre (facultatif)",
@@ -1330,6 +1321,8 @@
"Personal settings": "Personal settings",
"Store": "Store",
"Reports": "Reports",
- "Flows": "Flows"
+ "Flows": "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings.": "Configurez votre instance LaunchPad avec l'ordre des groupes, les données de démonstration, les rôles d'administration et les paramètres du pied de page.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles.": "Démarrez votre intranet : configurez les groupes, installez les données de démonstration et configurez les rôles d'administration."
}
}
diff --git a/l10n/ga.js b/l10n/ga.js
index 1e4c9c39b..6567cf536 100644
--- a/l10n/ga.js
+++ b/l10n/ga.js
@@ -926,14 +926,6 @@ OC.L10N.register(
"LaunchPad setup wizard" : "Draoi socraithe LaunchPad",
"Step {n} / {total}" : "Céim {n} / {total}",
"Welcome" : "Fáilte",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings." : "Cumraigh do shampla LaunchPad le stóráil, ordú grúpaí, sonraí taispeána, róil riaracháin, agus socruithe buntáisc.",
- "Storage backend" : "Inneall stórála",
- "Choose how LaunchPad stores dashboard content." : "Roghnaigh conas a stórálann LaunchPad ábhar deaise.",
- "Database (default)" : "Bunachar sonraí (réamhshocrú)",
- "Store dashboard content in the LaunchPad database table." : "Stóráil ábhar deaise i dtábla bhunachar sonraí LaunchPad.",
- "GroupFolder (recommended for org use)" : "GroupFolder (molta d'úsáid eagraíochta)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access." : "Stóráil ábhar deaise i nGroupFolders Nextcloud le haghaidh rochtana comhoibríche.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option." : "Níl an aip GroupFolder suiteáilte. Suiteáil 'Nextcloud GroupFolders' chun an rogha seo a úsáid.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace." : "Roghnaigh an t-ord ina bhfeictear grúpaí Nextcloud nuair a threoraíonn LaunchPad úsáideoirí chuig spás oibre.",
"Demo data" : "Sonraí taispeána",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only." : "Taispeánfar na taispeántais sonraí taispeána anseo a luaithe a sheoltar an cumas demo-data-showcases. Faoi láthair tá an chéim seo faisnéiseach amháin.",
@@ -952,7 +944,6 @@ OC.L10N.register(
"Finish" : "Críochnaigh",
"Run setup wizard" : "Rith an draoi socraithe",
"Run setup wizard again" : "Rith an draoi socraithe arís",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles." : "Cuir tús le d'inlíon: roghnaigh stóráil, cumraigh grúpaí, suiteáil sonraí taispeána, agus socraigh róil riaracháin.",
"Container" : "Coimeádán",
"Padding" : "Stuáil",
"Title (optional)" : "Teideal (roghnach)",
@@ -1363,7 +1354,9 @@ OC.L10N.register(
"Personal settings" : "Personal settings",
"Store" : "Store",
"Reports" : "Reports",
- "Flows" : "Flows"
+ "Flows" : "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings." : "Cumraigh do shampla LaunchPad le hordú grúpaí, sonraí taispeána, róil riaracháin, agus socruithe buntáisc.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles." : "Cuir tús le d'inlíon: cumraigh grúpaí, suiteáil sonraí taispeána, agus socraigh róil riaracháin."
},
"nplurals=5; plural=(n==1 ? 0 : n==2 ? 1 : n<7 ? 2 : n<11 ? 3 : 4);"
);
diff --git a/l10n/ga.json b/l10n/ga.json
index cab2a132a..bdcf80d6b 100644
--- a/l10n/ga.json
+++ b/l10n/ga.json
@@ -930,14 +930,6 @@
"LaunchPad setup wizard": "Draoi socraithe LaunchPad",
"Step {n} / {total}": "Céim {n} / {total}",
"Welcome": "Fáilte",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings.": "Cumraigh do shampla LaunchPad le stóráil, ordú grúpaí, sonraí taispeána, róil riaracháin, agus socruithe buntáisc.",
- "Storage backend": "Inneall stórála",
- "Choose how LaunchPad stores dashboard content.": "Roghnaigh conas a stórálann LaunchPad ábhar deaise.",
- "Database (default)": "Bunachar sonraí (réamhshocrú)",
- "Store dashboard content in the LaunchPad database table.": "Stóráil ábhar deaise i dtábla bhunachar sonraí LaunchPad.",
- "GroupFolder (recommended for org use)": "GroupFolder (molta d'úsáid eagraíochta)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access.": "Stóráil ábhar deaise i nGroupFolders Nextcloud le haghaidh rochtana comhoibríche.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option.": "Níl an aip GroupFolder suiteáilte. Suiteáil 'Nextcloud GroupFolders' chun an rogha seo a úsáid.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace.": "Roghnaigh an t-ord ina bhfeictear grúpaí Nextcloud nuair a threoraíonn LaunchPad úsáideoirí chuig spás oibre.",
"Demo data": "Sonraí taispeána",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only.": "Taispeánfar na taispeántais sonraí taispeána anseo a luaithe a sheoltar an cumas demo-data-showcases. Faoi láthair tá an chéim seo faisnéiseach amháin.",
@@ -956,7 +948,6 @@
"Finish": "Críochnaigh",
"Run setup wizard": "Rith an draoi socraithe",
"Run setup wizard again": "Rith an draoi socraithe arís",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles.": "Cuir tús le d'inlíon: roghnaigh stóráil, cumraigh grúpaí, suiteáil sonraí taispeána, agus socraigh róil riaracháin.",
"Container": "Coimeádán",
"Padding": "Stuáil",
"Title (optional)": "Teideal (roghnach)",
@@ -1333,6 +1324,8 @@
"Personal settings": "Personal settings",
"Store": "Store",
"Reports": "Reports",
- "Flows": "Flows"
+ "Flows": "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings.": "Cumraigh do shampla LaunchPad le hordú grúpaí, sonraí taispeána, róil riaracháin, agus socruithe buntáisc.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles.": "Cuir tús le d'inlíon: cumraigh grúpaí, suiteáil sonraí taispeána, agus socraigh róil riaracháin."
}
}
diff --git a/l10n/hr.js b/l10n/hr.js
index 2a8857d55..8efccee46 100644
--- a/l10n/hr.js
+++ b/l10n/hr.js
@@ -926,14 +926,6 @@ OC.L10N.register(
"LaunchPad setup wizard" : "Čarobnjak za postavljanje LaunchPada",
"Step {n} / {total}" : "Korak {n} / {total}",
"Welcome" : "Dobrodošli",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings." : "Konfigurirajte svoju LaunchPad instancu pomoću pohrane, redoslijeda grupa, demo podataka, administratorskih uloga i postavki podnožja.",
- "Storage backend" : "Pozadinski sustav pohrane",
- "Choose how LaunchPad stores dashboard content." : "Odaberite kako LaunchPad pohranjuje sadržaj nadzorne ploče.",
- "Database (default)" : "Baza podataka (zadano)",
- "Store dashboard content in the LaunchPad database table." : "Pohrani sadržaj nadzorne ploče u tablicu baze podataka LaunchPada.",
- "GroupFolder (recommended for org use)" : "GroupFolder (preporučeno za organizacijsku upotrebu)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access." : "Pohrani sadržaj nadzorne ploče u Nextcloud GroupFolders za zajednički pristup.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option." : "Aplikacija GroupFolder nije instalirana. Instalirajte 'Nextcloud GroupFolders' za korištenje ove opcije.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace." : "Odaberite redoslijed kojim se Nextcloud grupe pojavljuju kada LaunchPad usmjerava korisnike u radni prostor.",
"Demo data" : "Demo podaci",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only." : "Prikazi demo podataka pojavit će se ovdje kada se isporuči mogućnost demo-data-showcases. Za sada je ovaj korak samo informativan.",
@@ -952,7 +944,6 @@ OC.L10N.register(
"Finish" : "Završi",
"Run setup wizard" : "Pokreni čarobnjaka za postavljanje",
"Run setup wizard again" : "Ponovno pokreni čarobnjaka za postavljanje",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles." : "Pokrenite svoj intranet: odaberite pohranu, konfigurirajte grupe, instalirajte demo podatke i postavite administratorske uloge.",
"Container" : "Spremnik",
"Padding" : "Razmak",
"Title (optional)" : "Naslov (neobavezno)",
@@ -1363,7 +1354,9 @@ OC.L10N.register(
"Personal settings" : "Personal settings",
"Store" : "Store",
"Reports" : "Reports",
- "Flows" : "Flows"
+ "Flows" : "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings." : "Konfigurirajte svoju LaunchPad instancu s redoslijedom grupa, demo podacima, administratorskim ulogama i postavkama podnožja.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles." : "Pokrenite svoj intranet: konfigurirajte grupe, instalirajte demo podatke i postavite administratorske uloge."
},
"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"
);
diff --git a/l10n/hr.json b/l10n/hr.json
index 8fbf01850..c4affb2fc 100644
--- a/l10n/hr.json
+++ b/l10n/hr.json
@@ -928,14 +928,6 @@
"LaunchPad setup wizard": "Čarobnjak za postavljanje aplikacije LaunchPad",
"Step {n} / {total}": "Korak {n} / {total}",
"Welcome": "Dobrodošli",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings.": "Konfigurirajte svoju LaunchPad instancu s pohranom, redoslijedom grupa, demo podacima, administratorskim ulogama i postavkama podnožja.",
- "Storage backend": "Pozadinski sustav pohrane",
- "Choose how LaunchPad stores dashboard content.": "Odaberite kako LaunchPad pohranjuje sadržaj nadzorne ploče.",
- "Database (default)": "Baza podataka (zadano)",
- "Store dashboard content in the LaunchPad database table.": "Pohrani sadržaj nadzorne ploče u LaunchPad tablicu baze podataka.",
- "GroupFolder (recommended for org use)": "GroupFolder (preporučeno za organizacijsku upotrebu)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access.": "Pohrani sadržaj nadzorne ploče u Nextcloud GroupFolders za suradnički pristup.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option.": "Aplikacija GroupFolder nije instalirana. Instalirajte 'Nextcloud GroupFolders' da biste koristili ovu opciju.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace.": "Odaberite redoslijed kojim se Nextcloud grupe pojavljuju kada LaunchPad usmjerava korisnike u radni prostor.",
"Demo data": "Demo podaci",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only.": "Prikazi demo podataka pojavit će se ovdje kada se isporuči mogućnost demo-data-showcases. Za sada je ovaj korak samo informativan.",
@@ -954,7 +946,6 @@
"Finish": "Završi",
"Run setup wizard": "Pokreni čarobnjak za postavljanje",
"Run setup wizard again": "Ponovno pokreni čarobnjak za postavljanje",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles.": "Pokrenite svoj intranet: odaberite pohranu, konfigurirajte grupe, instalirajte demo podatke i postavite administratorske uloge.",
"Container": "Spremnik",
"Padding": "Razmak",
"Title (optional)": "Naslov (neobavezno)",
@@ -1331,6 +1322,8 @@
"Personal settings": "Personal settings",
"Store": "Store",
"Reports": "Reports",
- "Flows": "Flows"
+ "Flows": "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings.": "Konfigurirajte svoju LaunchPad instancu s redoslijedom grupa, demo podacima, administratorskim ulogama i postavkama podnožja.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles.": "Pokrenite svoj intranet: konfigurirajte grupe, instalirajte demo podatke i postavite administratorske uloge."
}
}
diff --git a/l10n/hu.js b/l10n/hu.js
index 263c90107..fcc0f09f3 100644
--- a/l10n/hu.js
+++ b/l10n/hu.js
@@ -926,14 +926,6 @@ OC.L10N.register(
"LaunchPad setup wizard" : "LaunchPad beállítóvarázsló",
"Step {n} / {total}" : "{n}. / {total}. lépés",
"Welcome" : "Üdvözöljük",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings." : "Állítsa be a LaunchPad-példányát tárolóval, csoportsorrenddel, bemutató adatokkal, rendszergazdai szerepkörökkel és láblécbeállításokkal.",
- "Storage backend" : "Tárolási háttérrendszer",
- "Choose how LaunchPad stores dashboard content." : "Válassza ki, hogyan tárolja a LaunchPad az irányítópult-tartalmat.",
- "Database (default)" : "Adatbázis (alapértelmezett)",
- "Store dashboard content in the LaunchPad database table." : "Az irányítópult-tartalom tárolása a LaunchPad adatbázistáblájában.",
- "GroupFolder (recommended for org use)" : "GroupFolder (szervezeti használathoz ajánlott)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access." : "Az irányítópult-tartalom tárolása Nextcloud GroupFolders mappákban az együttműködéses hozzáférés érdekében.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option." : "A GroupFolder alkalmazás nincs telepítve. Telepítse a „Nextcloud GroupFolders” alkalmazást ennek a beállításnak a használatához.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace." : "Válassza ki, milyen sorrendben jelenjenek meg a Nextcloud-csoportok, amikor a LaunchPad munkaterületre irányítja a felhasználókat.",
"Demo data" : "Bemutató adatok",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only." : "A bemutató adat-tárlatok itt jelennek meg, amint a demo-data-showcases képesség elérhetővé válik. Egyelőre ez a lépés csak tájékoztató jellegű.",
@@ -952,7 +944,6 @@ OC.L10N.register(
"Finish" : "Befejezés",
"Run setup wizard" : "Beállítóvarázsló futtatása",
"Run setup wizard again" : "Beállítóvarázsló újbóli futtatása",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles." : "Indítsa el az intranetjét: válasszon tárolót, állítson be csoportokat, telepítsen bemutató adatokat, és állítson be rendszergazdai szerepköröket.",
"Container" : "Tároló",
"Padding" : "Belső térköz",
"Title (optional)" : "Cím (választható)",
@@ -1363,7 +1354,9 @@ OC.L10N.register(
"Personal settings" : "Personal settings",
"Store" : "Store",
"Reports" : "Reports",
- "Flows" : "Flows"
+ "Flows" : "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings." : "Állítsa be LaunchPad-példányát a csoportsorrenddel, demoadatokkal, rendszergazdai szerepekkel és lábléc-beállításokkal.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles." : "Indítsa el intranetét: állítsa be a csoportokat, telepítsen demoadatokat, és állítsa be a rendszergazdai szerepeket."
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/hu.json b/l10n/hu.json
index db7aed4b0..2195008a7 100644
--- a/l10n/hu.json
+++ b/l10n/hu.json
@@ -927,14 +927,6 @@
"LaunchPad setup wizard": "LaunchPad beállítási varázsló",
"Step {n} / {total}": "{n}. / {total}. lépés",
"Welcome": "Üdvözöljük",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings.": "Állítsa be LaunchPad-példányát a tárolóval, csoportsorrenddel, demoadatokkal, rendszergazdai szerepekkel és lábléc-beállításokkal.",
- "Storage backend": "Tároló-háttérszolgáltatás",
- "Choose how LaunchPad stores dashboard content.": "Válassza ki, hogyan tárolja a LaunchPad az irányítópult-tartalmat.",
- "Database (default)": "Adatbázis (alapértelmezett)",
- "Store dashboard content in the LaunchPad database table.": "Az irányítópult-tartalom tárolása a LaunchPad adatbázistáblájában.",
- "GroupFolder (recommended for org use)": "GroupFolder (szervezeti használathoz ajánlott)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access.": "Az irányítópult-tartalom tárolása a Nextcloud GroupFolders mappáiban az együttműködéses hozzáférés érdekében.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option.": "A GroupFolder alkalmazás nincs telepítve. Telepítse a „Nextcloud GroupFolders” alkalmazást ennek a lehetőségnek a használatához.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace.": "Válassza ki, milyen sorrendben jelenjenek meg a Nextcloud-csoportok, amikor a LaunchPad a felhasználókat egy munkaterületre irányítja.",
"Demo data": "Demoadatok",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only.": "A demoadat-bemutatók itt jelennek meg, amint a demo-data-showcases képesség elérhetővé válik. Egyelőre ez a lépés csak tájékoztató jellegű.",
@@ -953,7 +945,6 @@
"Finish": "Befejezés",
"Run setup wizard": "Beállítási varázsló futtatása",
"Run setup wizard again": "Beállítási varázsló újrafuttatása",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles.": "Indítsa el intranetét: válasszon tárolót, állítsa be a csoportokat, telepítsen demoadatokat, és állítsa be a rendszergazdai szerepeket.",
"Container": "Tároló",
"Padding": "Belső térköz",
"Title (optional)": "Cím (választható)",
@@ -1330,6 +1321,8 @@
"Personal settings": "Personal settings",
"Store": "Store",
"Reports": "Reports",
- "Flows": "Flows"
+ "Flows": "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings.": "Állítsa be LaunchPad-példányát a csoportsorrenddel, demoadatokkal, rendszergazdai szerepekkel és lábléc-beállításokkal.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles.": "Indítsa el intranetét: állítsa be a csoportokat, telepítsen demoadatokat, és állítsa be a rendszergazdai szerepeket."
}
}
diff --git a/l10n/is.js b/l10n/is.js
index 160c212b8..52bc3ba57 100644
--- a/l10n/is.js
+++ b/l10n/is.js
@@ -926,14 +926,6 @@ OC.L10N.register(
"LaunchPad setup wizard" : "LaunchPad-uppsetningarálfur",
"Step {n} / {total}" : "Skref {n} / {total}",
"Welcome" : "Velkomin",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings." : "Stillið LaunchPad-tilvik yðar með geymslu, hóparöðun, sýnigögnum, stjórnandahlutverkum og fótstillingum.",
- "Storage backend" : "Geymslubakendi",
- "Choose how LaunchPad stores dashboard content." : "Veljið hvernig LaunchPad geymir stjórnborðsefni.",
- "Database (default)" : "Gagnagrunnur (sjálfgefið)",
- "Store dashboard content in the LaunchPad database table." : "Geyma stjórnborðsefni í LaunchPad-gagnagrunnstöflunni.",
- "GroupFolder (recommended for org use)" : "GroupFolder (mælt með fyrir stofnananotkun)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access." : "Geyma stjórnborðsefni í Nextcloud GroupFolders fyrir samvinnuaðgang.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option." : "GroupFolder-forritið er ekki uppsett. Setjið upp „Nextcloud GroupFolders“ til að nota þennan valkost.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace." : "Veljið röðina sem Nextcloud-hópar birtast í þegar LaunchPad beinir notendum á vinnusvæði.",
"Demo data" : "Sýnigögn",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only." : "Sýnigagnasýnishornin munu birtast hér þegar demo-data-showcases-geta verður afhent. Eins og er er þetta skref aðeins til upplýsinga.",
@@ -952,7 +944,6 @@ OC.L10N.register(
"Finish" : "Ljúka",
"Run setup wizard" : "Keyra uppsetningarálf",
"Run setup wizard again" : "Keyra uppsetningarálf aftur",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles." : "Komið innra netinu yðar af stað: veljið geymslu, stillið hópa, setjið upp sýnigögn og setjið upp stjórnandahlutverk.",
"Container" : "Ílát",
"Padding" : "Spássía",
"Title (optional)" : "Titill (valfrjálst)",
@@ -1363,7 +1354,9 @@ OC.L10N.register(
"Personal settings" : "Personal settings",
"Store" : "Store",
"Reports" : "Reports",
- "Flows" : "Flows"
+ "Flows" : "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings." : "Stilltu LaunchPad uppsetninguna þína með hóparöðun, kynningargögnum, kerfisstjórahlutverkum og fótstillingum.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles." : "Komdu innranetinu þínu af stað: stilltu hópa, settu upp kynningargögn og settu upp kerfisstjórahlutverk."
},
"nplurals=2; plural=(n%10!=1 || n%100==11);"
);
diff --git a/l10n/is.json b/l10n/is.json
index f04e7fe9f..fd97c9542 100644
--- a/l10n/is.json
+++ b/l10n/is.json
@@ -927,14 +927,6 @@
"LaunchPad setup wizard": "LaunchPad uppsetningarleiðsögn",
"Step {n} / {total}": "Skref {n} / {total}",
"Welcome": "Velkomin(n)",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings.": "Stilltu LaunchPad uppsetninguna þína með geymslu, hóparöðun, kynningargögnum, kerfisstjórahlutverkum og fótstillingum.",
- "Storage backend": "Geymslubakendi",
- "Choose how LaunchPad stores dashboard content.": "Veldu hvernig LaunchPad geymir mælaborðsefni.",
- "Database (default)": "Gagnagrunnur (sjálfgefið)",
- "Store dashboard content in the LaunchPad database table.": "Geyma mælaborðsefni í gagnagrunnstöflu LaunchPad.",
- "GroupFolder (recommended for org use)": "GroupFolder (mælt með fyrir stofnananotkun)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access.": "Geyma mælaborðsefni í Nextcloud GroupFolders fyrir samvinnuaðgang.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option.": "GroupFolder forritið er ekki uppsett. Settu upp 'Nextcloud GroupFolders' til að nota þennan valkost.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace.": "Veldu röðina sem Nextcloud hópar birtast í þegar LaunchPad beinir notendum á vinnusvæði.",
"Demo data": "Kynningargögn",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only.": "Sýnishorn kynningargagna birtast hér þegar demo-data-showcases eiginleikinn kemur. Eins og er er þetta skref aðeins til upplýsinga.",
@@ -953,7 +945,6 @@
"Finish": "Ljúka",
"Run setup wizard": "Keyra uppsetningarleiðsögn",
"Run setup wizard again": "Keyra uppsetningarleiðsögn aftur",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles.": "Komdu innranetinu þínu af stað: veldu geymslu, stilltu hópa, settu upp kynningargögn og settu upp kerfisstjórahlutverk.",
"Container": "Ílát",
"Padding": "Innfylling",
"Title (optional)": "Titill (valfrjálst)",
@@ -1330,6 +1321,8 @@
"Personal settings": "Personal settings",
"Store": "Store",
"Reports": "Reports",
- "Flows": "Flows"
+ "Flows": "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings.": "Stilltu LaunchPad uppsetninguna þína með hóparöðun, kynningargögnum, kerfisstjórahlutverkum og fótstillingum.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles.": "Komdu innranetinu þínu af stað: stilltu hópa, settu upp kynningargögn og settu upp kerfisstjórahlutverk."
}
}
diff --git a/l10n/it.js b/l10n/it.js
index 85143eb7f..6f7adff97 100644
--- a/l10n/it.js
+++ b/l10n/it.js
@@ -926,14 +926,6 @@ OC.L10N.register(
"LaunchPad setup wizard" : "Procedura guidata di configurazione di LaunchPad",
"Step {n} / {total}" : "Passo {n} / {total}",
"Welcome" : "Benvenuto",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings." : "Configura la tua istanza LaunchPad con archiviazione, ordinamento dei gruppi, dati dimostrativi, ruoli di amministrazione e impostazioni del piè di pagina.",
- "Storage backend" : "Backend di archiviazione",
- "Choose how LaunchPad stores dashboard content." : "Scegli come LaunchPad archivia il contenuto delle dashboard.",
- "Database (default)" : "Database (predefinito)",
- "Store dashboard content in the LaunchPad database table." : "Archivia il contenuto delle dashboard nella tabella del database di LaunchPad.",
- "GroupFolder (recommended for org use)" : "GroupFolder (consigliato per l'uso organizzativo)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access." : "Archivia il contenuto delle dashboard nelle GroupFolder di Nextcloud per l'accesso collaborativo.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option." : "L'app GroupFolder non è installata. Installi 'Nextcloud GroupFolders' per usare questa opzione.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace." : "Scegli l'ordine in cui i gruppi Nextcloud appaiono quando LaunchPad indirizza gli utenti a un'area di lavoro.",
"Demo data" : "Dati dimostrativi",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only." : "Le vetrine di dati dimostrativi appariranno qui una volta rilasciata la funzionalità demo-data-showcases. Per ora questo passo è solo informativo.",
@@ -952,7 +944,6 @@ OC.L10N.register(
"Finish" : "Fine",
"Run setup wizard" : "Avvia procedura guidata di configurazione",
"Run setup wizard again" : "Avvia di nuovo la procedura guidata di configurazione",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles." : "Avvia la tua intranet: scegli l'archiviazione, configura i gruppi, installa i dati dimostrativi e imposta i ruoli di amministrazione.",
"Container" : "Contenitore",
"Padding" : "Spaziatura interna",
"Title (optional)" : "Titolo (facoltativo)",
@@ -1363,7 +1354,9 @@ OC.L10N.register(
"Personal settings" : "Personal settings",
"Store" : "Store",
"Reports" : "Reports",
- "Flows" : "Flows"
+ "Flows" : "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings." : "Configura la tua istanza di LaunchPad con ordinamento dei gruppi, dati dimostrativi, ruoli di amministrazione e impostazioni del piè di pagina.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles." : "Avvia la tua intranet: configura i gruppi, installa i dati dimostrativi e imposta i ruoli di amministrazione."
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/it.json b/l10n/it.json
index 39bf8dec9..aef547f26 100644
--- a/l10n/it.json
+++ b/l10n/it.json
@@ -927,14 +927,6 @@
"LaunchPad setup wizard": "Procedura guidata di configurazione di LaunchPad",
"Step {n} / {total}": "Passaggio {n} / {total}",
"Welcome": "Benvenuto",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings.": "Configura la tua istanza di LaunchPad con archiviazione, ordinamento dei gruppi, dati dimostrativi, ruoli di amministrazione e impostazioni del piè di pagina.",
- "Storage backend": "Backend di archiviazione",
- "Choose how LaunchPad stores dashboard content.": "Scegli come LaunchPad archivia il contenuto delle dashboard.",
- "Database (default)": "Database (predefinito)",
- "Store dashboard content in the LaunchPad database table.": "Archivia il contenuto delle dashboard nella tabella del database di LaunchPad.",
- "GroupFolder (recommended for org use)": "GroupFolder (consigliato per l'uso organizzativo)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access.": "Archivia il contenuto delle dashboard nelle GroupFolder di Nextcloud per l'accesso collaborativo.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option.": "L'app GroupFolder non è installata. Installa 'Nextcloud GroupFolders' per usare questa opzione.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace.": "Scegli l'ordine in cui appaiono i gruppi Nextcloud quando LaunchPad indirizza gli utenti a uno spazio di lavoro.",
"Demo data": "Dati dimostrativi",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only.": "Gli esempi dimostrativi di dati appariranno qui una volta rilasciata la funzionalità demo-data-showcases. Per ora questo passaggio è solo informativo.",
@@ -953,7 +945,6 @@
"Finish": "Fine",
"Run setup wizard": "Avvia la procedura guidata di configurazione",
"Run setup wizard again": "Avvia di nuovo la procedura guidata di configurazione",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles.": "Avvia la tua intranet: scegli l'archiviazione, configura i gruppi, installa i dati dimostrativi e imposta i ruoli di amministrazione.",
"Container": "Contenitore",
"Padding": "Spaziatura interna",
"Title (optional)": "Titolo (facoltativo)",
@@ -1330,6 +1321,8 @@
"Personal settings": "Personal settings",
"Store": "Store",
"Reports": "Reports",
- "Flows": "Flows"
+ "Flows": "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings.": "Configura la tua istanza di LaunchPad con ordinamento dei gruppi, dati dimostrativi, ruoli di amministrazione e impostazioni del piè di pagina.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles.": "Avvia la tua intranet: configura i gruppi, installa i dati dimostrativi e imposta i ruoli di amministrazione."
}
}
diff --git a/l10n/lb.js b/l10n/lb.js
index 88a18e9fb..d5cc9247b 100644
--- a/l10n/lb.js
+++ b/l10n/lb.js
@@ -926,14 +926,6 @@ OC.L10N.register(
"LaunchPad setup wizard" : "LaunchPad-Ariichtungsassistent",
"Step {n} / {total}" : "Schrëtt {n} / {total}",
"Welcome" : "Wëllkomm",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings." : "Konfiguréiert Är LaunchPad-Instanz mat Späicher, Gruppeuerdnung, Demodaten, Administrateursrollen a Foussberäich-Astellungen.",
- "Storage backend" : "Späicher-Backend",
- "Choose how LaunchPad stores dashboard content." : "Wielt, wéi LaunchPad den Dashboard-Inhalt späichert.",
- "Database (default)" : "Datebank (Standard)",
- "Store dashboard content in the LaunchPad database table." : "Späichert den Dashboard-Inhalt an der LaunchPad-Datebanktabell.",
- "GroupFolder (recommended for org use)" : "GroupFolder (empfohlen fir organisatoresche Gebrauch)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access." : "Späichert den Dashboard-Inhalt an Nextcloud-GroupFolders fir e kollaborativen Zougang.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option." : "D'GroupFolder-Applikatioun ass net installéiert. Installéiert „Nextcloud GroupFolders“, fir dës Optioun ze benotzen.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace." : "Wielt d'Reiefolleg, an där d'Nextcloud-Gruppen erschéngen, wann LaunchPad d'Benotzer op en Aarbechtsberäich weiderleet.",
"Demo data" : "Demodaten",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only." : "D'Demodaten-Presentatioune erschéngen hei, soubal d'Funktioun demo-data-showcases ausgeliwwert gëtt. Fir de Moment ass dëse Schrëtt nëmmen informativ.",
@@ -952,7 +944,6 @@ OC.L10N.register(
"Finish" : "Ofschléissen",
"Run setup wizard" : "Ariichtungsassistent ausféieren",
"Run setup wizard again" : "Ariichtungsassistent nach eemol ausféieren",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles." : "Setzt Ären Intranet a Gang: wielt de Späicher, konfiguréiert d'Gruppen, installéiert Demodaten a riicht Administrateursrollen in.",
"Container" : "Container",
"Padding" : "Ofstand",
"Title (optional)" : "Titel (optional)",
@@ -1354,7 +1345,9 @@ OC.L10N.register(
"Personal settings" : "Personal settings",
"Store" : "Store",
"Reports" : "Reports",
- "Flows" : "Flows"
+ "Flows" : "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings." : "Konfiguréiert Är LaunchPad-Instanz mat Gruppeuerdnung, Demodaten, Administrateursrollen a Foussberäich-Astellungen.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles." : "Setzt Ären Intranet a Gang: konfiguréiert d'Gruppen, installéiert Demodaten a riicht Administrateursrollen in."
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/lb.json b/l10n/lb.json
index a1ec1ebcf..f9eec8422 100644
--- a/l10n/lb.json
+++ b/l10n/lb.json
@@ -927,14 +927,6 @@
"LaunchPad setup wizard": "LaunchPad-Ariichtungsassistent",
"Step {n} / {total}": "Schrëtt {n} / {total}",
"Welcome": "Wëllkomm",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings.": "Konfiguréiert Är LaunchPad-Instanz mat Späicher, Gruppeuerdnung, Demodaten, Administrateursrollen a Foussberäich-Astellungen.",
- "Storage backend": "Späicher-Backend",
- "Choose how LaunchPad stores dashboard content.": "Wielt, wéi LaunchPad den Dashboard-Inhalt späichert.",
- "Database (default)": "Datebank (Standard)",
- "Store dashboard content in the LaunchPad database table.": "Späichert den Dashboard-Inhalt an der LaunchPad-Datebanktabell.",
- "GroupFolder (recommended for org use)": "GroupFolder (empfohlen fir organisatoresche Gebrauch)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access.": "Späichert den Dashboard-Inhalt an Nextcloud-GroupFolders fir e kollaborativen Zougang.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option.": "D'GroupFolder-Applikatioun ass net installéiert. Installéiert „Nextcloud GroupFolders“, fir dës Optioun ze benotzen.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace.": "Wielt d'Reiefolleg, an där d'Nextcloud-Gruppen erschéngen, wann LaunchPad d'Benotzer op en Aarbechtsberäich weiderleet.",
"Demo data": "Demodaten",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only.": "D'Demodaten-Presentatioune erschéngen hei, soubal d'Funktioun demo-data-showcases ausgeliwwert gëtt. Fir de Moment ass dëse Schrëtt nëmmen informativ.",
@@ -953,7 +945,6 @@
"Finish": "Ofschléissen",
"Run setup wizard": "Ariichtungsassistent ausféieren",
"Run setup wizard again": "Ariichtungsassistent nach eemol ausféieren",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles.": "Setzt Ären Intranet a Gang: wielt de Späicher, konfiguréiert d'Gruppen, installéiert Demodaten a riicht Administrateursrollen in.",
"Container": "Container",
"Padding": "Ofstand",
"Title (optional)": "Titel (optional)",
@@ -1330,6 +1321,8 @@
"Personal settings": "Personal settings",
"Store": "Store",
"Reports": "Reports",
- "Flows": "Flows"
+ "Flows": "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings.": "Konfiguréiert Är LaunchPad-Instanz mat Gruppeuerdnung, Demodaten, Administrateursrollen a Foussberäich-Astellungen.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles.": "Setzt Ären Intranet a Gang: konfiguréiert d'Gruppen, installéiert Demodaten a riicht Administrateursrollen in."
}
}
diff --git a/l10n/lt.js b/l10n/lt.js
index af2a987e5..2044a38a4 100644
--- a/l10n/lt.js
+++ b/l10n/lt.js
@@ -926,14 +926,6 @@ OC.L10N.register(
"LaunchPad setup wizard" : "LaunchPad sąrankos vediklis",
"Step {n} / {total}" : "Žingsnis {n} / {total}",
"Welcome" : "Sveiki",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings." : "Sukonfigūruokite savo LaunchPad egzempliorių su saugykla, grupių tvarka, demonstraciniais duomenimis, administratorių vaidmenimis ir poraštės nustatymais.",
- "Storage backend" : "Saugyklos posistemė",
- "Choose how LaunchPad stores dashboard content." : "Pasirinkite, kaip LaunchPad saugo valdymo skydo turinį.",
- "Database (default)" : "Duomenų bazė (numatytoji)",
- "Store dashboard content in the LaunchPad database table." : "Saugoti valdymo skydo turinį LaunchPad duomenų bazės lentelėje.",
- "GroupFolder (recommended for org use)" : "GroupFolder (rekomenduojama organizaciniam naudojimui)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access." : "Saugoti valdymo skydo turinį Nextcloud GroupFolders bendradarbiavimo prieigai.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option." : "GroupFolder programa neįdiegta. Norėdami naudoti šią parinktį, įdiekite „Nextcloud GroupFolders“.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace." : "Pasirinkite tvarką, kuria Nextcloud grupės rodomos, kai LaunchPad nukreipia naudotojus į darbo sritį.",
"Demo data" : "Demonstraciniai duomenys",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only." : "Demonstracinių duomenų pavyzdžiai čia atsiras, kai bus pristatyta demo-data-showcases galimybė. Kol kas šis žingsnis yra tik informacinis.",
@@ -952,7 +944,6 @@ OC.L10N.register(
"Finish" : "Baigti",
"Run setup wizard" : "Paleisti sąrankos vediklį",
"Run setup wizard again" : "Paleisti sąrankos vediklį dar kartą",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles." : "Pradėkite savo intranetą: pasirinkite saugyklą, sukonfigūruokite grupes, įdiekite demonstracinius duomenis ir nustatykite administratorių vaidmenis.",
"Container" : "Konteineris",
"Padding" : "Vidinis tarpas",
"Title (optional)" : "Pavadinimas (neprivalomas)",
@@ -1363,7 +1354,9 @@ OC.L10N.register(
"Personal settings" : "Personal settings",
"Store" : "Store",
"Reports" : "Reports",
- "Flows" : "Flows"
+ "Flows" : "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings." : "Sukonfigūruokite savo LaunchPad egzempliorių su grupių tvarka, demonstraciniais duomenimis, administratoriaus vaidmenimis ir poraštės nustatymais.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles." : "Pradėkite savo intranetą: sukonfigūruokite grupes, įdiekite demonstracinius duomenis ir nustatykite administratoriaus vaidmenis."
},
"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2);"
);
diff --git a/l10n/lt.json b/l10n/lt.json
index 48d140036..627c91524 100644
--- a/l10n/lt.json
+++ b/l10n/lt.json
@@ -928,14 +928,6 @@
"LaunchPad setup wizard": "LaunchPad sąrankos vediklis",
"Step {n} / {total}": "Žingsnis {n} / {total}",
"Welcome": "Sveiki atvykę",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings.": "Sukonfigūruokite savo LaunchPad egzempliorių su saugykla, grupių tvarka, demonstraciniais duomenimis, administratoriaus vaidmenimis ir poraštės nustatymais.",
- "Storage backend": "Saugyklos posistemė",
- "Choose how LaunchPad stores dashboard content.": "Pasirinkite, kaip LaunchPad saugo skydelio turinį.",
- "Database (default)": "Duomenų bazė (numatytoji)",
- "Store dashboard content in the LaunchPad database table.": "Saugoti skydelio turinį LaunchPad duomenų bazės lentelėje.",
- "GroupFolder (recommended for org use)": "GroupFolder (rekomenduojama organizacijos naudojimui)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access.": "Saugoti skydelio turinį Nextcloud GroupFolders bendradarbiavimo prieigai.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option.": "GroupFolder programa neįdiegta. Norėdami naudoti šią parinktį, įdiekite „Nextcloud GroupFolders“.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace.": "Pasirinkite tvarką, kuria Nextcloud grupės rodomos, kai LaunchPad nukreipia naudotojus į darbo sritį.",
"Demo data": "Demonstraciniai duomenys",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only.": "Demonstracinių duomenų pavyzdžiai bus rodomi čia, kai pasirodys demo-data-showcases galimybė. Kol kas šis žingsnis yra tik informacinis.",
@@ -954,7 +946,6 @@
"Finish": "Baigti",
"Run setup wizard": "Paleisti sąrankos vediklį",
"Run setup wizard again": "Paleisti sąrankos vediklį iš naujo",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles.": "Pradėkite savo intranetą: pasirinkite saugyklą, sukonfigūruokite grupes, įdiekite demonstracinius duomenis ir nustatykite administratoriaus vaidmenis.",
"Container": "Talpykla",
"Padding": "Vidinis tarpas",
"Title (optional)": "Pavadinimas (neprivaloma)",
@@ -1331,6 +1322,8 @@
"Personal settings": "Personal settings",
"Store": "Store",
"Reports": "Reports",
- "Flows": "Flows"
+ "Flows": "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings.": "Sukonfigūruokite savo LaunchPad egzempliorių su grupių tvarka, demonstraciniais duomenimis, administratoriaus vaidmenimis ir poraštės nustatymais.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles.": "Pradėkite savo intranetą: sukonfigūruokite grupes, įdiekite demonstracinius duomenis ir nustatykite administratoriaus vaidmenis."
}
}
diff --git a/l10n/lv.js b/l10n/lv.js
index 64ba546fd..1afbd03eb 100644
--- a/l10n/lv.js
+++ b/l10n/lv.js
@@ -926,14 +926,6 @@ OC.L10N.register(
"LaunchPad setup wizard" : "LaunchPad iestatīšanas vednis",
"Step {n} / {total}" : "Solis {n} / {total}",
"Welcome" : "Laipni lūdzam",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings." : "Konfigurējiet savu LaunchPad instanci ar krātuvi, grupu kārtošanu, demonstrācijas datiem, administratora lomām un kājenes iestatījumiem.",
- "Storage backend" : "Krātuves aizmugursistēma",
- "Choose how LaunchPad stores dashboard content." : "Izvēlieties, kā LaunchPad glabā vadības paneļu saturu.",
- "Database (default)" : "Datubāze (noklusējums)",
- "Store dashboard content in the LaunchPad database table." : "Glabāt vadības paneļu saturu LaunchPad datubāzes tabulā.",
- "GroupFolder (recommended for org use)" : "GroupFolder (ieteicams organizācijas lietošanai)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access." : "Glabāt vadības paneļu saturu Nextcloud GroupFolders kopdarbam.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option." : "GroupFolder lietotne nav instalēta. Instalējiet 'Nextcloud GroupFolders', lai izmantotu šo opciju.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace." : "Izvēlieties secību, kādā Nextcloud grupas parādās, kad LaunchPad novirza lietotājus uz darbvietu.",
"Demo data" : "Demonstrācijas dati",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only." : "Demonstrācijas datu prezentācijas parādīsies šeit, tiklīdz būs pieejama demo-data-showcases funkcija. Pagaidām šis solis ir tikai informatīvs.",
@@ -952,7 +944,6 @@ OC.L10N.register(
"Finish" : "Pabeigt",
"Run setup wizard" : "Palaist iestatīšanas vedni",
"Run setup wizard again" : "Palaist iestatīšanas vedni vēlreiz",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles." : "Uzsāciet sava intraneta darbu: izvēlieties krātuvi, konfigurējiet grupas, instalējiet demonstrācijas datus un iestatiet administratora lomas.",
"Container" : "Konteiners",
"Padding" : "Iekšējā atstarpe",
"Title (optional)" : "Nosaukums (neobligāts)",
@@ -1363,7 +1354,9 @@ OC.L10N.register(
"Personal settings" : "Personal settings",
"Store" : "Store",
"Reports" : "Reports",
- "Flows" : "Flows"
+ "Flows" : "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings." : "Konfigurējiet savu LaunchPad instanci ar grupu kārtošanu, demonstrācijas datiem, administratora lomām un kājenes iestatījumiem.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles." : "Uzsāciet sava intraneta darbu: konfigurējiet grupas, instalējiet demonstrācijas datus un iestatiet administratora lomas."
},
"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2);"
);
diff --git a/l10n/lv.json b/l10n/lv.json
index 893c514a4..560cbf002 100644
--- a/l10n/lv.json
+++ b/l10n/lv.json
@@ -928,14 +928,6 @@
"LaunchPad setup wizard": "LaunchPad iestatīšanas vednis",
"Step {n} / {total}": "Solis {n} / {total}",
"Welcome": "Laipni lūdzam",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings.": "Konfigurējiet savu LaunchPad instanci ar krātuvi, grupu kārtošanu, demonstrācijas datiem, administratora lomām un kājenes iestatījumiem.",
- "Storage backend": "Krātuves aizmugursistēma",
- "Choose how LaunchPad stores dashboard content.": "Izvēlieties, kā LaunchPad glabā vadības paneļu saturu.",
- "Database (default)": "Datubāze (noklusējums)",
- "Store dashboard content in the LaunchPad database table.": "Glabāt vadības paneļu saturu LaunchPad datubāzes tabulā.",
- "GroupFolder (recommended for org use)": "GroupFolder (ieteicams organizācijas lietošanai)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access.": "Glabāt vadības paneļu saturu Nextcloud GroupFolders kopdarbam.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option.": "GroupFolder lietotne nav instalēta. Instalējiet 'Nextcloud GroupFolders', lai izmantotu šo opciju.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace.": "Izvēlieties secību, kādā Nextcloud grupas parādās, kad LaunchPad novirza lietotājus uz darbvietu.",
"Demo data": "Demonstrācijas dati",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only.": "Demonstrācijas datu prezentācijas parādīsies šeit, tiklīdz būs pieejama demo-data-showcases funkcija. Pagaidām šis solis ir tikai informatīvs.",
@@ -954,7 +946,6 @@
"Finish": "Pabeigt",
"Run setup wizard": "Palaist iestatīšanas vedni",
"Run setup wizard again": "Palaist iestatīšanas vedni vēlreiz",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles.": "Uzsāciet sava intraneta darbu: izvēlieties krātuvi, konfigurējiet grupas, instalējiet demonstrācijas datus un iestatiet administratora lomas.",
"Container": "Konteiners",
"Padding": "Iekšējā atstarpe",
"Title (optional)": "Nosaukums (neobligāts)",
@@ -1331,6 +1322,8 @@
"Personal settings": "Personal settings",
"Store": "Store",
"Reports": "Reports",
- "Flows": "Flows"
+ "Flows": "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings.": "Konfigurējiet savu LaunchPad instanci ar grupu kārtošanu, demonstrācijas datiem, administratora lomām un kājenes iestatījumiem.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles.": "Uzsāciet sava intraneta darbu: konfigurējiet grupas, instalējiet demonstrācijas datus un iestatiet administratora lomas."
}
}
diff --git a/l10n/mk.js b/l10n/mk.js
index 80b3ac41d..1b74e9617 100644
--- a/l10n/mk.js
+++ b/l10n/mk.js
@@ -926,14 +926,6 @@ OC.L10N.register(
"LaunchPad setup wizard" : "LaunchPad волшебник за поставување",
"Step {n} / {total}" : "Чекор {n} / {total}",
"Welcome" : "Добредојдовте",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings." : "Конфигурирајте ја вашата LaunchPad инстанца со складирање, подредување на групи, демо податоци, администраторски улоги и поставки на подножјето.",
- "Storage backend" : "Позадински систем за складирање",
- "Choose how LaunchPad stores dashboard content." : "Изберете како LaunchPad ја складира содржината на контролната табла.",
- "Database (default)" : "База на податоци (стандардно)",
- "Store dashboard content in the LaunchPad database table." : "Складирај ја содржината на контролната табла во табелата на базата на податоци на LaunchPad.",
- "GroupFolder (recommended for org use)" : "GroupFolder (препорачано за организациска употреба)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access." : "Складирај ја содржината на контролната табла во Nextcloud GroupFolders за колаборативен пристап.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option." : "Апликацијата GroupFolder не е инсталирана. Инсталирајте 'Nextcloud GroupFolders' за да ја користите оваа опција.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace." : "Изберете го редоследот по кој се појавуваат Nextcloud групите кога LaunchPad ги насочува корисниците кон работен простор.",
"Demo data" : "Демо податоци",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only." : "Приказите на демо податоци ќе се појават овде штом ќе се испорача можноста demo-data-showcases. Засега овој чекор е само информативен.",
@@ -952,7 +944,6 @@ OC.L10N.register(
"Finish" : "Заврши",
"Run setup wizard" : "Стартувај волшебник за поставување",
"Run setup wizard again" : "Стартувај го волшебникот за поставување повторно",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles." : "Стартувајте го вашиот интранет: изберете складирање, конфигурирајте групи, инсталирајте демо податоци и поставете администраторски улоги.",
"Container" : "Контејнер",
"Padding" : "Внатрешно растојание",
"Title (optional)" : "Наслов (изборно)",
@@ -1354,7 +1345,9 @@ OC.L10N.register(
"Personal settings" : "Personal settings",
"Store" : "Store",
"Reports" : "Reports",
- "Flows" : "Flows"
+ "Flows" : "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings." : "Конфигурирајте ја вашата LaunchPad инстанца со подредување на групи, демо податоци, администраторски улоги и поставки на подножјето.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles." : "Стартувајте го вашиот интранет: конфигурирајте групи, инсталирајте демо податоци и поставете администраторски улоги."
},
"nplurals=2; plural=(n%10==1 && n%100!=11 ? 0 : 1);"
);
diff --git a/l10n/mk.json b/l10n/mk.json
index e6c1b13dc..ad5e396fb 100644
--- a/l10n/mk.json
+++ b/l10n/mk.json
@@ -927,14 +927,6 @@
"LaunchPad setup wizard": "LaunchPad волшебник за поставување",
"Step {n} / {total}": "Чекор {n} / {total}",
"Welcome": "Добредојдовте",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings.": "Конфигурирајте ја вашата LaunchPad инстанца со складирање, подредување на групи, демо податоци, администраторски улоги и поставки на подножјето.",
- "Storage backend": "Позадински систем за складирање",
- "Choose how LaunchPad stores dashboard content.": "Изберете како LaunchPad ја складира содржината на контролната табла.",
- "Database (default)": "База на податоци (стандардно)",
- "Store dashboard content in the LaunchPad database table.": "Складирај ја содржината на контролната табла во табелата на базата на податоци на LaunchPad.",
- "GroupFolder (recommended for org use)": "GroupFolder (препорачано за организациска употреба)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access.": "Складирај ја содржината на контролната табла во Nextcloud GroupFolders за колаборативен пристап.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option.": "Апликацијата GroupFolder не е инсталирана. Инсталирајте 'Nextcloud GroupFolders' за да ја користите оваа опција.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace.": "Изберете го редоследот по кој се појавуваат Nextcloud групите кога LaunchPad ги насочува корисниците кон работен простор.",
"Demo data": "Демо податоци",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only.": "Приказите на демо податоци ќе се појават овде штом ќе се испорача можноста demo-data-showcases. Засега овој чекор е само информативен.",
@@ -953,7 +945,6 @@
"Finish": "Заврши",
"Run setup wizard": "Стартувај волшебник за поставување",
"Run setup wizard again": "Стартувај го волшебникот за поставување повторно",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles.": "Стартувајте го вашиот интранет: изберете складирање, конфигурирајте групи, инсталирајте демо податоци и поставете администраторски улоги.",
"Container": "Контејнер",
"Padding": "Внатрешно растојание",
"Title (optional)": "Наслов (изборно)",
@@ -1330,6 +1321,8 @@
"Personal settings": "Personal settings",
"Store": "Store",
"Reports": "Reports",
- "Flows": "Flows"
+ "Flows": "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings.": "Конфигурирајте ја вашата LaunchPad инстанца со подредување на групи, демо податоци, администраторски улоги и поставки на подножјето.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles.": "Стартувајте го вашиот интранет: конфигурирајте групи, инсталирајте демо податоци и поставете администраторски улоги."
}
}
diff --git a/l10n/mt.js b/l10n/mt.js
index 89b245f07..d986d2230 100644
--- a/l10n/mt.js
+++ b/l10n/mt.js
@@ -926,14 +926,6 @@ OC.L10N.register(
"LaunchPad setup wizard" : "Wizard tas-setup ta' LaunchPad",
"Step {n} / {total}" : "Pass {n} / {total}",
"Welcome" : "Merħba",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings." : "Ikkonfigura l-istanza ta' LaunchPad tiegħek bil-ħżin, l-ordni tal-gruppi, id-data demo, ir-rwoli tal-amministratur, u s-settings tal-footer.",
- "Storage backend" : "Backend tal-ħżin",
- "Choose how LaunchPad stores dashboard content." : "Agħżel kif LaunchPad jaħżen il-kontenut tad-dashboard.",
- "Database (default)" : "Database (prestabbilit)",
- "Store dashboard content in the LaunchPad database table." : "Aħżen il-kontenut tad-dashboard fit-tabella tad-database ta' LaunchPad.",
- "GroupFolder (recommended for org use)" : "GroupFolder (rakkomandat għall-użu mill-organizzazzjoni)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access." : "Aħżen il-kontenut tad-dashboard fil-GroupFolders ta' Nextcloud għal aċċess kollaborattiv.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option." : "L-app GroupFolder mhix installata. Installa 'Nextcloud GroupFolders' biex tuża din l-għażla.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace." : "Agħżel l-ordni li bih jidhru l-gruppi ta' Nextcloud meta LaunchPad jiggwida lill-utenti lejn spazju tax-xogħol.",
"Demo data" : "Data demo",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only." : "Il-wirjiet ta' data demo jidhru hawn ladarba l-kapaċità demo-data-showcases tiġi mwassla. Għalissa dan il-pass huwa informattiv biss.",
@@ -952,7 +944,6 @@ OC.L10N.register(
"Finish" : "Temm",
"Run setup wizard" : "Ħaddem il-wizard tas-setup",
"Run setup wizard again" : "Ħaddem il-wizard tas-setup mill-ġdid",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles." : "Ibda l-intranet tiegħek: agħżel il-ħżin, ikkonfigura l-gruppi, installa d-data demo, u ssettja r-rwoli tal-amministratur.",
"Container" : "Kontenitur",
"Padding" : "Imbuttatura",
"Title (optional)" : "Titlu (fakultattiv)",
@@ -1363,7 +1354,9 @@ OC.L10N.register(
"Personal settings" : "Personal settings",
"Store" : "Store",
"Reports" : "Reports",
- "Flows" : "Flows"
+ "Flows" : "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings." : "Ikkonfigura l-istanza ta' LaunchPad tiegħek bl-ordni tal-gruppi, id-data demo, ir-rwoli tal-amministratur, u s-settings tal-footer.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles." : "Ibda l-intranet tiegħek: ikkonfigura l-gruppi, installa d-data demo, u ssettja r-rwoli tal-amministratur."
},
"nplurals=4; plural=(n==1 ? 0 : n==0 || (n%100>1 && n%100<11) ? 1 : (n%100>10 && n%100<20) ? 2 : 3);"
);
diff --git a/l10n/mt.json b/l10n/mt.json
index 41f497916..dcf1477f8 100644
--- a/l10n/mt.json
+++ b/l10n/mt.json
@@ -929,14 +929,6 @@
"LaunchPad setup wizard": "Wizard tal-konfigurazzjoni ta' LaunchPad",
"Step {n} / {total}": "Pass {n} / {total}",
"Welcome": "Merħba",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings.": "Ikkonfigura l-istanza ta' LaunchPad tiegħek bil-ħażna, l-ordni tal-gruppi, id-data demo, ir-rwoli tal-amministratur, u s-settings tal-footer.",
- "Storage backend": "Backend tal-ħażna",
- "Choose how LaunchPad stores dashboard content.": "Agħżel kif LaunchPad jaħżen il-kontenut tad-dashboard.",
- "Database (default)": "Database (prestabilit)",
- "Store dashboard content in the LaunchPad database table.": "Aħżen il-kontenut tad-dashboard fit-tabella tad-database ta' LaunchPad.",
- "GroupFolder (recommended for org use)": "GroupFolder (rakkomandat għal użu organizzattiv)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access.": "Aħżen il-kontenut tad-dashboard fil-GroupFolders ta' Nextcloud għal aċċess kollaborattiv.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option.": "L-app GroupFolder mhix installata. Installa „Nextcloud GroupFolders“ biex tuża din l-għażla.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace.": "Agħżel l-ordni li bih jidhru l-gruppi ta' Nextcloud meta LaunchPad jindirizza l-utenti lejn spazju tax-xogħol.",
"Demo data": "Data demo",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only.": "Il-wirjiet tad-data demo jidhru hawn ladarba tinħareġ il-kapaċità demo-data-showcases. Għalissa dan il-pass huwa informattiv biss.",
@@ -955,7 +947,6 @@
"Finish": "Spiċċa",
"Run setup wizard": "Mexxi l-wizard tal-konfigurazzjoni",
"Run setup wizard again": "Mexxi l-wizard tal-konfigurazzjoni mill-ġdid",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles.": "Ibda l-intranet tiegħek: agħżel il-ħażna, ikkonfigura l-gruppi, installa d-data demo, u ssettja r-rwoli tal-amministratur.",
"Container": "Kontenitur",
"Padding": "Padding",
"Title (optional)": "Titlu (mhux obbligatorju)",
@@ -1332,6 +1323,8 @@
"Personal settings": "Personal settings",
"Store": "Store",
"Reports": "Reports",
- "Flows": "Flows"
+ "Flows": "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings.": "Ikkonfigura l-istanza ta' LaunchPad tiegħek bl-ordni tal-gruppi, id-data demo, ir-rwoli tal-amministratur, u s-settings tal-footer.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles.": "Ibda l-intranet tiegħek: ikkonfigura l-gruppi, installa d-data demo, u ssettja r-rwoli tal-amministratur."
}
}
diff --git a/l10n/nb.js b/l10n/nb.js
index 232711a65..4b728d3b3 100644
--- a/l10n/nb.js
+++ b/l10n/nb.js
@@ -926,14 +926,6 @@ OC.L10N.register(
"LaunchPad setup wizard" : "LaunchPad-oppsettsveiviser",
"Step {n} / {total}" : "Trinn {n} / {total}",
"Welcome" : "Velkommen",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings." : "Konfigurer LaunchPad-instansen din med lagring, grupperekkefølge, demodata, administratorroller og bunntekstinnstillinger.",
- "Storage backend" : "Lagringstjeneste",
- "Choose how LaunchPad stores dashboard content." : "Velg hvordan LaunchPad lagrer kontrollpanelinnhold.",
- "Database (default)" : "Database (standard)",
- "Store dashboard content in the LaunchPad database table." : "Lagre kontrollpanelinnhold i LaunchPad-databasetabellen.",
- "GroupFolder (recommended for org use)" : "GroupFolder (anbefalt for organisasjonsbruk)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access." : "Lagre kontrollpanelinnhold i Nextcloud GroupFolders for samarbeidstilgang.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option." : "GroupFolder-appen er ikke installert. Installer «Nextcloud GroupFolders» for å bruke dette alternativet.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace." : "Velg rekkefølgen Nextcloud-grupper vises i når LaunchPad ruter brukere til et arbeidsområde.",
"Demo data" : "Demodata",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only." : "Demodata-utstillingene vil vises her når demo-data-showcases-funksjonen leveres. For nå er dette trinnet kun informativt.",
@@ -952,7 +944,6 @@ OC.L10N.register(
"Finish" : "Fullfør",
"Run setup wizard" : "Kjør oppsettsveiviser",
"Run setup wizard again" : "Kjør oppsettsveiviseren på nytt",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles." : "Kom i gang med intranettet ditt: velg lagring, konfigurer grupper, installer demodata og sett opp administratorroller.",
"Container" : "Beholder",
"Padding" : "Innvendig marg",
"Title (optional)" : "Tittel (valgfri)",
@@ -1363,7 +1354,9 @@ OC.L10N.register(
"Personal settings" : "Personal settings",
"Store" : "Store",
"Reports" : "Reports",
- "Flows" : "Flows"
+ "Flows" : "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings." : "Konfigurer LaunchPad-instansen din med grupperekkefølge, demodata, administratorroller og bunntekstinnstillinger.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles." : "Kom i gang med intranettet ditt: konfigurer grupper, installer demodata og sett opp administratorroller."
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/nb.json b/l10n/nb.json
index c15ea424b..8d1cb6905 100644
--- a/l10n/nb.json
+++ b/l10n/nb.json
@@ -927,14 +927,6 @@
"LaunchPad setup wizard": "LaunchPad-oppsettsveiviser",
"Step {n} / {total}": "Trinn {n} / {total}",
"Welcome": "Velkommen",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings.": "Konfigurer LaunchPad-instansen din med lagring, grupperekkefølge, demodata, administratorroller og bunntekstinnstillinger.",
- "Storage backend": "Lagringsbackend",
- "Choose how LaunchPad stores dashboard content.": "Velg hvordan LaunchPad lagrer dashbordinnhold.",
- "Database (default)": "Database (standard)",
- "Store dashboard content in the LaunchPad database table.": "Lagre dashbordinnhold i LaunchPad-databasetabellen.",
- "GroupFolder (recommended for org use)": "GroupFolder (anbefalt for organisasjonsbruk)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access.": "Lagre dashbordinnhold i Nextcloud GroupFolders for samarbeidstilgang.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option.": "GroupFolder-appen er ikke installert. Installer «Nextcloud GroupFolders» for å bruke dette alternativet.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace.": "Velg rekkefølgen Nextcloud-grupper vises i når LaunchPad ruter brukere til et arbeidsområde.",
"Demo data": "Demodata",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only.": "Demodata-utstillingene vises her når demo-data-showcases-funksjonen lanseres. For nå er dette trinnet kun informativt.",
@@ -953,7 +945,6 @@
"Finish": "Fullfør",
"Run setup wizard": "Kjør oppsettsveiviser",
"Run setup wizard again": "Kjør oppsettsveiviser på nytt",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles.": "Kom i gang med intranettet ditt: velg lagring, konfigurer grupper, installer demodata og sett opp administratorroller.",
"Container": "Beholder",
"Padding": "Innvendig marg",
"Title (optional)": "Tittel (valgfritt)",
@@ -1330,6 +1321,8 @@
"Personal settings": "Personal settings",
"Store": "Store",
"Reports": "Reports",
- "Flows": "Flows"
+ "Flows": "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings.": "Konfigurer LaunchPad-instansen din med grupperekkefølge, demodata, administratorroller og bunntekstinnstillinger.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles.": "Kom i gang med intranettet ditt: konfigurer grupper, installer demodata og sett opp administratorroller."
}
}
diff --git a/l10n/nl.js b/l10n/nl.js
index d7a189596..bb0e8db5b 100644
--- a/l10n/nl.js
+++ b/l10n/nl.js
@@ -860,14 +860,6 @@ OC.L10N.register(
"LaunchPad setup wizard" : "LaunchPad installatiewizard",
"Step {n} / {total}" : "Stap {n} / {total}",
"Welcome" : "Welkom",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings." : "Configureer je LaunchPad met opslag, groepvolgorde, demo-data, beheerdersrollen en voettekstinstellingen.",
- "Storage backend" : "Opslag-backend",
- "Choose how LaunchPad stores dashboard content." : "Kies hoe LaunchPad dashboardinhoud opslaat.",
- "Database (default)" : "Database (standaard)",
- "Store dashboard content in the LaunchPad database table." : "Sla dashboardinhoud op in de LaunchPad-databasetabel.",
- "GroupFolder (recommended for org use)" : "GroupFolder (aanbevolen voor organisaties)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access." : "Sla dashboardinhoud op in Nextcloud GroupFolders voor gedeelde toegang.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option." : "De GroupFolder-app is niet geïnstalleerd. Installeer 'Nextcloud GroupFolders' om deze optie te gebruiken.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace." : "Kies de volgorde waarin Nextcloud-groepen verschijnen wanneer LaunchPad gebruikers naar een werkruimte stuurt.",
"Demo data" : "Demo-data",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only." : "De demo-showcases verschijnen hier zodra de demo-data-showcases-capability beschikbaar is. Deze stap is voorlopig alleen ter informatie.",
@@ -886,7 +878,6 @@ OC.L10N.register(
"Finish" : "Voltooien",
"Run setup wizard" : "Installatiewizard starten",
"Run setup wizard again" : "Installatiewizard opnieuw starten",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles." : "Start je intranet: kies opslag, configureer groepen, installeer demo-data en stel beheerdersrollen in.",
"Container" : "Container",
"Padding" : "Opvulling",
"Title (optional)" : "Titel (optioneel)",
@@ -1411,7 +1402,9 @@ OC.L10N.register(
"Personal settings" : "Persoonlijke instellingen",
"Store" : "Store",
"Reports" : "Rapporten",
- "Flows" : "Flows"
+ "Flows" : "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings." : "Configureer je LaunchPad met groepvolgorde, demo-data, beheerdersrollen en voettekstinstellingen.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles." : "Start je intranet: configureer groepen, installeer demo-data en stel beheerdersrollen in."
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/nl.json b/l10n/nl.json
index 80cbd6ff0..668f855a9 100644
--- a/l10n/nl.json
+++ b/l10n/nl.json
@@ -866,14 +866,6 @@
"LaunchPad setup wizard": "LaunchPad installatiewizard",
"Step {n} / {total}": "Stap {n} / {total}",
"Welcome": "Welkom",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings.": "Configureer je LaunchPad met opslag, groepvolgorde, demo-data, beheerdersrollen en voettekstinstellingen.",
- "Storage backend": "Opslag-backend",
- "Choose how LaunchPad stores dashboard content.": "Kies hoe LaunchPad dashboardinhoud opslaat.",
- "Database (default)": "Database (standaard)",
- "Store dashboard content in the LaunchPad database table.": "Sla dashboardinhoud op in de LaunchPad-databasetabel.",
- "GroupFolder (recommended for org use)": "GroupFolder (aanbevolen voor organisaties)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access.": "Sla dashboardinhoud op in Nextcloud GroupFolders voor gedeelde toegang.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option.": "De GroupFolder-app is niet geïnstalleerd. Installeer 'Nextcloud GroupFolders' om deze optie te gebruiken.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace.": "Kies de volgorde waarin Nextcloud-groepen verschijnen wanneer LaunchPad gebruikers naar een werkruimte stuurt.",
"Demo data": "Demo-data",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only.": "De demo-showcases verschijnen hier zodra de demo-data-showcases-capability beschikbaar is. Deze stap is voorlopig alleen ter informatie.",
@@ -892,7 +884,6 @@
"Finish": "Voltooien",
"Run setup wizard": "Installatiewizard starten",
"Run setup wizard again": "Installatiewizard opnieuw starten",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles.": "Start je intranet: kies opslag, configureer groepen, installeer demo-data en stel beheerdersrollen in.",
"Container": "Container",
"Padding": "Opvulling",
"Title (optional)": "Titel (optioneel)",
@@ -1388,6 +1379,8 @@
"Personal settings": "Persoonlijke instellingen",
"Store": "Store",
"Reports": "Rapporten",
- "Flows": "Flows"
+ "Flows": "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings.": "Configureer je LaunchPad met groepvolgorde, demo-data, beheerdersrollen en voettekstinstellingen.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles.": "Start je intranet: configureer groepen, installeer demo-data en stel beheerdersrollen in."
}
}
diff --git a/l10n/pl.js b/l10n/pl.js
index 9a8705cf1..4eb51dde3 100644
--- a/l10n/pl.js
+++ b/l10n/pl.js
@@ -926,14 +926,6 @@ OC.L10N.register(
"LaunchPad setup wizard" : "Kreator konfiguracji LaunchPad",
"Step {n} / {total}" : "Krok {n} / {total}",
"Welcome" : "Witamy",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings." : "Skonfiguruj swoją instancję LaunchPad z ustawieniami magazynu, kolejności grup, danych demonstracyjnych, ról administratorów i stopki.",
- "Storage backend" : "Zaplecze magazynu",
- "Choose how LaunchPad stores dashboard content." : "Wybierz, w jaki sposób LaunchPad przechowuje treść pulpitu.",
- "Database (default)" : "Baza danych (domyślnie)",
- "Store dashboard content in the LaunchPad database table." : "Przechowuj treść pulpitu w tabeli bazy danych LaunchPad.",
- "GroupFolder (recommended for org use)" : "GroupFolder (zalecane do użytku organizacyjnego)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access." : "Przechowuj treść pulpitu w folderach GroupFolders Nextcloud w celu wspólnego dostępu.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option." : "Aplikacja GroupFolder nie jest zainstalowana. Zainstaluj „Nextcloud GroupFolders”, aby skorzystać z tej opcji.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace." : "Wybierz kolejność, w jakiej grupy Nextcloud pojawiają się, gdy LaunchPad kieruje użytkowników do obszaru roboczego.",
"Demo data" : "Dane demonstracyjne",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only." : "Prezentacje danych demonstracyjnych pojawią się tutaj, gdy zostanie wdrożona funkcja demo-data-showcases. Na razie ten krok ma charakter wyłącznie informacyjny.",
@@ -952,7 +944,6 @@ OC.L10N.register(
"Finish" : "Zakończ",
"Run setup wizard" : "Uruchom kreatora konfiguracji",
"Run setup wizard again" : "Uruchom kreatora konfiguracji ponownie",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles." : "Rozpocznij konfigurację swojego intranetu: wybierz magazyn, skonfiguruj grupy, zainstaluj dane demonstracyjne i skonfiguruj role administratorów.",
"Container" : "Kontener",
"Padding" : "Wypełnienie",
"Title (optional)" : "Tytuł (opcjonalny)",
@@ -1363,7 +1354,9 @@ OC.L10N.register(
"Personal settings" : "Personal settings",
"Store" : "Store",
"Reports" : "Reports",
- "Flows" : "Flows"
+ "Flows" : "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings." : "Skonfiguruj swoją instancję LaunchPad, ustawiając kolejność grup, dane demonstracyjne, role administratora i ustawienia stopki.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles." : "Rozpocznij pracę z intranetem: skonfiguruj grupy, zainstaluj dane demonstracyjne i skonfiguruj role administratora."
},
"nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"
);
diff --git a/l10n/pl.json b/l10n/pl.json
index fe9f9f1da..c74afdc5c 100644
--- a/l10n/pl.json
+++ b/l10n/pl.json
@@ -928,14 +928,6 @@
"LaunchPad setup wizard": "Kreator konfiguracji LaunchPad",
"Step {n} / {total}": "Krok {n} / {total}",
"Welcome": "Witamy",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings.": "Skonfiguruj swoją instancję LaunchPad, ustawiając magazyn, kolejność grup, dane demonstracyjne, role administratora i ustawienia stopki.",
- "Storage backend": "Zaplecze magazynu",
- "Choose how LaunchPad stores dashboard content.": "Wybierz, jak LaunchPad przechowuje zawartość pulpitów nawigacyjnych.",
- "Database (default)": "Baza danych (domyślnie)",
- "Store dashboard content in the LaunchPad database table.": "Przechowuj zawartość pulpitu nawigacyjnego w tabeli bazy danych LaunchPad.",
- "GroupFolder (recommended for org use)": "GroupFolder (zalecane do użytku organizacyjnego)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access.": "Przechowuj zawartość pulpitu nawigacyjnego w GroupFolder Nextcloud, aby zapewnić wspólny dostęp.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option.": "Aplikacja GroupFolder nie jest zainstalowana. Zainstaluj „Nextcloud GroupFolders”, aby skorzystać z tej opcji.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace.": "Wybierz kolejność, w jakiej pojawiają się grupy Nextcloud, gdy LaunchPad kieruje użytkowników do obszaru roboczego.",
"Demo data": "Dane demonstracyjne",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only.": "Prezentacje danych demonstracyjnych pojawią się tutaj, gdy zostanie dostarczona funkcja demo-data-showcases. Na razie ten krok ma charakter wyłącznie informacyjny.",
@@ -954,7 +946,6 @@
"Finish": "Zakończ",
"Run setup wizard": "Uruchom kreator konfiguracji",
"Run setup wizard again": "Uruchom kreator konfiguracji ponownie",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles.": "Rozpocznij pracę z intranetem: wybierz magazyn, skonfiguruj grupy, zainstaluj dane demonstracyjne i skonfiguruj role administratora.",
"Container": "Kontener",
"Padding": "Dopełnienie",
"Title (optional)": "Tytuł (opcjonalny)",
@@ -1331,6 +1322,8 @@
"Personal settings": "Personal settings",
"Store": "Store",
"Reports": "Reports",
- "Flows": "Flows"
+ "Flows": "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings.": "Skonfiguruj swoją instancję LaunchPad, ustawiając kolejność grup, dane demonstracyjne, role administratora i ustawienia stopki.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles.": "Rozpocznij pracę z intranetem: skonfiguruj grupy, zainstaluj dane demonstracyjne i skonfiguruj role administratora."
}
}
diff --git a/l10n/pt.js b/l10n/pt.js
index 274bf36e1..342b620be 100644
--- a/l10n/pt.js
+++ b/l10n/pt.js
@@ -926,14 +926,6 @@ OC.L10N.register(
"LaunchPad setup wizard" : "Assistente de configuração do LaunchPad",
"Step {n} / {total}" : "Passo {n} / {total}",
"Welcome" : "Bem-vindo",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings." : "Configure a sua instância do LaunchPad com armazenamento, ordenação de grupos, dados de demonstração, funções de administrador e definições de rodapé.",
- "Storage backend" : "Servidor de armazenamento",
- "Choose how LaunchPad stores dashboard content." : "Escolha como o LaunchPad armazena o conteúdo dos painéis.",
- "Database (default)" : "Base de dados (predefinição)",
- "Store dashboard content in the LaunchPad database table." : "Armazenar o conteúdo dos painéis na tabela da base de dados do LaunchPad.",
- "GroupFolder (recommended for org use)" : "GroupFolder (recomendado para uso organizacional)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access." : "Armazenar o conteúdo dos painéis em GroupFolders do Nextcloud para acesso colaborativo.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option." : "A aplicação GroupFolder não está instalada. Instale 'Nextcloud GroupFolders' para utilizar esta opção.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace." : "Escolha a ordem em que os grupos do Nextcloud aparecem quando o LaunchPad encaminha os utilizadores para um espaço de trabalho.",
"Demo data" : "Dados de demonstração",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only." : "As mostras de dados de demonstração aparecerão aqui assim que a capacidade demo-data-showcases for lançada. Por agora, este passo é apenas informativo.",
@@ -952,7 +944,6 @@ OC.L10N.register(
"Finish" : "Concluir",
"Run setup wizard" : "Executar assistente de configuração",
"Run setup wizard again" : "Executar novamente o assistente de configuração",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles." : "Comece a sua intranet: escolha o armazenamento, configure grupos, instale dados de demonstração e configure funções de administrador.",
"Container" : "Contentor",
"Padding" : "Espaçamento interno",
"Title (optional)" : "Título (opcional)",
@@ -1363,7 +1354,9 @@ OC.L10N.register(
"Personal settings" : "Personal settings",
"Store" : "Store",
"Reports" : "Reports",
- "Flows" : "Flows"
+ "Flows" : "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings." : "Configure a sua instância do LaunchPad com ordenação de grupos, dados de demonstração, funções de administrador e definições de rodapé.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles." : "Coloque a sua intranet a funcionar: configure grupos, instale dados de demonstração e configure as funções de administrador."
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/pt.json b/l10n/pt.json
index 8c1f0535d..3692e0563 100644
--- a/l10n/pt.json
+++ b/l10n/pt.json
@@ -927,14 +927,6 @@
"LaunchPad setup wizard": "Assistente de configuração do LaunchPad",
"Step {n} / {total}": "Passo {n} / {total}",
"Welcome": "Bem-vindo",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings.": "Configure a sua instância do LaunchPad com armazenamento, ordenação de grupos, dados de demonstração, funções de administrador e definições de rodapé.",
- "Storage backend": "Serviço de armazenamento",
- "Choose how LaunchPad stores dashboard content.": "Escolha como o LaunchPad armazena o conteúdo dos painéis.",
- "Database (default)": "Base de dados (predefinição)",
- "Store dashboard content in the LaunchPad database table.": "Armazenar o conteúdo dos painéis na tabela da base de dados do LaunchPad.",
- "GroupFolder (recommended for org use)": "GroupFolder (recomendado para uso organizacional)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access.": "Armazenar o conteúdo dos painéis em GroupFolders do Nextcloud para acesso colaborativo.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option.": "A aplicação GroupFolder não está instalada. Instale 'Nextcloud GroupFolders' para utilizar esta opção.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace.": "Escolha a ordem pela qual os grupos do Nextcloud aparecem quando o LaunchPad encaminha os utilizadores para um espaço de trabalho.",
"Demo data": "Dados de demonstração",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only.": "As demonstrações de dados de exemplo aparecerão aqui assim que a funcionalidade demo-data-showcases for disponibilizada. Por agora, este passo é apenas informativo.",
@@ -953,7 +945,6 @@
"Finish": "Concluir",
"Run setup wizard": "Executar assistente de configuração",
"Run setup wizard again": "Executar novamente o assistente de configuração",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles.": "Coloque a sua intranet a funcionar: escolha o armazenamento, configure grupos, instale dados de demonstração e configure as funções de administrador.",
"Container": "Contentor",
"Padding": "Espaçamento interno",
"Title (optional)": "Título (opcional)",
@@ -1330,6 +1321,8 @@
"Personal settings": "Personal settings",
"Store": "Store",
"Reports": "Reports",
- "Flows": "Flows"
+ "Flows": "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings.": "Configure a sua instância do LaunchPad com ordenação de grupos, dados de demonstração, funções de administrador e definições de rodapé.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles.": "Coloque a sua intranet a funcionar: configure grupos, instale dados de demonstração e configure as funções de administrador."
}
}
diff --git a/l10n/rm.js b/l10n/rm.js
index da9498be9..ca0635814 100644
--- a/l10n/rm.js
+++ b/l10n/rm.js
@@ -926,14 +926,6 @@ OC.L10N.register(
"LaunchPad setup wizard" : "Assistent da configuraziun da LaunchPad",
"Step {n} / {total}" : "Pass {n} / {total}",
"Welcome" : "Bainvegni",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings." : "Configurai Sia instanza da LaunchPad cun memorisaziun, urden da las gruppas, datas demo, rollas d'administratur e parameters dal pe da pagina.",
- "Storage backend" : "Backend da memorisaziun",
- "Choose how LaunchPad stores dashboard content." : "Tscherni co che LaunchPad memorisescha il cuntegn da las tablas da cumond.",
- "Database (default)" : "Banca da datas (standard)",
- "Store dashboard content in the LaunchPad database table." : "Memorisar il cuntegn da las tablas da cumond en la tabella da la banca da datas da LaunchPad.",
- "GroupFolder (recommended for org use)" : "GroupFolder (recumandà per l'utilisaziun en organisaziuns)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access." : "Memorisar il cuntegn da las tablas da cumond en ils GroupFolders da Nextcloud per access collaborativ.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option." : "L'applicaziun GroupFolder n'è betg installada. Installai 'Nextcloud GroupFolders' per utilisar questa opziun.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace." : "Tscherni l'urden en il qual las gruppas da Nextcloud cumparan cura che LaunchPad maina ils utilisaders ad in spazi da lavur.",
"Demo data" : "Datas demo",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only." : "Las demonstraziuns da datas demo cumparan qua uschespert ch'il capацitad demo-data-showcases vegn furnida. Per il mument è quest pass mo informativ.",
@@ -952,7 +944,6 @@ OC.L10N.register(
"Finish" : "Finir",
"Run setup wizard" : "Exequir l'assistent da configuraziun",
"Run setup wizard again" : "Exequir l'assistent da configuraziun anc ina giada",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles." : "Mettai en funcziun Sia intranet: tscherni la memorisaziun, configurai las gruppas, installai datas demo, e definì las rollas d'administratur.",
"Container" : "Container",
"Padding" : "Distanza interna",
"Title (optional)" : "Titel (facultativ)",
@@ -1363,7 +1354,9 @@ OC.L10N.register(
"Personal settings" : "Personal settings",
"Store" : "Store",
"Reports" : "Reports",
- "Flows" : "Flows"
+ "Flows" : "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings." : "Configurai vossa instanza da LaunchPad cun urden da las gruppas, datas demo, rols dad administratur e parameters dal pe da pagina.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles." : "Cumenzai voss intranet: configurai las gruppas, installai datas demo, e configurai ils rols dad administratur."
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/rm.json b/l10n/rm.json
index ce4b9a7dc..34f8e5f41 100644
--- a/l10n/rm.json
+++ b/l10n/rm.json
@@ -927,14 +927,6 @@
"LaunchPad setup wizard": "Assistent d'installaziun da LaunchPad",
"Step {n} / {total}": "Pass {n} / {total}",
"Welcome": "Bainvegni",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings.": "Configurai vossa instanza da LaunchPad cun memorisaziun, urden da las gruppas, datas demo, rols dad administratur e parameters dal pe da pagina.",
- "Storage backend": "Backend da memorisaziun",
- "Choose how LaunchPad stores dashboard content.": "Tscherni co che LaunchPad memorisescha il cuntegn dal tabladi.",
- "Database (default)": "Banca da datas (predefinì)",
- "Store dashboard content in the LaunchPad database table.": "Memorisar il cuntegn dal tabladi en la tabella da la banca da datas da LaunchPad.",
- "GroupFolder (recommended for org use)": "GroupFolder (recumandà per l'utilisaziun organisatorica)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access.": "Memorisar il cuntegn dal tabladi en ils GroupFolders da Nextcloud per in access collaborativ.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option.": "L'applicaziun GroupFolder n'è betg installada. Installai 'Nextcloud GroupFolders' per utilisar questa opziun.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace.": "Tscherni l'urden en il qual las gruppas da Nextcloud cumparan cura che LaunchPad mussa als utilisaders ina zona da lavur.",
"Demo data": "Datas demo",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only.": "Las demonstraziuns da datas demo cumparan qua uschespert che la funcziunalitad demo-data-showcases è disponibla. Per il mument è quest pass mo informativ.",
@@ -953,7 +945,6 @@
"Finish": "Finir",
"Run setup wizard": "Exequir l'assistent d'installaziun",
"Run setup wizard again": "Exequir danovamain l'assistent d'installaziun",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles.": "Cumenzai voss intranet: tscherni la memorisaziun, configurai las gruppas, installai datas demo, e configurai ils rols dad administratur.",
"Container": "Container",
"Padding": "Spazi intern",
"Title (optional)": "Titel (facultativ)",
@@ -1330,6 +1321,8 @@
"Personal settings": "Personal settings",
"Store": "Store",
"Reports": "Reports",
- "Flows": "Flows"
+ "Flows": "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings.": "Configurai vossa instanza da LaunchPad cun urden da las gruppas, datas demo, rols dad administratur e parameters dal pe da pagina.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles.": "Cumenzai voss intranet: configurai las gruppas, installai datas demo, e configurai ils rols dad administratur."
}
}
diff --git a/l10n/ro.js b/l10n/ro.js
index 537e0dd8a..030aa81ba 100644
--- a/l10n/ro.js
+++ b/l10n/ro.js
@@ -926,14 +926,6 @@ OC.L10N.register(
"LaunchPad setup wizard" : "Asistent de configurare LaunchPad",
"Step {n} / {total}" : "Pasul {n} / {total}",
"Welcome" : "Bine ați venit",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings." : "Configurați instanța dumneavoastră LaunchPad cu stocare, ordonarea grupurilor, date demonstrative, roluri de administrator și setări de subsol.",
- "Storage backend" : "Backend de stocare",
- "Choose how LaunchPad stores dashboard content." : "Alegeți modul în care LaunchPad stochează conținutul tablourilor de bord.",
- "Database (default)" : "Bază de date (implicit)",
- "Store dashboard content in the LaunchPad database table." : "Stocați conținutul tabloului de bord în tabelul de bază de date LaunchPad.",
- "GroupFolder (recommended for org use)" : "GroupFolder (recomandat pentru uz organizațional)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access." : "Stocați conținutul tabloului de bord în GroupFolders Nextcloud pentru acces colaborativ.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option." : "Aplicația GroupFolder nu este instalată. Instalați „Nextcloud GroupFolders” pentru a utiliza această opțiune.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace." : "Alegeți ordinea în care apar grupurile Nextcloud atunci când LaunchPad direcționează utilizatorii către un spațiu de lucru.",
"Demo data" : "Date demonstrative",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only." : "Prezentările cu date demonstrative vor apărea aici odată ce capacitatea demo-data-showcases este livrată. Deocamdată, acest pas este doar informativ.",
@@ -952,7 +944,6 @@ OC.L10N.register(
"Finish" : "Finalizare",
"Run setup wizard" : "Rulați asistentul de configurare",
"Run setup wizard again" : "Rulați din nou asistentul de configurare",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles." : "Porniți intranetul dumneavoastră: alegeți stocarea, configurați grupurile, instalați date demonstrative și configurați rolurile de administrator.",
"Container" : "Container",
"Padding" : "Spațiere interioară",
"Title (optional)" : "Titlu (opțional)",
@@ -1363,7 +1354,9 @@ OC.L10N.register(
"Personal settings" : "Personal settings",
"Store" : "Store",
"Reports" : "Reports",
- "Flows" : "Flows"
+ "Flows" : "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings." : "Configurați instanța dvs. LaunchPad cu ordonarea grupurilor, date demonstrative, roluri de administrator și setări de subsol.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles." : "Începeți cu intranetul dvs.: configurați grupurile, instalați date demonstrative și configurați rolurile de administrator."
},
"nplurals=3; plural=(n==1 ? 0 : (n==0 || (n%100>0 && n%100<20)) ? 1 : 2);"
);
diff --git a/l10n/ro.json b/l10n/ro.json
index 8864aaf78..6e91e3bb6 100644
--- a/l10n/ro.json
+++ b/l10n/ro.json
@@ -928,14 +928,6 @@
"LaunchPad setup wizard": "Asistent de configurare LaunchPad",
"Step {n} / {total}": "Pasul {n} / {total}",
"Welcome": "Bun venit",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings.": "Configurați instanța dvs. LaunchPad cu stocare, ordonarea grupurilor, date demonstrative, roluri de administrator și setări de subsol.",
- "Storage backend": "Sistem de stocare",
- "Choose how LaunchPad stores dashboard content.": "Alegeți modul în care LaunchPad stochează conținutul tabloului de bord.",
- "Database (default)": "Bază de date (implicit)",
- "Store dashboard content in the LaunchPad database table.": "Stochează conținutul tabloului de bord în tabelul bazei de date LaunchPad.",
- "GroupFolder (recommended for org use)": "GroupFolder (recomandat pentru utilizare organizațională)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access.": "Stochează conținutul tabloului de bord în GroupFolders Nextcloud pentru acces colaborativ.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option.": "Aplicația GroupFolder nu este instalată. Instalați „Nextcloud GroupFolders” pentru a utiliza această opțiune.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace.": "Alegeți ordinea în care apar grupurile Nextcloud când LaunchPad direcționează utilizatorii către un spațiu de lucru.",
"Demo data": "Date demonstrative",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only.": "Vitrinele de date demonstrative vor apărea aici odată ce funcționalitatea demo-data-showcases este lansată. Deocamdată, acest pas este doar informativ.",
@@ -954,7 +946,6 @@
"Finish": "Finalizează",
"Run setup wizard": "Rulează asistentul de configurare",
"Run setup wizard again": "Rulează din nou asistentul de configurare",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles.": "Începeți cu intranetul dvs.: alegeți stocarea, configurați grupurile, instalați date demonstrative și configurați rolurile de administrator.",
"Container": "Container",
"Padding": "Spațiere internă",
"Title (optional)": "Titlu (opțional)",
@@ -1331,6 +1322,8 @@
"Personal settings": "Personal settings",
"Store": "Store",
"Reports": "Reports",
- "Flows": "Flows"
+ "Flows": "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings.": "Configurați instanța dvs. LaunchPad cu ordonarea grupurilor, date demonstrative, roluri de administrator și setări de subsol.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles.": "Începeți cu intranetul dvs.: configurați grupurile, instalați date demonstrative și configurați rolurile de administrator."
}
}
diff --git a/l10n/ru.js b/l10n/ru.js
index fb8047a75..befa7605d 100644
--- a/l10n/ru.js
+++ b/l10n/ru.js
@@ -926,14 +926,6 @@ OC.L10N.register(
"LaunchPad setup wizard" : "Мастер настройки LaunchPad",
"Step {n} / {total}" : "Шаг {n} / {total}",
"Welcome" : "Добро пожаловать",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings." : "Настройте свой экземпляр LaunchPad с хранилищем, порядком групп, демонстрационными данными, ролями администратора и настройками нижнего колонтитула.",
- "Storage backend" : "Серверная часть хранилища",
- "Choose how LaunchPad stores dashboard content." : "Выберите, как LaunchPad хранит содержимое панелей управления.",
- "Database (default)" : "База данных (по умолчанию)",
- "Store dashboard content in the LaunchPad database table." : "Хранить содержимое панелей управления в таблице базы данных LaunchPad.",
- "GroupFolder (recommended for org use)" : "GroupFolder (рекомендуется для использования в организации)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access." : "Хранить содержимое панелей управления в GroupFolders Nextcloud для совместного доступа.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option." : "Приложение GroupFolder не установлено. Установите «Nextcloud GroupFolders», чтобы использовать эту опцию.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace." : "Выберите порядок, в котором появляются группы Nextcloud, когда LaunchPad направляет пользователей в рабочее пространство.",
"Demo data" : "Демонстрационные данные",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only." : "Демонстрации демонстрационных данных появятся здесь, как только будет выпущена возможность demo-data-showcases. Пока этот шаг носит исключительно информационный характер.",
@@ -952,7 +944,6 @@ OC.L10N.register(
"Finish" : "Готово",
"Run setup wizard" : "Запустить мастер настройки",
"Run setup wizard again" : "Запустить мастер настройки снова",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles." : "Запустите свой интранет: выберите хранилище, настройте группы, установите демонстрационные данные и настройте роли администратора.",
"Container" : "Контейнер",
"Padding" : "Отступ",
"Title (optional)" : "Заголовок (необязательно)",
@@ -1363,7 +1354,9 @@ OC.L10N.register(
"Personal settings" : "Personal settings",
"Store" : "Store",
"Reports" : "Reports",
- "Flows" : "Flows"
+ "Flows" : "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings." : "Настройте свой экземпляр LaunchPad с помощью порядка групп, демонстрационных данных, ролей администраторов и настроек нижнего колонтитула.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles." : "Запустите свой интранет: настройте группы, установите демонстрационные данные и настройте роли администраторов."
},
"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"
);
diff --git a/l10n/ru.json b/l10n/ru.json
index db33b6f40..c788cd4bc 100644
--- a/l10n/ru.json
+++ b/l10n/ru.json
@@ -928,14 +928,6 @@
"LaunchPad setup wizard": "Мастер настройки LaunchPad",
"Step {n} / {total}": "Шаг {n} / {total}",
"Welcome": "Добро пожаловать",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings.": "Настройте свой экземпляр LaunchPad с помощью параметров хранилища, порядка групп, демонстрационных данных, ролей администраторов и настроек нижнего колонтитула.",
- "Storage backend": "Бэкенд хранилища",
- "Choose how LaunchPad stores dashboard content.": "Выберите, как LaunchPad хранит содержимое дашбордов.",
- "Database (default)": "База данных (по умолчанию)",
- "Store dashboard content in the LaunchPad database table.": "Хранить содержимое дашбордов в таблице базы данных LaunchPad.",
- "GroupFolder (recommended for org use)": "GroupFolder (рекомендуется для использования в организации)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access.": "Хранить содержимое дашбордов в Nextcloud GroupFolders для совместного доступа.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option.": "Приложение GroupFolder не установлено. Установите «Nextcloud GroupFolders», чтобы использовать эту опцию.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace.": "Выберите порядок, в котором отображаются группы Nextcloud, когда LaunchPad направляет пользователей в рабочее пространство.",
"Demo data": "Демонстрационные данные",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only.": "Демонстрационные примеры данных появятся здесь, как только будет выпущена возможность demo-data-showcases. Пока этот шаг носит только информационный характер.",
@@ -954,7 +946,6 @@
"Finish": "Завершить",
"Run setup wizard": "Запустить мастер настройки",
"Run setup wizard again": "Запустить мастер настройки снова",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles.": "Запустите свой интранет: выберите хранилище, настройте группы, установите демонстрационные данные и настройте роли администраторов.",
"Container": "Контейнер",
"Padding": "Отступ",
"Title (optional)": "Заголовок (необязательно)",
@@ -1331,6 +1322,8 @@
"Personal settings": "Personal settings",
"Store": "Store",
"Reports": "Reports",
- "Flows": "Flows"
+ "Flows": "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings.": "Настройте свой экземпляр LaunchPad с помощью порядка групп, демонстрационных данных, ролей администраторов и настроек нижнего колонтитула.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles.": "Запустите свой интранет: настройте группы, установите демонстрационные данные и настройте роли администраторов."
}
}
diff --git a/l10n/sk.js b/l10n/sk.js
index 662b55503..2429a34ef 100644
--- a/l10n/sk.js
+++ b/l10n/sk.js
@@ -926,14 +926,6 @@ OC.L10N.register(
"LaunchPad setup wizard" : "Sprievodca nastavením LaunchPad",
"Step {n} / {total}" : "Krok {n} / {total}",
"Welcome" : "Vitajte",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings." : "Nakonfigurujte svoju inštanciu LaunchPad pomocou úložiska, poradia skupín, demo údajov, administrátorskych rol a nastavení päty.",
- "Storage backend" : "Backend úložiska",
- "Choose how LaunchPad stores dashboard content." : "Vyberte, ako LaunchPad ukladá obsah násteniek.",
- "Database (default)" : "Databáza (predvolené)",
- "Store dashboard content in the LaunchPad database table." : "Ukladať obsah násteniek do databázovej tabuľky LaunchPad.",
- "GroupFolder (recommended for org use)" : "GroupFolder (odporúčané pre organizačné použitie)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access." : "Ukladať obsah násteniek do GroupFolders Nextcloud pre kolaboratívny prístup.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option." : "Aplikácia GroupFolder nie je nainštalovaná. Na použitie tejto možnosti nainštalujte „Nextcloud GroupFolders“.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace." : "Vyberte poradie, v ktorom sa skupiny Nextcloud zobrazujú, keď LaunchPad smeruje používateľov do pracovného priestoru.",
"Demo data" : "Demo údaje",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only." : "Ukážky demo údajov sa tu zobrazia po dodaní funkcie demo-data-showcases. Zatiaľ je tento krok iba informatívny.",
@@ -952,7 +944,6 @@ OC.L10N.register(
"Finish" : "Dokončiť",
"Run setup wizard" : "Spustiť sprievodcu nastavením",
"Run setup wizard again" : "Spustiť sprievodcu nastavením znova",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles." : "Začnite s vaším intranetom: vyberte úložisko, nakonfigurujte skupiny, nainštalujte demo údaje a nastavte administrátorské roly.",
"Container" : "Kontajner",
"Padding" : "Vnútorný okraj",
"Title (optional)" : "Názov (voliteľné)",
@@ -1363,7 +1354,9 @@ OC.L10N.register(
"Personal settings" : "Personal settings",
"Store" : "Store",
"Reports" : "Reports",
- "Flows" : "Flows"
+ "Flows" : "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings." : "Nakonfigurujte svoju inštanciu LaunchPad s usporiadaním skupín, demo údajmi, rolami administrátora a nastaveniami päty.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles." : "Spustite svoj intranet: nakonfigurujte skupiny, nainštalujte demo údaje a nastavte roly administrátora."
},
"nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;"
);
diff --git a/l10n/sk.json b/l10n/sk.json
index f7ae46bbb..e7fc1d922 100644
--- a/l10n/sk.json
+++ b/l10n/sk.json
@@ -928,14 +928,6 @@
"LaunchPad setup wizard": "Sprievodca nastavením LaunchPad",
"Step {n} / {total}": "Krok {n} / {total}",
"Welcome": "Vitajte",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings.": "Nakonfigurujte svoju inštanciu LaunchPad s úložiskom, usporiadaním skupín, demo údajmi, rolami administrátora a nastaveniami päty.",
- "Storage backend": "Backend úložiska",
- "Choose how LaunchPad stores dashboard content.": "Vyberte, ako LaunchPad ukladá obsah nástenky.",
- "Database (default)": "Databáza (predvolené)",
- "Store dashboard content in the LaunchPad database table.": "Ukladať obsah nástenky do databázovej tabuľky LaunchPad.",
- "GroupFolder (recommended for org use)": "GroupFolder (odporúčané pre organizačné použitie)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access.": "Ukladať obsah nástenky v GroupFolders Nextcloud pre kolaboratívny prístup.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option.": "Aplikácia GroupFolder nie je nainštalovaná. Na použitie tejto možnosti nainštalujte „Nextcloud GroupFolders“.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace.": "Vyberte poradie, v akom sa zobrazujú skupiny Nextcloud, keď LaunchPad smeruje používateľov do pracovného priestoru.",
"Demo data": "Demo údaje",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only.": "Ukážky demo údajov sa tu zobrazia po vydaní funkcie demo-data-showcases. Zatiaľ je tento krok iba informačný.",
@@ -954,7 +946,6 @@
"Finish": "Dokončiť",
"Run setup wizard": "Spustiť sprievodcu nastavením",
"Run setup wizard again": "Spustiť sprievodcu nastavením znova",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles.": "Spustite svoj intranet: vyberte úložisko, nakonfigurujte skupiny, nainštalujte demo údaje a nastavte roly administrátora.",
"Container": "Kontajner",
"Padding": "Vnútorné odsadenie",
"Title (optional)": "Názov (voliteľné)",
@@ -1331,6 +1322,8 @@
"Personal settings": "Personal settings",
"Store": "Store",
"Reports": "Reports",
- "Flows": "Flows"
+ "Flows": "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings.": "Nakonfigurujte svoju inštanciu LaunchPad s usporiadaním skupín, demo údajmi, rolami administrátora a nastaveniami päty.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles.": "Spustite svoj intranet: nakonfigurujte skupiny, nainštalujte demo údaje a nastavte roly administrátora."
}
}
diff --git a/l10n/sl.js b/l10n/sl.js
index 46c48e27a..9b3dbeb81 100644
--- a/l10n/sl.js
+++ b/l10n/sl.js
@@ -926,14 +926,6 @@ OC.L10N.register(
"LaunchPad setup wizard" : "Čarovnik za nastavitev LaunchPad",
"Step {n} / {total}" : "Korak {n} / {total}",
"Welcome" : "Dobrodošli",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings." : "Nastavite svoj primerek LaunchPad s shrambo, vrstnim redom skupin, demonstracijskimi podatki, skrbniškimi vlogami in nastavitvami noge.",
- "Storage backend" : "Zaledje shrambe",
- "Choose how LaunchPad stores dashboard content." : "Izberite, kako LaunchPad shranjuje vsebino nadzornih plošč.",
- "Database (default)" : "Podatkovna zbirka (privzeto)",
- "Store dashboard content in the LaunchPad database table." : "Shranite vsebino nadzorne plošče v tabelo podatkovne zbirke LaunchPad.",
- "GroupFolder (recommended for org use)" : "GroupFolder (priporočeno za organizacijsko uporabo)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access." : "Shranite vsebino nadzorne plošče v Nextcloud GroupFolders za sodelovalni dostop.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option." : "Aplikacija GroupFolder ni nameščena. Za uporabo te možnosti namestite »Nextcloud GroupFolders«.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace." : "Izberite vrstni red, v katerem se skupine Nextcloud prikažejo, ko LaunchPad usmerja uporabnike v delovni prostor.",
"Demo data" : "Demonstracijski podatki",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only." : "Predstavitve demonstracijskih podatkov se bodo prikazale tukaj, ko bo zmožnost demo-data-showcases na voljo. Za zdaj je ta korak zgolj informativen.",
@@ -952,7 +944,6 @@ OC.L10N.register(
"Finish" : "Dokončaj",
"Run setup wizard" : "Zaženi čarovnika za nastavitev",
"Run setup wizard again" : "Znova zaženi čarovnika za nastavitev",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles." : "Začnite z intranetom: izberite shrambo, nastavite skupine, namestite demonstracijske podatke in nastavite skrbniške vloge.",
"Container" : "Vsebnik",
"Padding" : "Notranji odmik",
"Title (optional)" : "Naslov (neobvezno)",
@@ -1363,7 +1354,9 @@ OC.L10N.register(
"Personal settings" : "Personal settings",
"Store" : "Store",
"Reports" : "Reports",
- "Flows" : "Flows"
+ "Flows" : "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings." : "Konfigurirajte svojo instanco LaunchPad z razvrščanjem skupin, demo podatki, skrbniškimi vlogami in nastavitvami noge.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles." : "Zaženite svoj intranet: konfigurirajte skupine, namestite demo podatke in nastavite skrbniške vloge."
},
"nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);"
);
diff --git a/l10n/sl.json b/l10n/sl.json
index a31dcaa79..b559c7105 100644
--- a/l10n/sl.json
+++ b/l10n/sl.json
@@ -929,14 +929,6 @@
"LaunchPad setup wizard": "Čarovnik za namestitev LaunchPad",
"Step {n} / {total}": "Korak {n} / {total}",
"Welcome": "Dobrodošli",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings.": "Konfigurirajte svojo instanco LaunchPad s shrambo, razvrščanjem skupin, demo podatki, skrbniškimi vlogami in nastavitvami noge.",
- "Storage backend": "Zaledje shrambe",
- "Choose how LaunchPad stores dashboard content.": "Izberite, kako LaunchPad shranjuje vsebino nadzornih plošč.",
- "Database (default)": "Zbirka podatkov (privzeto)",
- "Store dashboard content in the LaunchPad database table.": "Shrani vsebino nadzorne plošče v tabelo zbirke podatkov LaunchPad.",
- "GroupFolder (recommended for org use)": "GroupFolder (priporočeno za organizacijsko uporabo)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access.": "Shrani vsebino nadzorne plošče v Nextcloud GroupFolders za skupni dostop.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option.": "Aplikacija GroupFolder ni nameščena. Za uporabo te možnosti namestite 'Nextcloud GroupFolders'.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace.": "Izberite vrstni red, v katerem se prikažejo skupine Nextcloud, ko LaunchPad usmeri uporabnike v delovni prostor.",
"Demo data": "Demo podatki",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only.": "Predstavitve demo podatkov se bodo prikazale tukaj, ko bo dobavljena zmožnost demo-data-showcases. Za zdaj je ta korak samo informativen.",
@@ -955,7 +947,6 @@
"Finish": "Dokončaj",
"Run setup wizard": "Zaženi čarovnika za namestitev",
"Run setup wizard again": "Znova zaženi čarovnika za namestitev",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles.": "Zaženite svoj intranet: izberite shrambo, konfigurirajte skupine, namestite demo podatke in nastavite skrbniške vloge.",
"Container": "Vsebnik",
"Padding": "Notranji rob",
"Title (optional)": "Naslov (neobvezno)",
@@ -1332,6 +1323,8 @@
"Personal settings": "Personal settings",
"Store": "Store",
"Reports": "Reports",
- "Flows": "Flows"
+ "Flows": "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings.": "Konfigurirajte svojo instanco LaunchPad z razvrščanjem skupin, demo podatki, skrbniškimi vlogami in nastavitvami noge.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles.": "Zaženite svoj intranet: konfigurirajte skupine, namestite demo podatke in nastavite skrbniške vloge."
}
}
diff --git a/l10n/sq.js b/l10n/sq.js
index 27e4e125d..128d63521 100644
--- a/l10n/sq.js
+++ b/l10n/sq.js
@@ -926,14 +926,6 @@ OC.L10N.register(
"LaunchPad setup wizard" : "Asistenti i konfigurimit të LaunchPad",
"Step {n} / {total}" : "Hapi {n} / {total}",
"Welcome" : "Mirë se vini",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings." : "Konfiguroni instancën tuaj LaunchPad me ruajtjen, renditjen e grupeve, të dhënat demo, rolet e administratorit dhe cilësimet e fundfaqes.",
- "Storage backend" : "Backend-i i ruajtjes",
- "Choose how LaunchPad stores dashboard content." : "Zgjidhni se si LaunchPad ruan përmbajtjen e pultit.",
- "Database (default)" : "Bazë të dhënash (e parazgjedhur)",
- "Store dashboard content in the LaunchPad database table." : "Ruaj përmbajtjen e pultit në tabelën e bazës së të dhënave të LaunchPad.",
- "GroupFolder (recommended for org use)" : "GroupFolder (e rekomanduar për përdorim organizate)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access." : "Ruaj përmbajtjen e pultit në GroupFolders të Nextcloud për qasje bashkëpunuese.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option." : "Aplikacioni GroupFolder nuk është i instaluar. Instaloni 'Nextcloud GroupFolders' për të përdorur këtë opsion.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace." : "Zgjidhni renditjen me të cilën shfaqen grupet e Nextcloud kur LaunchPad i drejton përdoruesit te një hapësirë pune.",
"Demo data" : "Të dhëna demo",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only." : "Vitrinat e të dhënave demo do të shfaqen këtu pasi të lëshohet aftësia demo-data-showcases. Për momentin ky hap është vetëm informues.",
@@ -952,7 +944,6 @@ OC.L10N.register(
"Finish" : "Përfundo",
"Run setup wizard" : "Ekzekuto asistentin e konfigurimit",
"Run setup wizard again" : "Ekzekuto sërish asistentin e konfigurimit",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles." : "Nisni intranetin tuaj: zgjidhni ruajtjen, konfiguroni grupet, instaloni të dhënat demo dhe konfiguroni rolet e administratorit.",
"Container" : "Kontejner",
"Padding" : "Mbushje",
"Title (optional)" : "Titull (opsional)",
@@ -1363,7 +1354,9 @@ OC.L10N.register(
"Personal settings" : "Personal settings",
"Store" : "Store",
"Reports" : "Reports",
- "Flows" : "Flows"
+ "Flows" : "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings." : "Konfiguroni instancën tuaj LaunchPad me renditjen e grupeve, të dhënat demo, rolet e administratorit dhe cilësimet e fundpamjes.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles." : "Vini në punë intranetin tuaj: konfiguroni grupet, instaloni të dhëna demo dhe vendosni rolet e administratorit."
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/sq.json b/l10n/sq.json
index 3e1738c79..213fafa2e 100644
--- a/l10n/sq.json
+++ b/l10n/sq.json
@@ -927,14 +927,6 @@
"LaunchPad setup wizard": "Magjistari i konfigurimit të LaunchPad",
"Step {n} / {total}": "Hapi {n} / {total}",
"Welcome": "Mirë se vini",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings.": "Konfiguroni instancën tuaj LaunchPad me ruajtjen, renditjen e grupeve, të dhënat demo, rolet e administratorit dhe cilësimet e fundpamjes.",
- "Storage backend": "Sistemi i ruajtjes",
- "Choose how LaunchPad stores dashboard content.": "Zgjidhni se si LaunchPad ruan përmbajtjen e pultit.",
- "Database (default)": "Bazë të dhënash (parazgjedhje)",
- "Store dashboard content in the LaunchPad database table.": "Ruaj përmbajtjen e pultit në tabelën e bazës së të dhënave të LaunchPad.",
- "GroupFolder (recommended for org use)": "GroupFolder (rekomandohet për përdorim organizativ)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access.": "Ruaj përmbajtjen e pultit në GroupFolders të Nextcloud për qasje bashkëpunuese.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option.": "Aplikacioni GroupFolder nuk është i instaluar. Instaloni 'Nextcloud GroupFolders' për të përdorur këtë opsion.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace.": "Zgjidhni renditjen me të cilën shfaqen grupet Nextcloud kur LaunchPad i drejton përdoruesit te një hapësirë pune.",
"Demo data": "Të dhëna demo",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only.": "Vitrinat e të dhënave demo do të shfaqen këtu sapo të ofrohet aftësia demo-data-showcases. Për momentin ky hap është vetëm informativ.",
@@ -953,7 +945,6 @@
"Finish": "Përfundo",
"Run setup wizard": "Ekzekuto magjistarin e konfigurimit",
"Run setup wizard again": "Ekzekuto sërish magjistarin e konfigurimit",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles.": "Vini në punë intranetin tuaj: zgjidhni ruajtjen, konfiguroni grupet, instaloni të dhëna demo dhe vendosni rolet e administratorit.",
"Container": "Kontejner",
"Padding": "Mbushje",
"Title (optional)": "Titull (opsional)",
@@ -1330,6 +1321,8 @@
"Personal settings": "Personal settings",
"Store": "Store",
"Reports": "Reports",
- "Flows": "Flows"
+ "Flows": "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings.": "Konfiguroni instancën tuaj LaunchPad me renditjen e grupeve, të dhënat demo, rolet e administratorit dhe cilësimet e fundpamjes.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles.": "Vini në punë intranetin tuaj: konfiguroni grupet, instaloni të dhëna demo dhe vendosni rolet e administratorit."
}
}
diff --git a/l10n/sr.js b/l10n/sr.js
index d038903e7..0e846418e 100644
--- a/l10n/sr.js
+++ b/l10n/sr.js
@@ -926,14 +926,6 @@ OC.L10N.register(
"LaunchPad setup wizard" : "LaunchPad чаробњак за подешавање",
"Step {n} / {total}" : "Корак {n} / {total}",
"Welcome" : "Добродошли",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings." : "Подесите своју LaunchPad инстанцу са складиштем, редоследом група, демо подацима, администраторским улогама и подешавањима подножја.",
- "Storage backend" : "Позадинско складиште",
- "Choose how LaunchPad stores dashboard content." : "Изаберите како LaunchPad чува садржај контролне табле.",
- "Database (default)" : "База података (подразумевано)",
- "Store dashboard content in the LaunchPad database table." : "Чувај садржај контролне табле у LaunchPad табели базе података.",
- "GroupFolder (recommended for org use)" : "GroupFolder (препоручено за организациону употребу)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access." : "Чувај садржај контролне табле у Nextcloud GroupFolders за колаборативни приступ.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option." : "Апликација GroupFolder није инсталирана. Инсталирајте 'Nextcloud GroupFolders' да бисте користили ову опцију.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace." : "Изаберите редослед којим се Nextcloud групе појављују када LaunchPad усмерава кориснике ка радном простору.",
"Demo data" : "Демо подаци",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only." : "Прикази демо података ће се појавити овде када се испоручи могућност demo-data-showcases. За сада је овај корак само информативан.",
@@ -952,7 +944,6 @@ OC.L10N.register(
"Finish" : "Заврши",
"Run setup wizard" : "Покрени чаробњак за подешавање",
"Run setup wizard again" : "Поново покрени чаробњак за подешавање",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles." : "Покрените свој интранет: изаберите складиште, подесите групе, инсталирајте демо податке и подесите администраторске улоге.",
"Container" : "Контејнер",
"Padding" : "Унутрашњи размак",
"Title (optional)" : "Наслов (опционо)",
@@ -1354,6 +1345,8 @@ OC.L10N.register(
"Personal settings" : "Personal settings",
"Store" : "Store",
"Reports" : "Reports",
- "Flows" : "Flows"
+ "Flows" : "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings." : "Подесите своју LaunchPad инстанцу са редоследом група, демо подацима, администраторским улогама и подешавањима подножја.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles." : "Покрените свој интранет: подесите групе, инсталирајте демо податке и подесите администраторске улоге."
},
"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);");
diff --git a/l10n/sr.json b/l10n/sr.json
index af33753c4..fab01216a 100644
--- a/l10n/sr.json
+++ b/l10n/sr.json
@@ -928,14 +928,6 @@
"LaunchPad setup wizard": "LaunchPad чаробњак за подешавање",
"Step {n} / {total}": "Корак {n} / {total}",
"Welcome": "Добродошли",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings.": "Подесите своју LaunchPad инстанцу са складиштем, редоследом група, демо подацима, администраторским улогама и подешавањима подножја.",
- "Storage backend": "Позадинско складиште",
- "Choose how LaunchPad stores dashboard content.": "Изаберите како LaunchPad чува садржај контролне табле.",
- "Database (default)": "База података (подразумевано)",
- "Store dashboard content in the LaunchPad database table.": "Чувај садржај контролне табле у LaunchPad табели базе података.",
- "GroupFolder (recommended for org use)": "GroupFolder (препоручено за организациону употребу)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access.": "Чувај садржај контролне табле у Nextcloud GroupFolders за колаборативни приступ.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option.": "Апликација GroupFolder није инсталирана. Инсталирајте 'Nextcloud GroupFolders' да бисте користили ову опцију.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace.": "Изаберите редослед којим се Nextcloud групе појављују када LaunchPad усмерава кориснике ка радном простору.",
"Demo data": "Демо подаци",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only.": "Прикази демо података ће се појавити овде када се испоручи могућност demo-data-showcases. За сада је овај корак само информативан.",
@@ -954,7 +946,6 @@
"Finish": "Заврши",
"Run setup wizard": "Покрени чаробњак за подешавање",
"Run setup wizard again": "Поново покрени чаробњак за подешавање",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles.": "Покрените свој интранет: изаберите складиште, подесите групе, инсталирајте демо податке и подесите администраторске улоге.",
"Container": "Контејнер",
"Padding": "Унутрашњи размак",
"Title (optional)": "Наслов (опционо)",
@@ -1331,6 +1322,8 @@
"Personal settings": "Personal settings",
"Store": "Store",
"Reports": "Reports",
- "Flows": "Flows"
+ "Flows": "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings.": "Подесите своју LaunchPad инстанцу са редоследом група, демо подацима, администраторским улогама и подешавањима подножја.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles.": "Покрените свој интранет: подесите групе, инсталирајте демо податке и подесите администраторске улоге."
}
}
diff --git a/l10n/sv.js b/l10n/sv.js
index c0875623f..b8be35874 100644
--- a/l10n/sv.js
+++ b/l10n/sv.js
@@ -926,14 +926,6 @@ OC.L10N.register(
"LaunchPad setup wizard" : "LaunchPad-installationsguide",
"Step {n} / {total}" : "Steg {n} / {total}",
"Welcome" : "Välkommen",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings." : "Konfigurera din LaunchPad-instans med lagring, gruppordning, demonstrationsdata, administratörsroller och sidfotsinställningar.",
- "Storage backend" : "Lagringsbackend",
- "Choose how LaunchPad stores dashboard content." : "Välj hur LaunchPad lagrar instrumentpanelsinnehåll.",
- "Database (default)" : "Databas (standard)",
- "Store dashboard content in the LaunchPad database table." : "Lagra instrumentpanelsinnehåll i LaunchPad-databastabellen.",
- "GroupFolder (recommended for org use)" : "GroupFolder (rekommenderas för organisationsbruk)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access." : "Lagra instrumentpanelsinnehåll i Nextcloud GroupFolders för samarbetsåtkomst.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option." : "GroupFolder-appen är inte installerad. Installera 'Nextcloud GroupFolders' för att använda detta alternativ.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace." : "Välj i vilken ordning Nextcloud-grupper visas när LaunchPad dirigerar användare till en arbetsyta.",
"Demo data" : "Demonstrationsdata",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only." : "Demonstrationsdataexemplen visas här när funktionen demo-data-showcases lanseras. Tills vidare är detta steg endast informativt.",
@@ -952,7 +944,6 @@ OC.L10N.register(
"Finish" : "Slutför",
"Run setup wizard" : "Kör installationsguiden",
"Run setup wizard again" : "Kör installationsguiden igen",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles." : "Kom igång med ditt intranät: välj lagring, konfigurera grupper, installera demonstrationsdata och ställ in administratörsroller.",
"Container" : "Behållare",
"Padding" : "Utfyllnad",
"Title (optional)" : "Titel (valfritt)",
@@ -1363,7 +1354,9 @@ OC.L10N.register(
"Personal settings" : "Personal settings",
"Store" : "Store",
"Reports" : "Reports",
- "Flows" : "Flows"
+ "Flows" : "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings." : "Konfigurera din LaunchPad-instans med gruppordning, demodata, administratörsroller och sidfotsinställningar.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles." : "Kom igång med ditt intranät: konfigurera grupper, installera demodata och ställ in administratörsroller."
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/sv.json b/l10n/sv.json
index 7dde9c0a5..d5bdc2e28 100644
--- a/l10n/sv.json
+++ b/l10n/sv.json
@@ -927,14 +927,6 @@
"LaunchPad setup wizard": "LaunchPad-installationsguide",
"Step {n} / {total}": "Steg {n} / {total}",
"Welcome": "Välkommen",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings.": "Konfigurera din LaunchPad-instans med lagring, gruppordning, demodata, administratörsroller och sidfotsinställningar.",
- "Storage backend": "Lagringsbackend",
- "Choose how LaunchPad stores dashboard content.": "Välj hur LaunchPad lagrar instrumentpanelsinnehåll.",
- "Database (default)": "Databas (standard)",
- "Store dashboard content in the LaunchPad database table.": "Lagra instrumentpanelsinnehåll i LaunchPad-databastabellen.",
- "GroupFolder (recommended for org use)": "GroupFolder (rekommenderas för organisationsanvändning)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access.": "Lagra instrumentpanelsinnehåll i Nextcloud GroupFolders för samarbetsåtkomst.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option.": "GroupFolder-appen är inte installerad. Installera 'Nextcloud GroupFolders' för att använda detta alternativ.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace.": "Välj den ordning som Nextcloud-grupper visas i när LaunchPad dirigerar användare till en arbetsyta.",
"Demo data": "Demodata",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only.": "Demonstrationerna av demodata visas här när funktionen demo-data-showcases levereras. För närvarande är detta steg endast informativt.",
@@ -953,7 +945,6 @@
"Finish": "Slutför",
"Run setup wizard": "Kör installationsguiden",
"Run setup wizard again": "Kör installationsguiden igen",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles.": "Kom igång med ditt intranät: välj lagring, konfigurera grupper, installera demodata och ställ in administratörsroller.",
"Container": "Behållare",
"Padding": "Utfyllnad",
"Title (optional)": "Titel (valfritt)",
@@ -1330,6 +1321,8 @@
"Personal settings": "Personal settings",
"Store": "Store",
"Reports": "Reports",
- "Flows": "Flows"
+ "Flows": "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings.": "Konfigurera din LaunchPad-instans med gruppordning, demodata, administratörsroller och sidfotsinställningar.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles.": "Kom igång med ditt intranät: konfigurera grupper, installera demodata och ställ in administratörsroller."
}
}
diff --git a/l10n/tr.js b/l10n/tr.js
index b280f9d36..d0aed893d 100644
--- a/l10n/tr.js
+++ b/l10n/tr.js
@@ -926,14 +926,6 @@ OC.L10N.register(
"LaunchPad setup wizard" : "LaunchPad kurulum sihirbazı",
"Step {n} / {total}" : "Adım {n} / {total}",
"Welcome" : "Hoş geldiniz",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings." : "LaunchPad örneğinizi depolama, grup sıralaması, tanıtım verisi, yönetici rolleri ve alt bilgi ayarlarıyla yapılandırın.",
- "Storage backend" : "Depolama arka ucu",
- "Choose how LaunchPad stores dashboard content." : "LaunchPad uygulamasının pano içeriğini nasıl depolayacağını seçin.",
- "Database (default)" : "Veritabanı (varsayılan)",
- "Store dashboard content in the LaunchPad database table." : "Pano içeriğini LaunchPad veritabanı tablosunda depolayın.",
- "GroupFolder (recommended for org use)" : "GroupFolder (kuruluş kullanımı için önerilir)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access." : "İşbirlikçi erişim için pano içeriğini Nextcloud GroupFolders içinde depolayın.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option." : "GroupFolder uygulaması kurulu değil. Bu seçeneği kullanmak için 'Nextcloud GroupFolders' uygulamasını kurun.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace." : "LaunchPad kullanıcıları bir çalışma alanına yönlendirdiğinde Nextcloud gruplarının görüneceği sırayı seçin.",
"Demo data" : "Tanıtım verisi",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only." : "Tanıtım verisi vitrinleri, demo-data-showcases yeteneği yayımlandığında burada görünür. Şimdilik bu adım yalnızca bilgilendirme amaçlıdır.",
@@ -952,7 +944,6 @@ OC.L10N.register(
"Finish" : "Bitir",
"Run setup wizard" : "Kurulum sihirbazını çalıştır",
"Run setup wizard again" : "Kurulum sihirbazını yeniden çalıştır",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles." : "İç ağınızı başlatın: depolamayı seçin, grupları yapılandırın, tanıtım verisini kurun ve yönetici rollerini ayarlayın.",
"Container" : "Kapsayıcı",
"Padding" : "Dolgu",
"Title (optional)" : "Başlık (isteğe bağlı)",
@@ -1363,7 +1354,9 @@ OC.L10N.register(
"Personal settings" : "Personal settings",
"Store" : "Store",
"Reports" : "Reports",
- "Flows" : "Flows"
+ "Flows" : "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings." : "LaunchPad örneğinizi grup sıralaması, demo veri, yönetici rolleri ve alt bilgi ayarlarıyla yapılandırın.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles." : "Kurum içi ağınızı başlatın: grupları yapılandırın, demo veri kurun ve yönetici rollerini ayarlayın."
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/tr.json b/l10n/tr.json
index 94f40c7ed..8f30ec29d 100644
--- a/l10n/tr.json
+++ b/l10n/tr.json
@@ -927,14 +927,6 @@
"LaunchPad setup wizard": "LaunchPad kurulum sihirbazı",
"Step {n} / {total}": "Adım {n} / {total}",
"Welcome": "Hoş geldiniz",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings.": "LaunchPad örneğinizi depolama, grup sıralaması, demo veri, yönetici rolleri ve alt bilgi ayarlarıyla yapılandırın.",
- "Storage backend": "Depolama arka ucu",
- "Choose how LaunchPad stores dashboard content.": "LaunchPad'in pano içeriğini nasıl depolayacağını seçin.",
- "Database (default)": "Veritabanı (varsayılan)",
- "Store dashboard content in the LaunchPad database table.": "Pano içeriğini LaunchPad veritabanı tablosunda depolayın.",
- "GroupFolder (recommended for org use)": "GroupFolder (kuruluş kullanımı için önerilir)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access.": "İş birliğine dayalı erişim için pano içeriğini Nextcloud GroupFolders'ta depolayın.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option.": "GroupFolder uygulaması kurulu değil. Bu seçeneği kullanmak için 'Nextcloud GroupFolders' uygulamasını kurun.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace.": "LaunchPad kullanıcıları bir çalışma alanına yönlendirdiğinde Nextcloud gruplarının görünme sırasını seçin.",
"Demo data": "Demo veri",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only.": "Demo veri vitrinleri, demo-data-showcases yeteneği yayımlandığında burada görünecektir. Şimdilik bu adım yalnızca bilgilendirme amaçlıdır.",
@@ -953,7 +945,6 @@
"Finish": "Bitir",
"Run setup wizard": "Kurulum sihirbazını çalıştır",
"Run setup wizard again": "Kurulum sihirbazını yeniden çalıştır",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles.": "Kurum içi ağınızı başlatın: depolama seçin, grupları yapılandırın, demo veri kurun ve yönetici rollerini ayarlayın.",
"Container": "Kapsayıcı",
"Padding": "İç boşluk",
"Title (optional)": "Başlık (isteğe bağlı)",
@@ -1330,6 +1321,8 @@
"Personal settings": "Personal settings",
"Store": "Store",
"Reports": "Reports",
- "Flows": "Flows"
+ "Flows": "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings.": "LaunchPad örneğinizi grup sıralaması, demo veri, yönetici rolleri ve alt bilgi ayarlarıyla yapılandırın.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles.": "Kurum içi ağınızı başlatın: grupları yapılandırın, demo veri kurun ve yönetici rollerini ayarlayın."
}
}
diff --git a/l10n/uk.js b/l10n/uk.js
index cd872be42..23b11f51c 100644
--- a/l10n/uk.js
+++ b/l10n/uk.js
@@ -926,14 +926,6 @@ OC.L10N.register(
"LaunchPad setup wizard" : "Майстер налаштування LaunchPad",
"Step {n} / {total}" : "Крок {n} / {total}",
"Welcome" : "Ласкаво просимо",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings." : "Налаштуйте свій екземпляр LaunchPad зі сховищем, упорядкуванням груп, демонстраційними даними, ролями адміністратора та налаштуваннями нижнього колонтитула.",
- "Storage backend" : "Бекенд сховища",
- "Choose how LaunchPad stores dashboard content." : "Виберіть, як LaunchPad зберігає вміст інформаційних панелей.",
- "Database (default)" : "База даних (усталено)",
- "Store dashboard content in the LaunchPad database table." : "Зберігати вміст інформаційних панелей у таблиці бази даних LaunchPad.",
- "GroupFolder (recommended for org use)" : "GroupFolder (рекомендовано для організаційного використання)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access." : "Зберігати вміст інформаційних панелей у GroupFolders Nextcloud для спільного доступу.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option." : "Застосунок GroupFolder не встановлено. Встановіть «Nextcloud GroupFolders», щоб скористатися цією опцією.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace." : "Виберіть порядок, у якому з'являються групи Nextcloud, коли LaunchPad спрямовує користувачів до робочого простору.",
"Demo data" : "Демонстраційні дані",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only." : "Демонстраційні приклади даних з'являться тут, коли буде реалізовано можливість demo-data-showcases. Наразі цей крок є лише інформаційним.",
@@ -952,7 +944,6 @@ OC.L10N.register(
"Finish" : "Завершити",
"Run setup wizard" : "Запустити майстер налаштування",
"Run setup wizard again" : "Запустити майстер налаштування знову",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles." : "Розпочніть роботу свого інтранету: виберіть сховище, налаштуйте групи, встановіть демонстраційні дані та налаштуйте ролі адміністратора.",
"Container" : "Контейнер",
"Padding" : "Внутрішній відступ",
"Title (optional)" : "Заголовок (необов'язково)",
@@ -1363,7 +1354,9 @@ OC.L10N.register(
"Personal settings" : "Personal settings",
"Store" : "Store",
"Reports" : "Reports",
- "Flows" : "Flows"
+ "Flows" : "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings." : "Налаштуйте свій екземпляр LaunchPad з упорядкуванням груп, демонстраційними даними, ролями адміністраторів і налаштуваннями нижнього колонтитула.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles." : "Розпочніть роботу свого інтранету: налаштуйте групи, установіть демонстраційні дані та налаштуйте ролі адміністраторів."
},
"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"
);
diff --git a/l10n/uk.json b/l10n/uk.json
index e1275f8e1..fa9311c60 100644
--- a/l10n/uk.json
+++ b/l10n/uk.json
@@ -928,14 +928,6 @@
"LaunchPad setup wizard": "Майстер налаштування LaunchPad",
"Step {n} / {total}": "Крок {n} / {total}",
"Welcome": "Ласкаво просимо",
- "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings.": "Налаштуйте свій екземпляр LaunchPad зі сховищем, упорядкуванням груп, демонстраційними даними, ролями адміністраторів і налаштуваннями нижнього колонтитула.",
- "Storage backend": "Серверна частина сховища",
- "Choose how LaunchPad stores dashboard content.": "Виберіть, як LaunchPad зберігає вміст дашбордів.",
- "Database (default)": "База даних (за замовчуванням)",
- "Store dashboard content in the LaunchPad database table.": "Зберігати вміст дашбордів у таблиці бази даних LaunchPad.",
- "GroupFolder (recommended for org use)": "GroupFolder (рекомендовано для організаційного використання)",
- "Store dashboard content in Nextcloud GroupFolders for collaborative access.": "Зберігати вміст дашбордів у Nextcloud GroupFolders для спільного доступу.",
- "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option.": "Застосунок GroupFolder не встановлено. Установіть «Nextcloud GroupFolders», щоб використовувати цей параметр.",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace.": "Виберіть порядок, у якому групи Nextcloud з'являються, коли LaunchPad спрямовує користувачів до робочого простору.",
"Demo data": "Демонстраційні дані",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only.": "Демонстрації даних-зразків з'являться тут, щойно буде випущено можливість demo-data-showcases. Наразі цей крок має лише інформаційний характер.",
@@ -954,7 +946,6 @@
"Finish": "Завершити",
"Run setup wizard": "Запустити майстер налаштування",
"Run setup wizard again": "Запустити майстер налаштування знову",
- "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles.": "Розпочніть роботу свого інтранету: виберіть сховище, налаштуйте групи, установіть демонстраційні дані та налаштуйте ролі адміністраторів.",
"Container": "Контейнер",
"Padding": "Внутрішній відступ",
"Title (optional)": "Заголовок (необов'язково)",
@@ -1331,6 +1322,8 @@
"Personal settings": "Personal settings",
"Store": "Store",
"Reports": "Reports",
- "Flows": "Flows"
+ "Flows": "Flows",
+ "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings.": "Налаштуйте свій екземпляр LaunchPad з упорядкуванням груп, демонстраційними даними, ролями адміністраторів і налаштуваннями нижнього колонтитула.",
+ "Get your intranet started: configure groups, install demo data, and set up admin roles.": "Розпочніть роботу свого інтранету: налаштуйте групи, установіть демонстраційні дані та налаштуйте ролі адміністраторів."
}
}
diff --git a/lib/Command/SetupCommand.php b/lib/Command/SetupCommand.php
index f462f93d6..93cacc19c 100644
--- a/lib/Command/SetupCommand.php
+++ b/lib/Command/SetupCommand.php
@@ -45,7 +45,7 @@ class SetupCommand extends Command {
*
* @param SetupWizardService $wizardService Wizard orchestrator.
* @param AdminSettingsService $settings Group-order persistence
- * (Step 3 in the YAML
+ * (Step 2 in the YAML
* schema).
*/
public function __construct(
@@ -117,13 +117,12 @@ protected function execute(
return self::FAILURE;
}
- if (isset($config['storage_backend']) === false
- || is_string($config['storage_backend']) === false
- ) {
+ // Decision 131 retired the storage step. A file written for the
+ // seven-step wizard still runs; its storage_backend is not applied.
+ if (array_key_exists(key: 'storage_backend', array: $config) === true) {
$output->writeln(
- messages: "Invalid setup.yaml: missing field 'storage_backend'"
+ messages: "'storage_backend' is no longer used and was ignored."
);
- return self::FAILURE;
}
try {
@@ -154,55 +153,31 @@ protected function execute(
private function applySteps(array $config, OutputInterface $output): void {
$output->writeln(messages: 'Step 1: Welcome... done');
- $this->applyStorageStep(config: $config, output: $output);
$this->applyGroupOrderStep(config: $config, output: $output);
$this->skipUnimplementedStep(
- stepNumber: 4,
+ stepNumber: 3,
stepName: 'Demo data',
present: array_key_exists(key: 'demo_packages', array: $config),
output: $output
);
$this->skipUnimplementedStep(
- stepNumber: 5,
+ stepNumber: 4,
stepName: 'Admin roles',
present: array_key_exists(key: 'admin_role_group', array: $config),
output: $output
);
$this->skipUnimplementedStep(
- stepNumber: 6,
+ stepNumber: 5,
stepName: 'Footer config',
present: array_key_exists(key: 'footer_config', array: $config),
output: $output
);
- $output->writeln(messages: 'Step 7: Done... done');
+ $output->writeln(messages: 'Step 6: Done... done');
}//end applySteps()
/**
- * Apply Step 2 — storage backend (REQ-WIZ-003).
- *
- * @param array $config Parsed YAML.
- * @param OutputInterface $output CLI output.
- *
- * @return void
- */
- private function applyStorageStep(array $config, OutputInterface $output): void {
- $current = $this->wizardService->getContentStorage();
- $target = (string)$config['storage_backend'];
-
- if ($current === $target) {
- $output->writeln(
- messages: 'Step 2: Storage backend... already configured, skipping'
- );
- return;
- }
-
- $this->wizardService->setContentStorage(value: $target);
- $output->writeln(messages: 'Step 2: Storage backend... done');
- }//end applyStorageStep()
-
- /**
- * Apply Step 3 — group priority order (REQ-WIZ-004).
+ * Apply Step 2 — group priority order (REQ-WIZ-004).
*
* @param array $config Parsed YAML.
* @param OutputInterface $output CLI output.
@@ -214,7 +189,7 @@ private function applyGroupOrderStep(
OutputInterface $output,
): void {
if (array_key_exists(key: 'group_priority_order', array: $config) === false) {
- $output->writeln(messages: 'Step 3: Group order... skipped (not in config)');
+ $output->writeln(messages: 'Step 2: Group order... skipped (not in config)');
return;
}
@@ -228,13 +203,13 @@ private function applyGroupOrderStep(
$current = $this->settings->getGroupOrder();
if ($current === array_values(array: $groups)) {
$output->writeln(
- messages: 'Step 3: Group order... already configured, skipping'
+ messages: 'Step 2: Group order... already configured, skipping'
);
return;
}
$this->settings->setGroupOrder(groupIds: $groups);
- $output->writeln(messages: 'Step 3: Group order... done');
+ $output->writeln(messages: 'Step 2: Group order... done');
}//end applyGroupOrderStep()
/**
diff --git a/lib/Controller/AdminController.php b/lib/Controller/AdminController.php
index 0118f228d..d49f4faac 100644
--- a/lib/Controller/AdminController.php
+++ b/lib/Controller/AdminController.php
@@ -430,10 +430,6 @@ public function getSettings(): JSONResponse {
* @param array|null $linkCreateFileExts link-button-widget createFile
* extension allow-list
* (REQ-LBN-004).
- * @param string|null $launchpadContentStorage Content storage backend
- * (`database` or
- * `groupfolder`).
- * REQ-GFSB-006.
* @param string|null $defaultSharePermissionLevel Org-wide default share
* permission level
* (dashboard-sharing spec).
@@ -481,7 +477,6 @@ public function updateSettings(
?bool $allowMultiDash = null,
?int $defaultGridCols = null,
?array $linkCreateFileExts = null,
- ?string $launchpadContentStorage = null,
?string $defaultSharePermissionLevel = null,
?array $forcedShareGroups = null,
?bool $legacyWidgetBridgeEnabled = null,
@@ -508,7 +503,6 @@ public function updateSettings(
allowMultiDash: $allowMultiDash,
defaultGridCols: $defaultGridCols,
linkCreateFileExts: $linkCreateFileExts,
- contentStorage: $launchpadContentStorage,
defaultSharePermissionLevel: $defaultSharePermissionLevel,
forcedShareGroups: $forcedShareGroups,
legacyWidgetBridgeEnabled: $legacyWidgetBridgeEnabled,
@@ -1058,57 +1052,6 @@ public function completeWizard(): JSONResponse {
);
}//end completeWizard()
- /**
- * Persist the storage backend choice from Step 2 (REQ-WIZ-003).
- *
- * Validates the selection and writes `launchpad.content_storage`. The
- * GroupFolder option is server-side gated by the `groupfolders` app
- * dependency — selecting it without the app installed returns 400.
- * Admin-only.
- *
- * @param string|null $storage The chosen backend.
- *
- * @return JSONResponse The post-write wizard state, or 400/401/403.
- *
- * @spec openspec/specs/admin-templates/spec.md
- */
- #[AuthorizedAdminSetting(LaunchPadAdmin::class)]
- public function setWizardStorage(?string $storage = null): JSONResponse {
- $guard = $this->assertAdmin();
- if ($guard !== null) {
- return $guard;
- }
-
- if ($storage === null || $storage === '') {
- return new JSONResponse(
- data: ['error' => 'Field "storage" is required.'],
- statusCode: Http::STATUS_BAD_REQUEST
- );
- }
-
- if ($storage === SetupWizardService::STORAGE_GROUPFOLDER
- && $this->setupWizardService->hasGroupfolderApp() === false
- ) {
- return new JSONResponse(
- data: ['error' => 'GroupFolder app is not installed.'],
- statusCode: Http::STATUS_BAD_REQUEST
- );
- }
-
- try {
- $this->setupWizardService->setContentStorage(value: $storage);
- } catch (InvalidArgumentException) {
- return new JSONResponse(
- data: ['error' => 'Unsupported storage backend.'],
- statusCode: Http::STATUS_BAD_REQUEST
- );
- }
-
- return ResponseHelper::success(
- data: $this->setupWizardService->getWizardState()
- );
- }//end setWizardStorage()
-
/**
* Build the update data array from nullable parameters.
*
diff --git a/lib/Db/AdminSetting.php b/lib/Db/AdminSetting.php
index f322b537c..bb4056e8a 100644
--- a/lib/Db/AdminSetting.php
+++ b/lib/Db/AdminSetting.php
@@ -158,15 +158,6 @@ class AdminSetting extends Entity implements JsonSerializable {
*/
public const KEY_SETUP_WIZARD_COMPLETE = AdminSettingKey::SETUP_WIZARD_COMPLETE->value;
- /**
- * BC alias for AdminSettingKey::CONTENT_STORAGE.
- *
- * @var string
- *
- * @see AdminSettingKey::CONTENT_STORAGE
- */
- public const KEY_CONTENT_STORAGE = AdminSettingKey::CONTENT_STORAGE->value;
-
/**
* BC alias for AdminSettingKey::DEFAULT_SHARE_PERMISSION_LEVEL.
*
diff --git a/lib/Db/AdminSettingKey.php b/lib/Db/AdminSettingKey.php
index 7996eebb0..d4dff890e 100644
--- a/lib/Db/AdminSettingKey.php
+++ b/lib/Db/AdminSettingKey.php
@@ -15,7 +15,7 @@
* - Comments: COMMENTS_ENABLED_DEFAULT
* - Footer: FOOTER_ENABLED, FOOTER_HTML, FOOTER_CONFIG,
* FOOTER_BACKGROUND_COLOR, FOOTER_TEXT_COLOR
- * - Setup wizard: SETUP_WIZARD_COMPLETE, CONTENT_STORAGE
+ * - Setup wizard: SETUP_WIZARD_COMPLETE
* - Quick search: QUICKSEARCH_FALLBACK_TARGET (tile-quick-search)
*
* @category Db
@@ -55,7 +55,6 @@ enum AdminSettingKey: string {
case FOOTER_BACKGROUND_COLOR = 'footer_background_color';
case FOOTER_TEXT_COLOR = 'footer_text_color';
case SETUP_WIZARD_COMPLETE = 'setup_wizard_complete';
- case CONTENT_STORAGE = 'content_storage';
case DEFAULT_SHARE_PERMISSION_LEVEL = 'default_share_permission_level';
case FORCED_SHARE_GROUPS = 'forced_share_groups';
case LEGACY_WIDGET_BRIDGE_ENABLED = 'legacy_widget_bridge_enabled';
diff --git a/lib/Service/AdminSettingsService.php b/lib/Service/AdminSettingsService.php
index 0cc8cd44e..4c920f324 100644
--- a/lib/Service/AdminSettingsService.php
+++ b/lib/Service/AdminSettingsService.php
@@ -83,8 +83,6 @@ public function getSettings(): array {
$storedExt = self::DEFAULT_LINK_CREATE_FILE_EXTENSIONS;
}
- $storageKey = AdminSetting::KEY_CONTENT_STORAGE;
-
$sharePermKey = AdminSetting::KEY_DEFAULT_SHARE_PERMISSION_LEVEL;
$forcedShareKey = AdminSetting::KEY_FORCED_SHARE_GROUPS;
$bridgeKey = AdminSetting::KEY_LEGACY_WIDGET_BRIDGE_ENABLED;
@@ -125,7 +123,6 @@ public function getSettings(): array {
'defaultGridColumns' => $settings[$gridKey] ?? 12,
'linkCreateFileExtensions' => $storedExt,
// REQ-GFSB-006: surface active storage backend in admin settings.
- 'launchpad.content_storage' => $settings[$storageKey] ?? 'database',
// Dashboard-sharing spec: org-wide share defaults surfaced in
// Beheer ▸ Sharing. Default permission level mirrors the
// dashboard default; forced-share groups default to none.
@@ -202,13 +199,6 @@ public function clampQuota(mixed $value): int {
return $int;
}//end clampQuota()
- /**
- * Valid values for the `launchpad.content_storage` setting (REQ-GFSB-006).
- *
- * @var list
- */
- public const VALID_CONTENT_STORAGE_VALUES = ['database', 'groupfolder'];
-
/**
* Tile-quick-search REQ-QSEARCH-004: no-match fallback disabled — Enter
* on a zero-match query takes no navigation action.
@@ -296,10 +286,6 @@ private function isValidQuicksearchFallbackUrlTemplate(string $value): bool {
* extension
* allow-list
* (REQ-LBN-004).
- * @param string|null $contentStorage Content storage backend
- * (`database` or
- * `groupfolder`).
- * REQ-GFSB-006.
* @param string|null $defaultSharePermissionLevel Org-wide default share
* permission level
* (dashboard-sharing spec).
@@ -340,8 +326,8 @@ private function isValidQuicksearchFallbackUrlTemplate(string $value): bool {
*
* @return void
*
- * @throws \InvalidArgumentException When `$contentStorage`,
- * `$defaultSharePermissionLevel`, or
+ * @throws \InvalidArgumentException When
+ * `$defaultSharePermissionLevel` or
* `$quicksearchFallbackTarget` is not
* a valid value.
*
@@ -353,7 +339,6 @@ public function updateSettings(
?bool $allowMultiDash = null,
?int $defaultGridCols = null,
?array $linkCreateFileExts = null,
- ?string $contentStorage = null,
?string $defaultSharePermissionLevel = null,
?array $forcedShareGroups = null,
?bool $legacyWidgetBridgeEnabled = null,
@@ -397,19 +382,6 @@ public function updateSettings(
);
}
- if ($contentStorage !== null) {
- if (in_array(needle: $contentStorage, haystack: self::VALID_CONTENT_STORAGE_VALUES, strict: true) === false) {
- throw new InvalidArgumentException(
- message: "Invalid value for launchpad.content_storage. Must be 'db' or 'groupfolder'."
- );
- }
-
- $this->settingMapper->setSetting(
- key: AdminSetting::KEY_CONTENT_STORAGE,
- value: $contentStorage
- );
- }
-
$this->persistQuotaAndQuicksearchSettings(
maxDashboardsPerUser: $maxDashboardsPerUser,
maxWidgetsPerDashboard: $maxWidgetsPerDashboard,
diff --git a/lib/Service/SetupWizardService.php b/lib/Service/SetupWizardService.php
index e3ac48d26..9dfc78718 100644
--- a/lib/Service/SetupWizardService.php
+++ b/lib/Service/SetupWizardService.php
@@ -24,10 +24,8 @@
namespace OCA\LaunchPad\Service;
-use InvalidArgumentException;
use OCA\LaunchPad\Db\AdminSetting;
use OCA\LaunchPad\Db\AdminSettingMapper;
-use OCP\App\IAppManager;
/**
* Service backing the setup wizard state + completion endpoints.
@@ -35,7 +33,7 @@
* Status values follow the spec (REQ-WIZ-008): `'done'`, `'skipped'`,
* `'pending'`. The wizard "pending" state means a step has not been
* traversed yet (no sibling setting written). `'skipped'` is reserved for
- * the optional steps 5/6 — when their guarded capability isn't available,
+ * the optional steps 4/5 — when their guarded capability isn't available,
* the heuristic returns `'skipped'` so the UI can collapse the step.
*/
class SetupWizardService {
@@ -44,40 +42,15 @@ class SetupWizardService {
*
* @var integer
*/
- public const STEP_COUNT = 7;
-
- /**
- * Storage backend value: relational database (default).
- *
- * @var string
- */
- public const STORAGE_DATABASE = 'database';
-
- /**
- * Storage backend value: GroupFolder app.
- *
- * @var string
- */
- public const STORAGE_GROUPFOLDER = 'groupfolder';
-
- /**
- * GroupFolder dependency app id used by Step 2's tooltip gate.
- *
- * @var string
- */
- public const GROUPFOLDER_APP_ID = 'groupfolders';
+ public const STEP_COUNT = 6;
/**
* Constructor.
*
* @param AdminSettingMapper $settingMapper Admin-setting persistence.
- * @param IAppManager $appManager Used to detect the optional
- * `groupfolders` Nextcloud
- * app for Step 2's gate.
*/
public function __construct(
private readonly AdminSettingMapper $settingMapper,
- private readonly IAppManager $appManager,
) {
}//end __construct()
@@ -86,7 +59,7 @@ public function __construct(
*
* Shape: `{complete: bool, currentRecommendedStep: int,
* stepStatuses: array}`. Step 1 is always
- * `'done'`; Step 7 stays `'pending'` until the admin clicks Finish.
+ * `'done'`; Step 6 stays `'pending'` until the admin clicks Finish.
*
* @return array{complete: bool, currentRecommendedStep: int, stepStatuses: array}
* The wizard state payload.
@@ -124,58 +97,6 @@ public function markWizardComplete(): array {
return $this->getWizardState();
}//end markWizardComplete()
- /**
- * Whether the GroupFolder dependency is installed (REQ-WIZ-003).
- *
- * @return boolean True when the GroupFolder option may be selected.
- */
- public function hasGroupfolderApp(): bool {
- return $this->appManager->isInstalled(self::GROUPFOLDER_APP_ID);
- }//end hasGroupfolderApp()
-
- /**
- * Persist the storage backend choice from Step 2.
- *
- * @param string $value `'database'` or `'groupfolder'`.
- *
- * @return void
- *
- * @spec openspec/specs/setup-wizard/spec.md
- */
- public function setContentStorage(string $value): void {
- $allowed = [self::STORAGE_DATABASE, self::STORAGE_GROUPFOLDER];
- if (in_array(needle: $value, haystack: $allowed, strict: true) === false) {
- throw new InvalidArgumentException(
- message: 'Unsupported storage backend: ' . $value
- );
- }
-
- $this->settingMapper->setSetting(
- key: AdminSetting::KEY_CONTENT_STORAGE,
- value: $value
- );
- }//end setContentStorage()
-
- /**
- * Read the persisted storage backend choice with the safe default.
- *
- * @return string The persisted backend or `'database'` when unset.
- *
- * @spec openspec/specs/setup-wizard/spec.md
- */
- public function getContentStorage(): string {
- $value = $this->settingMapper->getValue(
- key: AdminSetting::KEY_CONTENT_STORAGE,
- default: null
- );
-
- if (is_string($value) === true && $value !== '') {
- return $value;
- }
-
- return self::STORAGE_DATABASE;
- }//end getContentStorage()
-
/**
* Whether the wizard has been completed at least once.
*
@@ -203,21 +124,15 @@ private function isWizardComplete(): bool {
private function computeStepStatuses(bool $complete): array {
$settings = $this->settingMapper->getAllAsArray();
- $hasStorage = isset($settings[AdminSetting::KEY_CONTENT_STORAGE]);
$groupOrder = ($settings[AdminSetting::KEY_GROUP_ORDER] ?? null);
$hasGroup = (is_array($groupOrder) === true && count($groupOrder) > 0);
$hasFooter = isset($settings[AdminSetting::KEY_FOOTER_CONFIG]);
- // Steps 4 (demos) and 5 (admin-roles) live in sibling capabilities
+ // Steps 3 (demos) and 4 (admin-roles) live in sibling capabilities
// not yet implemented in this branch; surface them as `'skipped'`
// so the wizard advances cleanly when the embed component is a
// local stub. They flip to `'done'` once the sibling capability
// ships and writes its own settings.
- $statusStorage = 'pending';
- if ($hasStorage === true) {
- $statusStorage = 'done';
- }
-
$statusGroup = 'pending';
if ($hasGroup === true) {
$statusGroup = 'done';
@@ -235,12 +150,11 @@ private function computeStepStatuses(bool $complete): array {
return [
'1' => 'done',
- '2' => $statusStorage,
- '3' => $statusGroup,
+ '2' => $statusGroup,
+ '3' => 'skipped',
'4' => 'skipped',
- '5' => 'skipped',
- '6' => $statusFooter,
- '7' => $statusDone,
+ '5' => $statusFooter,
+ '6' => $statusDone,
];
}//end computeStepStatuses()
diff --git a/openspec/specs/admin-settings/spec.md b/openspec/specs/admin-settings/spec.md
index 3a4bab5ca..d2af62d18 100644
--- a/openspec/specs/admin-settings/spec.md
+++ b/openspec/specs/admin-settings/spec.md
@@ -28,7 +28,6 @@ Settings are stored as key-value pairs:
| `default_permission_level` | `defaultPermissionLevel` | string | `add_only` | Default permission level for user-created dashboards |
| `default_grid_columns` | `defaultGridColumns` | integer | `12` | Default number of grid columns for new dashboards |
| `link_create_file_extensions` | `linkCreateFileExtensions` | `string[]` | `["txt","md","docx","xlsx","csv","odt"]` | Extensions the link-button widget's "create file" action may create (REQ-LBN-004). An empty stored list resolves to the default. |
-| `content_storage` | `launchpad.content_storage` | string | `database` | Active content storage backend (REQ-GFSB-006). The response key is dotted, unlike the others. |
| `default_share_permission_level` | `defaultSharePermissionLevel` | string | `add_only` | Org-wide default permission for a new share (dashboard-sharing spec) |
| `forced_share_groups` | `forcedShareGroups` | `string[]` | `[]` | Groups every new dashboard is force-shared with (dashboard-sharing spec) |
| `legacy_widget_bridge_enabled` | `legacyWidgetBridgeEnabled` | boolean | `false` | Whether the workspace reads Nextcloud's widget registry (legacy-widget-bridge spec) |
@@ -42,12 +41,12 @@ NOTE: The DB stores settings with snake_case keys, but the API response returns
## Requirements
### Requirement: Retrieve Admin Settings (REQ-ASET-001)
-Administrators MUST be able to retrieve all current admin settings via the API. The endpoint returns a flat JSON object with the thirteen settings listed under Defined Settings, each under its API response key. `group_order` is not among them; it has its own endpoints (REQ-ASET-012).
+Administrators MUST be able to retrieve all current admin settings via the API. The endpoint returns a flat JSON object with the twelve settings listed under Defined Settings, each under its API response key. `group_order` is not among them; it has its own endpoints (REQ-ASET-012).
#### Scenario: Get all settings with defaults
- GIVEN no admin settings have been explicitly configured (fresh installation)
- WHEN the admin sends GET /api/admin/settings
-- THEN the system MUST return HTTP 200 with all thirteen settings at their default values:
+- THEN the system MUST return HTTP 200 with all twelve settings at their default values:
```json
{
"defaultPermissionLevel": "add_only",
@@ -55,7 +54,6 @@ Administrators MUST be able to retrieve all current admin settings via the API.
"allowMultipleDashboards": true,
"defaultGridColumns": 12,
"linkCreateFileExtensions": ["txt", "md", "docx", "xlsx", "csv", "odt"],
- "launchpad.content_storage": "database",
"defaultSharePermissionLevel": "add_only",
"forcedShareGroups": [],
"legacyWidgetBridgeEnabled": false,
@@ -88,11 +86,17 @@ Administrators MUST be able to retrieve all current admin settings via the API.
#### Scenario: Settings response format consistency
- GIVEN the admin has configured various settings at different times
- WHEN GET /api/admin/settings is called
-- THEN the response MUST always return exactly the thirteen keys listed under Defined Settings: `defaultPermissionLevel`, `allowUserDashboards`, `allowMultipleDashboards`, `defaultGridColumns`, `linkCreateFileExtensions`, `launchpad.content_storage`, `defaultSharePermissionLevel`, `forcedShareGroups`, `legacyWidgetBridgeEnabled`, `maxDashboardsPerUser`, `maxWidgetsPerDashboard`, `quicksearchFallbackTarget`, `startPageWithoutNavigation`
+- THEN the response MUST always return exactly the twelve keys listed under Defined Settings: `defaultPermissionLevel`, `allowUserDashboards`, `allowMultipleDashboards`, `defaultGridColumns`, `linkCreateFileExtensions`, `defaultSharePermissionLevel`, `forcedShareGroups`, `legacyWidgetBridgeEnabled`, `maxDashboardsPerUser`, `maxWidgetsPerDashboard`, `quicksearchFallbackTarget`, `startPageWithoutNavigation`
- AND no additional keys MUST be present in the response
- NOTE: This scenario said "exactly four keys" long after the response had grown to twelve. The list above is taken from `AdminSettingsService::getSettings()`; a key added there MUST be added here and to the Defined Settings table.
- AND the response MUST be a flat JSON object (no nesting)
+#### Scenario: The retired content storage key is not returned
+- GIVEN an instance that still holds a `content_storage` row written by the retired setup-wizard storage step
+- WHEN the admin sends GET /api/admin/settings
+- THEN the response MUST NOT contain `launchpad.content_storage`
+- AND the stored row MUST stay unread (decision 131)
+
### Requirement: Update Admin Settings (REQ-ASET-002)
Administrators MUST be able to update individual or multiple admin settings in a single PUT request.
diff --git a/openspec/specs/groupfolder-storage-backend/spec.md b/openspec/specs/groupfolder-storage-backend/spec.md
index 7540bb8ec..386f3c67c 100644
--- a/openspec/specs/groupfolder-storage-backend/spec.md
+++ b/openspec/specs/groupfolder-storage-backend/spec.md
@@ -67,16 +67,10 @@ factory was reachable only through the three orphaned facade methods.
The `launchpad.content_storage` admin setting, `AdminSettingKey::CONTENT_STORAGE`,
`SetupWizardService::{getContentStorage,setContentStorage,hasGroupfolderApp}`,
`AdminController::setWizardStorage`, its route, and step 2 of the setup wizard
-are **retained**. They belong to the `setup-wizard` capability, which *writes*
-the setting; this capability was the *reader*. The setting was already inert
-before this change — nothing read it, because the readers were orphaned — so
-removing the reader does not make it worse.
-
-Retiring the wizard step is a separate, reviewable decision that changes the
-`setup-wizard` spec, and it is tracked on launchpad#87 rather than folded into
-this retirement. Anyone completing that follow-up should note that
-`tools/spec-annotations-allowlist.txt` carries entries for those three
-`SetupWizardService` methods and for `AdminController::setWizardStorage`.
+were retained by this retirement, because they belonged to the `setup-wizard`
+capability. They were removed afterwards, on 9 Oct 2026, by the change
+`retire-setup-wizard-storage-step` (decision 131). A `content_storage` row
+already stored stays in the table, unread.
`lib/Migration/Version002001Date20260603000000.php` and
`DashboardTableBuilder::addContentStorageColumns()` are also retained: a shipped
diff --git a/openspec/specs/setup-wizard/spec.md b/openspec/specs/setup-wizard/spec.md
index 278650eef..ec4d119bc 100644
--- a/openspec/specs/setup-wizard/spec.md
+++ b/openspec/specs/setup-wizard/spec.md
@@ -6,7 +6,7 @@ status: done
## Purpose
-The Setup Wizard is a multi-step first-run configuration flow for freshly installed LaunchPad instances. It guides administrators through selecting a storage backend, setting group priority order, installing optional demo data, assigning admin roles, and configuring footer content. The wizard detects first-run state via an admin-setting flag, supports both interactive and non-interactive (CLI) flows, and ensures all choices are persisted immediately so progress is not lost.
+The Setup Wizard is a multi-step first-run configuration flow for freshly installed LaunchPad instances. It guides administrators through setting group priority order, installing optional demo data, assigning admin roles, and configuring footer content. The wizard detects first-run state via an admin-setting flag, supports both interactive and non-interactive (CLI) flows, and ensures all choices are persisted immediately so progress is not lost.
## Data Model
@@ -16,12 +16,13 @@ The setup wizard does not define a new persistent table; it tracks state via exi
Wizard progress (per-step completion status) is derived heuristically from the state of underlying settings:
- Step 1 (Welcome): always "done" (no choice to persist)
-- Step 2 (Storage): "done" if `launchpad.content_storage` is set; "pending" otherwise
-- Step 3 (Group order): "done" if `launchpad.group_priority_order` is set; "pending" otherwise
-- Step 4 (Demo data): "done" if at least one demo package has been installed; "pending" otherwise
-- Step 5 (Admin roles): "skipped" if not visited; "done" if a "Dashboard Admin" role assignment exists
-- Step 6 (Footer): "skipped" if not visited; "done" if `launchpad.footer_config` is set
-- Step 7 (Done): "pending" until wizard completes
+- Step 2 (Group order): "done" if `launchpad.group_priority_order` is set; "pending" otherwise
+- Step 3 (Demo data): "done" if at least one demo package has been installed; "pending" otherwise
+- Step 4 (Admin roles): "skipped" if not visited; "done" if a "Dashboard Admin" role assignment exists
+- Step 5 (Footer): "skipped" if not visited; "done" if `launchpad.footer_config` is set
+- Step 6 (Done): "pending" until wizard completes
+
+The storage step that sat at Step 2 was retired on 9 Oct 2026 (decision 131, change `retire-setup-wizard-storage-step`). A `content_storage` row it wrote stays in the table and is never read.
## Requirements
@@ -49,7 +50,7 @@ The system MUST detect whether a LaunchPad instance is being initialized for the
#### Scenario: Banner includes descriptive text
- GIVEN a fresh LaunchPad instance showing the setup banner
- WHEN the admin views the banner
-- THEN the banner MUST include text explaining: "Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles."
+- THEN the banner MUST include text explaining: "Get your intranet started: configure groups, install demo data, and set up admin roles."
- NOTE: Banner style and wording are implementation-specific; requirement mandates presence and visibility only
#### Scenario: Banner has explicit "Run setup wizard" call-to-action
@@ -62,11 +63,11 @@ The system MUST detect whether a LaunchPad instance is being initialized for the
- WHEN the admin returns to `/apps/launchpad/admin/dashboards`
- THEN if `launchpad.setup_wizard_complete = false`, the banner MUST reappear
-### Requirement: REQ-WIZ-002 Multi-Step Wizard Flow with 7 Steps
+### Requirement: REQ-WIZ-002 Multi-Step Wizard Flow with 6 Steps
-The wizard MUST guide the admin through a linear, skippable sequence of 7 steps, each with Skip / Back / Next buttons. Only the "Finish" button on Step 7 marks the wizard complete.
+The wizard MUST guide the admin through a linear, skippable sequence of 6 steps, each with Skip / Back / Next buttons. Only the "Finish" button on Step 6 marks the wizard complete.
-> NOTE: Steps 2–6 each embed the canonical admin UI component from the corresponding sibling capability (`groupfolder-storage-backend`, `group-priority-order`, `demo-data-showcases`, `admin-roles`, `footer-customization`). The wizard is a thin orchestrator — it owns step sequencing and the completion flag; each sibling capability owns its own settings persistence. No settings surfaces are duplicated.
+> NOTE: Steps 2–5 each embed the canonical admin UI component from the corresponding sibling capability (`group-priority-order`, `demo-data-showcases`, `admin-roles`, `footer-customization`). The wizard is a thin orchestrator — it owns step sequencing and the completion flag; each sibling capability owns its own settings persistence. No settings surfaces are duplicated.
#### Scenario: Wizard starts at Step 1
- GIVEN the banner's "Run setup wizard" button is clicked
@@ -77,7 +78,7 @@ The wizard MUST guide the admin through a linear, skippable sequence of 7 steps,
#### Scenario: Next button advances to next step
- GIVEN the wizard is on Step 1
- WHEN the admin clicks "Next"
-- THEN the wizard advances to Step 2 (Storage backend)
+- THEN the wizard advances to Step 2 (Group order)
- AND the step counter MUST show "2 / 7"
- AND the Back button MUST become active
@@ -88,200 +89,163 @@ The wizard MUST guide the admin through a linear, skippable sequence of 7 steps,
- AND no data loss occurs (Step 3 values are preserved if re-visited)
#### Scenario: Skip button jumps to next step without commitment
-- GIVEN the wizard is on Step 5 (Admin roles, optional)
+- GIVEN the wizard is on Step 4 (Admin roles, optional)
- WHEN the admin clicks "Skip"
-- THEN the wizard advances to Step 6
-- AND the wizard completion status for Step 5 is marked "skipped" (not "done")
+- THEN the wizard advances to Step 5
+- AND the wizard completion status for Step 4 is marked "skipped" (not "done")
- NOTE: Skipping a step does not prevent the wizard from eventually marking complete
#### Scenario: Step 1 has no form; only explanatory text
- GIVEN Step 1 (Welcome) is displayed
- WHEN the admin views the step
-- THEN the step MUST show text explaining the wizard's purpose: "Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings."
+- THEN the step MUST show text explaining the wizard's purpose: "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings."
- AND there are no form fields to fill
- AND clicking "Next" advances without persisting any choice
-#### Scenario: Step 7 shows summary and Finish button
-- GIVEN the wizard reaches Step 7 (Done)
+#### Scenario: Step 6 shows summary and Finish button
+- GIVEN the wizard reaches Step 6 (Done)
- WHEN the admin views the step
- THEN the step MUST display a summary: "Your setup is complete. Click Finish to save changes and dismiss the setup banner."
- AND a "Finish" button (not "Next") MUST be present
- AND clicking "Finish" executes the completion action
#### Scenario: Finish button completes wizard
-- GIVEN the admin is on Step 7 with the "Finish" button visible
+- GIVEN the admin is on Step 6 with the "Finish" button visible
- WHEN the admin clicks "Finish"
- THEN the system MUST set `launchpad.setup_wizard_complete = true`
- AND the wizard modal MUST close
- AND the admin section MUST reload, banner gone
#### Scenario: Wizard progress persists across Back navigation
-- GIVEN the wizard is on Step 3 with group order selected
-- WHEN the admin clicks Back to Step 2, modifies storage backend, then clicks Next to Step 3
-- THEN the group order selection from Step 3 MUST still be present (no data loss)
+- GIVEN the wizard is on Step 3 after a group order was chosen on Step 2
+- WHEN the admin clicks Back to Step 2, then clicks Next to Step 3
+- THEN the group order chosen on Step 2 MUST still be present (no data loss)
-### Requirement: REQ-WIZ-003 Step 2 — Storage Backend Choice with GroupFolder Dependency Tooltip
+### Requirement: REQ-WIZ-004 Step 2 — Group Priority Order (Embedded Component Reuse)
-Step 2 MUST present two radio options: "Database (default)" and "GroupFolder (recommended for org use)". The GroupFolder option MUST be disabled with a tooltip if the `groupfolders` Nextcloud app is not installed.
+Step 2 MUST embed the existing `group-priority-order` admin UI component, allowing the admin to configure the priority order of Nextcloud groups for dashboard routing.
-#### Scenario: Storage backend selection shows two options
+#### Scenario: Step 2 embeds existing group-priority-order admin component
- GIVEN Step 2 is displayed
- WHEN the admin views the step
-- THEN two radio buttons MUST be visible:
- - "Database (default)" — with description "Store dashboard content in LaunchPad database table"
- - "GroupFolder (recommended for org use)" — with description "Store dashboard content in Nextcloud GroupFolders for collaborative access"
-- AND exactly one option MUST be pre-selected (default to "Database")
-
-#### Scenario: GroupFolder option disabled if app not installed
-- GIVEN the `groupfolders` Nextcloud app is NOT installed
-- WHEN Step 2 is displayed
-- THEN the "GroupFolder" radio option MUST be disabled (grayed out)
-- AND a tooltip MUST appear on hover: "GroupFolder app is not installed. Install 'Nextcloud GroupFolders' to use this option."
-- NOTE: Disabled state prevents selection; tooltip explains why
-
-#### Scenario: GroupFolder option enabled if app installed
-- GIVEN the `groupfolders` Nextcloud app IS installed
-- WHEN Step 2 is displayed
-- THEN the "GroupFolder" radio option MUST be enabled
-- AND the option is selectable
-
-#### Scenario: Storage choice persists immediately
-- GIVEN Step 2 is displayed with "Database" selected
-- WHEN the admin selects "GroupFolder" and clicks Next
-- THEN the system MUST immediately write `launchpad.content_storage = "groupfolder"` to admin settings
-- AND if the admin navigates away (Back, Browser close, etc.), the choice MUST be persisted (not lost)
-
-#### Scenario: Default is Database
-- GIVEN a fresh instance where no storage backend has been chosen
-- WHEN Step 2 is displayed
-- THEN "Database (default)" MUST be pre-selected
-- NOTE: This reflects the safe, always-available default
-
-### Requirement: REQ-WIZ-004 Step 3 — Group Priority Order (Embedded Component Reuse)
-
-Step 3 MUST embed the existing `group-priority-order` admin UI component, allowing the admin to configure the priority order of Nextcloud groups for dashboard routing.
-
-#### Scenario: Step 3 embeds existing group-priority-order admin component
-- GIVEN Step 3 is displayed
-- WHEN the admin views the step
- THEN the existing `group-priority-order` admin UI MUST be visible and functional within the wizard modal
- NOTE: The component is not duplicated; the wizard embeds the canonical admin component
#### Scenario: Group order choice persists immediately
-- GIVEN the admin reorders groups in Step 3 (e.g., "engineering" moved above "sales")
+- GIVEN the admin reorders groups in Step 2 (e.g., "engineering" moved above "sales")
- WHEN the admin clicks Next
- THEN the system MUST immediately persist the new order to `launchpad.group_priority_order`
- AND navigating away does not lose the change
-#### Scenario: Step 3 allows drag-and-drop reordering (existing UI behavior)
+#### Scenario: Step 2 allows drag-and-drop reordering (existing UI behavior)
- GIVEN the group-priority-order component is displayed
- WHEN the admin drags a group to a new position
- THEN the reorder MUST be reflected in the UI
- NOTE: Exact interaction (drag, arrow buttons, etc.) is determined by the embedded component's design
-### Requirement: REQ-WIZ-005 Step 4 — Demo Data Installation (Embedded Component Reuse)
+### Requirement: REQ-WIZ-005 Step 3 — Demo Data Installation (Embedded Component Reuse)
-Step 4 MUST embed the existing `demo-data-showcases` admin UI component, allowing the admin to optionally select and install demo packages.
+Step 3 MUST embed the existing `demo-data-showcases` admin UI component, allowing the admin to optionally select and install demo packages.
-#### Scenario: Step 4 embeds demo-data-showcases admin component
-- GIVEN Step 4 is displayed
+#### Scenario: Step 3 embeds demo-data-showcases admin component
+- GIVEN Step 3 is displayed
- WHEN the admin views the step
- THEN the existing `demo-data-showcases` admin UI MUST be visible within the wizard modal
- AND the component displays all available demo packages with checkboxes
- NOTE: The component is the canonical admin UI; wizard embeds it without duplication
#### Scenario: Demo data installation is optional
-- GIVEN Step 4 is displayed with zero demo packages selected
+- GIVEN Step 3 is displayed with zero demo packages selected
- WHEN the admin clicks "Skip" or "Next"
- THEN the wizard MUST advance without requiring any demo to be installed
-- NOTE: Step 4 is not skippable via a Skip button; all steps are traversed. However, selecting zero demos and clicking Next is equivalent to skipping.
+- NOTE: Step 3 is not skippable via a Skip button; all steps are traversed. However, selecting zero demos and clicking Next is equivalent to skipping.
#### Scenario: Checked demos are installed on Next
-- GIVEN the admin selects checkboxes for "Engineering Demo" and "Sales Demo" on Step 4
+- GIVEN the admin selects checkboxes for "Engineering Demo" and "Sales Demo" on Step 3
- WHEN the admin clicks "Next"
- THEN the system MUST trigger the installation of both demo packages
-- AND the wizard advances to Step 5 (installation happens asynchronously or blocks until done, implementation choice)
+- AND the wizard advances to Step 4 (installation happens asynchronously or blocks until done, implementation choice)
- NOTE: The demo-data-showcases component handles the actual installation logic
#### Scenario: Demo selection is persisted
-- GIVEN the admin selected "Engineering Demo" on Step 4 and advanced to Step 5
-- WHEN the admin navigates Back to Step 4
+- GIVEN the admin selected "Engineering Demo" on Step 3 and advanced to Step 4
+- WHEN the admin navigates Back to Step 3
- THEN the "Engineering Demo" checkbox MUST remain checked
- AND no re-installation happens (idempotent)
-### Requirement: REQ-WIZ-006 Step 5 — Admin Roles (Optional, Depends on admin-roles Capability)
+### Requirement: REQ-WIZ-006 Step 4 — Admin Roles (Optional, Depends on admin-roles Capability)
-Step 5 MUST allow the admin to optionally assign the "Dashboard Admin" role to one Nextcloud group. This step is skippable via a Skip button.
+Step 4 MUST allow the admin to optionally assign the "Dashboard Admin" role to one Nextcloud group. This step is skippable via a Skip button.
-#### Scenario: Step 5 shows group selector for admin role
+#### Scenario: Step 4 shows group selector for admin role
- GIVEN the `admin-roles` capability is available
-- AND Step 5 is displayed
+- AND Step 4 is displayed
- WHEN the admin views the step
- THEN a dropdown or multi-select MUST display all Nextcloud groups
- AND a description MUST explain: "Assign 'Dashboard Admin' role to a group to delegate LaunchPad administration to group members."
#### Scenario: Admin can select one group for Dashboard Admin role
-- GIVEN Step 5 is displayed with groups ["engineering", "sales", "marketing"]
+- GIVEN Step 4 is displayed with groups ["engineering", "sales", "marketing"]
- WHEN the admin selects "engineering"
- AND clicks "Next"
- THEN the system MUST call `RoleService::assignRole(groupId="engineering", role="admin", assignedBy=)`
- AND the role assignment MUST be created in `oc_launchpad_role_assignments`
-#### Scenario: Step 5 is skippable
-- GIVEN Step 5 is displayed
+#### Scenario: Step 4 is skippable
+- GIVEN Step 4 is displayed
- WHEN the admin clicks "Skip"
- THEN no role assignment is made
-- AND the wizard advances to Step 6
-- AND the completion status for Step 5 MUST be marked "skipped"
+- AND the wizard advances to Step 5
+- AND the completion status for Step 4 MUST be marked "skipped"
#### Scenario: No group selected is equivalent to skip
-- GIVEN Step 5 with no group selected
+- GIVEN Step 4 with no group selected
- WHEN the admin clicks "Next"
- THEN no role assignment is made (equivalent to Skip)
-- AND the wizard advances to Step 6
+- AND the wizard advances to Step 5
-#### Scenario: Step 5 unavailable if admin-roles capability missing
+#### Scenario: Step 4 unavailable if admin-roles capability missing
- GIVEN the `admin-roles` capability is NOT implemented or not available
-- WHEN the wizard would display Step 5
-- THEN Step 5 MUST be skipped automatically
-- AND the step counter jumps from Step 4 to Step 6
+- WHEN the wizard would display Step 4
+- THEN Step 4 MUST be skipped automatically
+- AND the step counter jumps from Step 3 to Step 5
- NOTE: Capability dependency is gracefully handled; missing capability does not break wizard
#### Scenario: Only one group can have admin role per wizard run
-- GIVEN Step 5 is displayed
+- GIVEN Step 4 is displayed
- WHEN the admin selects "engineering" and clicks "Next"
- THEN only the "engineering" group receives the admin role (not multiple groups)
- NOTE: If the admin wishes to assign admin role to multiple groups, they must run the wizard again or use the admin roles management UI directly
-### Requirement: REQ-WIZ-007 Step 6 — Footer Configuration (Optional, Depends on footer-customization Capability)
+### Requirement: REQ-WIZ-007 Step 5 — Footer Configuration (Optional, Depends on footer-customization Capability)
-Step 6 MUST allow the admin to optionally open and configure the footer using the "Structured mode" editor. This step is skippable via a Skip button.
+Step 5 MUST allow the admin to optionally open and configure the footer using the "Structured mode" editor. This step is skippable via a Skip button.
-#### Scenario: Step 6 shows footer editor
+#### Scenario: Step 5 shows footer editor
- GIVEN the `footer-customization` capability is available
-- AND Step 6 is displayed
+- AND Step 5 is displayed
- WHEN the admin views the step
- THEN the "Structured mode" footer editor MUST be visible and functional
- AND a description MUST explain: "Customize the footer content and appearance for your intranet."
#### Scenario: Footer edits persist immediately
-- GIVEN the admin edits footer content in Step 6
+- GIVEN the admin edits footer content in Step 5
- WHEN the admin clicks "Next"
- THEN the footer configuration MUST be saved to `launchpad.footer_config` or equivalent setting
- AND navigating away does not lose the changes
-#### Scenario: Step 6 is skippable
-- GIVEN Step 6 is displayed
+#### Scenario: Step 5 is skippable
+- GIVEN Step 5 is displayed
- WHEN the admin clicks "Skip"
- THEN no footer changes are made
-- AND the wizard advances to Step 7
-- AND the completion status for Step 6 MUST be marked "skipped"
+- AND the wizard advances to Step 6
+- AND the completion status for Step 5 MUST be marked "skipped"
-#### Scenario: Step 6 unavailable if footer-customization capability missing
+#### Scenario: Step 5 unavailable if footer-customization capability missing
- GIVEN the `footer-customization` capability is NOT implemented or not available
-- WHEN the wizard would display Step 6
-- THEN Step 6 MUST be skipped automatically
-- AND the step counter jumps from Step 5 to Step 7
+- WHEN the wizard would display Step 5
+- THEN Step 5 MUST be skipped automatically
+- AND the step counter jumps from Step 4 to Step 6
- NOTE: Capability dependency is gracefully handled; missing capability does not break wizard
### Requirement: REQ-WIZ-008 Get Wizard State via API Endpoint
@@ -304,17 +268,16 @@ The system MUST expose a `GET /api/admin/setup-wizard/state` endpoint that retur
"3": "pending",
"4": "pending",
"5": "pending",
- "6": "pending",
- "7": "pending"
+ "6": "pending"
}
}
```
-- NOTE: Step 1 is always "done" (no choice required). Steps 2–7 start "pending".
+- NOTE: Step 1 is always "done" (no choice required). Steps 2–6 start "pending".
#### Scenario: Endpoint returns recommended step for re-runs
-- GIVEN a LaunchPad instance with storage backend set but group order not yet set
+- GIVEN a LaunchPad instance where the group order is not yet set
- WHEN an NC admin calls `GET /api/admin/setup-wizard/state`
-- THEN the response MUST include `currentRecommendedStep: 3`
+- THEN the response MUST include `currentRecommendedStep: 2`
- NOTE: The heuristic picks the first step with status != "done"
#### Scenario: Endpoint returns complete=true when wizard finished
@@ -329,11 +292,17 @@ The system MUST expose a `GET /api/admin/setup-wizard/state` endpoint that retur
- THEN the system MUST return HTTP 403 (Forbidden)
#### Scenario: Endpoint includes skipped step status
-- GIVEN the wizard has been run with Step 5 skipped
+- GIVEN the wizard has been run with Step 4 skipped
- WHEN an NC admin calls `GET /api/admin/setup-wizard/state`
-- THEN the response MUST include `"5": "skipped"` in stepStatuses
+- THEN the response MUST include `"4": "skipped"` in stepStatuses
- NOTE: Skipped status indicates the step was traversed but no action taken; does not block wizard completion
+#### Scenario: A stored storage choice from the retired step is ignored
+- GIVEN an instance that still holds a `content_storage` row written by the retired storage step
+- WHEN an NC admin calls `GET /api/admin/setup-wizard/state`
+- THEN `stepStatuses` MUST hold exactly six entries
+- AND the stored row MUST NOT change any step status or `currentRecommendedStep`
+
### Requirement: REQ-WIZ-009 Mark Wizard Complete via API Endpoint
The system MUST expose a `POST /api/admin/setup-wizard/complete` endpoint that idempotently sets `launchpad.setup_wizard_complete = true` and returns the current wizard state.
@@ -367,12 +336,11 @@ The system MUST expose a `POST /api/admin/setup-wizard/complete` endpoint that i
The system MUST expose a CLI command `php occ launchpad:setup` that accepts a `--config=/path/setup.yaml` argument to perform wizard setup non-interactively, ideal for Infrastructure-as-Code deployments.
-> NOTE: The YAML config schema (fields: `storage_backend`, `group_priority_order`, `demo_packages`, `admin_role_group`, `footer_config`) is formally defined in the `cli-commands` sibling spec. Optional steps (admin roles, footer) may be omitted from the config file entirely; omitting them is equivalent to skipping those steps in the wizard and does not cause an error.
+> NOTE: The YAML config schema (fields: `group_priority_order`, `demo_packages`, `admin_role_group`, `footer_config`) is formally defined in the `cli-commands` sibling spec. Optional steps (admin roles, footer) may be omitted from the config file entirely; omitting them is equivalent to skipping those steps in the wizard and does not cause an error.
#### Scenario: CLI command accepts YAML config file
- GIVEN a YAML file `/tmp/setup.yaml` with content:
```yaml
- storage_backend: "groupfolder"
group_priority_order: ["engineering", "sales"]
demo_packages: ["engineering-demo", "sales-demo"]
admin_role_group: "engineering"
@@ -382,31 +350,36 @@ The system MUST expose a CLI command `php occ launchpad:setup` that accepts a `-
```
- WHEN an admin runs `php occ launchpad:setup --config=/tmp/setup.yaml`
- THEN the system MUST:
- - Set `launchpad.content_storage = "groupfolder"` (Step 2)
- - Set `launchpad.group_priority_order = ["engineering", "sales"]` (Step 3)
- - Install "engineering-demo" and "sales-demo" (Step 4)
- - Assign "Dashboard Admin" role to "engineering" (Step 5)
- - Set `launchpad.footer_config` with provided config (Step 6)
- - Set `launchpad.setup_wizard_complete = true` (Step 7)
+ - Set `launchpad.group_priority_order = ["engineering", "sales"]` (Step 2)
+ - Install "engineering-demo" and "sales-demo" (Step 3)
+ - Assign "Dashboard Admin" role to "engineering" (Step 4)
+ - Set `launchpad.footer_config` with provided config (Step 5)
+ - Set `launchpad.setup_wizard_complete = true` (Step 6)
#### Scenario: CLI command validates YAML schema
-- GIVEN a malformed YAML file (e.g., missing required fields)
+- GIVEN a YAML file whose top level is not a map, or whose `group_priority_order` is not a list
- WHEN `php occ launchpad:setup --config=/tmp/setup.yaml` is run
-- THEN the system MUST output an error: "Invalid setup.yaml: missing field 'storage_backend'"
+- THEN the system MUST output an error starting with "Invalid setup.yaml:"
- AND the command MUST exit with non-zero status
-- AND no settings are applied
+
+#### Scenario: An old config that still sets storage_backend keeps working
+- GIVEN a YAML file written for the seven-step wizard, with `storage_backend: "groupfolder"`
+- WHEN `php occ launchpad:setup --config=/tmp/setup.yaml` is run
+- THEN the command MUST print that `storage_backend` is no longer used and was ignored
+- AND it MUST apply the other steps and exit with status 0
+- AND it MUST NOT write `launchpad.content_storage`
#### Scenario: CLI command is verbose by default
- GIVEN the command runs successfully
- WHEN the output is examined
-- THEN it MUST show progress: "Step 1: Welcome... done", "Step 2: Storage backend... done", etc.
+- THEN it MUST show progress: "Step 1: Welcome... done", "Step 2: Group order... done", etc.
- AND final message: "Setup wizard completed successfully."
#### Scenario: CLI command skips optional steps if not in config
-- GIVEN a YAML file with only `storage_backend` and `group_priority_order`
+- GIVEN a YAML file with only `group_priority_order`
- WHEN the command runs
-- THEN Steps 5 and 6 MUST be skipped (no error)
-- AND Steps 2 and 3 MUST execute with provided values
+- THEN Steps 4 and 5 MUST be skipped (no error)
+- AND Step 2 MUST execute with the provided value
- AND the wizard completes
#### Scenario: CLI command is idempotent
@@ -424,27 +397,20 @@ The wizard MUST be re-runnable even after `launchpad.setup_wizard_complete = tru
- GIVEN a LaunchPad instance with `launchpad.setup_wizard_complete = true`
- WHEN an NC admin navigates to `/apps/launchpad/admin` and clicks "Run setup wizard again" (or uses a Re-run button in a separate admin UI section)
- THEN the wizard modal MUST open
-- AND all step values MUST reflect current settings (storage backend, group order, etc.)
+- AND all step values MUST reflect current settings (group order, demo data, etc.)
- AND the admin can make changes
-#### Scenario: Re-running from Step 2 updates storage backend
-- GIVEN the instance has `launchpad.content_storage = "database"`
-- AND the admin re-runs the wizard and navigates to Step 2
-- WHEN the admin selects "GroupFolder" and clicks Next
-- THEN the system MUST update `launchpad.content_storage = "groupfolder"`
-- AND the previous choice "database" is overwritten (not appended or merged)
-
#### Scenario: Re-running does not re-install demo packages
- GIVEN the instance has already installed "engineering-demo"
-- AND the admin re-runs the wizard to Step 4
+- AND the admin re-runs the wizard to Step 3
- WHEN the admin unchecks "engineering-demo" and clicks Next
- THEN the system MUST NOT re-install the demo (it's already there)
- NOTE: Demo installation is one-time; re-running the wizard does not trigger redundant installations. If the admin wishes to un-install, they must use a separate admin UI for demo management.
#### Scenario: Re-running preserves skipped steps
-- GIVEN the wizard was run previously with Step 5 skipped
+- GIVEN the wizard was run previously with Step 4 skipped
- AND the admin re-runs the wizard
-- WHEN Step 5 is displayed
+- WHEN Step 4 is displayed
- THEN the step MUST show "Skip" button as before
- AND if the admin clicks Skip again, the behavior is consistent with the first run
diff --git a/src/components/admin/AdminSettings.vue b/src/components/admin/AdminSettings.vue
index b700af41b..ed25f08d8 100644
--- a/src/components/admin/AdminSettings.vue
+++ b/src/components/admin/AdminSettings.vue
@@ -24,7 +24,7 @@
{{
t(
'launchpad',
- 'Get your intranet started: choose storage, configure groups, install demo data, and set up admin roles.',
+ 'Get your intranet started: configure groups, install demo data, and set up admin roles.',
)
}}
diff --git a/src/modals/SetupWizardModal.vue b/src/modals/SetupWizardModal.vue
index 3b5f9c418..8d196dd28 100644
--- a/src/modals/SetupWizardModal.vue
+++ b/src/modals/SetupWizardModal.vue
@@ -29,85 +29,14 @@
{{
t(
'launchpad',
- 'Configure your LaunchPad instance with storage, group ordering, demo data, admin roles, and footer settings.',
+ 'Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings.',
)
}}
{{
@@ -231,10 +160,11 @@ import { logger } from '../utils/logger.js'
* SetupWizardModal — multi-step first-run wizard (REQ-WIZ-002).
*
* The shell owns navigation state; per-step bodies inline simple UI for
- * Welcome / Storage / Done and embed canonical sibling components for
- * Group order. Steps 4-6 render lightweight stubs because their sibling
- * capabilities (demo-data-showcases, admin-roles, footer-customization)
- * ship on parallel branches; the merge agent reconciles by replacing
+ * Welcome / Done and embed canonical sibling components for Group order.
+ * The storage step was retired (decision 131). Steps 3-5 render
+ * lightweight stubs because their sibling capabilities
+ * (demo-data-showcases, admin-roles, footer-customization) ship on
+ * parallel branches; the merge agent reconciles by replacing
* the stub blocks with the real embeds.
*/
export default {
@@ -252,10 +182,8 @@ export default {
data() {
return {
currentStep: 1,
- totalSteps: 7,
+ totalSteps: 6,
loading: false,
- storage: 'database',
- groupfolderAvailable: false,
completed: false,
}
},
@@ -276,8 +204,6 @@ export default {
try {
const { data } = await api.getSetupWizardState()
if (data) {
- this.storage = data.contentStorage || 'database'
- this.groupfolderAvailable = !!data.groupfolderAvailable
if (data.currentRecommendedStep && !data.complete) {
this.currentStep = Math.max(
1,
@@ -287,7 +213,7 @@ export default {
}
} catch (error) {
// REQ-WIZ-008: a failed state read MUST NOT block the wizard.
- // Default to Step 1 with a Database default.
+ // Default to Step 1.
logger.warn('Failed to load wizard state', error)
}
},
@@ -299,10 +225,6 @@ export default {
}
this.loading = true
try {
- if (this.currentStep === 2) {
- await api.setSetupWizardStorage(this.storage)
- }
-
if (this.isFinalStep) {
await api.completeSetupWizard()
this.completed = true
@@ -379,26 +301,6 @@ export default {
margin: 0 0 8px;
}
-.setup-wizard__radio {
- display: flex;
- gap: 12px;
- padding: 12px;
- border: 1px solid var(--color-border);
- border-radius: var(--border-radius);
- cursor: pointer;
-}
-
-.setup-wizard__radio--disabled {
- opacity: 0.5;
- cursor: not-allowed;
-}
-
-.setup-wizard__hint {
- color: var(--color-text-maxcontrast);
- margin: 4px 0 0;
- font-size: 13px;
-}
-
.setup-wizard__note {
padding: 12px;
background: var(--color-background-dark);
diff --git a/src/modals/__tests__/SetupWizardModal.spec.js b/src/modals/__tests__/SetupWizardModal.spec.js
index 1133ad915..34a28b231 100644
--- a/src/modals/__tests__/SetupWizardModal.spec.js
+++ b/src/modals/__tests__/SetupWizardModal.spec.js
@@ -3,9 +3,9 @@
* SPDX-License-Identifier: EUPL-1.2
*
* Vitest unit tests for `SetupWizardModal.vue`. Covers REQ-WIZ-002
- * (multi-step shell), REQ-WIZ-003 (storage backend persistence on
- * Step 2 → Next), REQ-WIZ-008 (state load on mount), and REQ-WIZ-009
- * (Finish triggers `completeSetupWizard`).
+ * (six-step shell; the storage step was retired by decision 131),
+ * REQ-WIZ-008 (state load on mount), and REQ-WIZ-009 (Finish triggers
+ * `completeSetupWizard`).
*
* The `api` module is mocked at the import boundary so no HTTP traffic
* is generated. The embedded `GroupPriorityOrder` is stubbed to avoid
@@ -20,7 +20,6 @@ import { api } from '../../services/api.js'
vi.mock('../../services/api.js', () => ({
api: {
getSetupWizardState: vi.fn(),
- setSetupWizardStorage: vi.fn(),
completeSetupWizard: vi.fn(),
},
}))
@@ -51,14 +50,9 @@ beforeEach(() => {
data: {
complete: false,
currentRecommendedStep: 1,
- contentStorage: 'database',
- groupfolderAvailable: true,
stepStatuses: { 1: 'done', 2: 'pending' },
},
})
- api.setSetupWizardStorage
- .mockReset()
- .mockResolvedValue({ data: { complete: false } })
api.completeSetupWizard
.mockReset()
.mockResolvedValue({ data: { complete: true } })
@@ -97,7 +91,7 @@ describe('SetupWizardModal', () => {
expect(wrapper.find('[data-test="setup-wizard-counter"]').exists()).toBe(
true,
)
- expect(wrapper.vm.totalSteps).toBe(7)
+ expect(wrapper.vm.totalSteps).toBe(6)
})
it('REQ-WIZ-002: Next advances; Back returns; counter updates', async () => {
@@ -112,30 +106,24 @@ describe('SetupWizardModal', () => {
expect(wrapper.vm.currentStep).toBe(1)
})
- it('REQ-WIZ-003: Step 2 Next persists the storage choice via the API', async () => {
+ it('decision 131: Step 2 is the group order and no storage step renders', async () => {
const wrapper = mountWizard()
await flush()
- // Advance to Step 2.
await wrapper.find('[data-test="setup-wizard-next"]').trigger('click')
await flush()
expect(wrapper.vm.currentStep).toBe(2)
-
- // Change selection then click Next.
- wrapper.vm.storage = 'groupfolder'
- await wrapper.find('[data-test="setup-wizard-next"]').trigger('click')
- await flush()
-
- expect(api.setSetupWizardStorage).toHaveBeenCalledWith('groupfolder')
- expect(wrapper.vm.currentStep).toBe(3)
+ expect(wrapper.find('.group-priority-stub').exists()).toBe(true)
+ expect(wrapper.find('[data-test="storage-database"]').exists()).toBe(false)
+ expect(wrapper.find('[data-test="storage-groupfolder"]').exists()).toBe(false)
})
- it('REQ-WIZ-002: Step 7 Next is labelled Finish and calls completeSetupWizard', async () => {
+ it('REQ-WIZ-002: Step 6 Next is labelled Finish and calls completeSetupWizard', async () => {
const wrapper = mountWizard()
await flush()
- // Jump straight to step 7 to keep the test focused.
- wrapper.vm.currentStep = 7
+ // Jump straight to step 6 to keep the test focused.
+ wrapper.vm.currentStep = 6
await flush()
// The Vue mixin's t() stub returns the bare key; the component
@@ -150,27 +138,6 @@ describe('SetupWizardModal', () => {
expect(wrapper.emitted('close')).toBeTruthy()
})
- it('REQ-WIZ-003: GroupFolder radio is disabled when the app is missing', async () => {
- api.getSetupWizardState.mockResolvedValueOnce({
- data: {
- complete: false,
- currentRecommendedStep: 1,
- contentStorage: 'database',
- groupfolderAvailable: false,
- stepStatuses: { 1: 'done' },
- },
- })
- const wrapper = mountWizard()
- await flush()
-
- // Advance to step 2.
- await wrapper.find('[data-test="setup-wizard-next"]').trigger('click')
- await flush()
-
- const radio = wrapper.find('[data-test="storage-groupfolder"]')
- expect(radio.attributes('disabled')).toBeDefined()
- })
-
it('REQ-WIZ-002: Skip advances without committing on optional steps', async () => {
const wrapper = mountWizard()
await flush()
@@ -179,6 +146,6 @@ describe('SetupWizardModal', () => {
await wrapper.find('[data-test="setup-wizard-skip"]').trigger('click')
expect(wrapper.vm.currentStep).toBe(5)
- expect(api.setSetupWizardStorage).not.toHaveBeenCalled()
+ expect(api.completeSetupWizard).not.toHaveBeenCalled()
})
})
diff --git a/src/services/api.js b/src/services/api.js
index 05ddcb2f4..22eccc485 100644
--- a/src/services/api.js
+++ b/src/services/api.js
@@ -952,17 +952,6 @@ export const api = {
return axios.post(`${baseUrl}/api/admin/setup-wizard/complete`)
},
- /**
- * Persist the setup wizard's content-storage choice (REQ-WIZ-003).
- *
- * @param {string} storage Backend to store widget content in —
- * `database` or `groupfolder`.
- * @return {Promise} Axios response resolving to the updated wizard state.
- */
- setSetupWizardStorage(storage) {
- return axios.post(`${baseUrl}/api/admin/setup-wizard/storage`, { storage })
- },
-
/**
* Export dashboards as a downloadable archive (REQ-EXIM-002..004).
* Requires Nextcloud-admin, gated server-side; the admin UI renders the
diff --git a/tests/Unit/Controller/AdminControllerSetupWizardTest.php b/tests/Unit/Controller/AdminControllerSetupWizardTest.php
index 94c2a8afb..b4fd958cf 100644
--- a/tests/Unit/Controller/AdminControllerSetupWizardTest.php
+++ b/tests/Unit/Controller/AdminControllerSetupWizardTest.php
@@ -3,9 +3,9 @@
/**
* AdminController Setup-Wizard Test
*
- * Covers the `getWizardState`, `completeWizard`, and `setWizardStorage`
- * endpoints added by the `setup-wizard` change (REQ-WIZ-008, REQ-WIZ-009,
- * REQ-WIZ-003).
+ * Covers the `getWizardState` and `completeWizard` endpoints added by the
+ * `setup-wizard` change (REQ-WIZ-008, REQ-WIZ-009), and pins that the
+ * retired storage-step endpoint stays gone (decision 131).
*
* @category Test
* @package OCA\LaunchPad\Tests\Unit\Controller
@@ -136,52 +136,13 @@ public function testCompleteWizardRejectsNonAdminWith403(): void {
$this->assertSame(Http::STATUS_FORBIDDEN, $response->getStatus());
}
- public function testSetWizardStorageRejectsEmptyValue(): void {
- $this->loginAsAdmin();
- $this->wizardService->expects($this->never())->method('setContentStorage');
-
- $response = $this->controller->setWizardStorage(storage: '');
-
- $this->assertSame(Http::STATUS_BAD_REQUEST, $response->getStatus());
- }
-
- public function testSetWizardStorageRejectsGroupfolderWhenAppMissing(): void {
- $this->loginAsAdmin();
- $this->wizardService->method('hasGroupfolderApp')->willReturn(false);
- $this->wizardService->expects($this->never())->method('setContentStorage');
-
- $response = $this->controller->setWizardStorage(storage: 'groupfolder');
-
- $this->assertSame(Http::STATUS_BAD_REQUEST, $response->getStatus());
- }
-
- public function testSetWizardStorageWritesAndReturnsState(): void {
- $this->loginAsAdmin();
- $this->wizardService->method('hasGroupfolderApp')->willReturn(true);
-
- $this->wizardService->expects($this->once())
- ->method('setContentStorage')
- ->with('groupfolder');
-
- $payload = [
- 'complete' => false,
- 'currentRecommendedStep' => 3,
- 'stepStatuses' => ['2' => 'done'],
- ];
- $this->wizardService->method('getWizardState')->willReturn($payload);
-
- $response = $this->controller->setWizardStorage(storage: 'groupfolder');
-
- $this->assertSame(Http::STATUS_OK, $response->getStatus());
- $this->assertSame($payload, $response->getData());
- }
-
- public function testSetWizardStorageReturns401WhenLoggedOut(): void {
- $this->userSession->method('getUser')->willReturn(null);
- $this->wizardService->expects($this->never())->method('setContentStorage');
-
- $response = $this->controller->setWizardStorage(storage: 'database');
+ public function testTheStorageStepEndpointIsRetired(): void {
+ // Decision 131: the wizard no longer asks where content is stored.
+ $this->assertFalse(method_exists(AdminController::class, 'setWizardStorage'));
- $this->assertSame(Http::STATUS_UNAUTHORIZED, $response->getStatus());
+ $routes = file_get_contents(__DIR__ . '/../../../appinfo/routes.php');
+ $this->assertIsString($routes);
+ $this->assertStringNotContainsString('setup-wizard/storage', $routes);
+ $this->assertStringNotContainsString('setWizardStorage', $routes);
}
}
diff --git a/tests/Unit/Db/AdminSettingKeyTest.php b/tests/Unit/Db/AdminSettingKeyTest.php
index 1d8d601e7..dd6f9c3a9 100644
--- a/tests/Unit/Db/AdminSettingKeyTest.php
+++ b/tests/Unit/Db/AdminSettingKeyTest.php
@@ -87,7 +87,6 @@ public static function bcAliasProvider(): array {
'FOOTER_BACKGROUND_COLOR' => [AdminSetting::KEY_FOOTER_BACKGROUND_COLOR, AdminSettingKey::FOOTER_BACKGROUND_COLOR],
'FOOTER_TEXT_COLOR' => [AdminSetting::KEY_FOOTER_TEXT_COLOR, AdminSettingKey::FOOTER_TEXT_COLOR],
'SETUP_WIZARD_COMPLETE' => [AdminSetting::KEY_SETUP_WIZARD_COMPLETE, AdminSettingKey::SETUP_WIZARD_COMPLETE],
- 'CONTENT_STORAGE' => [AdminSetting::KEY_CONTENT_STORAGE, AdminSettingKey::CONTENT_STORAGE],
];
}//end bcAliasProvider()
@@ -111,7 +110,6 @@ public function testFromWorksForEveryAlias(): void {
AdminSetting::KEY_FOOTER_BACKGROUND_COLOR,
AdminSetting::KEY_FOOTER_TEXT_COLOR,
AdminSetting::KEY_SETUP_WIZARD_COMPLETE,
- AdminSetting::KEY_CONTENT_STORAGE,
];
foreach ($aliases as $alias) {
@@ -120,4 +118,15 @@ public function testFromWorksForEveryAlias(): void {
$this->assertSame(expected: $alias, actual: $case->value);
}
}//end testFromWorksForEveryAlias()
+
+ /**
+ * Decision 131: the content storage key is retired; a stored row is
+ * left unread, so the enum must not resolve it.
+ *
+ * @return void
+ */
+ public function testContentStorageKeyIsRetired(): void {
+ $this->assertNull(AdminSettingKey::tryFrom('content_storage'));
+ $this->assertFalse(defined(AdminSetting::class . '::KEY_CONTENT_STORAGE'));
+ }//end testContentStorageKeyIsRetired()
}//end class
diff --git a/tests/Unit/Service/AdminSettingsServiceTest.php b/tests/Unit/Service/AdminSettingsServiceTest.php
index 131e203d3..a77e867e2 100644
--- a/tests/Unit/Service/AdminSettingsServiceTest.php
+++ b/tests/Unit/Service/AdminSettingsServiceTest.php
@@ -72,7 +72,6 @@ public function testGetSettingsReturnsExactlyTheDocumentedKeysAtTheirDefaults():
'allowMultipleDashboards' => true,
'defaultGridColumns' => 12,
'linkCreateFileExtensions' => ['txt', 'md', 'docx', 'xlsx', 'csv', 'odt'],
- 'launchpad.content_storage' => 'database',
'defaultSharePermissionLevel' => 'add_only',
'forcedShareGroups' => [],
'legacyWidgetBridgeEnabled' => false,
@@ -155,8 +154,8 @@ public function testGetSettingsReturnsCamelCaseKeys(): void {
$this->assertArrayHasKey('allowMultipleDashboards', $settings);
$this->assertArrayHasKey('defaultGridColumns', $settings);
$this->assertArrayHasKey('linkCreateFileExtensions', $settings);
- // REQ-GFSB-006: content storage backend key added.
- $this->assertArrayHasKey('launchpad.content_storage', $settings);
+ // Decision 131: the content storage key is retired.
+ $this->assertArrayNotHasKey('launchpad.content_storage', $settings);
// dashboard-sharing + legacy-widget-bridge spec keys.
$this->assertArrayHasKey('defaultSharePermissionLevel', $settings);
$this->assertArrayHasKey('forcedShareGroups', $settings);
diff --git a/tests/Unit/Service/SetupWizardServiceTest.php b/tests/Unit/Service/SetupWizardServiceTest.php
index ce7840d91..0b3f93f05 100644
--- a/tests/Unit/Service/SetupWizardServiceTest.php
+++ b/tests/Unit/Service/SetupWizardServiceTest.php
@@ -3,9 +3,9 @@
/**
* SetupWizardService Test
*
- * Covers REQ-WIZ-001 (state flag), REQ-WIZ-003 (storage backend
- * persistence + GroupFolder gate), REQ-WIZ-008 (wizard state heuristic),
- * and REQ-WIZ-009 (idempotent completion).
+ * Covers REQ-WIZ-001 (state flag), REQ-WIZ-008 (wizard state heuristic),
+ * REQ-WIZ-009 (idempotent completion) and the retired storage step
+ * (decision 131: six steps, a stored content_storage value stays unread).
*
* @category Test
* @package OCA\LaunchPad\Tests\Unit\Service
@@ -21,11 +21,9 @@
namespace Unit\Service;
-use InvalidArgumentException;
use OCA\LaunchPad\Db\AdminSetting;
use OCA\LaunchPad\Db\AdminSettingMapper;
use OCA\LaunchPad\Service\SetupWizardService;
-use OCP\App\IAppManager;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
@@ -35,15 +33,10 @@ class SetupWizardServiceTest extends TestCase {
/** @var AdminSettingMapper&MockObject */
private $settingMapper;
- /** @var IAppManager&MockObject */
- private $appManager;
-
protected function setUp(): void {
$this->settingMapper = $this->createMock(AdminSettingMapper::class);
- $this->appManager = $this->createMock(IAppManager::class);
$this->service = new SetupWizardService(
- settingMapper: $this->settingMapper,
- appManager: $this->appManager
+ settingMapper: $this->settingMapper
);
}
@@ -56,28 +49,59 @@ public function testGetWizardStateOnFreshInstance(): void {
$state = $this->service->getWizardState();
$this->assertFalse($state['complete']);
+ // Decision 131: the storage step is retired, so Step 2 is the
+ // group order and it is the first step left to do.
+ $this->assertSame(2, $state['currentRecommendedStep']);
+ $this->assertSame(
+ [
+ 1 => 'done',
+ 2 => 'pending',
+ 3 => 'skipped',
+ 4 => 'skipped',
+ 5 => 'skipped',
+ 6 => 'pending',
+ ],
+ $state['stepStatuses']
+ );
+ }
+
+ public function testWizardHasSixStepsWithoutAStorageStep(): void {
+ $this->assertSame(6, SetupWizardService::STEP_COUNT);
+ $this->assertFalse(method_exists(SetupWizardService::class, 'setContentStorage'));
+ $this->assertFalse(method_exists(SetupWizardService::class, 'getContentStorage'));
+ $this->assertFalse(method_exists(SetupWizardService::class, 'hasGroupfolderApp'));
+ }
+
+ public function testAStoredContentStorageValueIsLeftUnread(): void {
+ $this->settingMapper->method('getValue')
+ ->with(AdminSetting::KEY_SETUP_WIZARD_COMPLETE, false)
+ ->willReturn(false);
+ // A row written by the retired step stays in the table; the state
+ // must not change because of it.
+ $this->settingMapper->method('getAllAsArray')->willReturn([
+ 'content_storage' => 'groupfolder',
+ ]);
+
+ $state = $this->service->getWizardState();
+
$this->assertSame(2, $state['currentRecommendedStep']);
- $this->assertSame('done', $state['stepStatuses']['1']);
+ $this->assertCount(6, $state['stepStatuses']);
$this->assertSame('pending', $state['stepStatuses']['2']);
- $this->assertSame('pending', $state['stepStatuses']['3']);
- $this->assertSame('skipped', $state['stepStatuses']['4']);
- $this->assertSame('skipped', $state['stepStatuses']['5']);
- $this->assertSame('skipped', $state['stepStatuses']['6']);
- $this->assertSame('pending', $state['stepStatuses']['7']);
}
- public function testGetWizardStateAfterStorageWritten(): void {
+ public function testGetWizardStateAfterGroupOrderWritten(): void {
$this->settingMapper->method('getValue')
->with(AdminSetting::KEY_SETUP_WIZARD_COMPLETE, false)
->willReturn(false);
$this->settingMapper->method('getAllAsArray')->willReturn([
- AdminSetting::KEY_CONTENT_STORAGE => 'database',
+ AdminSetting::KEY_GROUP_ORDER => ['engineering'],
]);
$state = $this->service->getWizardState();
$this->assertSame('done', $state['stepStatuses']['2']);
- // Step 3 still pending so the recommended step jumps to 3.
+ // Steps 3 and 4 are 'skipped' (sibling capabilities pending), so
+ // the first non-'done' step is 3.
$this->assertSame(3, $state['currentRecommendedStep']);
}
@@ -86,7 +110,6 @@ public function testGetWizardStateAfterCompletion(): void {
->with(AdminSetting::KEY_SETUP_WIZARD_COMPLETE, false)
->willReturn(true);
$this->settingMapper->method('getAllAsArray')->willReturn([
- AdminSetting::KEY_CONTENT_STORAGE => 'database',
AdminSetting::KEY_GROUP_ORDER => ['engineering'],
AdminSetting::KEY_FOOTER_CONFIG => ['layout' => 'structured'],
]);
@@ -94,13 +117,13 @@ public function testGetWizardStateAfterCompletion(): void {
$state = $this->service->getWizardState();
$this->assertTrue($state['complete']);
- $this->assertSame('done', $state['stepStatuses']['7']);
$this->assertSame('done', $state['stepStatuses']['6']);
- // Steps 4/5 are 'skipped' (sibling capabilities pending), so the
- // first non-'done' status is Step 4. The wizard "complete" flag
+ $this->assertSame('done', $state['stepStatuses']['5']);
+ // Steps 3/4 are 'skipped' (sibling capabilities pending), so the
+ // first non-'done' status is Step 3. The wizard "complete" flag
// is the source of truth for hiding the banner; the recommended
// step is purely a UX hint per REQ-WIZ-008.
- $this->assertSame(4, $state['currentRecommendedStep']);
+ $this->assertSame(3, $state['currentRecommendedStep']);
}
public function testMarkWizardCompleteSetsFlagAndReturnsState(): void {
@@ -132,46 +155,4 @@ public function testMarkWizardCompleteIsIdempotent(): void {
$this->assertSame($first, $second);
}
-
- public function testGetGroupfolderAvailabilityDelegates(): void {
- $this->appManager
- ->expects($this->once())
- ->method('isInstalled')
- ->with('groupfolders')
- ->willReturn(true);
-
- $this->assertTrue($this->service->hasGroupfolderApp());
- }
-
- public function testSetContentStorageRejectsUnsupportedValue(): void {
- $this->settingMapper->expects($this->never())->method('setSetting');
- $this->expectException(InvalidArgumentException::class);
-
- $this->service->setContentStorage(value: 'cassette-tape');
- }
-
- public function testSetContentStoragePersistsKnownValues(): void {
- $this->settingMapper
- ->expects($this->once())
- ->method('setSetting')
- ->with(AdminSetting::KEY_CONTENT_STORAGE, 'groupfolder');
-
- $this->service->setContentStorage(value: SetupWizardService::STORAGE_GROUPFOLDER);
- }
-
- public function testGetContentStorageDefaultsToDatabase(): void {
- $this->settingMapper->method('getValue')
- ->with(AdminSetting::KEY_CONTENT_STORAGE, null)
- ->willReturn(null);
-
- $this->assertSame('database', $this->service->getContentStorage());
- }
-
- public function testGetContentStorageReturnsPersisted(): void {
- $this->settingMapper->method('getValue')
- ->with(AdminSetting::KEY_CONTENT_STORAGE, null)
- ->willReturn('groupfolder');
-
- $this->assertSame('groupfolder', $this->service->getContentStorage());
- }
}
diff --git a/tests/integration/launchpad.postman_collection.json b/tests/integration/launchpad.postman_collection.json
index cd18e12bb..1f5877a50 100644
--- a/tests/integration/launchpad.postman_collection.json
+++ b/tests/integration/launchpad.postman_collection.json
@@ -4818,48 +4818,6 @@
}
]
},
- {
- "name": "POST /api/admin/setup-wizard/storage",
- "request": {
- "method": "POST",
- "header": [
- {
- "key": "Content-Type",
- "value": "application/json"
- }
- ],
- "body": {
- "mode": "raw",
- "raw": "{\"storage\":\"openregister\"}"
- },
- "url": {
- "raw": "{{baseUrl}}/index.php/apps/launchpad/api/admin/setup-wizard/storage",
- "host": [
- "{{baseUrl}}"
- ],
- "path": [
- "index.php",
- "apps",
- "launchpad",
- "api",
- "admin",
- "setup-wizard",
- "storage"
- ]
- }
- },
- "event": [
- {
- "listen": "test",
- "script": {
- "type": "text/javascript",
- "exec": [
- "pm.test('2xx or 4xx', () => pm.expect([200, 204, 400, 403]).to.include(pm.response.code));"
- ]
- }
- }
- ]
- },
{
"name": "POST /api/admin/setup-wizard/complete",
"request": {
diff --git a/tools/spec-annotations-allowlist.txt b/tools/spec-annotations-allowlist.txt
index b772a7cf2..03085f434 100644
--- a/tools/spec-annotations-allowlist.txt
+++ b/tools/spec-annotations-allowlist.txt
@@ -39,7 +39,6 @@ OCA\LaunchPad\Controller\AdminController::refreshFeedsNow
OCA\LaunchPad\Controller\AdminController::uploadTemplatePreviewImage
OCA\LaunchPad\Controller\AdminController::getWizardState
OCA\LaunchPad\Controller\AdminController::completeWizard
-OCA\LaunchPad\Controller\AdminController::setWizardStorage
OCA\LaunchPad\Controller\AdminDemoShowcasesController::index
OCA\LaunchPad\Controller\AdminDemoShowcasesController::install
OCA\LaunchPad\Controller\AdminDemoShowcasesController::destroy
@@ -671,9 +670,6 @@ OCA\LaunchPad\Service\RoleService::isViewer
OCA\LaunchPad\Service\RoleService::canMutate
OCA\LaunchPad\Service\SetupWizardService::getWizardState
OCA\LaunchPad\Service\SetupWizardService::markWizardComplete
-OCA\LaunchPad\Service\SetupWizardService::hasGroupfolderApp
-OCA\LaunchPad\Service\SetupWizardService::setContentStorage
-OCA\LaunchPad\Service\SetupWizardService::getContentStorage
OCA\LaunchPad\Service\SvgSanitiser::sanitize
OCA\LaunchPad\Service\TileUpdater::applyTileConfig
OCA\LaunchPad\Service\TileUpdater::applyTileUpdates
From e9c6b019b5c1d0a8f841e61b9f7042233157b544 Mon Sep 17 00:00:00 2001
From: Ruben van der Linde
Date: Fri, 9 Oct 2026 23:11:08 +0200
Subject: [PATCH 34/60] feat(setup-wizard): the wizard has six steps, the
storage step and its endpoint are gone
Decision 131 (Q-launchpad-1): drop setup-wizard step 2, POST
/api/admin/setup-wizard/storage, AdminSettingKey::CONTENT_STORAGE and the
three SetupWizardService storage methods. A stored content_storage row stays
unread. occ launchpad:setup no longer needs storage_backend and says an old
field was ignored. GET /api/admin/settings returns twelve keys.
The code half landed in the coordinator's wip daa7a920; this commit adds the
command test, the two test fixes for the twelve-key settings, the formatted
Vitest spec and the archived change.
---
.../proposal.md | 33 ++
.../specs/admin-settings/spec.md | 61 +++
.../specs/setup-wizard/spec.md | 397 ++++++++++++++++++
.../tasks.md | 11 +
src/modals/__tests__/SetupWizardModal.spec.js | 4 +-
tests/Unit/Command/SetupCommandTest.php | 109 +++++
.../AdminControllerSettingsAliasTest.php | 1 -
.../Unit/Service/AdminSettingsServiceTest.php | 2 +-
8 files changed, 615 insertions(+), 3 deletions(-)
create mode 100644 openspec/changes/archive/2026-10-09-retire-setup-wizard-storage-step/proposal.md
create mode 100644 openspec/changes/archive/2026-10-09-retire-setup-wizard-storage-step/specs/admin-settings/spec.md
create mode 100644 openspec/changes/archive/2026-10-09-retire-setup-wizard-storage-step/specs/setup-wizard/spec.md
create mode 100644 openspec/changes/archive/2026-10-09-retire-setup-wizard-storage-step/tasks.md
create mode 100644 tests/Unit/Command/SetupCommandTest.php
diff --git a/openspec/changes/archive/2026-10-09-retire-setup-wizard-storage-step/proposal.md b/openspec/changes/archive/2026-10-09-retire-setup-wizard-storage-step/proposal.md
new file mode 100644
index 000000000..e73e2dd2e
--- /dev/null
+++ b/openspec/changes/archive/2026-10-09-retire-setup-wizard-storage-step/proposal.md
@@ -0,0 +1,33 @@
+# Proposal: retire setup-wizard step 2 (content storage)
+
+## Why
+
+The group-folder storage backend was retired (archived change
+`2026-10-09-retire-groupfolder-storage-backend`). Setup-wizard step 2 still asked
+the admin where dashboard content is stored and saved `launchpad.content_storage`,
+a setting nothing reads. The question was Q-launchpad-1; Ruben answered it on
+9 Oct 2026 (decision 131): retire the step.
+
+## What
+
+- The wizard has six steps: Welcome, Group order, Demo data, Admin roles, Footer,
+ Done. The storage step, its radio options and its strings are gone.
+- `POST /api/admin/setup-wizard/storage` and `AdminController::setWizardStorage()`
+ are removed.
+- `AdminSettingKey::CONTENT_STORAGE`, `AdminSetting::KEY_CONTENT_STORAGE` and
+ `SetupWizardService::{getContentStorage,setContentStorage,hasGroupfolderApp}`
+ are removed, with the now unused `IAppManager` dependency.
+- `GET /api/admin/settings` no longer returns `launchpad.content_storage`, and
+ `PUT /api/admin/settings` no longer accepts `launchpadContentStorage` (an old
+ client that still sends it is ignored, like any unknown key).
+- `occ launchpad:setup` no longer requires `storage_backend`. An old YAML file
+ that still carries it runs as before and prints that the field was ignored.
+- A stored `content_storage` row stays in `oc_launchpad_admin_settings`, unread.
+ No migration deletes it.
+
+## Impact
+
+- Specs: `setup-wizard` (REQ-WIZ-003 removed, steps renumbered),
+ `admin-settings` (REQ-ASET-001 has twelve keys).
+- No register or schema change, so no `info.xml` version bump is needed.
+- No matrix row carries this change.
diff --git a/openspec/changes/archive/2026-10-09-retire-setup-wizard-storage-step/specs/admin-settings/spec.md b/openspec/changes/archive/2026-10-09-retire-setup-wizard-storage-step/specs/admin-settings/spec.md
new file mode 100644
index 000000000..6b79da23d
--- /dev/null
+++ b/openspec/changes/archive/2026-10-09-retire-setup-wizard-storage-step/specs/admin-settings/spec.md
@@ -0,0 +1,61 @@
+# Delta: admin-settings
+
+## MODIFIED Requirements
+
+### Requirement: Retrieve Admin Settings (REQ-ASET-001)
+
+Administrators MUST be able to retrieve all current admin settings via the API. The endpoint returns a flat JSON object with the twelve settings listed under Defined Settings, each under its API response key. `group_order` is not among them; it has its own endpoints (REQ-ASET-012).
+
+#### Scenario: Get all settings with defaults
+- GIVEN no admin settings have been explicitly configured (fresh installation)
+- WHEN the admin sends GET /api/admin/settings
+- THEN the system MUST return HTTP 200 with all twelve settings at their default values:
+ ```json
+ {
+ "defaultPermissionLevel": "add_only",
+ "allowUserDashboards": false,
+ "allowMultipleDashboards": true,
+ "defaultGridColumns": 12,
+ "linkCreateFileExtensions": ["txt", "md", "docx", "xlsx", "csv", "odt"],
+ "defaultSharePermissionLevel": "add_only",
+ "forcedShareGroups": [],
+ "legacyWidgetBridgeEnabled": false,
+ "maxDashboardsPerUser": 0,
+ "maxWidgetsPerDashboard": 0,
+ "quicksearchFallbackTarget": "none",
+ "startPageWithoutNavigation": false
+ }
+ ```
+- NOTE: `allowUserDashboards` defaults to `false` (REQ-ASET-003) — admins MUST opt in to personal dashboard creation.
+
+#### Scenario: Get settings after modification
+- GIVEN the admin has set `allowUserDashboards` to `false`
+- WHEN the admin sends GET /api/admin/settings
+- THEN the response MUST carry `"allowUserDashboards": false`
+- AND the other eleven keys MUST still be present with their current values
+
+#### Scenario: Non-admin user retrieves settings
+- GIVEN a regular user "alice"
+- WHEN she sends GET /api/admin/settings
+- THEN the system MUST return HTTP 403
+- AND admin settings MUST NOT be exposed to non-admin users
+
+#### Scenario: Settings used by non-admin endpoints
+- GIVEN the admin has set `allowUserDashboards` to `false`
+- WHEN user "alice" sends POST /api/dashboard (to create a dashboard)
+- THEN the system MUST internally check the `allowUserDashboards` setting via `PermissionService::canCreateDashboard()`
+- AND the non-admin user MUST NOT need to call GET /api/admin/settings to experience the effect
+
+#### Scenario: Settings response format consistency
+- GIVEN the admin has configured various settings at different times
+- WHEN GET /api/admin/settings is called
+- THEN the response MUST always return exactly the twelve keys listed under Defined Settings: `defaultPermissionLevel`, `allowUserDashboards`, `allowMultipleDashboards`, `defaultGridColumns`, `linkCreateFileExtensions`, `defaultSharePermissionLevel`, `forcedShareGroups`, `legacyWidgetBridgeEnabled`, `maxDashboardsPerUser`, `maxWidgetsPerDashboard`, `quicksearchFallbackTarget`, `startPageWithoutNavigation`
+- AND no additional keys MUST be present in the response
+- NOTE: This scenario said "exactly four keys" long after the response had grown to twelve. The list above is taken from `AdminSettingsService::getSettings()`; a key added there MUST be added here and to the Defined Settings table.
+- AND the response MUST be a flat JSON object (no nesting)
+
+#### Scenario: The retired content storage key is not returned
+- GIVEN an instance that still holds a `content_storage` row written by the retired setup-wizard storage step
+- WHEN the admin sends GET /api/admin/settings
+- THEN the response MUST NOT contain `launchpad.content_storage`
+- AND the stored row MUST stay unread (decision 131)
diff --git a/openspec/changes/archive/2026-10-09-retire-setup-wizard-storage-step/specs/setup-wizard/spec.md b/openspec/changes/archive/2026-10-09-retire-setup-wizard-storage-step/specs/setup-wizard/spec.md
new file mode 100644
index 000000000..028b4cd81
--- /dev/null
+++ b/openspec/changes/archive/2026-10-09-retire-setup-wizard-storage-step/specs/setup-wizard/spec.md
@@ -0,0 +1,397 @@
+# Delta: setup-wizard
+
+## RENAMED Requirements
+
+- FROM: `### Requirement: REQ-WIZ-002 Multi-Step Wizard Flow with 7 Steps`
+- TO: `### Requirement: REQ-WIZ-002 Multi-Step Wizard Flow with 6 Steps`
+
+- FROM: `### Requirement: REQ-WIZ-004 Step 3 — Group Priority Order (Embedded Component Reuse)`
+- TO: `### Requirement: REQ-WIZ-004 Step 2 — Group Priority Order (Embedded Component Reuse)`
+
+- FROM: `### Requirement: REQ-WIZ-005 Step 4 — Demo Data Installation (Embedded Component Reuse)`
+- TO: `### Requirement: REQ-WIZ-005 Step 3 — Demo Data Installation (Embedded Component Reuse)`
+
+- FROM: `### Requirement: REQ-WIZ-006 Step 5 — Admin Roles (Optional, Depends on admin-roles Capability)`
+- TO: `### Requirement: REQ-WIZ-006 Step 4 — Admin Roles (Optional, Depends on admin-roles Capability)`
+
+- FROM: `### Requirement: REQ-WIZ-007 Step 6 — Footer Configuration (Optional, Depends on footer-customization Capability)`
+- TO: `### Requirement: REQ-WIZ-007 Step 5 — Footer Configuration (Optional, Depends on footer-customization Capability)`
+
+## REMOVED Requirements
+
+### Requirement: REQ-WIZ-003 Step 2 — Storage Backend Choice with GroupFolder Dependency Tooltip
+
+**Reason**: decision 131 (9 Oct 2026) retired the storage step; the group-folder backend it chose between is gone.
+
+**Migration**: none. A stored `content_storage` row stays unread.
+
+## MODIFIED Requirements
+
+### Requirement: REQ-WIZ-001 Detect First-Run State via Admin Setting Flag
+
+The system MUST detect whether a LaunchPad instance is being initialized for the first time by consulting the `launchpad.setup_wizard_complete` boolean flag. When the flag is `false`, the admin section MUST display a banner prompting the admin to run the wizard.
+
+> NOTE: This is a net-new LaunchPad capability with no counterpart in the source app. The source ships CLI-only setup via `the source app:setup`; no frontend first-run detection, wizard route, or onboarding component exists in the source. LaunchPad adds this flag and banner on top of the same underlying service primitives.
+
+#### Scenario: Fresh instance shows banner
+- GIVEN a freshly installed LaunchPad with `launchpad.setup_wizard_complete = false`
+- WHEN an NC admin opens `/apps/launchpad/admin/dashboards`
+- THEN the page MUST display a prominent banner: "Run setup wizard" with a button linking to the wizard flow
+- AND the banner MUST remain visible until the admin clicks "Finish" in the wizard
+
+#### Scenario: Completed instance hides banner
+- GIVEN a LaunchPad instance with `launchpad.setup_wizard_complete = true`
+- WHEN an NC admin opens `/apps/launchpad/admin/dashboards`
+- THEN the banner MUST NOT be displayed
+- AND the admin section loads normally
+
+#### Scenario: Banner includes descriptive text
+- GIVEN a fresh LaunchPad instance showing the setup banner
+- WHEN the admin views the banner
+- THEN the banner MUST include text explaining: "Get your intranet started: configure groups, install demo data, and set up admin roles."
+- NOTE: Banner style and wording are implementation-specific; requirement mandates presence and visibility only
+
+#### Scenario: Banner has explicit "Run setup wizard" call-to-action
+- GIVEN the banner is displayed
+- WHEN the admin clicks the "Run setup wizard" button
+- THEN the wizard modal MUST open and begin at Step 1 (Welcome)
+
+#### Scenario: Admin can dismiss and re-access banner
+- GIVEN an admin who dismissed the banner or navigated away
+- WHEN the admin returns to `/apps/launchpad/admin/dashboards`
+- THEN if `launchpad.setup_wizard_complete = false`, the banner MUST reappear
+
+### Requirement: REQ-WIZ-002 Multi-Step Wizard Flow with 6 Steps
+
+The wizard MUST guide the admin through a linear, skippable sequence of 6 steps, each with Skip / Back / Next buttons. Only the "Finish" button on Step 6 marks the wizard complete.
+
+> NOTE: Steps 2–5 each embed the canonical admin UI component from the corresponding sibling capability (`group-priority-order`, `demo-data-showcases`, `admin-roles`, `footer-customization`). The wizard is a thin orchestrator — it owns step sequencing and the completion flag; each sibling capability owns its own settings persistence. No settings surfaces are duplicated.
+
+#### Scenario: Wizard starts at Step 1
+- GIVEN the banner's "Run setup wizard" button is clicked
+- WHEN the wizard modal opens
+- THEN Step 1 (Welcome / Overview) MUST be displayed
+- AND the step counter MUST show "1 / 7"
+
+#### Scenario: Next button advances to next step
+- GIVEN the wizard is on Step 1
+- WHEN the admin clicks "Next"
+- THEN the wizard advances to Step 2 (Group order)
+- AND the step counter MUST show "2 / 7"
+- AND the Back button MUST become active
+
+#### Scenario: Back button returns to previous step
+- GIVEN the wizard is on Step 3
+- WHEN the admin clicks "Back"
+- THEN the wizard returns to Step 2
+- AND no data loss occurs (Step 3 values are preserved if re-visited)
+
+#### Scenario: Skip button jumps to next step without commitment
+- GIVEN the wizard is on Step 4 (Admin roles, optional)
+- WHEN the admin clicks "Skip"
+- THEN the wizard advances to Step 5
+- AND the wizard completion status for Step 4 is marked "skipped" (not "done")
+- NOTE: Skipping a step does not prevent the wizard from eventually marking complete
+
+#### Scenario: Step 1 has no form; only explanatory text
+- GIVEN Step 1 (Welcome) is displayed
+- WHEN the admin views the step
+- THEN the step MUST show text explaining the wizard's purpose: "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings."
+- AND there are no form fields to fill
+- AND clicking "Next" advances without persisting any choice
+
+#### Scenario: Step 6 shows summary and Finish button
+- GIVEN the wizard reaches Step 6 (Done)
+- WHEN the admin views the step
+- THEN the step MUST display a summary: "Your setup is complete. Click Finish to save changes and dismiss the setup banner."
+- AND a "Finish" button (not "Next") MUST be present
+- AND clicking "Finish" executes the completion action
+
+#### Scenario: Finish button completes wizard
+- GIVEN the admin is on Step 6 with the "Finish" button visible
+- WHEN the admin clicks "Finish"
+- THEN the system MUST set `launchpad.setup_wizard_complete = true`
+- AND the wizard modal MUST close
+- AND the admin section MUST reload, banner gone
+
+#### Scenario: Wizard progress persists across Back navigation
+- GIVEN the wizard is on Step 3 after a group order was chosen on Step 2
+- WHEN the admin clicks Back to Step 2, then clicks Next to Step 3
+- THEN the group order chosen on Step 2 MUST still be present (no data loss)
+
+### Requirement: REQ-WIZ-004 Step 2 — Group Priority Order (Embedded Component Reuse)
+
+Step 2 MUST embed the existing `group-priority-order` admin UI component, allowing the admin to configure the priority order of Nextcloud groups for dashboard routing.
+
+#### Scenario: Step 2 embeds existing group-priority-order admin component
+- GIVEN Step 2 is displayed
+- WHEN the admin views the step
+- THEN the existing `group-priority-order` admin UI MUST be visible and functional within the wizard modal
+- NOTE: The component is not duplicated; the wizard embeds the canonical admin component
+
+#### Scenario: Group order choice persists immediately
+- GIVEN the admin reorders groups in Step 2 (e.g., "engineering" moved above "sales")
+- WHEN the admin clicks Next
+- THEN the system MUST immediately persist the new order to `launchpad.group_priority_order`
+- AND navigating away does not lose the change
+
+#### Scenario: Step 2 allows drag-and-drop reordering (existing UI behavior)
+- GIVEN the group-priority-order component is displayed
+- WHEN the admin drags a group to a new position
+- THEN the reorder MUST be reflected in the UI
+- NOTE: Exact interaction (drag, arrow buttons, etc.) is determined by the embedded component's design
+
+### Requirement: REQ-WIZ-005 Step 3 — Demo Data Installation (Embedded Component Reuse)
+
+Step 3 MUST embed the existing `demo-data-showcases` admin UI component, allowing the admin to optionally select and install demo packages.
+
+#### Scenario: Step 3 embeds demo-data-showcases admin component
+- GIVEN Step 3 is displayed
+- WHEN the admin views the step
+- THEN the existing `demo-data-showcases` admin UI MUST be visible within the wizard modal
+- AND the component displays all available demo packages with checkboxes
+- NOTE: The component is the canonical admin UI; wizard embeds it without duplication
+
+#### Scenario: Demo data installation is optional
+- GIVEN Step 3 is displayed with zero demo packages selected
+- WHEN the admin clicks "Skip" or "Next"
+- THEN the wizard MUST advance without requiring any demo to be installed
+- NOTE: Step 3 is not skippable via a Skip button; all steps are traversed. However, selecting zero demos and clicking Next is equivalent to skipping.
+
+#### Scenario: Checked demos are installed on Next
+- GIVEN the admin selects checkboxes for "Engineering Demo" and "Sales Demo" on Step 3
+- WHEN the admin clicks "Next"
+- THEN the system MUST trigger the installation of both demo packages
+- AND the wizard advances to Step 4 (installation happens asynchronously or blocks until done, implementation choice)
+- NOTE: The demo-data-showcases component handles the actual installation logic
+
+#### Scenario: Demo selection is persisted
+- GIVEN the admin selected "Engineering Demo" on Step 3 and advanced to Step 4
+- WHEN the admin navigates Back to Step 3
+- THEN the "Engineering Demo" checkbox MUST remain checked
+- AND no re-installation happens (idempotent)
+
+### Requirement: REQ-WIZ-006 Step 4 — Admin Roles (Optional, Depends on admin-roles Capability)
+
+Step 4 MUST allow the admin to optionally assign the "Dashboard Admin" role to one Nextcloud group. This step is skippable via a Skip button.
+
+#### Scenario: Step 4 shows group selector for admin role
+- GIVEN the `admin-roles` capability is available
+- AND Step 4 is displayed
+- WHEN the admin views the step
+- THEN a dropdown or multi-select MUST display all Nextcloud groups
+- AND a description MUST explain: "Assign 'Dashboard Admin' role to a group to delegate LaunchPad administration to group members."
+
+#### Scenario: Admin can select one group for Dashboard Admin role
+- GIVEN Step 4 is displayed with groups ["engineering", "sales", "marketing"]
+- WHEN the admin selects "engineering"
+- AND clicks "Next"
+- THEN the system MUST call `RoleService::assignRole(groupId="engineering", role="admin", assignedBy=)`
+- AND the role assignment MUST be created in `oc_launchpad_role_assignments`
+
+#### Scenario: Step 4 is skippable
+- GIVEN Step 4 is displayed
+- WHEN the admin clicks "Skip"
+- THEN no role assignment is made
+- AND the wizard advances to Step 5
+- AND the completion status for Step 4 MUST be marked "skipped"
+
+#### Scenario: No group selected is equivalent to skip
+- GIVEN Step 4 with no group selected
+- WHEN the admin clicks "Next"
+- THEN no role assignment is made (equivalent to Skip)
+- AND the wizard advances to Step 5
+
+#### Scenario: Step 4 unavailable if admin-roles capability missing
+- GIVEN the `admin-roles` capability is NOT implemented or not available
+- WHEN the wizard would display Step 4
+- THEN Step 4 MUST be skipped automatically
+- AND the step counter jumps from Step 3 to Step 5
+- NOTE: Capability dependency is gracefully handled; missing capability does not break wizard
+
+#### Scenario: Only one group can have admin role per wizard run
+- GIVEN Step 4 is displayed
+- WHEN the admin selects "engineering" and clicks "Next"
+- THEN only the "engineering" group receives the admin role (not multiple groups)
+- NOTE: If the admin wishes to assign admin role to multiple groups, they must run the wizard again or use the admin roles management UI directly
+
+### Requirement: REQ-WIZ-007 Step 5 — Footer Configuration (Optional, Depends on footer-customization Capability)
+
+Step 5 MUST allow the admin to optionally open and configure the footer using the "Structured mode" editor. This step is skippable via a Skip button.
+
+#### Scenario: Step 5 shows footer editor
+- GIVEN the `footer-customization` capability is available
+- AND Step 5 is displayed
+- WHEN the admin views the step
+- THEN the "Structured mode" footer editor MUST be visible and functional
+- AND a description MUST explain: "Customize the footer content and appearance for your intranet."
+
+#### Scenario: Footer edits persist immediately
+- GIVEN the admin edits footer content in Step 5
+- WHEN the admin clicks "Next"
+- THEN the footer configuration MUST be saved to `launchpad.footer_config` or equivalent setting
+- AND navigating away does not lose the changes
+
+#### Scenario: Step 5 is skippable
+- GIVEN Step 5 is displayed
+- WHEN the admin clicks "Skip"
+- THEN no footer changes are made
+- AND the wizard advances to Step 6
+- AND the completion status for Step 5 MUST be marked "skipped"
+
+#### Scenario: Step 5 unavailable if footer-customization capability missing
+- GIVEN the `footer-customization` capability is NOT implemented or not available
+- WHEN the wizard would display Step 5
+- THEN Step 5 MUST be skipped automatically
+- AND the step counter jumps from Step 4 to Step 6
+- NOTE: Capability dependency is gracefully handled; missing capability does not break wizard
+
+### Requirement: REQ-WIZ-008 Get Wizard State via API Endpoint
+
+The system MUST expose a `GET /api/admin/setup-wizard/state` endpoint that returns the wizard's completion status, current recommended step for re-runs, and per-step status.
+
+> NOTE: Auto-launch behaviour (opening the wizard automatically when `setup_wizard_complete = false`) is intentionally SHOULD/MAY, not MUST. CLI-provisioned installs may set settings incrementally mid-`occ` run, meaning an admin who views the page before the CLI command finishes would see `complete = false` with settings partially applied. The banner-link fallback is always available and is the reliable path; auto-launch is a UX convenience for fresh browser sessions only.
+
+#### Scenario: Endpoint returns complete state on fresh instance
+- GIVEN a fresh LaunchPad instance with `launchpad.setup_wizard_complete = false`
+- WHEN an NC admin calls `GET /api/admin/setup-wizard/state`
+- THEN the response MUST be HTTP 200 with JSON:
+ ```json
+ {
+ "complete": false,
+ "currentRecommendedStep": 1,
+ "stepStatuses": {
+ "1": "done",
+ "2": "pending",
+ "3": "pending",
+ "4": "pending",
+ "5": "pending",
+ "6": "pending"
+ }
+ }
+ ```
+- NOTE: Step 1 is always "done" (no choice required). Steps 2–6 start "pending".
+
+#### Scenario: Endpoint returns recommended step for re-runs
+- GIVEN a LaunchPad instance where the group order is not yet set
+- WHEN an NC admin calls `GET /api/admin/setup-wizard/state`
+- THEN the response MUST include `currentRecommendedStep: 2`
+- NOTE: The heuristic picks the first step with status != "done"
+
+#### Scenario: Endpoint returns complete=true when wizard finished
+- GIVEN a LaunchPad instance with `launchpad.setup_wizard_complete = true`
+- WHEN an NC admin calls `GET /api/admin/setup-wizard/state`
+- THEN the response MUST include `complete: true`
+- AND `currentRecommendedStep` MAY be 1 (optional) or reflect the last visited step (implementation choice)
+
+#### Scenario: Non-admin requests receive 403
+- GIVEN a non-admin user
+- WHEN they call `GET /api/admin/setup-wizard/state`
+- THEN the system MUST return HTTP 403 (Forbidden)
+
+#### Scenario: Endpoint includes skipped step status
+- GIVEN the wizard has been run with Step 4 skipped
+- WHEN an NC admin calls `GET /api/admin/setup-wizard/state`
+- THEN the response MUST include `"4": "skipped"` in stepStatuses
+- NOTE: Skipped status indicates the step was traversed but no action taken; does not block wizard completion
+
+#### Scenario: A stored storage choice from the retired step is ignored
+- GIVEN an instance that still holds a `content_storage` row written by the retired storage step
+- WHEN an NC admin calls `GET /api/admin/setup-wizard/state`
+- THEN `stepStatuses` MUST hold exactly six entries
+- AND the stored row MUST NOT change any step status or `currentRecommendedStep`
+
+### Requirement: REQ-WIZ-010 CLI Command for Non-Interactive Setup (IaC-Friendly)
+
+The system MUST expose a CLI command `php occ launchpad:setup` that accepts a `--config=/path/setup.yaml` argument to perform wizard setup non-interactively, ideal for Infrastructure-as-Code deployments.
+
+> NOTE: The YAML config schema (fields: `group_priority_order`, `demo_packages`, `admin_role_group`, `footer_config`) is formally defined in the `cli-commands` sibling spec. Optional steps (admin roles, footer) may be omitted from the config file entirely; omitting them is equivalent to skipping those steps in the wizard and does not cause an error.
+
+#### Scenario: CLI command accepts YAML config file
+- GIVEN a YAML file `/tmp/setup.yaml` with content:
+ ```yaml
+ group_priority_order: ["engineering", "sales"]
+ demo_packages: ["engineering-demo", "sales-demo"]
+ admin_role_group: "engineering"
+ footer_config:
+ layout: "structured"
+ items: [...]
+ ```
+- WHEN an admin runs `php occ launchpad:setup --config=/tmp/setup.yaml`
+- THEN the system MUST:
+ - Set `launchpad.group_priority_order = ["engineering", "sales"]` (Step 2)
+ - Install "engineering-demo" and "sales-demo" (Step 3)
+ - Assign "Dashboard Admin" role to "engineering" (Step 4)
+ - Set `launchpad.footer_config` with provided config (Step 5)
+ - Set `launchpad.setup_wizard_complete = true` (Step 6)
+
+#### Scenario: CLI command validates YAML schema
+- GIVEN a YAML file whose top level is not a map, or whose `group_priority_order` is not a list
+- WHEN `php occ launchpad:setup --config=/tmp/setup.yaml` is run
+- THEN the system MUST output an error starting with "Invalid setup.yaml:"
+- AND the command MUST exit with non-zero status
+
+#### Scenario: An old config that still sets storage_backend keeps working
+- GIVEN a YAML file written for the seven-step wizard, with `storage_backend: "groupfolder"`
+- WHEN `php occ launchpad:setup --config=/tmp/setup.yaml` is run
+- THEN the command MUST print that `storage_backend` is no longer used and was ignored
+- AND it MUST apply the other steps and exit with status 0
+- AND it MUST NOT write `launchpad.content_storage`
+
+#### Scenario: CLI command is verbose by default
+- GIVEN the command runs successfully
+- WHEN the output is examined
+- THEN it MUST show progress: "Step 1: Welcome... done", "Step 2: Group order... done", etc.
+- AND final message: "Setup wizard completed successfully."
+
+#### Scenario: CLI command skips optional steps if not in config
+- GIVEN a YAML file with only `group_priority_order`
+- WHEN the command runs
+- THEN Steps 4 and 5 MUST be skipped (no error)
+- AND Step 2 MUST execute with the provided value
+- AND the wizard completes
+
+#### Scenario: CLI command is idempotent
+- GIVEN a YAML config that was applied once
+- WHEN the same command runs again with the same config
+- THEN the system MUST return success (HTTP 200 equivalent)
+- AND no duplicate role assignments or demo installations occur
+- NOTE: The system MUST detect existing assignments/installations and skip them
+
+### Requirement: REQ-WIZ-011 Wizard is Re-Runnable
+
+The wizard MUST be re-runnable even after `launchpad.setup_wizard_complete = true`. Re-running MUST NOT undo earlier choices; it MUST re-walk the steps showing current state and allowing updates.
+
+#### Scenario: Admin can run wizard again after completion
+- GIVEN a LaunchPad instance with `launchpad.setup_wizard_complete = true`
+- WHEN an NC admin navigates to `/apps/launchpad/admin` and clicks "Run setup wizard again" (or uses a Re-run button in a separate admin UI section)
+- THEN the wizard modal MUST open
+- AND all step values MUST reflect current settings (group order, demo data, etc.)
+- AND the admin can make changes
+
+#### Scenario: Re-running does not re-install demo packages
+- GIVEN the instance has already installed "engineering-demo"
+- AND the admin re-runs the wizard to Step 3
+- WHEN the admin unchecks "engineering-demo" and clicks Next
+- THEN the system MUST NOT re-install the demo (it's already there)
+- NOTE: Demo installation is one-time; re-running the wizard does not trigger redundant installations. If the admin wishes to un-install, they must use a separate admin UI for demo management.
+
+#### Scenario: Re-running preserves skipped steps
+- GIVEN the wizard was run previously with Step 4 skipped
+- AND the admin re-runs the wizard
+- WHEN Step 4 is displayed
+- THEN the step MUST show "Skip" button as before
+- AND if the admin clicks Skip again, the behavior is consistent with the first run
+
+#### Scenario: Step 1 is always done on re-run
+- GIVEN a re-run of the wizard
+- WHEN Step 1 is displayed
+- THEN no new action is required; clicking Next immediately advances
+
+#### Scenario: API endpoint reports re-runnable state
+- GIVEN the instance has `launchpad.setup_wizard_complete = true`
+- WHEN an NC admin calls `GET /api/admin/setup-wizard/state`
+- THEN the response MUST indicate `complete: true`
+- AND the admin UI MUST show a "Run setup wizard again" button (not a "Run setup wizard" banner)
+- NOTE: The UI signals that re-running is available and safe
+
diff --git a/openspec/changes/archive/2026-10-09-retire-setup-wizard-storage-step/tasks.md b/openspec/changes/archive/2026-10-09-retire-setup-wizard-storage-step/tasks.md
new file mode 100644
index 000000000..2f49c2e55
--- /dev/null
+++ b/openspec/changes/archive/2026-10-09-retire-setup-wizard-storage-step/tasks.md
@@ -0,0 +1,11 @@
+# Tasks: retire-setup-wizard-storage-step
+
+- [x] 1. Red tests first: `SetupWizardServiceTest` (six steps, no storage methods, a stored `content_storage` stays unread), `AdminControllerSetupWizardTest` (no `setWizardStorage`, no route), `AdminSettingKeyTest` (`tryFrom('content_storage')` is null), `AdminSettingsServiceTest` (twelve keys), new `SetupCommandTest` (no `storage_backend` needed, an old field is ignored, steps 1-6). Red run: `build-round/wizard-red.log`.
+- [x] 2. Remove the step from `SetupWizardService` (`STEP_COUNT = 6`, renumbered `computeStepStatuses()`, the three methods and `IAppManager` gone).
+- [x] 3. Remove `AdminController::setWizardStorage()`, its route in `appinfo/routes.php`, and the `launchpadContentStorage` parameter of `updateSettings()`.
+- [x] 4. Remove `AdminSettingKey::CONTENT_STORAGE`, `AdminSetting::KEY_CONTENT_STORAGE`, and the key's read and write in `AdminSettingsService`.
+- [x] 5. `SetupCommand`: `storage_backend` optional and ignored with a notice; steps renumbered 1-6.
+- [x] 6. `SetupWizardModal.vue`: drop the storage step (six steps); `api.setSetupWizardStorage()` removed; Vitest spec updated.
+- [x] 7. Welcome text and admin banner no longer mention storage, in every shipped locale; the strings only the retired step used are removed from every locale.
+- [x] 8. Postman collection: the storage request removed.
+- [x] 9. Specs: `setup-wizard` and `admin-settings` amended (deltas in this change are the record). The main specs were written by hand from the same transform, because `openspec validate` refuses a MODIFIED block that renames scenarios (Step N to Step N-1); the change was then archived with `--skip-specs`. Data Model and Defined Settings sections edited by hand.
diff --git a/src/modals/__tests__/SetupWizardModal.spec.js b/src/modals/__tests__/SetupWizardModal.spec.js
index 34a28b231..b7871f858 100644
--- a/src/modals/__tests__/SetupWizardModal.spec.js
+++ b/src/modals/__tests__/SetupWizardModal.spec.js
@@ -115,7 +115,9 @@ describe('SetupWizardModal', () => {
expect(wrapper.vm.currentStep).toBe(2)
expect(wrapper.find('.group-priority-stub').exists()).toBe(true)
expect(wrapper.find('[data-test="storage-database"]').exists()).toBe(false)
- expect(wrapper.find('[data-test="storage-groupfolder"]').exists()).toBe(false)
+ expect(wrapper.find('[data-test="storage-groupfolder"]').exists()).toBe(
+ false,
+ )
})
it('REQ-WIZ-002: Step 6 Next is labelled Finish and calls completeSetupWizard', async () => {
diff --git a/tests/Unit/Command/SetupCommandTest.php b/tests/Unit/Command/SetupCommandTest.php
new file mode 100644
index 000000000..64f56f210
--- /dev/null
+++ b/tests/Unit/Command/SetupCommandTest.php
@@ -0,0 +1,109 @@
+
+ * @copyright 2026 Conduction b.v.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * SPDX-FileCopyrightText: 2026 LaunchPad Contributors
+ * SPDX-License-Identifier: EUPL-1.2
+ */
+
+declare(strict_types=1);
+
+namespace OCA\LaunchPad\Tests\Unit\Command;
+
+use OCA\LaunchPad\Command\SetupCommand;
+use OCA\LaunchPad\Service\AdminSettingsService;
+use OCA\LaunchPad\Service\SetupWizardService;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Console\Tester\CommandTester;
+
+class SetupCommandTest extends TestCase {
+ /** @var SetupWizardService&MockObject */
+ private $wizard;
+
+ /** @var AdminSettingsService&MockObject */
+ private $settings;
+
+ /** @var list */
+ private array $files = [];
+
+ protected function setUp(): void {
+ parent::setUp();
+ $this->wizard = $this->createMock(SetupWizardService::class);
+ $this->settings = $this->createMock(AdminSettingsService::class);
+ }
+
+ protected function tearDown(): void {
+ foreach ($this->files as $file) {
+ @unlink($file);
+ }
+ parent::tearDown();
+ }
+
+ private function yaml(string $body): string {
+ $file = tempnam(sys_get_temp_dir(), 'lp-setup-');
+ $this->assertIsString($file);
+ file_put_contents($file, $body);
+ $this->files[] = $file;
+ return $file;
+ }
+
+ private function runSetup(string $body): CommandTester {
+ $tester = new CommandTester(new SetupCommand($this->wizard, $this->settings));
+ $tester->execute(['--config' => $this->yaml($body)]);
+ return $tester;
+ }
+
+ public function testAConfigWithoutStorageBackendSucceeds(): void {
+ $this->settings->method('getGroupOrder')->willReturn([]);
+ $this->settings->expects($this->once())->method('setGroupOrder')->with(['engineering', 'sales']);
+ $this->wizard->expects($this->once())->method('markWizardComplete');
+
+ $tester = $this->runSetup("group_priority_order: [\"engineering\", \"sales\"]\n");
+
+ $this->assertSame(0, $tester->getStatusCode());
+ $display = $tester->getDisplay();
+ $this->assertStringContainsString('Step 1: Welcome... done', $display);
+ $this->assertStringContainsString('Step 2: Group order... done', $display);
+ $this->assertStringContainsString('Step 3: Demo data... skipped (not in config)', $display);
+ $this->assertStringContainsString('Step 4: Admin roles... skipped (not in config)', $display);
+ $this->assertStringContainsString('Step 5: Footer config... skipped (not in config)', $display);
+ $this->assertStringContainsString('Step 6: Done... done', $display);
+ $this->assertStringNotContainsString('Storage backend', $display);
+ $this->assertStringContainsString('Setup wizard completed successfully.', $display);
+ }
+
+ public function testAnOldConfigWithStorageBackendStillRunsAndSaysTheFieldIsIgnored(): void {
+ $this->settings->method('getGroupOrder')->willReturn(['engineering']);
+ $this->settings->expects($this->never())->method('setGroupOrder');
+ $this->wizard->expects($this->once())->method('markWizardComplete');
+
+ $tester = $this->runSetup("storage_backend: \"groupfolder\"\ngroup_priority_order: [\"engineering\"]\n");
+
+ $this->assertSame(0, $tester->getStatusCode());
+ $display = $tester->getDisplay();
+ $this->assertStringContainsString("'storage_backend' is no longer used and was ignored", $display);
+ $this->assertStringContainsString('Step 2: Group order... already configured, skipping', $display);
+ }
+
+ public function testAnEmptyMapStillCompletesTheWizard(): void {
+ $this->wizard->expects($this->once())->method('markWizardComplete');
+
+ $tester = $this->runSetup("{}\n");
+
+ $this->assertSame(0, $tester->getStatusCode());
+ $this->assertStringContainsString('Step 2: Group order... skipped (not in config)', $tester->getDisplay());
+ }
+}
diff --git a/tests/Unit/Controller/AdminControllerSettingsAliasTest.php b/tests/Unit/Controller/AdminControllerSettingsAliasTest.php
index a1a58a52b..a453d9c91 100644
--- a/tests/Unit/Controller/AdminControllerSettingsAliasTest.php
+++ b/tests/Unit/Controller/AdminControllerSettingsAliasTest.php
@@ -177,7 +177,6 @@ public function testStartPageWithoutNavigationIsPassedThrough(): void {
$this->isNull(),
$this->isNull(),
$this->isNull(),
- $this->isNull(),
$this->isTrue(),
);
diff --git a/tests/Unit/Service/AdminSettingsServiceTest.php b/tests/Unit/Service/AdminSettingsServiceTest.php
index a77e867e2..7b9fbb19e 100644
--- a/tests/Unit/Service/AdminSettingsServiceTest.php
+++ b/tests/Unit/Service/AdminSettingsServiceTest.php
@@ -167,7 +167,7 @@ public function testGetSettingsReturnsCamelCaseKeys(): void {
$this->assertArrayHasKey('quicksearchFallbackTarget', $settings);
// runtime-shell REQ-SHELL-009.
$this->assertArrayHasKey('startPageWithoutNavigation', $settings);
- $this->assertCount(13, $settings);
+ $this->assertCount(12, $settings);
}//end testGetSettingsReturnsCamelCaseKeys()
// ----- dashboard-quota-limits REQ-QUOTA-001 -----
From 18b3b9b36faf1256d578d99d6fe3444abadf3985 Mon Sep 17 00:00:00 2001
From: Ruben van der Linde
Date: Fri, 9 Oct 2026 23:11:15 +0200
Subject: [PATCH 35/60] feat(activity): LaunchPad events can reach the activity
email digest, with per-event settings
Four activity settings in one LaunchPad group (REQ-DIGE-001): shared and
published go to the digest by default, updated and acknowledged stay in the
stream only. Registered in info.xml (validated against app-info.xsd), labels
in all 37 locales. engagement-activity-digest stays open for its live check
(task 5); e-digest moves to building.
---
appinfo/info.xml | 9 ++
l10n/be.js | 6 +-
l10n/be.json | 6 +-
l10n/bg.js | 6 +-
l10n/bg.json | 6 +-
l10n/bs.js | 6 +-
l10n/bs.json | 6 +-
l10n/ca.js | 6 +-
l10n/ca.json | 6 +-
l10n/cs.js | 6 +-
l10n/cs.json | 6 +-
l10n/da.js | 6 +-
l10n/da.json | 6 +-
l10n/de.js | 6 +-
l10n/de.json | 6 +-
l10n/el.js | 6 +-
l10n/el.json | 6 +-
l10n/en.js | 6 +-
l10n/en.json | 6 +-
l10n/es.js | 6 +-
l10n/es.json | 6 +-
l10n/et.js | 6 +-
l10n/et.json | 6 +-
l10n/fi.js | 6 +-
l10n/fi.json | 6 +-
l10n/fr.js | 6 +-
l10n/fr.json | 6 +-
l10n/ga.js | 6 +-
l10n/ga.json | 6 +-
l10n/hr.js | 6 +-
l10n/hr.json | 6 +-
l10n/hu.js | 6 +-
l10n/hu.json | 6 +-
l10n/is.js | 6 +-
l10n/is.json | 6 +-
l10n/it.js | 6 +-
l10n/it.json | 6 +-
l10n/lb.js | 6 +-
l10n/lb.json | 6 +-
l10n/lt.js | 6 +-
l10n/lt.json | 6 +-
l10n/lv.js | 6 +-
l10n/lv.json | 6 +-
l10n/mk.js | 6 +-
l10n/mk.json | 6 +-
l10n/mt.js | 6 +-
l10n/mt.json | 6 +-
l10n/nb.js | 6 +-
l10n/nb.json | 6 +-
l10n/nl.js | 6 +-
l10n/nl.json | 6 +-
l10n/pl.js | 6 +-
l10n/pl.json | 6 +-
l10n/pt.js | 6 +-
l10n/pt.json | 6 +-
l10n/rm.js | 6 +-
l10n/rm.json | 6 +-
l10n/ro.js | 6 +-
l10n/ro.json | 6 +-
l10n/ru.js | 6 +-
l10n/ru.json | 6 +-
l10n/sk.js | 6 +-
l10n/sk.json | 6 +-
l10n/sl.js | 6 +-
l10n/sl.json | 6 +-
l10n/sq.js | 6 +-
l10n/sq.json | 6 +-
l10n/sr.js | 6 +-
l10n/sr.json | 6 +-
l10n/sv.js | 6 +-
l10n/sv.json | 6 +-
l10n/tr.js | 6 +-
l10n/tr.json | 6 +-
l10n/uk.js | 6 +-
l10n/uk.json | 6 +-
.../Settings/DashboardAcknowledgedSetting.php | 76 +++++++++++++
.../Settings/DashboardPublishedSetting.php | 76 +++++++++++++
.../Settings/DashboardSharedSetting.php | 76 +++++++++++++
.../Settings/DashboardUpdatedSetting.php | 76 +++++++++++++
.../Settings/LaunchPadActivitySetting.php | 107 ++++++++++++++++++
.../engagement-activity-digest/tasks.md | 6 +-
openspec/parity/capabilities.json | 2 +-
.../Settings/ActivitySettingsTest.php | 96 ++++++++++++++++
83 files changed, 890 insertions(+), 78 deletions(-)
create mode 100644 lib/Activity/Settings/DashboardAcknowledgedSetting.php
create mode 100644 lib/Activity/Settings/DashboardPublishedSetting.php
create mode 100644 lib/Activity/Settings/DashboardSharedSetting.php
create mode 100644 lib/Activity/Settings/DashboardUpdatedSetting.php
create mode 100644 lib/Activity/Settings/LaunchPadActivitySetting.php
create mode 100644 tests/Unit/Activity/Settings/ActivitySettingsTest.php
diff --git a/appinfo/info.xml b/appinfo/info.xml
index 96e62910f..e4df38c90 100644
--- a/appinfo/info.xml
+++ b/appinfo/info.xml
@@ -176,6 +176,15 @@ Vrij en open source onder de EUPL-1.2-licentie.
+
+
+ OCA\LaunchPad\Activity\Settings\DashboardSharedSetting
+ OCA\LaunchPad\Activity\Settings\DashboardPublishedSetting
+ OCA\LaunchPad\Activity\Settings\DashboardUpdatedSetting
+ OCA\LaunchPad\Activity\Settings\DashboardAcknowledgedSetting
+
+
+
+
+
import { CnAppRoot } from '@conduction/nextcloud-vue'
import { computed } from 'vue'
+import UserSettings from './views/settings/UserSettings.vue'
import customComponents from './customComponents.js'
import { ICON_CATALOGUE } from './services/iconCatalogue.js'
import { permits } from './utils/permissions.js'
@@ -111,6 +127,7 @@ export default {
name: 'App',
components: {
CnAppRoot,
+ UserSettings,
},
inject: {
@@ -367,5 +384,68 @@ export default {
}
},
},
+
+ /** @spec openspec/specs/people-widget/spec.md */
+ mounted() {
+ window.addEventListener('hashchange', this.openUserSettingsFromHash)
+ this.openUserSettingsFromHash()
+ },
+
+ /** @spec openspec/specs/people-widget/spec.md */
+ beforeUnmount() {
+ window.removeEventListener('hashchange', this.openUserSettingsFromHash)
+ },
+
+ methods: {
+ /**
+ * The mounted CnAppRoot, or null before it mounts.
+ *
+ * @return {object|null} The CnAppRoot instance.
+ * @spec openspec/specs/people-widget/spec.md
+ */
+ appRoot() {
+ return this.$refs.appRoot || null
+ },
+
+ /**
+ * Open the user settings dialog when the location hash is
+ * `#user-settings`, the deep link that replaces the old forms in
+ * Nextcloud's personal settings.
+ *
+ * @return {void}
+ * @spec openspec/specs/people-widget/spec.md
+ */
+ openUserSettingsFromHash() {
+ const root = this.appRoot()
+ if (root && window.location.hash === '#user-settings') {
+ root.userSettingsOpen = true
+ }
+ },
+
+ /**
+ * The walkthrough replay section CnAppRoot draws in its own fallback,
+ * which filling the #user-settings slot switches off. Reads CnAppRoot's
+ * state and labels, so the wording and the behaviour stay the library's.
+ *
+ * @return {object|null} Section props for UserSettings, or null without a tour.
+ * @spec openspec/specs/people-widget/spec.md
+ */
+ walkthroughSettings() {
+ const root = this.appRoot()
+ if (!root || !root.walkthroughEnabled) {
+ return null
+ }
+ const inProgress = Boolean(root.walkthroughProgressValue)
+ return {
+ sectionName: root.restartWalkthroughSectionName,
+ hint: root.restartWalkthroughHint,
+ inProgress,
+ continueLabel: root.continueWalkthroughLabel,
+ restartLabel: inProgress
+ ? root.startOverWalkthroughLabel
+ : root.restartWalkthroughLabel,
+ }
+ },
+ },
}
diff --git a/src/__tests__/App.navigationPanel.spec.js b/src/__tests__/App.navigationPanel.spec.js
index 18f29c600..d967d4f01 100644
--- a/src/__tests__/App.navigationPanel.spec.js
+++ b/src/__tests__/App.navigationPanel.spec.js
@@ -25,6 +25,9 @@ vi.mock('@conduction/nextcloud-vue', () => ({
template:
'
',
},
+ // The user settings dialog's library sections (src/views/settings/UserSettings.vue).
+ CnCredentials: { name: 'CnCredentials', template: '' },
+ CnNotificationPreferences: { name: 'CnNotificationPreferences', template: '' },
}))
vi.mock('../customComponents.js', () => ({ default: {} }))
vi.mock('../services/iconCatalogue.js', () => ({ ICON_CATALOGUE: [] }))
diff --git a/src/__tests__/App.supportNote.spec.js b/src/__tests__/App.supportNote.spec.js
index 31c6eea01..4e3746b75 100644
--- a/src/__tests__/App.supportNote.spec.js
+++ b/src/__tests__/App.supportNote.spec.js
@@ -11,6 +11,9 @@ import { describe, expect, it, vi } from 'vitest'
vi.mock('@conduction/nextcloud-vue', () => ({
CnAppRoot: { name: 'CnAppRoot', template: '' },
+ // The user settings dialog's library sections (src/views/settings/UserSettings.vue).
+ CnCredentials: { name: 'CnCredentials', template: '' },
+ CnNotificationPreferences: { name: 'CnNotificationPreferences', template: '' },
}))
vi.mock('../customComponents.js', () => ({ default: {} }))
vi.mock('../services/iconCatalogue.js', () => ({ ICON_CATALOGUE: [] }))
diff --git a/src/personal.js b/src/personal.js
deleted file mode 100644
index bc8990fbc..000000000
--- a/src/personal.js
+++ /dev/null
@@ -1,26 +0,0 @@
-/**
- * SPDX-FileCopyrightText: 2026 Conduction B.V.
- * SPDX-License-Identifier: EUPL-1.2
- *
- * Personal settings entry: the LaunchPad profile fields a person fills in
- * themselves, such as their expertise tags (REQ-PEX-002). The form loads its
- * fields from the API, so this page needs no initial state.
- *
- * @spec openspec/specs/people-widget/spec.md
- */
-
-import { translatePlural as n, translate as t } from '@nextcloud/l10n'
-import { createApp, h } from 'vue'
-import ProfileFieldsSettings from './views/settings/ProfileFieldsSettings.vue'
-
-import './publicPath.js'
-
-const app = createApp({
- name: 'LaunchpadPersonalRoot',
- render: () => h(ProfileFieldsSettings),
-})
-
-app.mixin({ methods: { t, n } })
-app.mount('#launchpad-personal-settings')
-
-export default app
diff --git a/src/views/settings/ProfileFieldsSettings.vue b/src/views/settings/ProfileFieldsSettings.vue
index 4b87124ba..585d59ce6 100644
--- a/src/views/settings/ProfileFieldsSettings.vue
+++ b/src/views/settings/ProfileFieldsSettings.vue
@@ -4,14 +4,22 @@
-->
-
+
+
+
+ {{
+ t(
+ 'launchpad',
+ 'Fields your organisation adds to your profile. Colleagues see them in profile widgets.',
+ )
+ }}
+
' },
NcButton: { template: '' },
NcSelect: {
diff --git a/templates/settings/personal.php b/templates/settings/personal.php
deleted file mode 100644
index 19965281a..000000000
--- a/templates/settings/personal.php
+++ /dev/null
@@ -1,8 +0,0 @@
-
- * SPDX-License-Identifier: EUPL-1.2
- */
-?>
-
-
diff --git a/tests/Unit/Service/ProfileFieldsWiringTest.php b/tests/Unit/Service/ProfileFieldsWiringTest.php
index 4be2030dd..ad7913ead 100644
--- a/tests/Unit/Service/ProfileFieldsWiringTest.php
+++ b/tests/Unit/Service/ProfileFieldsWiringTest.php
@@ -4,8 +4,7 @@
* ProfileFieldsWiringTest
*
* widgets-people-expertise-and-fields from the callers' side: the daily sync
- * job, the sign-in, profile-edit and delete listener, the personal settings
- * section and the API shape of a stored value, each through the real
+ * job, the sign-in, profile-edit and delete listener and the API shape of a stored value, each through the real
* ProfileFieldService on in-memory values. A failure in one person's sync is
* counted and logged, never thrown.
*
@@ -31,7 +30,6 @@
use OCA\LaunchPad\Listener\ProfileFieldsListener;
use OCA\LaunchPad\Service\AdminTemplateService;
use OCA\LaunchPad\Service\ProfileFieldService;
-use OCA\LaunchPad\Settings\LaunchPadPersonal;
use OCP\Accounts\IAccount;
use OCP\Accounts\IAccountManager;
use OCP\Accounts\IAccountProperty;
@@ -184,34 +182,6 @@ public function testTheListenerFollowsSignInEditAndDelete(): void {
$this->assertSame(['Profile field sync failed: backend gone'], $this->warnings, 'a failure never breaks the sign-in');
}//end testTheListenerFollowsSignInEditAndDelete()
- /**
- * The personal section shows only once a field is defined.
- *
- * @return void
- */
- public function testThePersonalSectionShowsOnceAFieldExists(): void {
- $personal = new LaunchPadPersonal(profileFields: $this->service);
- $this->assertNull($personal->getSection());
- $this->assertSame(90, $personal->getPriority());
-
- $this->service->saveDefinitions(raw: [['key' => 'expertise', 'label' => 'Expertise', 'type' => 'tags', 'source' => 'self']]);
- $this->assertSame('personal-info', $personal->getSection());
- }//end testThePersonalSectionShowsOnceAFieldExists()
-
- /**
- * The personal form loads the personal bundle into its template.
- *
- * @return void
- */
- public function testThePersonalFormRendersItsTemplate(): void {
- if (class_exists(class: '\OC', autoload: false) === false) {
- $this->markTestSkipped(message: 'Util::addScript needs a live Nextcloud; CI boots one.');
- }
-
- $form = (new LaunchPadPersonal(profileFields: $this->service))->getForm();
- $this->assertSame('settings/personal', $form->getTemplateName());
- }//end testThePersonalFormRendersItsTemplate()
-
/**
* A stored value's API shape.
*
diff --git a/webpack.config.js b/webpack.config.js
index 9d8dda989..b9df11e32 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -14,7 +14,6 @@ webpackConfig.entry = {
// Anonymous full-screen kiosk player (/apps/launchpad/kiosk/{token}).
kiosk: path.join(__dirname, 'src', 'kiosk.js'),
// Personal settings: the person's own profile fields (REQ-PEX-002).
- personal: path.join(__dirname, 'src', 'personal.js'),
}
webpackConfig.output = {
From b62a6a819d1fe842de466841003eb265a9b99d98 Mon Sep 17 00:00:00 2001
From: Ruben van der Linde
Date: Mon, 12 Oct 2026 07:40:57 +0200
Subject: [PATCH 41/60] fix(workspace): dock the start page controls in the
greeting row (#840)
---
src/styles/workspace.css | 38 ++++++++++
src/utils/__tests__/headerActionsDock.spec.js | 69 +++++++++++++++++++
src/utils/headerActionsDock.js | 54 +++++++++++++++
src/views/Views.vue | 61 +++++++++++++++-
4 files changed, 220 insertions(+), 2 deletions(-)
create mode 100644 src/utils/__tests__/headerActionsDock.spec.js
create mode 100644 src/utils/headerActionsDock.js
diff --git a/src/styles/workspace.css b/src/styles/workspace.css
index 12e4c860f..5874c3edd 100644
--- a/src/styles/workspace.css
+++ b/src/styles/workspace.css
@@ -244,6 +244,44 @@
text-decoration: none !important;
}
+/* With the workspace controls docked in the greeting row
+ (headerActionsDock.js), the row is the greeting, then the actions at its
+ right, as LpStart draws it. "Startpagina aanpassen" stays at the right of
+ the greeting text, which now ends where the controls begin, so the two
+ never overlap. */
+.launchpad-container .cn-header-widget--ground:has(> .launchpad-header-actions) {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ position: static;
+}
+
+.launchpad-container
+ .cn-header-widget--ground:has(> .launchpad-header-actions)
+ > .cn-header-widget__content {
+ position: relative;
+ flex: 1 1 auto;
+ min-width: 0;
+ align-self: stretch;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+}
+
+/* The library's 8px top margin would put the button 8px below the
+ controls' centre line. */
+.launchpad-container
+ .cn-header-widget--ground:has(> .launchpad-header-actions)
+ .cn-header-widget__cta {
+ margin-top: 0;
+}
+
+.launchpad-container .launchpad-header-actions {
+ flex: none;
+ display: flex;
+ align-items: center;
+}
+
/* A section label reads as a heading at the left edge. */
.launchpad-container .cn-label-widget {
justify-content: flex-start !important;
diff --git a/src/utils/__tests__/headerActionsDock.spec.js b/src/utils/__tests__/headerActionsDock.spec.js
new file mode 100644
index 000000000..0da87944b
--- /dev/null
+++ b/src/utils/__tests__/headerActionsDock.spec.js
@@ -0,0 +1,69 @@
+/**
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
+ * SPDX-License-Identifier: EUPL-1.2
+ *
+ * The top-right controls dock into a greeting row on the page ground and
+ * float otherwise (LpStart; they covered "Startpagina aanpassen" floating).
+ *
+ * @spec openspec/specs/runtime-shell/spec.md#requirement-req-shell-009-start-page-without-navigation-panel
+ */
+
+import { beforeEach, describe, expect, it } from 'vitest'
+import { HEADER_ACTIONS_CLASS, resolveHeaderActionsTarget } from '../headerActionsDock.js'
+
+/**
+ * @param {string} html Inner HTML of the container.
+ * @return {HTMLElement} The container, attached to the document.
+ */
+function container(html) {
+ const el = document.createElement('div')
+ el.className = 'launchpad-container'
+ el.innerHTML = html
+ document.body.appendChild(el)
+ return el
+}
+
+const GROUND = '
'
+
+describe('resolveHeaderActionsTarget', () => {
+ beforeEach(() => {
+ document.body.innerHTML = ''
+ })
+
+ it('floats when there is no container', () => {
+ expect(resolveHeaderActionsTarget(null, false)).toBeNull()
+ })
+
+ it('floats on a dashboard without a greeting on the page ground', () => {
+ const el = container('
')
+ expect(resolveHeaderActionsTarget(el, false)).toBeNull()
+ expect(el.querySelector('.' + HEADER_ACTIONS_CLASS)).toBeNull()
+ })
+
+ it('docks into the greeting row, after the greeting, and reuses the same element', () => {
+ const el = container(GROUND)
+ const target = resolveHeaderActionsTarget(el, false)
+ const header = el.querySelector('.cn-header-widget--ground')
+ expect(target).not.toBeNull()
+ expect(target.parentElement).toBe(header)
+ expect(header.lastElementChild).toBe(target)
+ expect(resolveHeaderActionsTarget(el, false)).toBe(target)
+ expect(el.querySelectorAll('.' + HEADER_ACTIONS_CLASS)).toHaveLength(1)
+ })
+
+ it('floats while editing and removes the actions element', () => {
+ const el = container(GROUND)
+ resolveHeaderActionsTarget(el, false)
+ expect(resolveHeaderActionsTarget(el, true)).toBeNull()
+ expect(el.querySelector('.' + HEADER_ACTIONS_CLASS)).toBeNull()
+ })
+
+ it('moves to the new greeting row when another dashboard replaces it', () => {
+ const el = container(GROUND)
+ const first = resolveHeaderActionsTarget(el, false)
+ el.innerHTML = GROUND
+ const second = resolveHeaderActionsTarget(el, false)
+ expect(second).not.toBe(first)
+ expect(second.parentElement).toBe(el.querySelector('.cn-header-widget--ground'))
+ })
+})
diff --git a/src/utils/headerActionsDock.js b/src/utils/headerActionsDock.js
new file mode 100644
index 000000000..48837c2f1
--- /dev/null
+++ b/src/utils/headerActionsDock.js
@@ -0,0 +1,54 @@
+/**
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
+ * SPDX-License-Identifier: EUPL-1.2
+ *
+ * Where the workspace's top-right controls (dashboard actions, dashboard
+ * switcher, start page menu, share) render.
+ *
+ * Without a greeting row they float at the top right, as before. When the
+ * dashboard opens with a greeting on the page ground (a header widget with
+ * `ground`), LpStart draws the page's actions in that row, right of
+ * "Startpagina aanpassen". Floating, they sat on top of that button and
+ * covered it. So the controls dock into the greeting row instead: this
+ * module finds the row and gives it one actions element to teleport into.
+ */
+
+/** Class of the element the controls are teleported into. */
+export const HEADER_ACTIONS_CLASS = 'launchpad-header-actions'
+
+/** The greeting row the controls dock into. */
+const GROUND_HEADER_SELECTOR = '.cn-header-widget--ground'
+
+/**
+ * Find (or create) the element inside the greeting row that holds the
+ * controls, or null when they should float: no greeting row on the page,
+ * or the dashboard is being edited (the row is then a draggable grid item).
+ * Removes an actions element that is no longer used, so the row lays out
+ * as before when the controls float.
+ *
+ * @spec openspec/specs/runtime-shell/spec.md#requirement-req-shell-009-start-page-without-navigation-panel
+ * @param {Element|null} container The dashboard grid container.
+ * @param {boolean} isEditMode Whether the dashboard is being edited.
+ * @return {HTMLElement|null} The element to teleport the controls into.
+ */
+export function resolveHeaderActionsTarget(container, isEditMode) {
+ if (!container) {
+ return null
+ }
+ const header = container.querySelector(GROUND_HEADER_SELECTOR)
+ for (const stale of container.querySelectorAll('.' + HEADER_ACTIONS_CLASS)) {
+ if (isEditMode || stale.parentElement !== header) {
+ stale.remove()
+ }
+ }
+ if (!header || isEditMode) {
+ return null
+ }
+ let target = header.querySelector(':scope > .' + HEADER_ACTIONS_CLASS)
+ if (!target) {
+ target = document.createElement('div')
+ target.className = HEADER_ACTIONS_CLASS
+ header.appendChild(target)
+ }
+ return target
+}
diff --git a/src/views/Views.vue b/src/views/Views.vue
index e3308f04a..9b38fc83a 100644
--- a/src/views/Views.vue
+++ b/src/views/Views.vue
@@ -76,8 +76,15 @@
Both halves have to move for the ordering between them to mean
anything. -->
-
-
+
+
+
-
-
+
+
import { CnAppRoot } from '@conduction/nextcloud-vue'
import { computed } from 'vue'
-import UserSettings from './views/settings/UserSettings.vue'
+import ProfileFieldsSettings from './views/settings/ProfileFieldsSettings.vue'
import customComponents from './customComponents.js'
import { ICON_CATALOGUE } from './services/iconCatalogue.js'
import { permits } from './utils/permissions.js'
@@ -127,7 +126,7 @@ export default {
name: 'App',
components: {
CnAppRoot,
- UserSettings,
+ ProfileFieldsSettings,
},
inject: {
@@ -254,6 +253,18 @@ export default {
},
computed: {
+ /**
+ * The sections of the user settings dialog, in board LpPersoonlijk's
+ * order: the app's own (My profile), then the credentials, and the
+ * library's notifications, walkthrough replay and integrations.
+ *
+ * @return {string[]} Section keys for CnAppRoot.
+ * @spec openspec/specs/people-widget/spec.md
+ */
+ userSettingsSections() {
+ return ['app', 'credentials', 'notifications', 'walkthrough', 'integrations']
+ },
+
/**
* Whether the navigation panel is left out of this route: the admin
* option is on AND the route is the dashboard view (`/` or
@@ -421,31 +432,6 @@ export default {
root.userSettingsOpen = true
}
},
-
- /**
- * The walkthrough replay section CnAppRoot draws in its own fallback,
- * which filling the #user-settings slot switches off. Reads CnAppRoot's
- * state and labels, so the wording and the behaviour stay the library's.
- *
- * @return {object|null} Section props for UserSettings, or null without a tour.
- * @spec openspec/specs/people-widget/spec.md
- */
- walkthroughSettings() {
- const root = this.appRoot()
- if (!root || !root.walkthroughEnabled) {
- return null
- }
- const inProgress = Boolean(root.walkthroughProgressValue)
- return {
- sectionName: root.restartWalkthroughSectionName,
- hint: root.restartWalkthroughHint,
- inProgress,
- continueLabel: root.continueWalkthroughLabel,
- restartLabel: inProgress
- ? root.startOverWalkthroughLabel
- : root.restartWalkthroughLabel,
- }
- },
},
}
diff --git a/src/__tests__/App.supportNote.spec.js b/src/__tests__/App.supportNote.spec.js
index 4e3746b75..31c6eea01 100644
--- a/src/__tests__/App.supportNote.spec.js
+++ b/src/__tests__/App.supportNote.spec.js
@@ -11,9 +11,6 @@ import { describe, expect, it, vi } from 'vitest'
vi.mock('@conduction/nextcloud-vue', () => ({
CnAppRoot: { name: 'CnAppRoot', template: '' },
- // The user settings dialog's library sections (src/views/settings/UserSettings.vue).
- CnCredentials: { name: 'CnCredentials', template: '' },
- CnNotificationPreferences: { name: 'CnNotificationPreferences', template: '' },
}))
vi.mock('../customComponents.js', () => ({ default: {} }))
vi.mock('../services/iconCatalogue.js', () => ({ ICON_CATALOGUE: [] }))
diff --git a/src/views/settings/UserSettings.vue b/src/views/settings/UserSettings.vue
deleted file mode 100644
index adea7a393..000000000
--- a/src/views/settings/UserSettings.vue
+++ /dev/null
@@ -1,121 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
From d477945dd2e91b0ec441d4d527d8b46fcea52eb2 Mon Sep 17 00:00:00 2001
From: Ruben van der Linde
Date: Mon, 12 Oct 2026 20:18:06 +0200
Subject: [PATCH 45/60] fix(modals): a modal shows its title once (#847)
* chore(deps): bump @conduction/nextcloud-vue to ^2.81.0
* test(views): give the loading-shim spec time for the 2.81 library import
* fix(modals): a modal shows its title once
Every NcModal in the app drew its title twice: the h2 in the body, as the
boards do, and NcModal's own name bar floating over the top bar. The name
stays on the modal as its accessible label; its visible copy is hidden for
the ten modals that draw their own title.
---
src/admin.js | 1 +
src/main.js | 1 +
src/modals/AcknowledgementReportModal.vue | 1 +
src/modals/DashboardConfigModal.vue | 7 ++++++-
src/modals/SetupWizardModal.vue | 1 +
src/modals/TemplateEditorModal.vue | 1 +
src/modals/TemplateResyncModal.vue | 1 +
src/modals/TileEditor.vue | 1 +
src/modals/VersionHistoryModal.vue | 1 +
src/modals/VisibilityRulesModal.vue | 1 +
src/modals/WidgetMovePanel.vue | 1 +
src/modals/WidgetPickerModal.vue | 1 +
src/styles/modals.css | 13 +++++++++++++
13 files changed, 30 insertions(+), 1 deletion(-)
create mode 100644 src/styles/modals.css
diff --git a/src/admin.js b/src/admin.js
index 9ea2374ec..3a1bbe871 100644
--- a/src/admin.js
+++ b/src/admin.js
@@ -27,6 +27,7 @@ import { widgetBridge } from './services/widgetBridge.js'
import { loadInitialState } from './utils/loadInitialState.js'
import './publicPath.js'
+import './styles/modals.css'
// Reference the imported singleton so its side-effecting construction is not
// tree-shaken / flagged as an unused import.
diff --git a/src/main.js b/src/main.js
index c2412c1bb..fd82a3229 100644
--- a/src/main.js
+++ b/src/main.js
@@ -59,6 +59,7 @@ import './publicPath.js'
import './services/widgetBridge.js'
import 'gridstack/dist/gridstack.min.css'
import './styles/workspace.css'
+import './styles/modals.css'
registerBuiltinDashboardWidgets()
// The library's own strings ("Good afternoon, {name}" in the greeting header,
diff --git a/src/modals/AcknowledgementReportModal.vue b/src/modals/AcknowledgementReportModal.vue
index 62ead5c68..3309ca0db 100644
--- a/src/modals/AcknowledgementReportModal.vue
+++ b/src/modals/AcknowledgementReportModal.vue
@@ -17,6 +17,7 @@
diff --git a/src/modals/DashboardConfigModal.vue b/src/modals/DashboardConfigModal.vue
index 688e17020..e7fe0bba1 100644
--- a/src/modals/DashboardConfigModal.vue
+++ b/src/modals/DashboardConfigModal.vue
@@ -4,7 +4,12 @@
-->
-
+
{{ modalTitle }}
diff --git a/src/modals/SetupWizardModal.vue b/src/modals/SetupWizardModal.vue
index 8d196dd28..5c0e3ec05 100644
--- a/src/modals/SetupWizardModal.vue
+++ b/src/modals/SetupWizardModal.vue
@@ -5,6 +5,7 @@
diff --git a/src/modals/TemplateEditorModal.vue b/src/modals/TemplateEditorModal.vue
index 1a527b4e7..6e405bc9f 100644
--- a/src/modals/TemplateEditorModal.vue
+++ b/src/modals/TemplateEditorModal.vue
@@ -18,6 +18,7 @@
diff --git a/src/modals/TileEditor.vue b/src/modals/TileEditor.vue
index f7c6740d5..dedd625c9 100644
--- a/src/modals/TileEditor.vue
+++ b/src/modals/TileEditor.vue
@@ -11,6 +11,7 @@
renders empty and never writes back. Use `v-model` throughout. -->
diff --git a/src/modals/VersionHistoryModal.vue b/src/modals/VersionHistoryModal.vue
index 74073762f..07e520c48 100644
--- a/src/modals/VersionHistoryModal.vue
+++ b/src/modals/VersionHistoryModal.vue
@@ -6,6 +6,7 @@
diff --git a/src/modals/VisibilityRulesModal.vue b/src/modals/VisibilityRulesModal.vue
index 3b1ff5480..adffe50d5 100644
--- a/src/modals/VisibilityRulesModal.vue
+++ b/src/modals/VisibilityRulesModal.vue
@@ -6,6 +6,7 @@
diff --git a/src/modals/WidgetMovePanel.vue b/src/modals/WidgetMovePanel.vue
index 7efeb22f7..7fdeb94ab 100644
--- a/src/modals/WidgetMovePanel.vue
+++ b/src/modals/WidgetMovePanel.vue
@@ -6,6 +6,7 @@
diff --git a/src/modals/WidgetPickerModal.vue b/src/modals/WidgetPickerModal.vue
index 4480d9d11..87fd23b08 100644
--- a/src/modals/WidgetPickerModal.vue
+++ b/src/modals/WidgetPickerModal.vue
@@ -6,6 +6,7 @@
diff --git a/src/styles/modals.css b/src/styles/modals.css
new file mode 100644
index 000000000..b9d3d9cc6
--- /dev/null
+++ b/src/styles/modals.css
@@ -0,0 +1,13 @@
+/*
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
+ * SPDX-License-Identifier: EUPL-1.2
+ *
+ * LaunchPad's modals draw their own title (an h2 in the body, as the boards
+ * do). NcModal also draws its `name` in a header bar over the backdrop, so
+ * the title showed twice, the second one floating over the top bar. The name
+ * stays on the modal for the accessible label; only its visible copy is
+ * hidden.
+ */
+.modal-mask.launchpad-modal .modal-header__name {
+ display: none;
+}
From 32c64350e3658433d3c972bc52a24e9dfcf21f02 Mon Sep 17 00:00:00 2001
From: Ruben van der Linde
Date: Tue, 13 Oct 2026 10:58:55 +0200
Subject: [PATCH 46/60] fix(i18n): the manifest's strings are translated (#849)
CnAppRoot defaults its translate prop to the identity, and App.vue never
passed one. Menu labels, page titles and descriptions and the setup copy
stayed English for a Dutch reader, although l10n/nl.js carries every one.
---
src/App.vue | 18 +++++++++++
src/__tests__/App.translate.spec.js | 49 +++++++++++++++++++++++++++++
2 files changed, 67 insertions(+)
create mode 100644 src/__tests__/App.translate.spec.js
diff --git a/src/App.vue b/src/App.vue
index 5bd94a241..2a0f31c9d 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -22,6 +22,7 @@
:permissions="permissions"
:supportDialog="supportNoteForThisAccount"
:userSettingsSections="userSettingsSections"
+ :translate="translateForApp"
appId="launchpad">
-
-
-
+
@@ -171,8 +165,12 @@
+
-
+
+
-
-
-
@@ -448,9 +438,7 @@ import FormatListBulleted from 'vue-material-design-icons/FormatListBulleted.vue
import Lock from 'vue-material-design-icons/Lock.vue'
import Plus from 'vue-material-design-icons/Plus.vue'
import ShareVariant from 'vue-material-design-icons/ShareVariant.vue'
-import StarOutline from 'vue-material-design-icons/StarOutline.vue'
import Translate from 'vue-material-design-icons/Translate.vue'
-import Tune from 'vue-material-design-icons/Tune.vue'
import DetailsTab from '../components/Workspace/config/DetailsTab.vue'
import LanguagesTab from '../components/Workspace/config/LanguagesTab.vue'
import { api } from '../services/api.js'
@@ -482,9 +470,7 @@ export default {
Close,
Account,
AccountGroup,
- Tune,
ShareVariant,
- StarOutline,
ContentCopy,
Check,
Lock,
@@ -556,7 +542,9 @@ export default {
data() {
return {
- currentTab: this.initialTab || 'general',
+ currentTab:
+ this.initialTab === 'default' ? 'general' : this.initialTab || 'general',
+
form: {
name: '',
description: '',
@@ -657,37 +645,30 @@ export default {
*/
tabs() {
const list = [
- { id: 'general', label: t('launchpad', 'General'), icon: Tune },
+ {
+ id: 'general',
+ label: t('launchpad', 'Details'),
+ icon: FormatListBulleted,
+ },
]
+ // dashboard-language-and-details-tabs: language versions, for the
+ // owner (a dashboard needs a uuid first, so the strip is hidden
+ // while creating).
+ if (this.canManageShares && this.dashboard?.uuid) {
+ list.push({
+ id: 'languages',
+ label: t('launchpad', 'Languages'),
+ icon: Translate,
+ })
+ }
if (this.canManageShares) {
list.push({
id: 'sharing',
label: t('launchpad', 'Sharing'),
icon: ShareVariant,
+ count: this.localShares.length,
})
}
- list.push({
- id: 'default',
- label: t('launchpad', 'Default'),
- icon: StarOutline,
- })
- // dashboard-language-and-details-tabs: language versions and
- // detail fields, for the owner (a dashboard needs a uuid first,
- // so the strip is hidden while creating).
- if (this.canManageShares && this.dashboard?.uuid) {
- list.push(
- {
- id: 'languages',
- label: t('launchpad', 'Languages'),
- icon: Translate,
- },
- {
- id: 'details',
- label: t('launchpad', 'Details'),
- icon: FormatListBulleted,
- },
- )
- }
return list
},
@@ -783,7 +764,9 @@ export default {
// top-bar share action opens directly on Sharing). Fall back
// to General if the requested tab is the share tab but the
// user cannot manage shares.
- const wanted = this.initialTab || 'general'
+ // `default` was its own tab; the switch now sits under Details.
+ const wanted
+ = this.initialTab === 'default' ? 'general' : this.initialTab || 'general'
this.currentTab =
wanted === 'sharing' && !this.canManageShares
? 'general'
@@ -1457,4 +1440,16 @@ export default {
.dashboard-config__error {
color: var(--color-error-text);
}
+
+/* The count on the Delen tab (board LpDashboardInstellingen). */
+.dashboard-config__tab-count {
+ min-width: 20px;
+ padding: 0 6px;
+ border-radius: var(--border-radius-pill);
+ background: var(--color-background-dark);
+ font-size: 12px;
+ font-weight: 600;
+ line-height: 20px;
+ text-align: center;
+}
diff --git a/src/modals/__tests__/DashboardConfigModal.spec.js b/src/modals/__tests__/DashboardConfigModal.spec.js
index a8b67b9de..8c7eee1fe 100644
--- a/src/modals/__tests__/DashboardConfigModal.spec.js
+++ b/src/modals/__tests__/DashboardConfigModal.spec.js
@@ -143,4 +143,44 @@ describe('DashboardConfigModal tab split', () => {
})
expect(wrapper.vm.currentTab).toBe('general')
})
+
+ it('has the board\'s three tabs: Details, Languages, Sharing', () => {
+ const wrapper = mountModal()
+ const ids = wrapper
+ .findAll('[role="tab"]')
+ .map((tab) => tab.attributes('data-test'))
+ expect(ids).toEqual([
+ 'config-tab-general',
+ 'config-tab-languages',
+ 'config-tab-sharing',
+ ])
+ expect(wrapper.find('[data-test="config-tab-general"]').text()).toContain(
+ 'Details',
+ )
+ })
+
+ it('puts the default switch under Details', () => {
+ const wrapper = mountModal()
+ const general = wrapper.find('[data-test="config-panel-general"]')
+ expect(general.text()).toContain('Default dashboard')
+ expect(wrapper.find('[data-test="config-panel-default"]').exists()).toBe(
+ false,
+ )
+ })
+
+ it('lands on Details when an old caller asks for the default tab', () => {
+ const wrapper = mountModal({ initialTab: 'default' })
+ expect(wrapper.vm.currentTab).toBe('general')
+ })
+
+ it('counts the shares on the Sharing tab', async () => {
+ const wrapper = mountModal()
+ // Let the modal's own share load settle first; it resets the list.
+ await new Promise((resolve) => setTimeout(resolve, 0))
+ wrapper.vm.localShares = [{ shareWith: 'a' }, { shareWith: 'b' }]
+ await wrapper.vm.$nextTick()
+ expect(
+ wrapper.find('[data-test="config-tab-count-sharing"]').text(),
+ ).toBe('2')
+ })
})
From aba6820720bf76288a1449084963a36ff48f0a7b Mon Sep 17 00:00:00 2001
From: Ruben van der Linde
Date: Tue, 13 Oct 2026 20:44:03 +0200
Subject: [PATCH 52/60] fix(styles): launchpad loads the library stylesheet
itself (#861)
main.js, public.js and admin.js never imported
@conduction/nextcloud-vue/css/index.css. The board look, widget and dialog
styles only arrived when another app's bundle on the page carried them,
which is why the Flows toolbar centred its buttons. pipelinq, dossiq and
shillinq import it the same way.
---
src/admin.js | 3 +++
src/main.js | 3 +++
src/public.js | 3 +++
3 files changed, 9 insertions(+)
diff --git a/src/admin.js b/src/admin.js
index 3a1bbe871..454074cf0 100644
--- a/src/admin.js
+++ b/src/admin.js
@@ -27,6 +27,9 @@ import { widgetBridge } from './services/widgetBridge.js'
import { loadInitialState } from './utils/loadInitialState.js'
import './publicPath.js'
+// The library's own stylesheet (board look, widgets, dialogs). Without it
+// the page only had the library CSS another app's bundle happened to load.
+import '@conduction/nextcloud-vue/css/index.css'
import './styles/modals.css'
// Reference the imported singleton so its side-effecting construction is not
diff --git a/src/main.js b/src/main.js
index fd82a3229..cfeeaf82a 100644
--- a/src/main.js
+++ b/src/main.js
@@ -58,6 +58,9 @@ import { currentPermissions } from './utils/permissions.js'
import './publicPath.js'
import './services/widgetBridge.js'
import 'gridstack/dist/gridstack.min.css'
+// The library's own stylesheet (board look, widgets, dialogs). Without it
+// the page only had the library CSS another app's bundle happened to load.
+import '@conduction/nextcloud-vue/css/index.css'
import './styles/workspace.css'
import './styles/modals.css'
registerBuiltinDashboardWidgets()
diff --git a/src/public.js b/src/public.js
index fb22f764f..ac8c17ef2 100644
--- a/src/public.js
+++ b/src/public.js
@@ -23,6 +23,9 @@ import DashboardPublicShareView from './views/DashboardPublicShareView.vue'
import './publicPath.js'
import 'gridstack/dist/gridstack.min.css'
+// The library's own stylesheet (board look, widgets, dialogs). Without it
+// the page only had the library CSS another app's bundle happened to load.
+import '@conduction/nextcloud-vue/css/index.css'
registerBuiltinDashboardWidgets()
// The library's own strings (the greeting header among them) in the
// viewer's language. See main.js.
From e726f8385fdd55a64eb699f3b4ab25d13439e21c Mon Sep 17 00:00:00 2001
From: Ruben van der Linde
Date: Tue, 13 Oct 2026 23:50:54 +0200
Subject: [PATCH 53/60] chore(deps): launchpad builds on nextcloud-vue 2.82.0
(#864)
The library stylesheet check holds on the new dist: cn-store-page is in
the CSS and library-style.js stamps data-cn-library. No app workaround
needed removing; the app already imports the library stylesheet (#861).
---
package-lock.json | 8 ++++----
package.json | 2 +-
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index ba11c1cf3..1c2c2a33b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,7 +9,7 @@
"version": "1.0.0",
"license": "EUPL-1.2",
"dependencies": {
- "@conduction/nextcloud-vue": "^2.81.0",
+ "@conduction/nextcloud-vue": "^2.82.0",
"@mdi/js": "^7.4.47",
"@nextcloud/axios": "^2.6.0",
"@nextcloud/dialogs": "^7.4.1",
@@ -1973,9 +1973,9 @@
}
},
"node_modules/@conduction/nextcloud-vue": {
- "version": "2.81.0",
- "resolved": "https://registry.npmjs.org/@conduction/nextcloud-vue/-/nextcloud-vue-2.81.0.tgz",
- "integrity": "sha512-NqKwqGSklswT6TWTSIa5/y3S43I5VUwWdYvklgrShxDclDKNj86pQFo65cTz0g1qVlFwWB1VIuUt2Y70/FTvig==",
+ "version": "2.82.0",
+ "resolved": "https://registry.npmjs.org/@conduction/nextcloud-vue/-/nextcloud-vue-2.82.0.tgz",
+ "integrity": "sha512-GyvYAQq7cRQvqr/SoUwC6uhkkyb87MAr+7bjh+WO0BaGmWVn8yJJcx75YCtgLzAQziNuhkU8vv5eM4u3nXqXng==",
"license": "EUPL-1.2",
"dependencies": {
"@ckpack/vue-color": "^1.6.0",
diff --git a/package.json b/package.json
index 8264d0540..7a57d4eef 100644
--- a/package.json
+++ b/package.json
@@ -35,7 +35,7 @@
"prepare": "git config core.hooksPath .githooks || true"
},
"dependencies": {
- "@conduction/nextcloud-vue": "^2.81.0",
+ "@conduction/nextcloud-vue": "^2.82.0",
"@mdi/js": "^7.4.47",
"@nextcloud/axios": "^2.6.0",
"@nextcloud/dialogs": "^7.4.1",
From 6c3915d01ca65ad77db150d513611e45f7c74ff6 Mon Sep 17 00:00:00 2001
From: Ruben van der Linde
Date: Wed, 14 Oct 2026 11:09:02 +0200
Subject: [PATCH 54/60] fix(admin): the admin page draws in the library admin
shell, board look, opening on Algemeen (#862)
* fix(admin): the admin page opens on Algemeen, in the board's cards and tabs
The setup banner, the defaults, group-shared dashboards and the default-app
note sat above the tab strip on every tab. They now form the first tab,
Algemeen, as cards in two columns (board LpBeheerAlgemeen), and the page
opens there. The tab strip takes the board's 44px tabs with a 3px
underline, and four tab names follow the board: Bediening, Versies en
analyse, Organisatiemenu, Demogegevens.
* fix(admin): the admin page uses the full width, as the board
* Revert "fix(admin): the admin page uses the full width, as the board"
This reverts commit c0829bf8aa17c061101a4e702949e2ab89ca7b54.
* chore(deps): launchpad builds on nextcloud-vue 2.82.0
The library stylesheet check holds on the new dist: cn-store-page is in
the CSS and library-style.js stamps data-cn-library. No app workaround
needed removing; the app already imports the library stylesheet (#861).
* fix(admin): the admin page draws in the library admin shell, board look
CnAdminSettingsShell with look="board" draws the h1 that names the open
tab, the path line (Beheerinstellingen / Launchpad / , with "geldt
voor de hele organisatie" on Algemeen) and the Documentatie button. No
version card: the board has none.
The tab strip is a plain child of the shell, so it spans the grid without
a card or heading. On Algemeen the defaults, group-shared dashboards and
the default-app note are CnSettingsSection cards in the shell's two
columns. The app's own card and grid CSS goes: the library draws both.
l10n: two strings. nl carries the board's Dutch; the other required
locales carry the English text.
* fix(admin): switches as the board draws them, no strip under the banner
The three on/off defaults are switches. The tab panel takes its tab's
height and draws no white surface below the setup banner. The spec keeps
the router's other exports and lint is clean.
---
l10n/be.js | 4 +-
l10n/be.json | 4 +-
l10n/bg.js | 4 +-
l10n/bg.json | 4 +-
l10n/bs.js | 4 +-
l10n/bs.json | 4 +-
l10n/ca.js | 4 +-
l10n/ca.json | 4 +-
l10n/cs.js | 4 +-
l10n/cs.json | 4 +-
l10n/da.js | 4 +-
l10n/da.json | 4 +-
l10n/de.js | 4 +-
l10n/de.json | 4 +-
l10n/el.js | 4 +-
l10n/el.json | 4 +-
l10n/en.js | 4 +-
l10n/en.json | 4 +-
l10n/es.js | 4 +-
l10n/es.json | 4 +-
l10n/et.js | 4 +-
l10n/et.json | 4 +-
l10n/fi.js | 4 +-
l10n/fi.json | 4 +-
l10n/fr.js | 4 +-
l10n/fr.json | 4 +-
l10n/ga.js | 4 +-
l10n/ga.json | 4 +-
l10n/hr.js | 4 +-
l10n/hr.json | 4 +-
l10n/hu.js | 4 +-
l10n/hu.json | 4 +-
l10n/is.js | 4 +-
l10n/is.json | 4 +-
l10n/it.js | 4 +-
l10n/it.json | 4 +-
l10n/lb.js | 4 +-
l10n/lb.json | 4 +-
l10n/lt.js | 4 +-
l10n/lt.json | 4 +-
l10n/lv.js | 4 +-
l10n/lv.json | 4 +-
l10n/mk.js | 4 +-
l10n/mk.json | 4 +-
l10n/mt.js | 4 +-
l10n/mt.json | 4 +-
l10n/nb.js | 4 +-
l10n/nb.json | 4 +-
l10n/nl.js | 12 +-
l10n/nl.json | 12 +-
l10n/pl.js | 4 +-
l10n/pl.json | 4 +-
l10n/pt.js | 4 +-
l10n/pt.json | 4 +-
l10n/rm.js | 4 +-
l10n/rm.json | 4 +-
l10n/ro.js | 4 +-
l10n/ro.json | 4 +-
l10n/ru.js | 4 +-
l10n/ru.json | 4 +-
l10n/sk.js | 4 +-
l10n/sk.json | 4 +-
l10n/sl.js | 4 +-
l10n/sl.json | 4 +-
l10n/sq.js | 4 +-
l10n/sq.json | 4 +-
l10n/sr.js | 4 +-
l10n/sr.json | 4 +-
l10n/sv.js | 4 +-
l10n/sv.json | 4 +-
l10n/tr.js | 4 +-
l10n/tr.json | 4 +-
l10n/uk.js | 4 +-
l10n/uk.json | 4 +-
src/components/admin/AdminSettings.vue | 300 ++++++++++--------
src/components/admin/BeheerTabs.vue | 24 +-
.../__tests__/AdminSettings.tabs.spec.js | 97 ++++++
77 files changed, 511 insertions(+), 222 deletions(-)
create mode 100644 src/components/admin/__tests__/AdminSettings.tabs.spec.js
diff --git a/l10n/be.js b/l10n/be.js
index fec6d33b0..e5c4e5c00 100644
--- a/l10n/be.js
+++ b/l10n/be.js
@@ -1354,6 +1354,8 @@ OC.L10N.register(
"You acknowledge an announcement on a dashboard" : "Вы пацвярджаеце аб'яву на дашбордзе",
"Credentials" : "Credentials",
"My profile" : "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}" : "Launchpad administration: {tab}",
+ "applies to the whole organisation" : "applies to the whole organisation"
},
"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);");
diff --git a/l10n/be.json b/l10n/be.json
index 0a684d7f3..80c821e84 100644
--- a/l10n/be.json
+++ b/l10n/be.json
@@ -1331,6 +1331,8 @@
"You acknowledge an announcement on a dashboard": "Вы пацвярджаеце аб'яву на дашбордзе",
"Credentials": "Credentials",
"My profile": "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}": "Launchpad administration: {tab}",
+ "applies to the whole organisation": "applies to the whole organisation"
}
}
diff --git a/l10n/bg.js b/l10n/bg.js
index 23305feba..fa031aff4 100644
--- a/l10n/bg.js
+++ b/l10n/bg.js
@@ -1363,7 +1363,9 @@ OC.L10N.register(
"You acknowledge an announcement on a dashboard" : "Потвърждавате съобщение на табло",
"Credentials" : "Credentials",
"My profile" : "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}" : "Launchpad administration: {tab}",
+ "applies to the whole organisation" : "applies to the whole organisation"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/bg.json b/l10n/bg.json
index df4efb559..f2b03d839 100644
--- a/l10n/bg.json
+++ b/l10n/bg.json
@@ -1330,6 +1330,8 @@
"You acknowledge an announcement on a dashboard": "Потвърждавате съобщение на табло",
"Credentials": "Credentials",
"My profile": "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}": "Launchpad administration: {tab}",
+ "applies to the whole organisation": "applies to the whole organisation"
}
}
diff --git a/l10n/bs.js b/l10n/bs.js
index 391a7600f..d58b87bd5 100644
--- a/l10n/bs.js
+++ b/l10n/bs.js
@@ -1363,7 +1363,9 @@ OC.L10N.register(
"You acknowledge an announcement on a dashboard" : "Potvrđujete obavještenje na kontrolnoj tabli",
"Credentials" : "Credentials",
"My profile" : "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}" : "Launchpad administration: {tab}",
+ "applies to the whole organisation" : "applies to the whole organisation"
},
"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"
);
diff --git a/l10n/bs.json b/l10n/bs.json
index bc28656fc..74cbf5e5f 100644
--- a/l10n/bs.json
+++ b/l10n/bs.json
@@ -1331,6 +1331,8 @@
"You acknowledge an announcement on a dashboard": "Potvrđujete obavještenje na kontrolnoj tabli",
"Credentials": "Credentials",
"My profile": "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}": "Launchpad administration: {tab}",
+ "applies to the whole organisation": "applies to the whole organisation"
}
}
diff --git a/l10n/ca.js b/l10n/ca.js
index d9b44c9e3..eb4c90671 100644
--- a/l10n/ca.js
+++ b/l10n/ca.js
@@ -1363,7 +1363,9 @@ OC.L10N.register(
"You acknowledge an announcement on a dashboard" : "Confirmeu un avís en un tauler",
"Credentials" : "Credentials",
"My profile" : "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}" : "Launchpad administration: {tab}",
+ "applies to the whole organisation" : "applies to the whole organisation"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/ca.json b/l10n/ca.json
index 9978f349a..bbccacb67 100644
--- a/l10n/ca.json
+++ b/l10n/ca.json
@@ -1330,6 +1330,8 @@
"You acknowledge an announcement on a dashboard": "Confirmeu un avís en un tauler",
"Credentials": "Credentials",
"My profile": "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}": "Launchpad administration: {tab}",
+ "applies to the whole organisation": "applies to the whole organisation"
}
}
diff --git a/l10n/cs.js b/l10n/cs.js
index 09b1b56a6..cf9e7c0c1 100644
--- a/l10n/cs.js
+++ b/l10n/cs.js
@@ -1363,7 +1363,9 @@ OC.L10N.register(
"You acknowledge an announcement on a dashboard" : "Potvrzujete oznámení na přehledu",
"Credentials" : "Credentials",
"My profile" : "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}" : "Launchpad administration: {tab}",
+ "applies to the whole organisation" : "applies to the whole organisation"
},
"nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;"
);
diff --git a/l10n/cs.json b/l10n/cs.json
index 2b69461d7..582dcc8fb 100644
--- a/l10n/cs.json
+++ b/l10n/cs.json
@@ -1331,6 +1331,8 @@
"You acknowledge an announcement on a dashboard": "Potvrzujete oznámení na přehledu",
"Credentials": "Credentials",
"My profile": "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}": "Launchpad administration: {tab}",
+ "applies to the whole organisation": "applies to the whole organisation"
}
}
diff --git a/l10n/da.js b/l10n/da.js
index ba5b47f65..1d063c09f 100644
--- a/l10n/da.js
+++ b/l10n/da.js
@@ -1363,7 +1363,9 @@ OC.L10N.register(
"You acknowledge an announcement on a dashboard" : "Du bekræfter en meddelelse på et dashboard",
"Credentials" : "Credentials",
"My profile" : "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}" : "Launchpad administration: {tab}",
+ "applies to the whole organisation" : "applies to the whole organisation"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/da.json b/l10n/da.json
index 244778755..f11811fe5 100644
--- a/l10n/da.json
+++ b/l10n/da.json
@@ -1330,6 +1330,8 @@
"You acknowledge an announcement on a dashboard": "Du bekræfter en meddelelse på et dashboard",
"Credentials": "Credentials",
"My profile": "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}": "Launchpad administration: {tab}",
+ "applies to the whole organisation": "applies to the whole organisation"
}
}
diff --git a/l10n/de.js b/l10n/de.js
index 2c22d742c..a29a4c7a0 100644
--- a/l10n/de.js
+++ b/l10n/de.js
@@ -1363,7 +1363,9 @@ OC.L10N.register(
"You acknowledge an announcement on a dashboard" : "Sie bestätigen eine Mitteilung auf einem Dashboard",
"Credentials" : "Credentials",
"My profile" : "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}" : "Launchpad administration: {tab}",
+ "applies to the whole organisation" : "applies to the whole organisation"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/de.json b/l10n/de.json
index 3d1282bb5..14213c779 100644
--- a/l10n/de.json
+++ b/l10n/de.json
@@ -1330,6 +1330,8 @@
"You acknowledge an announcement on a dashboard": "Sie bestätigen eine Mitteilung auf einem Dashboard",
"Credentials": "Credentials",
"My profile": "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}": "Launchpad administration: {tab}",
+ "applies to the whole organisation": "applies to the whole organisation"
}
}
diff --git a/l10n/el.js b/l10n/el.js
index 51105713e..b239cd700 100644
--- a/l10n/el.js
+++ b/l10n/el.js
@@ -1363,7 +1363,9 @@ OC.L10N.register(
"You acknowledge an announcement on a dashboard" : "Επιβεβαιώνετε μια ανακοίνωση σε πίνακα ελέγχου",
"Credentials" : "Credentials",
"My profile" : "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}" : "Launchpad administration: {tab}",
+ "applies to the whole organisation" : "applies to the whole organisation"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/el.json b/l10n/el.json
index a4c597db6..c42c201d9 100644
--- a/l10n/el.json
+++ b/l10n/el.json
@@ -1330,6 +1330,8 @@
"You acknowledge an announcement on a dashboard": "Επιβεβαιώνετε μια ανακοίνωση σε πίνακα ελέγχου",
"Credentials": "Credentials",
"My profile": "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}": "Launchpad administration: {tab}",
+ "applies to the whole organisation": "applies to the whole organisation"
}
}
diff --git a/l10n/en.js b/l10n/en.js
index b772ff28a..bd911c289 100644
--- a/l10n/en.js
+++ b/l10n/en.js
@@ -1343,7 +1343,9 @@ OC.L10N.register(
"You acknowledge an announcement on a dashboard" : "You acknowledge an announcement on a dashboard",
"Credentials" : "Credentials",
"My profile" : "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}" : "Launchpad administration: {tab}",
+ "applies to the whole organisation" : "applies to the whole organisation"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/en.json b/l10n/en.json
index d5d671c46..251226a96 100644
--- a/l10n/en.json
+++ b/l10n/en.json
@@ -1308,6 +1308,8 @@
"You acknowledge an announcement on a dashboard": "You acknowledge an announcement on a dashboard",
"Credentials": "Credentials",
"My profile": "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}": "Launchpad administration: {tab}",
+ "applies to the whole organisation": "applies to the whole organisation"
}
}
diff --git a/l10n/es.js b/l10n/es.js
index 31dc722aa..da07e0859 100644
--- a/l10n/es.js
+++ b/l10n/es.js
@@ -1363,7 +1363,9 @@ OC.L10N.register(
"You acknowledge an announcement on a dashboard" : "Confirma un aviso en un panel",
"Credentials" : "Credentials",
"My profile" : "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}" : "Launchpad administration: {tab}",
+ "applies to the whole organisation" : "applies to the whole organisation"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/es.json b/l10n/es.json
index 82461bb3f..3c37eacaf 100644
--- a/l10n/es.json
+++ b/l10n/es.json
@@ -1330,6 +1330,8 @@
"You acknowledge an announcement on a dashboard": "Confirma un aviso en un panel",
"Credentials": "Credentials",
"My profile": "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}": "Launchpad administration: {tab}",
+ "applies to the whole organisation": "applies to the whole organisation"
}
}
diff --git a/l10n/et.js b/l10n/et.js
index d773ee298..5f62dd240 100644
--- a/l10n/et.js
+++ b/l10n/et.js
@@ -1363,7 +1363,9 @@ OC.L10N.register(
"You acknowledge an announcement on a dashboard" : "Kinnitad töölaual olevat teadet",
"Credentials" : "Credentials",
"My profile" : "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}" : "Launchpad administration: {tab}",
+ "applies to the whole organisation" : "applies to the whole organisation"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/et.json b/l10n/et.json
index cb47b2822..466a6834e 100644
--- a/l10n/et.json
+++ b/l10n/et.json
@@ -1330,6 +1330,8 @@
"You acknowledge an announcement on a dashboard": "Kinnitad töölaual olevat teadet",
"Credentials": "Credentials",
"My profile": "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}": "Launchpad administration: {tab}",
+ "applies to the whole organisation": "applies to the whole organisation"
}
}
diff --git a/l10n/fi.js b/l10n/fi.js
index 056c7e212..76a61fa01 100644
--- a/l10n/fi.js
+++ b/l10n/fi.js
@@ -1363,7 +1363,9 @@ OC.L10N.register(
"You acknowledge an announcement on a dashboard" : "Kuittaat kojelaudan tiedotteen",
"Credentials" : "Credentials",
"My profile" : "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}" : "Launchpad administration: {tab}",
+ "applies to the whole organisation" : "applies to the whole organisation"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/fi.json b/l10n/fi.json
index 38d9688d7..b46e4d804 100644
--- a/l10n/fi.json
+++ b/l10n/fi.json
@@ -1330,6 +1330,8 @@
"You acknowledge an announcement on a dashboard": "Kuittaat kojelaudan tiedotteen",
"Credentials": "Credentials",
"My profile": "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}": "Launchpad administration: {tab}",
+ "applies to the whole organisation": "applies to the whole organisation"
}
}
diff --git a/l10n/fr.js b/l10n/fr.js
index 198bdeb48..b852ba2b7 100644
--- a/l10n/fr.js
+++ b/l10n/fr.js
@@ -1363,7 +1363,9 @@ OC.L10N.register(
"You acknowledge an announcement on a dashboard" : "Vous confirmez une annonce sur un tableau de bord",
"Credentials" : "Credentials",
"My profile" : "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}" : "Launchpad administration: {tab}",
+ "applies to the whole organisation" : "applies to the whole organisation"
},
"nplurals=2; plural=(n > 1);"
);
diff --git a/l10n/fr.json b/l10n/fr.json
index 09ef36767..c21a6f9ca 100644
--- a/l10n/fr.json
+++ b/l10n/fr.json
@@ -1330,6 +1330,8 @@
"You acknowledge an announcement on a dashboard": "Vous confirmez une annonce sur un tableau de bord",
"Credentials": "Credentials",
"My profile": "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}": "Launchpad administration: {tab}",
+ "applies to the whole organisation": "applies to the whole organisation"
}
}
diff --git a/l10n/ga.js b/l10n/ga.js
index 501f83bd4..a4cb61762 100644
--- a/l10n/ga.js
+++ b/l10n/ga.js
@@ -1363,7 +1363,9 @@ OC.L10N.register(
"You acknowledge an announcement on a dashboard" : "Admhaíonn tú fógra ar dheais",
"Credentials" : "Credentials",
"My profile" : "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}" : "Launchpad administration: {tab}",
+ "applies to the whole organisation" : "applies to the whole organisation"
},
"nplurals=5; plural=(n==1 ? 0 : n==2 ? 1 : n<7 ? 2 : n<11 ? 3 : 4);"
);
diff --git a/l10n/ga.json b/l10n/ga.json
index 04086a9c3..7afcd864a 100644
--- a/l10n/ga.json
+++ b/l10n/ga.json
@@ -1333,6 +1333,8 @@
"You acknowledge an announcement on a dashboard": "Admhaíonn tú fógra ar dheais",
"Credentials": "Credentials",
"My profile": "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}": "Launchpad administration: {tab}",
+ "applies to the whole organisation": "applies to the whole organisation"
}
}
diff --git a/l10n/hr.js b/l10n/hr.js
index 6749e519f..a0db5ced9 100644
--- a/l10n/hr.js
+++ b/l10n/hr.js
@@ -1363,7 +1363,9 @@ OC.L10N.register(
"You acknowledge an announcement on a dashboard" : "Potvrđujete obavijest na nadzornoj ploči",
"Credentials" : "Credentials",
"My profile" : "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}" : "Launchpad administration: {tab}",
+ "applies to the whole organisation" : "applies to the whole organisation"
},
"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"
);
diff --git a/l10n/hr.json b/l10n/hr.json
index 63961ddcc..7b60e3169 100644
--- a/l10n/hr.json
+++ b/l10n/hr.json
@@ -1331,6 +1331,8 @@
"You acknowledge an announcement on a dashboard": "Potvrđujete obavijest na nadzornoj ploči",
"Credentials": "Credentials",
"My profile": "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}": "Launchpad administration: {tab}",
+ "applies to the whole organisation": "applies to the whole organisation"
}
}
diff --git a/l10n/hu.js b/l10n/hu.js
index 04dfdf726..97fdb3a1d 100644
--- a/l10n/hu.js
+++ b/l10n/hu.js
@@ -1363,7 +1363,9 @@ OC.L10N.register(
"You acknowledge an announcement on a dashboard" : "Ön nyugtáz egy közleményt egy irányítópulton",
"Credentials" : "Credentials",
"My profile" : "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}" : "Launchpad administration: {tab}",
+ "applies to the whole organisation" : "applies to the whole organisation"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/hu.json b/l10n/hu.json
index 0f2288ddb..8c0bc79e3 100644
--- a/l10n/hu.json
+++ b/l10n/hu.json
@@ -1330,6 +1330,8 @@
"You acknowledge an announcement on a dashboard": "Ön nyugtáz egy közleményt egy irányítópulton",
"Credentials": "Credentials",
"My profile": "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}": "Launchpad administration: {tab}",
+ "applies to the whole organisation": "applies to the whole organisation"
}
}
diff --git a/l10n/is.js b/l10n/is.js
index 163cb36a8..fee331188 100644
--- a/l10n/is.js
+++ b/l10n/is.js
@@ -1363,7 +1363,9 @@ OC.L10N.register(
"You acknowledge an announcement on a dashboard" : "Þú staðfestir tilkynningu á mælaborði",
"Credentials" : "Credentials",
"My profile" : "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}" : "Launchpad administration: {tab}",
+ "applies to the whole organisation" : "applies to the whole organisation"
},
"nplurals=2; plural=(n%10!=1 || n%100==11);"
);
diff --git a/l10n/is.json b/l10n/is.json
index 931b62555..83ec25f8e 100644
--- a/l10n/is.json
+++ b/l10n/is.json
@@ -1330,6 +1330,8 @@
"You acknowledge an announcement on a dashboard": "Þú staðfestir tilkynningu á mælaborði",
"Credentials": "Credentials",
"My profile": "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}": "Launchpad administration: {tab}",
+ "applies to the whole organisation": "applies to the whole organisation"
}
}
diff --git a/l10n/it.js b/l10n/it.js
index e679c2317..aaf48aee6 100644
--- a/l10n/it.js
+++ b/l10n/it.js
@@ -1363,7 +1363,9 @@ OC.L10N.register(
"You acknowledge an announcement on a dashboard" : "Confermi un avviso su una dashboard",
"Credentials" : "Credentials",
"My profile" : "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}" : "Launchpad administration: {tab}",
+ "applies to the whole organisation" : "applies to the whole organisation"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/it.json b/l10n/it.json
index 42e372a93..0116ae598 100644
--- a/l10n/it.json
+++ b/l10n/it.json
@@ -1330,6 +1330,8 @@
"You acknowledge an announcement on a dashboard": "Confermi un avviso su una dashboard",
"Credentials": "Credentials",
"My profile": "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}": "Launchpad administration: {tab}",
+ "applies to the whole organisation": "applies to the whole organisation"
}
}
diff --git a/l10n/lb.js b/l10n/lb.js
index eeb8e3379..d639067d4 100644
--- a/l10n/lb.js
+++ b/l10n/lb.js
@@ -1354,7 +1354,9 @@ OC.L10N.register(
"You acknowledge an announcement on a dashboard" : "Dir bestätegt eng Matdeelung op engem Dashboard",
"Credentials" : "Credentials",
"My profile" : "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}" : "Launchpad administration: {tab}",
+ "applies to the whole organisation" : "applies to the whole organisation"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/lb.json b/l10n/lb.json
index 0cfa82ffb..2c6791abc 100644
--- a/l10n/lb.json
+++ b/l10n/lb.json
@@ -1330,6 +1330,8 @@
"You acknowledge an announcement on a dashboard": "Dir bestätegt eng Matdeelung op engem Dashboard",
"Credentials": "Credentials",
"My profile": "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}": "Launchpad administration: {tab}",
+ "applies to the whole organisation": "applies to the whole organisation"
}
}
diff --git a/l10n/lt.js b/l10n/lt.js
index d22025a9e..06ac39ec7 100644
--- a/l10n/lt.js
+++ b/l10n/lt.js
@@ -1363,7 +1363,9 @@ OC.L10N.register(
"You acknowledge an announcement on a dashboard" : "Patvirtinate pranešimą skydelyje",
"Credentials" : "Credentials",
"My profile" : "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}" : "Launchpad administration: {tab}",
+ "applies to the whole organisation" : "applies to the whole organisation"
},
"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2);"
);
diff --git a/l10n/lt.json b/l10n/lt.json
index 953b5588c..a6f6ebdf2 100644
--- a/l10n/lt.json
+++ b/l10n/lt.json
@@ -1331,6 +1331,8 @@
"You acknowledge an announcement on a dashboard": "Patvirtinate pranešimą skydelyje",
"Credentials": "Credentials",
"My profile": "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}": "Launchpad administration: {tab}",
+ "applies to the whole organisation": "applies to the whole organisation"
}
}
diff --git a/l10n/lv.js b/l10n/lv.js
index 276353424..d80714be3 100644
--- a/l10n/lv.js
+++ b/l10n/lv.js
@@ -1363,7 +1363,9 @@ OC.L10N.register(
"You acknowledge an announcement on a dashboard" : "Jūs apstiprināt paziņojumu informācijas panelī",
"Credentials" : "Credentials",
"My profile" : "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}" : "Launchpad administration: {tab}",
+ "applies to the whole organisation" : "applies to the whole organisation"
},
"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2);"
);
diff --git a/l10n/lv.json b/l10n/lv.json
index 65dc70503..71d5b7fe4 100644
--- a/l10n/lv.json
+++ b/l10n/lv.json
@@ -1331,6 +1331,8 @@
"You acknowledge an announcement on a dashboard": "Jūs apstiprināt paziņojumu informācijas panelī",
"Credentials": "Credentials",
"My profile": "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}": "Launchpad administration: {tab}",
+ "applies to the whole organisation": "applies to the whole organisation"
}
}
diff --git a/l10n/mk.js b/l10n/mk.js
index 5539df625..91e32f560 100644
--- a/l10n/mk.js
+++ b/l10n/mk.js
@@ -1354,7 +1354,9 @@ OC.L10N.register(
"You acknowledge an announcement on a dashboard" : "Потврдувате известување на контролна табла",
"Credentials" : "Credentials",
"My profile" : "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}" : "Launchpad administration: {tab}",
+ "applies to the whole organisation" : "applies to the whole organisation"
},
"nplurals=2; plural=(n%10==1 && n%100!=11 ? 0 : 1);"
);
diff --git a/l10n/mk.json b/l10n/mk.json
index efc999301..0a211c6e9 100644
--- a/l10n/mk.json
+++ b/l10n/mk.json
@@ -1330,6 +1330,8 @@
"You acknowledge an announcement on a dashboard": "Потврдувате известување на контролна табла",
"Credentials": "Credentials",
"My profile": "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}": "Launchpad administration: {tab}",
+ "applies to the whole organisation": "applies to the whole organisation"
}
}
diff --git a/l10n/mt.js b/l10n/mt.js
index 615afcaa8..d25281d80 100644
--- a/l10n/mt.js
+++ b/l10n/mt.js
@@ -1363,7 +1363,9 @@ OC.L10N.register(
"You acknowledge an announcement on a dashboard" : "Tikkonferma avviż fuq dashboard",
"Credentials" : "Credentials",
"My profile" : "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}" : "Launchpad administration: {tab}",
+ "applies to the whole organisation" : "applies to the whole organisation"
},
"nplurals=4; plural=(n==1 ? 0 : n==0 || (n%100>1 && n%100<11) ? 1 : (n%100>10 && n%100<20) ? 2 : 3);"
);
diff --git a/l10n/mt.json b/l10n/mt.json
index 80a9e100a..cf721f220 100644
--- a/l10n/mt.json
+++ b/l10n/mt.json
@@ -1332,6 +1332,8 @@
"You acknowledge an announcement on a dashboard": "Tikkonferma avviż fuq dashboard",
"Credentials": "Credentials",
"My profile": "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}": "Launchpad administration: {tab}",
+ "applies to the whole organisation": "applies to the whole organisation"
}
}
diff --git a/l10n/nb.js b/l10n/nb.js
index d4b73cc46..ae1068861 100644
--- a/l10n/nb.js
+++ b/l10n/nb.js
@@ -1363,7 +1363,9 @@ OC.L10N.register(
"You acknowledge an announcement on a dashboard" : "Du bekrefter en kunngjøring på et dashbord",
"Credentials" : "Credentials",
"My profile" : "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}" : "Launchpad administration: {tab}",
+ "applies to the whole organisation" : "applies to the whole organisation"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/nb.json b/l10n/nb.json
index f56aa8074..ca931e191 100644
--- a/l10n/nb.json
+++ b/l10n/nb.json
@@ -1330,6 +1330,8 @@
"You acknowledge an announcement on a dashboard": "Du bekrefter en kunngjøring på et dashbord",
"Credentials": "Credentials",
"My profile": "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}": "Launchpad administration: {tab}",
+ "applies to the whole organisation": "applies to the whole organisation"
}
}
diff --git a/l10n/nl.js b/l10n/nl.js
index 1de4d5d28..3c8ca2665 100644
--- a/l10n/nl.js
+++ b/l10n/nl.js
@@ -860,7 +860,7 @@ OC.L10N.register(
"Step {n} / {total}" : "Stap {n} / {total}",
"Welcome" : "Welkom",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace." : "Kies de volgorde waarin Nextcloud-groepen verschijnen wanneer LaunchPad gebruikers naar een werkruimte stuurt.",
- "Demo data" : "Demo-data",
+ "Demo data" : "Demogegevens",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only." : "De demo-showcases verschijnen hier zodra de demo-data-showcases-capability beschikbaar is. Deze stap is voorlopig alleen ter informatie.",
"Skip this step for now — demo packages can be installed later from the admin section." : "Sla deze stap voor nu over — demo-pakketten kunnen later vanuit het beheerdergedeelte worden geïnstalleerd.",
"Admin roles" : "Beheerdersrollen",
@@ -928,11 +928,11 @@ OC.L10N.register(
"Delete role permission for group \"{group}\"?" : "Rolrecht voor groep \"{group}\" verwijderen?",
"Administration areas" : "Beheergebieden",
"Templates" : "Sjablonen",
- "Operations" : "Beheer",
+ "Operations" : "Bediening",
"Roles & Permissions" : "Rollen en rechten",
- "Versioning & Audit" : "Versiebeheer en audit",
+ "Versioning & Audit" : "Versies en analyse",
"Sharing" : "Delen",
- "Org navigation" : "Organisatienavigatie",
+ "Org navigation" : "Organisatiemenu",
"Widgets with visibility rules" : "Widgets met zichtbaarheidsregels",
"Every widget placement that has at least one conditional visibility rule, across all dashboards." : "Elke widgetplaatsing met ten minste één voorwaardelijke zichtbaarheidsregel, op alle dashboards.",
"No visibility rules yet" : "Nog geen zichtbaarheidsregels",
@@ -1625,7 +1625,9 @@ OC.L10N.register(
"Operation failed" : "De bewerking is mislukt",
"Continue to the administration settings" : "Verder naar de beheerinstellingen",
"Opening the administration settings…" : "Beheerinstellingen openen…",
- "Versions of {name}" : "Versies van {name}"
+ "Versions of {name}" : "Versies van {name}",
+ "Launchpad administration: {tab}" : "Launchpad-beheer: {tab}",
+ "applies to the whole organisation" : "geldt voor de hele organisatie"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/nl.json b/l10n/nl.json
index beb493801..2ed96feee 100644
--- a/l10n/nl.json
+++ b/l10n/nl.json
@@ -867,7 +867,7 @@
"Step {n} / {total}": "Stap {n} / {total}",
"Welcome": "Welkom",
"Pick the order Nextcloud groups appear when LaunchPad routes users to a workspace.": "Kies de volgorde waarin Nextcloud-groepen verschijnen wanneer LaunchPad gebruikers naar een werkruimte stuurt.",
- "Demo data": "Demo-data",
+ "Demo data": "Demogegevens",
"The demo data showcases will appear here once the demo-data-showcases capability ships. For now this step is informational only.": "De demo-showcases verschijnen hier zodra de demo-data-showcases-capability beschikbaar is. Deze stap is voorlopig alleen ter informatie.",
"Skip this step for now — demo packages can be installed later from the admin section.": "Sla deze stap voor nu over — demo-pakketten kunnen later vanuit het beheerdergedeelte worden geïnstalleerd.",
"Admin roles": "Beheerdersrollen",
@@ -935,11 +935,11 @@
"Delete role permission for group \"{group}\"?": "Rolrecht voor groep \"{group}\" verwijderen?",
"Administration areas": "Beheergebieden",
"Templates": "Sjablonen",
- "Operations": "Beheer",
+ "Operations": "Bediening",
"Roles & Permissions": "Rollen en rechten",
- "Versioning & Audit": "Versiebeheer en audit",
+ "Versioning & Audit": "Versies en analyse",
"Sharing": "Delen",
- "Org navigation": "Organisatienavigatie",
+ "Org navigation": "Organisatiemenu",
"Widgets with visibility rules": "Widgets met zichtbaarheidsregels",
"Every widget placement that has at least one conditional visibility rule, across all dashboards.": "Elke widgetplaatsing met ten minste één voorwaardelijke zichtbaarheidsregel, op alle dashboards.",
"No visibility rules yet": "Nog geen zichtbaarheidsregels",
@@ -1639,6 +1639,8 @@
"Last dashboard cannot be deleted": "Het laatste dashboard kun je niet verwijderen",
"Continue to the administration settings": "Verder naar de beheerinstellingen",
"Opening the administration settings…": "Beheerinstellingen openen…",
- "Versions of {name}": "Versies van {name}"
+ "Versions of {name}": "Versies van {name}",
+ "Launchpad administration: {tab}": "Launchpad-beheer: {tab}",
+ "applies to the whole organisation": "geldt voor de hele organisatie"
}
}
diff --git a/l10n/pl.js b/l10n/pl.js
index 6d21dc2c5..e0ee46265 100644
--- a/l10n/pl.js
+++ b/l10n/pl.js
@@ -1363,7 +1363,9 @@ OC.L10N.register(
"You acknowledge an announcement on a dashboard" : "Potwierdzasz ogłoszenie na pulpicie",
"Credentials" : "Credentials",
"My profile" : "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}" : "Launchpad administration: {tab}",
+ "applies to the whole organisation" : "applies to the whole organisation"
},
"nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"
);
diff --git a/l10n/pl.json b/l10n/pl.json
index 32c4aab0a..a157f2755 100644
--- a/l10n/pl.json
+++ b/l10n/pl.json
@@ -1331,6 +1331,8 @@
"You acknowledge an announcement on a dashboard": "Potwierdzasz ogłoszenie na pulpicie",
"Credentials": "Credentials",
"My profile": "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}": "Launchpad administration: {tab}",
+ "applies to the whole organisation": "applies to the whole organisation"
}
}
diff --git a/l10n/pt.js b/l10n/pt.js
index 178eff389..65eb861de 100644
--- a/l10n/pt.js
+++ b/l10n/pt.js
@@ -1363,7 +1363,9 @@ OC.L10N.register(
"You acknowledge an announcement on a dashboard" : "Confirma um aviso num painel",
"Credentials" : "Credentials",
"My profile" : "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}" : "Launchpad administration: {tab}",
+ "applies to the whole organisation" : "applies to the whole organisation"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/pt.json b/l10n/pt.json
index 08b1fee80..490ca03dc 100644
--- a/l10n/pt.json
+++ b/l10n/pt.json
@@ -1330,6 +1330,8 @@
"You acknowledge an announcement on a dashboard": "Confirma um aviso num painel",
"Credentials": "Credentials",
"My profile": "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}": "Launchpad administration: {tab}",
+ "applies to the whole organisation": "applies to the whole organisation"
}
}
diff --git a/l10n/rm.js b/l10n/rm.js
index 0daf7a5bb..4ebe3e96f 100644
--- a/l10n/rm.js
+++ b/l10n/rm.js
@@ -1363,7 +1363,9 @@ OC.L10N.register(
"You acknowledge an announcement on a dashboard" : "Vus confermais in avis sin in dashboard",
"Credentials" : "Credentials",
"My profile" : "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}" : "Launchpad administration: {tab}",
+ "applies to the whole organisation" : "applies to the whole organisation"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/rm.json b/l10n/rm.json
index 2e8dabec5..fd151222c 100644
--- a/l10n/rm.json
+++ b/l10n/rm.json
@@ -1330,6 +1330,8 @@
"You acknowledge an announcement on a dashboard": "Vus confermais in avis sin in dashboard",
"Credentials": "Credentials",
"My profile": "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}": "Launchpad administration: {tab}",
+ "applies to the whole organisation": "applies to the whole organisation"
}
}
diff --git a/l10n/ro.js b/l10n/ro.js
index aabc4c3d3..24a8e9900 100644
--- a/l10n/ro.js
+++ b/l10n/ro.js
@@ -1363,7 +1363,9 @@ OC.L10N.register(
"You acknowledge an announcement on a dashboard" : "Confirmați un anunț pe un tablou de bord",
"Credentials" : "Credentials",
"My profile" : "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}" : "Launchpad administration: {tab}",
+ "applies to the whole organisation" : "applies to the whole organisation"
},
"nplurals=3; plural=(n==1 ? 0 : (n==0 || (n%100>0 && n%100<20)) ? 1 : 2);"
);
diff --git a/l10n/ro.json b/l10n/ro.json
index b8adfcc22..642f5d48c 100644
--- a/l10n/ro.json
+++ b/l10n/ro.json
@@ -1331,6 +1331,8 @@
"You acknowledge an announcement on a dashboard": "Confirmați un anunț pe un tablou de bord",
"Credentials": "Credentials",
"My profile": "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}": "Launchpad administration: {tab}",
+ "applies to the whole organisation": "applies to the whole organisation"
}
}
diff --git a/l10n/ru.js b/l10n/ru.js
index cb70df88a..3602bb9bd 100644
--- a/l10n/ru.js
+++ b/l10n/ru.js
@@ -1363,7 +1363,9 @@ OC.L10N.register(
"You acknowledge an announcement on a dashboard" : "Вы подтверждаете объявление на панели",
"Credentials" : "Credentials",
"My profile" : "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}" : "Launchpad administration: {tab}",
+ "applies to the whole organisation" : "applies to the whole organisation"
},
"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"
);
diff --git a/l10n/ru.json b/l10n/ru.json
index ef408f121..0e6d21668 100644
--- a/l10n/ru.json
+++ b/l10n/ru.json
@@ -1331,6 +1331,8 @@
"You acknowledge an announcement on a dashboard": "Вы подтверждаете объявление на панели",
"Credentials": "Credentials",
"My profile": "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}": "Launchpad administration: {tab}",
+ "applies to the whole organisation": "applies to the whole organisation"
}
}
diff --git a/l10n/sk.js b/l10n/sk.js
index 754061dda..064e6d751 100644
--- a/l10n/sk.js
+++ b/l10n/sk.js
@@ -1363,7 +1363,9 @@ OC.L10N.register(
"You acknowledge an announcement on a dashboard" : "Potvrdzujete oznámenie na prehľade",
"Credentials" : "Credentials",
"My profile" : "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}" : "Launchpad administration: {tab}",
+ "applies to the whole organisation" : "applies to the whole organisation"
},
"nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;"
);
diff --git a/l10n/sk.json b/l10n/sk.json
index 412c3ace2..f3ae63623 100644
--- a/l10n/sk.json
+++ b/l10n/sk.json
@@ -1331,6 +1331,8 @@
"You acknowledge an announcement on a dashboard": "Potvrdzujete oznámenie na prehľade",
"Credentials": "Credentials",
"My profile": "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}": "Launchpad administration: {tab}",
+ "applies to the whole organisation": "applies to the whole organisation"
}
}
diff --git a/l10n/sl.js b/l10n/sl.js
index 9080ccab6..c48df985e 100644
--- a/l10n/sl.js
+++ b/l10n/sl.js
@@ -1363,7 +1363,9 @@ OC.L10N.register(
"You acknowledge an announcement on a dashboard" : "Potrjujete obvestilo na nadzorni plošči",
"Credentials" : "Credentials",
"My profile" : "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}" : "Launchpad administration: {tab}",
+ "applies to the whole organisation" : "applies to the whole organisation"
},
"nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);"
);
diff --git a/l10n/sl.json b/l10n/sl.json
index c229850ea..267374e12 100644
--- a/l10n/sl.json
+++ b/l10n/sl.json
@@ -1332,6 +1332,8 @@
"You acknowledge an announcement on a dashboard": "Potrjujete obvestilo na nadzorni plošči",
"Credentials": "Credentials",
"My profile": "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}": "Launchpad administration: {tab}",
+ "applies to the whole organisation": "applies to the whole organisation"
}
}
diff --git a/l10n/sq.js b/l10n/sq.js
index 600dd9f4d..56354edef 100644
--- a/l10n/sq.js
+++ b/l10n/sq.js
@@ -1363,7 +1363,9 @@ OC.L10N.register(
"You acknowledge an announcement on a dashboard" : "Konfirmoni një njoftim në një panel",
"Credentials" : "Credentials",
"My profile" : "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}" : "Launchpad administration: {tab}",
+ "applies to the whole organisation" : "applies to the whole organisation"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/sq.json b/l10n/sq.json
index 01d7664a0..66a6ac55b 100644
--- a/l10n/sq.json
+++ b/l10n/sq.json
@@ -1330,6 +1330,8 @@
"You acknowledge an announcement on a dashboard": "Konfirmoni një njoftim në një panel",
"Credentials": "Credentials",
"My profile": "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}": "Launchpad administration: {tab}",
+ "applies to the whole organisation": "applies to the whole organisation"
}
}
diff --git a/l10n/sr.js b/l10n/sr.js
index 48c1abd1b..b6bfa42c8 100644
--- a/l10n/sr.js
+++ b/l10n/sr.js
@@ -1354,6 +1354,8 @@ OC.L10N.register(
"You acknowledge an announcement on a dashboard" : "Потврђујете обавештење на контролној табли",
"Credentials" : "Credentials",
"My profile" : "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}" : "Launchpad administration: {tab}",
+ "applies to the whole organisation" : "applies to the whole organisation"
},
"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);");
diff --git a/l10n/sr.json b/l10n/sr.json
index 45906576f..d76369702 100644
--- a/l10n/sr.json
+++ b/l10n/sr.json
@@ -1331,6 +1331,8 @@
"You acknowledge an announcement on a dashboard": "Потврђујете обавештење на контролној табли",
"Credentials": "Credentials",
"My profile": "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}": "Launchpad administration: {tab}",
+ "applies to the whole organisation": "applies to the whole organisation"
}
}
diff --git a/l10n/sv.js b/l10n/sv.js
index ec248d6e9..03d79683d 100644
--- a/l10n/sv.js
+++ b/l10n/sv.js
@@ -1363,7 +1363,9 @@ OC.L10N.register(
"You acknowledge an announcement on a dashboard" : "Du bekräftar ett meddelande på en instrumentpanel",
"Credentials" : "Credentials",
"My profile" : "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}" : "Launchpad administration: {tab}",
+ "applies to the whole organisation" : "applies to the whole organisation"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/sv.json b/l10n/sv.json
index 2d29e137b..e94a17196 100644
--- a/l10n/sv.json
+++ b/l10n/sv.json
@@ -1330,6 +1330,8 @@
"You acknowledge an announcement on a dashboard": "Du bekräftar ett meddelande på en instrumentpanel",
"Credentials": "Credentials",
"My profile": "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}": "Launchpad administration: {tab}",
+ "applies to the whole organisation": "applies to the whole organisation"
}
}
diff --git a/l10n/tr.js b/l10n/tr.js
index e4d49cc52..b6f909427 100644
--- a/l10n/tr.js
+++ b/l10n/tr.js
@@ -1363,7 +1363,9 @@ OC.L10N.register(
"You acknowledge an announcement on a dashboard" : "Bir panodaki duyuruyu onaylarsınız",
"Credentials" : "Credentials",
"My profile" : "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}" : "Launchpad administration: {tab}",
+ "applies to the whole organisation" : "applies to the whole organisation"
},
"nplurals=2; plural=(n != 1);"
);
diff --git a/l10n/tr.json b/l10n/tr.json
index 1a3b6ac0d..3c06bffbe 100644
--- a/l10n/tr.json
+++ b/l10n/tr.json
@@ -1330,6 +1330,8 @@
"You acknowledge an announcement on a dashboard": "Bir panodaki duyuruyu onaylarsınız",
"Credentials": "Credentials",
"My profile": "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}": "Launchpad administration: {tab}",
+ "applies to the whole organisation": "applies to the whole organisation"
}
}
diff --git a/l10n/uk.js b/l10n/uk.js
index 5086cea84..2f9b20802 100644
--- a/l10n/uk.js
+++ b/l10n/uk.js
@@ -1363,7 +1363,9 @@ OC.L10N.register(
"You acknowledge an announcement on a dashboard" : "Ви підтверджуєте оголошення на панелі",
"Credentials" : "Credentials",
"My profile" : "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets." : "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}" : "Launchpad administration: {tab}",
+ "applies to the whole organisation" : "applies to the whole organisation"
},
"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"
);
diff --git a/l10n/uk.json b/l10n/uk.json
index 436915b9d..eac212516 100644
--- a/l10n/uk.json
+++ b/l10n/uk.json
@@ -1331,6 +1331,8 @@
"You acknowledge an announcement on a dashboard": "Ви підтверджуєте оголошення на панелі",
"Credentials": "Credentials",
"My profile": "My profile",
- "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets."
+ "Fields your organisation adds to your profile. Colleagues see them in profile widgets.": "Fields your organisation adds to your profile. Colleagues see them in profile widgets.",
+ "Launchpad administration: {tab}": "Launchpad administration: {tab}",
+ "applies to the whole organisation": "applies to the whole organisation"
}
}
diff --git a/src/components/admin/AdminSettings.vue b/src/components/admin/AdminSettings.vue
index ed25f08d8..c07bb6882 100644
--- a/src/components/admin/AdminSettings.vue
+++ b/src/components/admin/AdminSettings.vue
@@ -5,54 +5,109 @@
-
-
-
-
- {{ t('launchpad', 'Run setup wizard') }}
-
- {{
- t(
- 'launchpad',
- 'Get your intranet started: configure groups, install demo data, and set up admin roles.',
- )
- }}
-
-
-
-
-
-
diff --git a/src/components/admin/DashboardRegistrySettings.vue b/src/components/admin/DashboardRegistrySettings.vue
index 07b971175..e32fbea04 100644
--- a/src/components/admin/DashboardRegistrySettings.vue
+++ b/src/components/admin/DashboardRegistrySettings.vue
@@ -6,8 +6,8 @@
+ id stable. The link also carries ?tab=sharing, because the admin page
+ only mounts the open tab's content. -->
{
- it('puts General first', () => {
- const tabs = AdminSettings.computed.beheerTabs.call({ t })
- expect(tabs[0]).toEqual({ slug: 'general', label: 'General' })
- expect(tabs[1].slug).toBe('templates')
+ const tabs = AdminSettings.computed.beheerTabs.call({ t })
+ const render = AdminSettings.render?.toString() ?? ''
+
+ it('puts General first, the tab the shell opens on', () => {
+ expect(tabs[0]).toEqual({ id: 'general', label: 'General' })
+ expect(tabs[1].id).toBe('templates')
+ })
+
+ it('hands the twelve tabs to the admin shell as an underline strip', () => {
+ expect(tabs.map((tab) => tab.id)).toEqual([
+ 'general',
+ 'templates',
+ 'operations',
+ 'roles-permissions',
+ 'versioning-audit',
+ 'sharing',
+ 'kiosk',
+ 'org-navigation',
+ 'demo-data',
+ 'group-dashboards',
+ 'detail-fields',
+ 'profile-fields',
+ ])
+ expect(source).toContain(':tabs="beheerTabs"')
+ expect(render).toContain('tabsVariant: "underline"')
+ })
+
+ it('remembers the last tab under the key the page used before', () => {
+ expect(render).toContain('tabStorageKey: "launchpad.admin.activeTab"')
+ })
+
+ it('opens the tab named by ?tab=, the shell default query key', () => {
+ expect(source).not.toContain('tabQueryKey')
})
- it('opens on General', () => {
- const render = AdminSettings.render?.toString() ?? ''
- expect(render).toContain('defaultTab: "general"')
+ it('gives every tab its own slot', () => {
+ for (const tab of tabs) {
+ expect(source).toContain(``)
+ }
+ })
+
+ it('mounts a tab\'s content only while that tab is open', () => {
+ for (const tab of tabs.slice(1)) {
+ const slot = new RegExp(
+ `\\s*<\\w+\\s+v-if="activeTab === '${tab.id}'"`,
+ )
+ expect(source).toMatch(slot)
+ }
+ })
+
+ it('follows the tab the shell reports', () => {
+ expect(source).toContain('@tabChange="onTabChange"')
+ const ctx = { activeTab: 'general' }
+ AdminSettings.methods.onTabChange.call(ctx, 'sharing')
+ expect(ctx.activeTab).toBe('sharing')
+ })
+
+ it('shows the setup wizard banner in the General panel', () => {
+ const general = source.indexOf('')
+ const banner = source.indexOf('data-test="setup-wizard-banner"')
+ const templates = source.indexOf('')
+ expect(general).toBeGreaterThan(-1)
+ expect(banner).toBeGreaterThan(general)
+ expect(banner).toBeLessThan(templates)
})
it('draws the defaults as a card of General only', () => {
diff --git a/src/components/admin/__tests__/BeheerTabs.spec.js b/src/components/admin/__tests__/BeheerTabs.spec.js
deleted file mode 100644
index 6c2f90313..000000000
--- a/src/components/admin/__tests__/BeheerTabs.spec.js
+++ /dev/null
@@ -1,90 +0,0 @@
-/**
- * SPDX-FileCopyrightText: 2024 Conduction B.V.
- * SPDX-License-Identifier: EUPL-1.2
- *
- * Vitest unit tests for `BeheerTabs.vue` (admin-settings spec). Covers:
- * - tab switching renders only the active tab's slot
- * - selecting a tab persists the slug to localStorage
- * - the `?tab=` query string wins over the persisted value on first paint
- * - the default tab is used when neither query nor storage is set
- */
-
-import { mount } from '@vue/test-utils'
-import { beforeEach, describe, expect, it } from 'vitest'
-import BeheerTabs, { ACTIVE_TAB_STORAGE_KEY } from '../BeheerTabs.vue'
-
-const tabs = [
- { slug: 'templates', label: 'Templates' },
- { slug: 'operations', label: 'Operations' },
- { slug: 'roles-permissions', label: 'Roles & Permissions' },
-]
-
-function mountTabs(props = {}, search = '') {
- // Stub window.location.search for the deep-link path.
- delete window.location
- window.location = { search }
- return mount(BeheerTabs, {
- propsData: { tabs, defaultTab: 'templates', ...props },
- // VTU v2 merged v1's `scopedSlots` into `slots`; the v1 key is
- // silently ignored, so every slot rendered empty.
- slots: {
- templates: '
TEMPLATES
',
- operations: '
OPERATIONS
',
- 'roles-permissions': '
ROLES
',
- },
- })
-}
-
-beforeEach(() => {
- globalThis.t = (_app, key) => key
- localStorage.clear()
-})
-
-describe('BeheerTabs', () => {
- it('renders only the default tab slot on first visit', () => {
- const wrapper = mountTabs()
- expect(wrapper.find('[data-test="slot-templates"]').exists()).toBe(true)
- expect(wrapper.find('[data-test="slot-operations"]').exists()).toBe(false)
- expect(wrapper.find('[data-test="slot-roles"]').exists()).toBe(false)
- })
-
- it('switches the active tab and renders only that slot', async () => {
- const wrapper = mountTabs()
- await wrapper.find('[data-test="tab-operations"]').trigger('click')
- expect(wrapper.find('[data-test="slot-operations"]').exists()).toBe(true)
- expect(wrapper.find('[data-test="slot-templates"]').exists()).toBe(false)
- })
-
- it('persists the selected tab to localStorage', async () => {
- const wrapper = mountTabs()
- await wrapper.find('[data-test="tab-roles-permissions"]').trigger('click')
- expect(localStorage.getItem(ACTIVE_TAB_STORAGE_KEY)).toBe(
- 'roles-permissions',
- )
- })
-
- it('restores the persisted tab on next mount', () => {
- localStorage.setItem(ACTIVE_TAB_STORAGE_KEY, 'operations')
- const wrapper = mountTabs()
- expect(wrapper.find('[data-test="slot-operations"]').exists()).toBe(true)
- })
-
- it('deep-links to a tab via the ?tab= query string (wins over storage)', () => {
- localStorage.setItem(ACTIVE_TAB_STORAGE_KEY, 'operations')
- const wrapper = mountTabs({}, '?tab=roles-permissions')
- expect(wrapper.find('[data-test="slot-roles"]').exists()).toBe(true)
- expect(wrapper.find('[data-test="slot-operations"]').exists()).toBe(false)
- })
-
- it('emits change with the active slug on mount and on switch', async () => {
- const wrapper = mountTabs()
- expect(wrapper.emitted('change')[0]).toEqual(['templates'])
- await wrapper.find('[data-test="tab-operations"]').trigger('click')
- expect(wrapper.emitted('change').at(-1)).toEqual(['operations'])
- })
-
- it('ignores an unknown ?tab= value and falls back to the default', () => {
- const wrapper = mountTabs({}, '?tab=does-not-exist')
- expect(wrapper.find('[data-test="slot-templates"]').exists()).toBe(true)
- })
-})
diff --git a/tests/Unit/Settings/ConnectionsDeclarationTest.php b/tests/Unit/Settings/ConnectionsDeclarationTest.php
index c25e9101f..e5567af5a 100644
--- a/tests/Unit/Settings/ConnectionsDeclarationTest.php
+++ b/tests/Unit/Settings/ConnectionsDeclarationTest.php
@@ -256,8 +256,9 @@ public function testEverySettingsLinkPointsAtAnExistingElement(): void {
/**
* The registry link opens the tab that renders its anchor.
*
- * BeheerTabs renders only the active tab, so an anchor on the Sharing tab
- * does not exist in the page while another tab is open.
+ * The admin page mounts a tab's content only while that tab is open, so an
+ * anchor on the Sharing tab does not exist in the page while another tab
+ * is open. The admin shell opens the tab named by its `tab` query key.
*
* @return void
*/
@@ -268,13 +269,11 @@ public function testTheRegistryLinkOpensTheSharingTab(): void {
$registry = (string) file_get_contents($this->root() . '/src/components/admin/DashboardRegistrySettings.vue');
$this->assertSame(expected: '/settings/admin/launchpad?tab=sharing#section-dashboard-registry', actual: $url);
- $this->assertStringContainsString(needle: "slug: 'sharing'", haystack: $admin);
+ $this->assertStringContainsString(needle: "id: 'sharing'", haystack: $admin);
+ $this->assertStringContainsString(needle: '', haystack: $admin);
$this->assertStringContainsString(needle: '', haystack: $sharing);
$this->assertStringContainsString(needle: 'id="section-dashboard-registry"', haystack: $registry);
- $this->assertStringContainsString(
- needle: "params.get('tab')",
- haystack: (string) file_get_contents($this->root() . '/src/components/admin/BeheerTabs.vue')
- );
+ $this->assertStringNotContainsString(needle: 'tabQueryKey', haystack: $admin);
}//end testTheRegistryLinkOpensTheSharingTab()
/**
diff --git a/tests/e2e/admin-group-management.spec.ts b/tests/e2e/admin-group-management.spec.ts
index 42cd80680..da70cf31d 100644
--- a/tests/e2e/admin-group-management.spec.ts
+++ b/tests/e2e/admin-group-management.spec.ts
@@ -16,6 +16,7 @@
*/
import { expect, test } from '@playwright/test'
+import { adminTab } from './support/adminTab.ts'
import { BASE_URL as BASE } from './support/baseUrl.ts'
const ADMIN = {
@@ -40,7 +41,7 @@ test.describe('admin-group-management — Group dashboards tab', () => {
}) => {
await page.goto(SETTINGS_URL)
// Switch to the Group dashboards tab via its data-test slug.
- const tabButton = page.locator('[data-test="tab-group-dashboards"]')
+ const tabButton = adminTab(page, 'group-dashboards')
await expect(tabButton).toBeVisible({ timeout: 20_000 })
await tabButton.click()
@@ -55,7 +56,7 @@ test.describe('admin-group-management — Group dashboards tab', () => {
page,
}) => {
await page.goto(SETTINGS_URL)
- await page.locator('[data-test="tab-group-dashboards"]').click()
+ await adminTab(page, 'group-dashboards').click()
await page.locator('[data-test="group-dashboards-create-default"]').click()
const modal = page.locator('[data-test="create-group-dashboard-modal"]')
@@ -84,7 +85,7 @@ test.describe('admin-group-management — Group dashboards tab', () => {
page,
}) => {
await page.goto(SETTINGS_URL)
- await page.locator('[data-test="tab-group-dashboards"]').click()
+ await adminTab(page, 'group-dashboards').click()
await page.locator('[data-test="group-dashboards-manage-default"]').click()
const modal = page.locator('[data-test="manage-group-dashboards-modal"]')
diff --git a/tests/e2e/admin-templates-page.spec.ts b/tests/e2e/admin-templates-page.spec.ts
index 7121b0c90..b61562efc 100644
--- a/tests/e2e/admin-templates-page.spec.ts
+++ b/tests/e2e/admin-templates-page.spec.ts
@@ -5,16 +5,15 @@
* End-to-end coverage for the admin Beheer ▸ Templates page
* (`src/components/admin/tabs/TemplatesPage.vue`, admin-templates spec).
*
- * This is the ONLY place dashboard templates can be managed, and it is a
- * default-mounted tab: `AdminSettings.vue` passes `default-tab="templates"`
- * to `BeheerTabs`, which renders exactly one panel at a time. So the page is
- * what an admin sees on arrival at `/settings/admin/launchpad`, and if it
- * fails to mount the admin area opens on an empty panel with no error.
+ * This is the ONLY place dashboard templates can be managed. It is the
+ * Templates tab of the admin shell, opened here through `?tab=templates`, and
+ * `AdminSettings.vue` mounts a tab's content only while that tab is open. If
+ * the page fails to mount, the tab opens on an empty panel with no error.
*
* What is asserted, and why each one is here rather than a screenshot:
*
- * 1. The page mounts as the default panel — `[data-test="panel-templates"]`
- * AND the component's own root `[data-test="templates-page"]`. The panel
+ * 1. The page mounts in its panel — `[data-tab="templates"]` AND the
+ * component's own root `[data-test="templates-page"]`. The panel
* alone would still be present if the slot rendered nothing.
* 2. Its two structural affordances are live: the section heading and the
* Create-template CTA.
@@ -22,7 +21,7 @@
* open in the editor (ADR-004 modal isolation), so this crossing is the
* page's own behaviour, not the modal's.
* 4. Leaving the tab UNMOUNTS the page and returning re-mounts it. That is
- * the contract `BeheerTabs` provides ("content only renders when
+ * the contract `AdminSettings.vue` keeps ("content only renders when
* active"), and it is the one that makes the page's `created()` list
* fetch re-run — a panel that stayed mounted would silently serve stale
* templates after a change made elsewhere.
@@ -32,6 +31,7 @@
*/
import { expect, test } from '@playwright/test'
+import { adminTab } from './support/adminTab.ts'
import { BASE_URL as BASE } from './support/baseUrl.ts'
const ADMIN = {
@@ -39,7 +39,7 @@ const ADMIN = {
pass: process.env.NC_ADMIN_PASS ?? 'admin',
}
-const SETTINGS_URL = `${BASE}/index.php/settings/admin/launchpad`
+const SETTINGS_URL = `${BASE}/index.php/settings/admin/launchpad?tab=templates`
// The component under test, named after the file it covers. The selector is
// unchanged — this only makes the link between spec and component readable in
@@ -57,14 +57,14 @@ test.describe('admin-templates — Templates page', () => {
.setHTTPCredentials({ username: ADMIN.user, password: ADMIN.pass })
})
- test('renders as the default Beheer panel with its create affordance', async ({
+ test('renders in the Templates panel with its create affordance', async ({
page,
}) => {
await page.goto(SETTINGS_URL)
// The panel is the tab host; the inner root is TemplatesPage itself.
// Both, because an empty slot still renders the panel.
- await expect(page.locator('[data-test="panel-templates"]')).toBeVisible({
+ await expect(page.locator('[data-tab="templates"]')).toBeVisible({
timeout: 20_000,
})
const templatesPage = page.locator(TemplatesPage)
@@ -104,15 +104,15 @@ test.describe('admin-templates — Templates page', () => {
timeout: 20_000,
})
- // BeheerTabs renders only the active panel, so switching tabs must
+ // The admin page mounts only the open tab's content, so switching tabs must
// remove TemplatesPage from the DOM entirely — that unmount is what
// makes its created() hook re-fetch the template list on return.
- await page.locator('[data-test="tab-group-dashboards"]').click()
+ await adminTab(page, 'group-dashboards').click()
await expect(page.locator(TemplatesPage)).toBeHidden({
timeout: 10_000,
})
- await page.locator('[data-test="tab-templates"]').click()
+ await adminTab(page, 'templates').click()
await expect(page.locator(TemplatesPage)).toBeVisible({
timeout: 10_000,
})
diff --git a/tests/e2e/dashboard-export-import-roundtrip.spec.ts b/tests/e2e/dashboard-export-import-roundtrip.spec.ts
index b748c22c4..53c8737b3 100644
--- a/tests/e2e/dashboard-export-import-roundtrip.spec.ts
+++ b/tests/e2e/dashboard-export-import-roundtrip.spec.ts
@@ -49,6 +49,7 @@
import type { APIRequestContext, request } from '@playwright/test'
import { expect, test } from '@playwright/test'
+import { adminTab } from './support/adminTab.ts'
import { BASE_URL as BASE } from './support/baseUrl.ts'
const ADMIN = {
@@ -179,7 +180,7 @@ test.describe('dashboard export and import — widget configuration survives', (
.context()
.setHTTPCredentials({ username: ADMIN.user, password: ADMIN.pass })
await page.goto(SETTINGS_PAGE, { waitUntil: 'domcontentloaded' })
- await page.locator('[data-test="tab-operations"]').click({ timeout: 30_000 })
+ await adminTab(page, 'operations').click({ timeout: 30_000 })
await page.locator('[data-test="import-file-input"]').setInputFiles({
name: `launchpad-roundtrip-${STAMP}.zip`,
diff --git a/tests/e2e/dashboard-import-archives.spec.ts b/tests/e2e/dashboard-import-archives.spec.ts
index 68a14df8b..94e9272aa 100644
--- a/tests/e2e/dashboard-import-archives.spec.ts
+++ b/tests/e2e/dashboard-import-archives.spec.ts
@@ -25,6 +25,7 @@
import type { APIRequestContext, Page, request } from '@playwright/test'
import { expect, test } from '@playwright/test'
+import { adminTab } from './support/adminTab.ts'
import { BASE_URL as BASE } from './support/baseUrl.ts'
import { buildZip, dashboardPayload, manifest } from './support/zipArchive.ts'
@@ -59,7 +60,7 @@ async function openOperationsTab(page: Page): Promise {
waitUntil: 'domcontentloaded',
timeout: 60_000,
})
- await page.locator('[data-test="tab-operations"]').click({ timeout: 30_000 })
+ await adminTab(page, 'operations').click({ timeout: 30_000 })
await expect(page.locator('[data-test="import-file-input"]')).toBeVisible({
timeout: 20_000,
})
diff --git a/tests/e2e/dashboard-site-export.spec.ts b/tests/e2e/dashboard-site-export.spec.ts
index 9c35f699c..508330a86 100644
--- a/tests/e2e/dashboard-site-export.spec.ts
+++ b/tests/e2e/dashboard-site-export.spec.ts
@@ -34,6 +34,7 @@ import { expect, test } from '@playwright/test'
import * as fs from 'fs'
import * as os from 'os'
import * as path from 'path'
+import { adminTab } from './support/adminTab.ts'
import { BASE_URL as BASE } from './support/baseUrl.ts'
import {
buildZip,
@@ -71,7 +72,7 @@ async function openOperationsTab(page: Page): Promise {
waitUntil: 'domcontentloaded',
timeout: 60_000,
})
- await page.locator('[data-test="tab-operations"]').click({ timeout: 30_000 })
+ await adminTab(page, 'operations').click({ timeout: 30_000 })
await expect(page.locator('[data-test="export-site-button"]')).toBeVisible({
timeout: 20_000,
})
diff --git a/tests/e2e/role-based-content.spec.ts b/tests/e2e/role-based-content.spec.ts
index baec72a02..3908f6304 100644
--- a/tests/e2e/role-based-content.spec.ts
+++ b/tests/e2e/role-based-content.spec.ts
@@ -19,6 +19,7 @@
*/
import { expect, test } from '@playwright/test'
+import { adminTab } from './support/adminTab.ts'
import { BASE_URL as BASE } from './support/baseUrl.ts'
const ADMIN_URL = `${BASE}/index.php/settings/admin/launchpad`
@@ -53,7 +54,7 @@ test('admin settings page contains role-layout-defaults section', async ({
// The RoleLayoutDefaultsSection (REQ-RFP-002 / Task 6) lives inside the
// "Roles & Permissions" admin tab (slug `roles-permissions`). Select that
// tab before asserting the section.
- const rolesTab = page.locator('[data-test="tab-roles-permissions"]')
+ const rolesTab = adminTab(page, 'roles-permissions')
await expect(rolesTab).toBeVisible({ timeout: 15_000 })
await rolesTab.click()
diff --git a/tests/e2e/support/adminTab.ts b/tests/e2e/support/adminTab.ts
new file mode 100644
index 000000000..031801429
--- /dev/null
+++ b/tests/e2e/support/adminTab.ts
@@ -0,0 +1,23 @@
+/*
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
+ * SPDX-License-Identifier: EUPL-1.2
+ *
+ * The strip button of one admin settings tab, found by the tab's id.
+ *
+ * The admin page draws its tabs with the library's CnAdminSettingsShell. Each
+ * panel holds a `[data-tab=""]` element, and the strip button names its
+ * panel in `aria-controls`, so the id finds the button whatever the locale.
+ */
+
+import type { Locator, Page } from '@playwright/test'
+
+/**
+ * @param page The admin settings page.
+ * @param id The tab id, as in `AdminSettings.vue`'s `beheerTabs`.
+ * @return The tab's strip button.
+ */
+export function adminTab(page: Page, id: string): Locator {
+ return page.locator(
+ `xpath=//button[@role="tab"][@aria-controls=//*[*[@data-tab="${id}"]]/@id]`,
+ )
+}
From 277c8ea7a0aca1ecc884a59dde76e098c78cff6d Mon Sep 17 00:00:00 2001
From: Ruben van der Linde
Date: Fri, 16 Oct 2026 16:03:19 +0200
Subject: [PATCH 58/60] chore(deps): nextcloud-vue 2.89.2 (#874)
Patch line on top of 2.89.0: dashboard form submitLabel reads the app's
translation (#1677), filtered menu counts follow changed objects (#1674),
a size-to-content dashboard cell that grows after load no longer recurses
into a stack overflow (#1680).
---
package-lock.json | 8 ++++----
package.json | 2 +-
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 331f6f613..1fe969017 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,7 +9,7 @@
"version": "1.0.0",
"license": "EUPL-1.2",
"dependencies": {
- "@conduction/nextcloud-vue": "^2.89.0",
+ "@conduction/nextcloud-vue": "^2.89.2",
"@mdi/js": "^7.4.47",
"@nextcloud/axios": "^2.6.0",
"@nextcloud/dialogs": "^7.4.1",
@@ -1973,9 +1973,9 @@
}
},
"node_modules/@conduction/nextcloud-vue": {
- "version": "2.89.0",
- "resolved": "https://registry.npmjs.org/@conduction/nextcloud-vue/-/nextcloud-vue-2.89.0.tgz",
- "integrity": "sha512-4HrVvq7tsYqWnj78ybMuhpvMOFVdgqSgbAwB2VoZ8ockNWvhdtj3ekwqFl3q6A2Q/hUs9waP0DnGvyy2oAjHbg==",
+ "version": "2.89.2",
+ "resolved": "https://registry.npmjs.org/@conduction/nextcloud-vue/-/nextcloud-vue-2.89.2.tgz",
+ "integrity": "sha512-JWB7m+f0eg/QPqbNB8FvvkXdRcL5G6PHWMC8vRJzPA2dgoAIgubvqizEetPHATPSKKZnRtiIm/Vjwg0vN9+s9A==",
"license": "EUPL-1.2",
"dependencies": {
"@ckpack/vue-color": "^1.6.0",
diff --git a/package.json b/package.json
index df3d5d6db..9a15c875d 100644
--- a/package.json
+++ b/package.json
@@ -35,7 +35,7 @@
"prepare": "git config core.hooksPath .githooks || true"
},
"dependencies": {
- "@conduction/nextcloud-vue": "^2.89.0",
+ "@conduction/nextcloud-vue": "^2.89.2",
"@mdi/js": "^7.4.47",
"@nextcloud/axios": "^2.6.0",
"@nextcloud/dialogs": "^7.4.1",
From 43eab8b5b2cdeeea05808828398d8c8a4d4eae38 Mon Sep 17 00:00:00 2001
From: Ruben van der Linde
Date: Fri, 16 Oct 2026 16:20:18 +0200
Subject: [PATCH 59/60] fix(reports): dashboard report tiles link to registered
routes (#876)
The DashboardsReport stat tiles and the recent list's view-all link
resolved the route names "dashboards" and "admin-templates", which no
page registers, so vue-router threw MATCHER_NOT_FOUND on every render.
They now use Workspace and admin-templates-index, the names the menu
already uses. A spec checks every named route in the manifest against
the routes routesFromManifest builds.
Assisted-by: ClaudeCode:claude-opus-5-5
---
src/manifest.json | 10 +--
.../__tests__/manifestRouteNames.spec.js | 66 +++++++++++++++++++
2 files changed, 71 insertions(+), 5 deletions(-)
create mode 100644 src/utils/__tests__/manifestRouteNames.spec.js
diff --git a/src/manifest.json b/src/manifest.json
index 17d711f12..36f52533d 100644
--- a/src/manifest.json
+++ b/src/manifest.json
@@ -260,7 +260,7 @@
"label": "Dashboards",
"icon": "ViewDashboardOutline",
"route": {
- "name": "dashboards"
+ "name": "Workspace"
},
"format": {
"style": "decimal",
@@ -281,7 +281,7 @@
"label": "Personal",
"icon": "ViewDashboardOutline",
"route": {
- "name": "dashboards"
+ "name": "Workspace"
},
"format": {
"style": "decimal",
@@ -305,7 +305,7 @@
"label": "Shared with a group",
"icon": "Sitemap",
"route": {
- "name": "dashboards"
+ "name": "Workspace"
},
"format": {
"style": "decimal",
@@ -329,7 +329,7 @@
"label": "Admin templates",
"icon": "FileReplaceOutline",
"route": {
- "name": "admin-templates"
+ "name": "admin-templates-index"
},
"format": {
"style": "decimal",
@@ -379,7 +379,7 @@
"hideHeader": true,
"emptyText": "No dashboards yet",
"viewAllRoute": {
- "name": "dashboards"
+ "name": "Workspace"
},
"viewAllLabel": "Open a dashboard",
"columns": [
diff --git a/src/utils/__tests__/manifestRouteNames.spec.js b/src/utils/__tests__/manifestRouteNames.spec.js
new file mode 100644
index 000000000..721624ef7
--- /dev/null
+++ b/src/utils/__tests__/manifestRouteNames.spec.js
@@ -0,0 +1,66 @@
+/**
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
+ * SPDX-License-Identifier: EUPL-1.2
+ *
+ * Every named route the manifest links to must be a route the router has.
+ * `routesFromManifest` names one route per page id and nothing else, so an
+ * unknown name only shows up at runtime as a vue-router MATCHER_NOT_FOUND
+ * error in the browser console.
+ *
+ * @spec openspec/specs/runtime-shell/spec.md
+ */
+
+import { describe, expect, it } from 'vitest'
+import manifest from '../../manifest.json'
+import { routesFromManifest } from '../manifestRoutes.js'
+
+/**
+ * Collect every `{ name }` object held under a key ending in "route"
+ * (route, viewAllRoute, ...), with the JSON path it was found at.
+ *
+ * @param {*} node The manifest node to walk.
+ * @param {string} path The JSON path of `node`.
+ * @param {Array} found Accumulator.
+ *
+ * @return {Array<{path: string, name: string}>} The named route references.
+ */
+function namedRouteRefs(node, path = '', found = []) {
+ if (Array.isArray(node)) {
+ node.forEach((child, i) => namedRouteRefs(child, `${path}[${i}]`, found))
+ } else if (node && typeof node === 'object') {
+ for (const [key, value] of Object.entries(node)) {
+ if (/route$/i.test(key) && value && typeof value === 'object' && typeof value.name === 'string') {
+ found.push({ path: `${path}.${key}`, name: value.name })
+ }
+ namedRouteRefs(value, `${path}.${key}`, found)
+ }
+ }
+ return found
+}
+
+describe('manifest route names', () => {
+ const routeNames = new Set(routesFromManifest(manifest, {}).map((r) => r.name).filter(Boolean))
+
+ it('finds the named route references it checks', () => {
+ expect(namedRouteRefs(manifest).length).toBeGreaterThan(0)
+ })
+
+ it('links widgets only to routes the router registers', () => {
+ const unknown = namedRouteRefs(manifest).filter((ref) => !routeNames.has(ref.name))
+ expect(unknown).toEqual([])
+ })
+
+ it('points every menu route at a registered route', () => {
+ const unknown = (manifest.menu ?? [])
+ .filter((item) => typeof item.route === 'string' && !routeNames.has(item.route))
+ .map((item) => ({ id: item.id, route: item.route }))
+ expect(unknown).toEqual([])
+ })
+
+ it('flags a name the router does not have', () => {
+ const refs = namedRouteRefs({ widgets: [{ content: { route: { name: 'dashboards' } } }] })
+ expect(refs.filter((ref) => !routeNames.has(ref.name))).toEqual([
+ { path: '.widgets[0].content.route', name: 'dashboards' },
+ ])
+ })
+})
From c5a4490ddf9541c4bac3ba085e533ca8c8656b93 Mon Sep 17 00:00:00 2001
From: Ruben van der Linde
Date: Fri, 16 Oct 2026 19:48:49 +0200
Subject: [PATCH 60/60] feat(admin): nextcloud-vue 2.90.0, the admin shell
mounts only the open tab (#878)
Bump @conduction/nextcloud-vue to ^2.90.0 and adopt CnAdminSettingsShell's
lazyTabs on the admin page, replacing the per-panel v-if on activeTab. Deep
links (?tab=), the remembered tab, the tab-change breadcrumb and the
sharing tab behave as before.
Assisted-by: ClaudeCode:claude-opus-5-5
---
package-lock.json | 8 ++---
package.json | 2 +-
src/components/admin/AdminSettings.vue | 30 +++++++++----------
.../__tests__/AdminSettings.tabs.spec.js | 11 +++----
4 files changed, 25 insertions(+), 26 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 1fe969017..9054d2306 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,7 +9,7 @@
"version": "1.0.0",
"license": "EUPL-1.2",
"dependencies": {
- "@conduction/nextcloud-vue": "^2.89.2",
+ "@conduction/nextcloud-vue": "^2.90.0",
"@mdi/js": "^7.4.47",
"@nextcloud/axios": "^2.6.0",
"@nextcloud/dialogs": "^7.4.1",
@@ -1973,9 +1973,9 @@
}
},
"node_modules/@conduction/nextcloud-vue": {
- "version": "2.89.2",
- "resolved": "https://registry.npmjs.org/@conduction/nextcloud-vue/-/nextcloud-vue-2.89.2.tgz",
- "integrity": "sha512-JWB7m+f0eg/QPqbNB8FvvkXdRcL5G6PHWMC8vRJzPA2dgoAIgubvqizEetPHATPSKKZnRtiIm/Vjwg0vN9+s9A==",
+ "version": "2.90.0",
+ "resolved": "https://registry.npmjs.org/@conduction/nextcloud-vue/-/nextcloud-vue-2.90.0.tgz",
+ "integrity": "sha512-2D+wjr+W5FaJbJx77KCKAWP/RfdMs6PsBYDeQ0g+/Unj2Wi/Pec9zXLJClrYE1f+IJYGq3H3WKArUGjUsatCXw==",
"license": "EUPL-1.2",
"dependencies": {
"@ckpack/vue-color": "^1.6.0",
diff --git a/package.json b/package.json
index 9a15c875d..f50f99ee0 100644
--- a/package.json
+++ b/package.json
@@ -35,7 +35,7 @@
"prepare": "git config core.hooksPath .githooks || true"
},
"dependencies": {
- "@conduction/nextcloud-vue": "^2.89.2",
+ "@conduction/nextcloud-vue": "^2.90.0",
"@mdi/js": "^7.4.47",
"@nextcloud/axios": "^2.6.0",
"@nextcloud/dialogs": "^7.4.1",
diff --git a/src/components/admin/AdminSettings.vue b/src/components/admin/AdminSettings.vue
index e5c450c37..0214f247a 100644
--- a/src/components/admin/AdminSettings.vue
+++ b/src/components/admin/AdminSettings.vue
@@ -21,11 +21,12 @@
:tabs="beheerTabs"
tabsVariant="underline"
tabStorageKey="launchpad.admin.activeTab"
+ lazyTabs
@tabChange="onTabChange">
@@ -64,42 +65,39 @@