From fc8cd1ba71288f64600546609976e6f9f01c9a4e Mon Sep 17 00:00:00 2001 From: "Enjeck C." Date: Sun, 9 Aug 2026 20:16:37 +0100 Subject: [PATCH 1/3] feat(navigation): add LoadAdditionalEntriesListener Signed-off-by: Enjeck C. --- lib/AppInfo/Application.php | 6 +-- .../LoadAdditionalEntriesListener.php | 41 +++++++++++++++++++ lib/Service/ContextService.php | 34 ++++++++------- 3 files changed, 60 insertions(+), 21 deletions(-) create mode 100644 lib/Listener/LoadAdditionalEntriesListener.php diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 108b7686f0..bb01f37bc5 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -17,7 +17,7 @@ use OCA\Tables\Event\ViewDeletedEvent; use OCA\Tables\Listener\AddMissingIndicesListener; use OCA\Tables\Listener\AnalyticsDatasourceListener; -use OCA\Tables\Listener\BeforeTemplateRenderedListener; +use OCA\Tables\Listener\LoadAdditionalEntriesListener; use OCA\Tables\Listener\LoadAdditionalListener; use OCA\Tables\Listener\ReceiverCleanupListener; use OCA\Tables\Listener\TablesReferenceListener; @@ -39,11 +39,11 @@ use OCP\AppFramework\Bootstrap\IBootContext; use OCP\AppFramework\Bootstrap\IBootstrap; use OCP\AppFramework\Bootstrap\IRegistrationContext; -use OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent; use OCP\Collaboration\Reference\RenderReferenceEvent; use OCP\Collaboration\Resources\LoadAdditionalScriptsEvent; use OCP\DB\Events\AddMissingIndicesEvent; use OCP\Group\Events\GroupDeletedEvent; +use OCP\Navigation\Events\LoadAdditionalEntriesEvent; use OCP\User\Events\BeforeUserDeletedEvent; use OCP\User\Events\UserDeletedEvent; use Psr\Container\ContainerInterface; @@ -85,7 +85,7 @@ public function register(IRegistrationContext $context): void { $context->registerEventListener(BeforeUserDeletedEvent::class, UserDeletedListener::class); $context->registerEventListener(DatasourceEvent::class, AnalyticsDatasourceListener::class); $context->registerEventListener(RenderReferenceEvent::class, TablesReferenceListener::class); - $context->registerEventListener(BeforeTemplateRenderedEvent::class, BeforeTemplateRenderedListener::class); + $context->registerEventListener(LoadAdditionalEntriesEvent::class, LoadAdditionalEntriesListener::class); $context->registerEventListener(LoadAdditionalScriptsEvent::class, LoadAdditionalListener::class); $context->registerEventListener(TableDeletedEvent::class, WhenTableDeletedAuditLogListener::class); $context->registerEventListener(ViewDeletedEvent::class, WhenViewDeletedAuditLogListener::class); diff --git a/lib/Listener/LoadAdditionalEntriesListener.php b/lib/Listener/LoadAdditionalEntriesListener.php new file mode 100644 index 0000000000..583e285b11 --- /dev/null +++ b/lib/Listener/LoadAdditionalEntriesListener.php @@ -0,0 +1,41 @@ + + */ +class LoadAdditionalEntriesListener implements IEventListener { + public function __construct( + protected IUserSession $userSession, + protected ContextService $contextService, + ) { + } + + /** + * @inheritDoc + */ + public function handle(Event $event): void { + if (!$event instanceof LoadAdditionalEntriesEvent) { + return; + } + + $user = $this->userSession->getUser(); + if ($user === null) { + return; + } + + $this->contextService->addToNavigation($user->getUID()); + } +} diff --git a/lib/Service/ContextService.php b/lib/Service/ContextService.php index 9a32490199..9ac80d0661 100644 --- a/lib/Service/ContextService.php +++ b/lib/Service/ContextService.php @@ -80,25 +80,23 @@ public function findForNavigation(string $userId): array { public function addToNavigation(string $userId): void { $contexts = $this->findForNavigation($userId); foreach ($contexts as $context) { - $this->navigationManager->add(function () use ($context) { - $iconRelPath = 'material/' . $context->getIcon() . '.svg'; - if (file_exists(__DIR__ . '/../../img/' . $iconRelPath)) { - $iconUrl = $this->urlGenerator->imagePath(Application::APP_ID, $iconRelPath); - } else { - $iconUrl = $this->urlGenerator->imagePath('core', 'places/default-app-icon.svg'); - } + $iconRelPath = 'material/' . $context->getIcon() . '.svg'; + if (file_exists(__DIR__ . '/../../img/' . $iconRelPath)) { + $iconUrl = $this->urlGenerator->imagePath(Application::APP_ID, $iconRelPath); + } else { + $iconUrl = $this->urlGenerator->imagePath('core', 'places/default-app-icon.svg'); + } + + $contextUrl = $this->urlGenerator->linkToRoute('tables.page.context', ['contextId' => $context->getId()]); - $contextUrl = $this->urlGenerator->linkToRoute('tables.page.context', ['contextId' => $context->getId()]); - - return [ - 'id' => Application::APP_ID . '_application_' . $context->getId(), - 'name' => $context->getName(), - 'href' => $contextUrl, - 'icon' => $iconUrl, - 'order' => 500, - 'type' => 'link', - ]; - }); + $this->navigationManager->add([ + 'id' => Application::APP_ID . '_application_' . $context->getId(), + 'name' => $context->getName(), + 'href' => $contextUrl, + 'icon' => $iconUrl, + 'order' => 500, + 'type' => 'link', + ]); } } From 9a9789067449e990a51850b762df55c21c450303 Mon Sep 17 00:00:00 2001 From: Kostiantyn Miakshyn Date: Wed, 12 Aug 2026 01:08:47 +0200 Subject: [PATCH 2/3] feat(navigation): remove BeforeTemplateRenderedListener Signed-off-by: Kostiantyn Miakshyn --- .../BeforeTemplateRenderedListener.php | 41 ------------------- 1 file changed, 41 deletions(-) delete mode 100644 lib/Listener/BeforeTemplateRenderedListener.php diff --git a/lib/Listener/BeforeTemplateRenderedListener.php b/lib/Listener/BeforeTemplateRenderedListener.php deleted file mode 100644 index 7bce96c24e..0000000000 --- a/lib/Listener/BeforeTemplateRenderedListener.php +++ /dev/null @@ -1,41 +0,0 @@ - - */ -class BeforeTemplateRenderedListener implements IEventListener { - public function __construct( - protected IUserSession $userSession, - protected ContextService $contextService, - ) { - } - - /** - * @inheritDoc - */ - public function handle(Event $event): void { - if (!$event instanceof BeforeTemplateRenderedEvent) { - return; - } - - $user = $this->userSession->getUser(); - if ($user === null) { - return; - } - - $this->contextService->addToNavigation($user->getUID()); - } -} From 579dcc2936dd2629e3f3c9dec123d35232f8997f Mon Sep 17 00:00:00 2001 From: "Enjeck C." Date: Wed, 12 Aug 2026 07:37:22 +0100 Subject: [PATCH 3/3] refactor: update type hints and improve docblocks Signed-off-by: Enjeck C. --- lib/Activity/ActivityManager.php | 35 +++++++++++++------ lib/Activity/ChangeSet.php | 4 +-- lib/Analytics/AnalyticsDatasource.php | 2 +- lib/Db/LegacyRowMapper.php | 2 +- lib/Db/RowCellMapperSuper.php | 5 ++- lib/Db/RowCellSuper.php | 2 +- .../Version000800Date20240213123743.php | 20 +++++------ lib/Model/RowDataInput.php | 5 ++- lib/Service/ConfigService.php | 2 ++ lib/Service/ImportService.php | 5 ++- .../ValueObject/ColumnOrderInformation.php | 2 +- 11 files changed, 55 insertions(+), 29 deletions(-) diff --git a/lib/Activity/ActivityManager.php b/lib/Activity/ActivityManager.php index 1927e61def..cbffc42e30 100644 --- a/lib/Activity/ActivityManager.php +++ b/lib/Activity/ActivityManager.php @@ -81,7 +81,15 @@ public function __construct( ) { } - public function triggerEvent($objectType, $object, $subject, $additionalParams = [], $author = null) { + /** + * @param Column|Row2|Table|View $object + * @param (Share|\OCA\Tables\Model\ImportStats)[]|null|string $additionalParams + * @param (array|null)[]|null|string $author + * + * @psalm-param array{importStats?: \OCA\Tables\Model\ImportStats, share?: Share}|null|string $additionalParams + * @psalm-param array{before: array|null, after: array|null}|null|string $author + */ + public function triggerEvent(string $objectType, Table|Column|Row2|View $object, string $subject, array|string|null $additionalParams = [], array|string|null $author = null) { if ($author === null) { $author = $this->userId; } @@ -97,7 +105,11 @@ public function triggerEvent($objectType, $object, $subject, $additionalParams = } } - public function triggerUpdateEvents($objectType, ChangeSet $changeSet, $subject) { + /** + * @psalm-param 'tables_table'|'tables_view' $objectType + * @psalm-param 'table_update'|'view_update' $subject + */ + public function triggerUpdateEvents(string $objectType, ChangeSet $changeSet, string $subject) { $previousEntity = $changeSet->getBefore(); $entity = $changeSet->getAfter(); $events = []; @@ -134,7 +146,10 @@ public function triggerUpdateEvents($objectType, ChangeSet $changeSet, $subject) } } - private function createEvent($objectType, $object, $subject, $additionalParams = [], $author = null) { + /** + * @psalm-param array{before?: mixed, after?: mixed} $additionalParams + */ + private function createEvent($objectType, $object, string $subject, array $additionalParams = [], $author = null) { if ($object instanceof Table) { $objectTitle = $object->getTitle(); $table = $object; @@ -539,7 +554,7 @@ private function getViewColumnIds(mixed $view): array { return $columnIds; } - public function getActivitySubject($language, $subjectIdentifier, $subjectParams = [], $ownActivity = false) { + public function getActivitySubject(string $language, $subjectIdentifier, array $subjectParams = [], bool $ownActivity = false) { $l = $this->l10nFactory->get(Application::APP_ID, $language); $isViewContext = $subjectParams['isViewContext'] ?? false; $isViewObject = ($subjectParams['objectType'] ?? null) === self::TABLES_OBJECT_VIEW; @@ -577,7 +592,7 @@ public function getActivitySubject($language, $subjectIdentifier, $subjectParams return ''; } - private function formatTableActivity($l, string $subjectIdentifier, bool $ownActivity): ?string { + private function formatTableActivity(\OCP\IL10N $l, string $subjectIdentifier, bool $ownActivity): ?string { return match ($subjectIdentifier) { self::SUBJECT_TABLE_CREATE => $ownActivity ? $l->t('You have created a new table {table}') : $l->t('{user} has created a new table {table}'), self::SUBJECT_TABLE_UPDATE => $ownActivity ? $l->t('You have updated the table {table}') : $l->t('{user} has updated the table {table}'), @@ -588,7 +603,7 @@ private function formatTableActivity($l, string $subjectIdentifier, bool $ownAct }; } - private function formatViewActivity($l, string $subjectIdentifier, bool $ownActivity): ?string { + private function formatViewActivity(\OCP\IL10N $l, string $subjectIdentifier, bool $ownActivity): ?string { return match ($subjectIdentifier) { self::SUBJECT_VIEW_CREATE => $ownActivity ? $l->t('You have created a new view {view} in table {table}') : $l->t('{user} has created a new view {view} in table {table}'), self::SUBJECT_VIEW_UPDATE => $ownActivity ? $l->t('You have updated the view {view} in table {table}') : $l->t('{user} has updated the view {view} in table {table}'), @@ -599,7 +614,7 @@ private function formatViewActivity($l, string $subjectIdentifier, bool $ownActi }; } - private function formatRowActivity($l, string $subjectIdentifier, array $subjectParams, bool $ownActivity, bool $isViewContext): ?string { + private function formatRowActivity(\OCP\IL10N $l, string $subjectIdentifier, array $subjectParams, bool $ownActivity, bool $isViewContext): ?string { switch ($subjectIdentifier) { case self::SUBJECT_ROW_CREATE: if ($isViewContext) { @@ -664,7 +679,7 @@ private function formatRowUpdateActivity($l, array $subjectParams, bool $ownActi ); } - private function formatColumnActivity($l, string $subjectIdentifier, bool $ownActivity, bool $isViewContext): ?string { + private function formatColumnActivity(\OCP\IL10N $l, string $subjectIdentifier, bool $ownActivity, bool $isViewContext): ?string { if ($isViewContext) { return match ($subjectIdentifier) { self::SUBJECT_COLUMN_CREATE => $ownActivity ? $l->t('You have created a new column {column} in view {view}') : $l->t('{user} has created a new column {column} in view {view}'), @@ -682,7 +697,7 @@ private function formatColumnActivity($l, string $subjectIdentifier, bool $ownAc }; } - private function formatShareActivity($l, string $subjectIdentifier, array $subjectParams, bool $ownActivity, bool $isViewObject, mixed $sharedWith): ?string { + private function formatShareActivity(\OCP\IL10N $l, string $subjectIdentifier, array $subjectParams, bool $ownActivity, bool $isViewObject, mixed $sharedWith): ?string { $sharedWithYou = $this->isSharedWithYou($subjectParams, $ownActivity); $isLinkShare = $this->isLinkShare($sharedWith); @@ -766,7 +781,7 @@ private function isLinkShare(mixed $sharedWith): bool { return is_array($sharedWith) && ($sharedWith['type'] ?? null) === ShareReceiverType::LINK; } - public function getActivityMessage($language, $subjectIdentifier) { + public function getActivityMessage(string $language, $subjectIdentifier) { $l = $this->l10nFactory->get(Application::APP_ID, $language); switch ($subjectIdentifier) { diff --git a/lib/Activity/ChangeSet.php b/lib/Activity/ChangeSet.php index 4ec67cde9d..ea850a537a 100644 --- a/lib/Activity/ChangeSet.php +++ b/lib/Activity/ChangeSet.php @@ -24,11 +24,11 @@ public function __construct( } } - public function setBefore($before) { + public function setBefore(Entity $before) { $this->before = clone $before; } - public function setAfter($after) { + public function setAfter(Entity $after) { $this->after = clone $after; } diff --git a/lib/Analytics/AnalyticsDatasource.php b/lib/Analytics/AnalyticsDatasource.php index 9963c9f8d6..0d0ee5e29d 100644 --- a/lib/Analytics/AnalyticsDatasource.php +++ b/lib/Analytics/AnalyticsDatasource.php @@ -338,7 +338,7 @@ private function formatBooleanValue(mixed $value): string { return ''; } - private function formatTextValue(Column $column, mixed $value): string { + private function formatTextValue(Column $column, string $value): string { if ($value === null || $value === '') { return ''; } diff --git a/lib/Db/LegacyRowMapper.php b/lib/Db/LegacyRowMapper.php index 95f04a2bb7..b39859ca07 100644 --- a/lib/Db/LegacyRowMapper.php +++ b/lib/Db/LegacyRowMapper.php @@ -82,7 +82,7 @@ public function find(int $id): LegacyRow { return $this->findEntity($qb); } - private function buildFilterByColumnType($qb, array $filter, string $filterId): ?IQueryFunction { + private function buildFilterByColumnType(IQueryBuilder $qb, array $filter, string $filterId): ?IQueryFunction { try { $columnQbClassName = 'OCA\Tables\Db\ColumnTypes\\'; $type = explode('-', $filter['columnType'])[0]; diff --git a/lib/Db/RowCellMapperSuper.php b/lib/Db/RowCellMapperSuper.php index be377c5d87..460e2095e8 100644 --- a/lib/Db/RowCellMapperSuper.php +++ b/lib/Db/RowCellMapperSuper.php @@ -41,7 +41,10 @@ public function formatRowData(Column $column, array $row) { * Transform value from a filter rule to the actual query parameter used * for constructing the view filter query */ - public function filterValueToQueryParam(Column $column, mixed $value): mixed { + /** + * @param array|float|null|string $value + */ + public function filterValueToQueryParam(Column $column, array|string|float|null $value): mixed { return $value; } diff --git a/lib/Db/RowCellSuper.php b/lib/Db/RowCellSuper.php index 29c3e366bb..58fc632fa6 100644 --- a/lib/Db/RowCellSuper.php +++ b/lib/Db/RowCellSuper.php @@ -60,7 +60,7 @@ public function setColumnIdWrapper(int $columnId) { $this->setColumnId($columnId); } - public function setValueWrapper($value) { + public function setValueWrapper(array|float|null $value) { $this->setValue($value); } } diff --git a/lib/Migration/Version000800Date20240213123743.php b/lib/Migration/Version000800Date20240213123743.php index d265cc873b..be71737079 100644 --- a/lib/Migration/Version000800Date20240213123743.php +++ b/lib/Migration/Version000800Date20240213123743.php @@ -11,7 +11,6 @@ use Closure; use Doctrine\DBAL\Schema\SchemaException; -use Doctrine\DBAL\Schema\Table; use OCP\DB\ISchemaWrapper; use OCP\DB\Types; use OCP\Migration\IOutput; @@ -39,15 +38,12 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt return $schema; } - protected function shouldAddTable(string $tableName, ISchemaWrapper $schema): ?Table { - return !$schema->hasTable($tableName) ? $schema->createTable($tableName) : null; - } - /** * @throws SchemaException */ protected function haveContextTable(ISchemaWrapper $schema): void { - if ($table = $this->shouldAddTable(self::PREFIX . 'context', $schema)) { + if (!$schema->hasTable(self::PREFIX . 'context')) { + $table = $schema->createTable(self::PREFIX . 'context'); $table->addColumn('id', Types::INTEGER, ['autoincrement' => true, 'notnull' => true]); $table->addColumn('name', Types::STRING, ['notnull' => true, 'length' => 200]); $table->addColumn('icon', Types::STRING, ['notnull' => true, 'length' => 64]); @@ -63,7 +59,8 @@ protected function haveContextTable(ISchemaWrapper $schema): void { * @throws SchemaException */ protected function haveContextNodeRelationTable(ISchemaWrapper $schema): void { - if ($table = $this->shouldAddTable(self::PREFIX . 'rel_context_node', $schema)) { + if (!$schema->hasTable(self::PREFIX . 'rel_context_node')) { + $table = $schema->createTable(self::PREFIX . 'rel_context_node'); $table->addColumn('id', Types::INTEGER, ['autoincrement' => true, 'notnull' => true]); $table->addColumn('context_id', Types::INTEGER, ['notnull' => true]); $table->addColumn('node_id', Types::INTEGER, ['notnull' => true]); @@ -78,7 +75,8 @@ protected function haveContextNodeRelationTable(ISchemaWrapper $schema): void { * @throws SchemaException */ protected function havePageTable(ISchemaWrapper $schema): void { - if ($table = $this->shouldAddTable(self::PREFIX . 'page', $schema)) { + if (!$schema->hasTable(self::PREFIX . 'page')) { + $table = $schema->createTable(self::PREFIX . 'page'); $table->addColumn('id', Types::INTEGER, ['autoincrement' => true, 'notnull' => true]); $table->addColumn('context_id', Types::INTEGER, ['notnull' => true]); $table->addColumn('page_type', Types::STRING, ['notnull' => true, 'length' => 32]); @@ -91,7 +89,8 @@ protected function havePageTable(ISchemaWrapper $schema): void { * @throws SchemaException */ protected function havePageContentTable(ISchemaWrapper $schema): void { - if ($table = $this->shouldAddTable(self::PREFIX . 'page_content', $schema)) { + if (!$schema->hasTable(self::PREFIX . 'page_content')) { + $table = $schema->createTable(self::PREFIX . 'page_content'); $table->addColumn('id', Types::INTEGER, ['autoincrement' => true, 'notnull' => true]); $table->addColumn('page_id', Types::INTEGER, ['notnull' => true]); $table->addColumn('node_rel_id', Types::INTEGER, ['notnull' => true]); @@ -105,7 +104,8 @@ protected function havePageContentTable(ISchemaWrapper $schema): void { * @throws SchemaException */ protected function haveNavigationTable(ISchemaWrapper $schema): void { - if ($table = $this->shouldAddTable(self::PREFIX . 'navigation', $schema)) { + if (!$schema->hasTable(self::PREFIX . 'navigation')) { + $table = $schema->createTable(self::PREFIX . 'navigation'); $table->addColumn('share_id', Types::INTEGER, ['notnull' => true]); $table->addColumn('display_mode', Types::INTEGER, ['notnull' => true]); $table->addColumn('user_id', Types::STRING, ['notnull' => true, 'length' => 64, 'default' => '']); diff --git a/lib/Model/RowDataInput.php b/lib/Model/RowDataInput.php index dbef7fde0b..21f54cd2f9 100644 --- a/lib/Model/RowDataInput.php +++ b/lib/Model/RowDataInput.php @@ -24,7 +24,10 @@ class RowDataInput implements ArrayAccess, Iterator { /** @psalm-var array */ protected array $data = []; - public function add(int $columnId, mixed $value): self { + /** + * @param array|float|int|null|string $value + */ + public function add(int $columnId, array|string|int|float|null $value): self { $this->data[] = [self::DATA_KEY => $columnId, self::DATA_VAL => $value]; return $this; } diff --git a/lib/Service/ConfigService.php b/lib/Service/ConfigService.php index 1dfbb334a3..91bcf4a9ac 100644 --- a/lib/Service/ConfigService.php +++ b/lib/Service/ConfigService.php @@ -68,6 +68,8 @@ public function get(string $key): string|bool { * * @throws BadRequestError * @throws PermissionError + * + * @param bool|numeric|string $value */ public function set(string $key, mixed $value): void { $userId = $this->getUserId(); diff --git a/lib/Service/ImportService.php b/lib/Service/ImportService.php index d8adc4b521..81a0d78f24 100644 --- a/lib/Service/ImportService.php +++ b/lib/Service/ImportService.php @@ -704,7 +704,10 @@ private function upsertRow(Row $row, array $columnBusinesses): void { } } - private function valueToDateTimeImmutable(mixed $value): ?DateTimeImmutable { + /** + * @param null|string $value + */ + private function valueToDateTimeImmutable(string|null $value): ?DateTimeImmutable { if ( $value === false || $value === null diff --git a/lib/Service/ValueObject/ColumnOrderInformation.php b/lib/Service/ValueObject/ColumnOrderInformation.php index e7d0a12945..f0b4b4192b 100644 --- a/lib/Service/ValueObject/ColumnOrderInformation.php +++ b/lib/Service/ValueObject/ColumnOrderInformation.php @@ -84,7 +84,7 @@ public function jsonSerialize(): array { ]; } - protected function ensureType(string $offset, mixed $value): int|bool { + protected function ensureType(string $offset, bool|int $value): int|bool { return match ($offset) { self::KEY_ID, self::KEY_ORDER => (int)$value,