From 6d0313585172faf28f94a2e4cf86e54375bb3ddc Mon Sep 17 00:00:00 2001 From: Kostiantyn Miakshyn Date: Sat, 18 Jul 2026 22:14:40 +0200 Subject: [PATCH 1/3] feat: add possibility generate links for concrete rows Signed-off-by: Kostiantyn Miakshyn --- lib/Controller/RowController.php | 15 +++++++++------ lib/Db/ColumnMapper.php | 8 ++++++-- src/modules/main/sections/ElementTitle.vue | 16 ++++++++++++++++ src/modules/main/sections/MainWrapper.vue | 7 +++++++ src/store/data.js | 6 +++--- 5 files changed, 41 insertions(+), 11 deletions(-) diff --git a/lib/Controller/RowController.php b/lib/Controller/RowController.php index 144089f7e4..31fe1419cc 100644 --- a/lib/Controller/RowController.php +++ b/lib/Controller/RowController.php @@ -8,6 +8,7 @@ namespace OCA\Tables\Controller; use OCA\Tables\AppInfo\Application; +use OCA\Tables\Db\Column; use OCA\Tables\Middleware\Attribute\RequirePermission; use OCA\Tables\Service\RowService; use OCP\AppFramework\Controller; @@ -30,17 +31,19 @@ public function __construct( #[NoAdminRequired] #[RequirePermission(permission: Application::PERMISSION_READ, type: Application::NODE_TYPE_TABLE, idParam: 'tableId')] - public function index(int $tableId): DataResponse { - return $this->handleError(function () use ($tableId) { - return $this->service->findAllByTable($tableId, $this->userId); + public function index(int $tableId, string $customFilters = ''): DataResponse { + return $this->handleError(function () use ($tableId, $customFilters) { + $customFilters = json_decode($customFilters, true) ?? []; + return $this->service->findAllByTable($tableId, $this->userId, customFilters: $customFilters); }); } #[NoAdminRequired] #[RequirePermission(permission: Application::PERMISSION_READ, type: Application::NODE_TYPE_VIEW, idParam: 'viewId')] - public function indexView(int $viewId): DataResponse { - return $this->handleError(function () use ($viewId) { - return $this->service->findAllByView($viewId, $this->userId); + public function indexView(int $viewId, string $customFilters = ''): DataResponse { + return $this->handleError(function () use ($viewId, $customFilters) { + $customFilters = json_decode($customFilters, true) ?? []; + return $this->service->findAllByView($viewId, $this->userId, customFilters: $customFilters); }); } diff --git a/lib/Db/ColumnMapper.php b/lib/Db/ColumnMapper.php index 527b728d97..6bc60e3fd7 100644 --- a/lib/Db/ColumnMapper.php +++ b/lib/Db/ColumnMapper.php @@ -190,7 +190,7 @@ public function countColumns(int $tableId): int { * This method efficiently loads column data for a given set of columns, filters, and sorts * by fetching all required data in a single database operation. */ - public function preloadColumns(array $columns, ?array $filters = null, ?array $sorts = null): void { + public function preloadColumns(array $columns, ?array $filters = null, ?array $sorts = null, array $customFilters = []): void { $columnIds = $columns; if (!is_null($sorts) && count($sorts) > 0) { $columnIds = [...$columns, ...array_column($sorts, 'columnId')]; @@ -200,7 +200,11 @@ public function preloadColumns(array $columns, ?array $filters = null, ?array $s array_push($columnIds, ...array_column($filterGroup, 'columnId')); } } - + if (!is_null($customFilters) && count($customFilters) > 0) { + foreach ($customFilters as $filterGroup) { + array_push($columnIds, ...array_column($filterGroup, 'columnId')); + } + } $this->findAll(array_unique($columnIds)); } diff --git a/src/modules/main/sections/ElementTitle.vue b/src/modules/main/sections/ElementTitle.vue index e8c30e9334..7cef2c60dd 100644 --- a/src/modules/main/sections/ElementTitle.vue +++ b/src/modules/main/sections/ElementTitle.vue @@ -74,12 +74,28 @@ export default { }, isViewSettingSet() { + if (this.$route.query.customFilters?.length > 0) { + return true + } + return !(!this.viewSetting || ((!this.viewSetting.hiddenColumns || this.viewSetting.hiddenColumns.length === 0) && (!this.viewSetting.sorting) && (!this.viewSetting.filter || this.viewSetting.filter.length === 0))) }, }, methods: { resetLocalAdjustments() { + if (this.$route.query.customFilters?.length > 0) { + this.$router.push({ + path: this.$route.path, + query: { + ...this.$route.query, + customFilters: undefined, + }, + }) + + return + } + this.$emit('update:viewSetting', {}) }, }, diff --git a/src/modules/main/sections/MainWrapper.vue b/src/modules/main/sections/MainWrapper.vue index 723939de4f..968cb3aa25 100644 --- a/src/modules/main/sections/MainWrapper.vue +++ b/src/modules/main/sections/MainWrapper.vue @@ -86,6 +86,9 @@ export default { computed: { ...mapState(useTablesStore, ['activeRowId']), + customFiltered() { + return this.$route.query.customFilters + }, }, watch: { @@ -95,6 +98,9 @@ export default { activeRowId() { this.reload() }, + customFiltered() { + this.reload(true) + }, }, beforeMount() { @@ -183,6 +189,7 @@ export default { await this.loadRowsFromBE({ viewId: this.isView ? this.element.id : null, tableId: !this.isView ? this.element.id : null, + customFilters: this.$route.query.customFilters, }) } else { await this.removeRows({ diff --git a/src/store/data.js b/src/store/data.js index 75fc23a92f..3a0c5f5417 100644 --- a/src/store/data.js +++ b/src/store/data.js @@ -233,16 +233,16 @@ export const useDataStore = defineStore('data', { }, // ROWS - async loadRowsFromBE({ tableId, viewId }) { + async loadRowsFromBE({ tableId, viewId, customFilters = null }) { const stateId = genStateKey(!!(viewId), viewId ?? tableId) this.loading[stateId] = true let res = null try { if (viewId) { - res = await axios.get(generateUrl('/apps/tables/row/view/' + viewId)) + res = await axios.get(generateUrl('/apps/tables/row/view/' + viewId), { params: { customFilters } }) } else { - res = await axios.get(generateUrl('/apps/tables/row/table/' + tableId)) + res = await axios.get(generateUrl('/apps/tables/row/table/' + tableId), { params: { customFilters } }) } } catch (e) { displayError(e, t('tables', 'Could not load rows.')) From 5e4e16b662ca63a7636afd5e2b53f5d3e81d877c Mon Sep 17 00:00:00 2001 From: Kostiantyn Miakshyn Date: Sat, 18 Jul 2026 22:24:43 +0200 Subject: [PATCH 2/3] feat: add possibility generate links for concrete rows Signed-off-by: Kostiantyn Miakshyn --- lib/Controller/RowController.php | 1 - lib/Db/Row2Mapper.php | 44 ++++++++++++------- lib/Service/RowService.php | 9 ++-- .../components/ncTable/sections/Options.vue | 29 +++++++++++- 4 files changed, 61 insertions(+), 22 deletions(-) diff --git a/lib/Controller/RowController.php b/lib/Controller/RowController.php index 31fe1419cc..ae929bf5fd 100644 --- a/lib/Controller/RowController.php +++ b/lib/Controller/RowController.php @@ -8,7 +8,6 @@ namespace OCA\Tables\Controller; use OCA\Tables\AppInfo\Application; -use OCA\Tables\Db\Column; use OCA\Tables\Middleware\Attribute\RequirePermission; use OCA\Tables\Service\RowService; use OCP\AppFramework\Controller; diff --git a/lib/Db/Row2Mapper.php b/lib/Db/Row2Mapper.php index 7ec48500f9..ccada060d3 100644 --- a/lib/Db/Row2Mapper.php +++ b/lib/Db/Row2Mapper.php @@ -132,15 +132,15 @@ public function setUserId(string $userId): void { * @return int[] * @throws InternalError */ - private function getWantedRowIds(string $userId, int $tableId, ?array $filter = null, ?array $sort = null, ?int $limit = null, ?int $offset = null): array { + private function getWantedRowIds(string $userId, int $tableId, ?array $filter = null, ?array $sort = null, ?int $limit = null, ?int $offset = null, array $customFilters = []): array { $qb = $this->db->getQueryBuilder(); $qb->select('sleeves.id') ->from('tables_row_sleeves', 'sleeves') ->where($qb->expr()->eq('table_id', $qb->createNamedParameter($tableId, IQueryBuilder::PARAM_INT))); - if ($filter) { - $this->addFilterToQuery($qb, $filter, $userId); + if ($filter || $customFilters) { + $this->addFilterToQuery($qb, $filter ?? [], $customFilters ?? [], $userId); } $this->addSortQueryForMultipleSleeveFinder($qb, 'sleeves', $sort); @@ -172,14 +172,15 @@ private function getWantedRowIds(string $userId, int $tableId, ?array $filter = * @param array|null $filter * @param array|null $sort * @param string|null $userId + * @param array $customFilters * @return Row2[] * @throws InternalError */ - public function findAll(array $showColumnIds, int $tableId, ?int $limit = null, ?int $offset = null, ?array $filter = null, ?array $sort = null, ?string $userId = null): array { + public function findAll(array $showColumnIds, int $tableId, ?int $limit = null, ?int $offset = null, ?array $filter = null, ?array $sort = null, ?string $userId = null, array $customFilters = []): array { try { $this->columnMapper->preloadColumns($showColumnIds, $filter, $sort); - $wantedRowIdsArray = $this->getWantedRowIds($userId, $tableId, $filter, $sort, $limit, $offset); + $wantedRowIdsArray = $this->getWantedRowIds($userId, $tableId, $filter, $sort, $limit, $offset, $customFilters); // Get rows without SQL sorting $rows = $this->getRows($wantedRowIdsArray, $showColumnIds); @@ -273,16 +274,22 @@ private function getRowsChunk(array $rowIds, array $columnIds): array { /** * @throws InternalError */ - private function addFilterToQuery(IQueryBuilder $qb, array $filters, string $userId): void { - // TODO move this into service - $this->replacePlaceholderValues($filters, $userId); - + private function addFilterToQuery(IQueryBuilder $qb, array $filters, array $customFilters, string $userId): void { + $conditions = []; if (count($filters) > 0) { - $qb->andWhere( - $qb->expr()->orX( - ...$this->getFilterGroups($qb, $filters) - ) - ); + $this->replacePlaceholderValues($filters, $userId); + $conditions[] = $qb->expr()->orX(...$this->getFilterGroups($qb, $filters)); + } + + if (count($customFilters) > 0) { + $this->replacePlaceholderValues($customFilters, $userId); + $conditions[] = $qb->expr()->orX(...$this->getFilterGroups($qb, $customFilters)); + } + + if (count($conditions) == 1) { + $qb->andWhere($conditions[0]); + } elseif (count($conditions) > 1) { + $qb->andWhere($qb->expr()->andX(...$conditions)); } } @@ -350,7 +357,7 @@ private function addSortQueryForMultipleSleeveFinder(IQueryBuilder $qb, string $ private function replacePlaceholderValues(array &$filters, string $userId): void { foreach ($filters as &$filterGroup) { foreach ($filterGroup as &$filter) { - if (str_starts_with($filter['value'], '@')) { + if (is_string($filter['value']) && str_starts_with($filter['value'], '@')) { $columnId = (int)($filter['columnId'] ?? 0); $column = $columnId > 0 ? $this->columnMapper->find($columnId) : null; $filter['value'] = $this->columnsHelper->resolveSearchValue($filter['value'], $userId, $column); @@ -593,14 +600,14 @@ private function getFilterExpression(IQueryBuilder $qb, Column $column, string $ /** * @throws InternalError */ - private function getMetaFilterExpression(IQueryBuilder $qb, int $columnId, string $operator, string $value): IQueryBuilder { + private function getMetaFilterExpression(IQueryBuilder $qb, int $columnId, string $operator, string|array $value): IQueryBuilder { $qb2 = $this->db->getQueryBuilder(); $qb2->select('id'); $qb2->from('tables_row_sleeves'); switch ($columnId) { case Column::TYPE_META_ID: - $qb2->where($this->getSqlOperator($operator, $qb, 'id', (int)$value, IQueryBuilder::PARAM_INT)); + $qb2->where($this->getSqlOperator($operator, $qb, 'id', (array)$value, IQueryBuilder::PARAM_INT_ARRAY)); break; case Column::TYPE_META_CREATED_BY: $qb2->where($this->getSqlOperator($operator, $qb, 'created_by', $value, IQueryBuilder::PARAM_STR)); @@ -638,6 +645,9 @@ private function getSqlOperator(string $operator, IQueryBuilder $qb, string $col case 'does-not-contain': return $qb->expr()->notLike($columnName, $qb->createNamedParameter('%' . $this->db->escapeLikeParameter($value) . '%', $paramType)); case 'is-equal': + if (is_array($value)) { + return $qb->expr()->in($columnName, $qb->createNamedParameter($value, $paramType)); + } return $qb->expr()->eq($columnName, $qb->createNamedParameter($value, $paramType)); case 'is-not-equal': return $qb->expr()->neq($columnName, $qb->createNamedParameter($value, $paramType)); diff --git a/lib/Service/RowService.php b/lib/Service/RowService.php index 9e800499ef..b57df94d50 100644 --- a/lib/Service/RowService.php +++ b/lib/Service/RowService.php @@ -90,11 +90,12 @@ public function formatRowsForPublicShare(array $rows): array { * @param string $userId * @param ?int $limit * @param ?int $offset + * @param array $customFilters * @return Row2[] * @throws InternalError * @throws PermissionError */ - public function findAllByTable(int $tableId, string $userId, ?int $limit = null, ?int $offset = null): array { + public function findAllByTable(int $tableId, string $userId, ?int $limit = null, ?int $offset = null, array $customFilters = []): array { try { if ($this->permissionsService->canReadRowsByElementId($tableId, 'table', $userId)) { $tableColumns = $this->columnMapper->findAllByTable($tableId); @@ -103,7 +104,7 @@ public function findAllByTable(int $tableId, string $userId, ?int $limit = null, $table = $this->tableMapper->find($tableId); $sort = $table->getSortArray() ?: null; - $rows = $this->row2Mapper->findAll($showColumnIds, $tableId, $limit, $offset, null, $sort, $userId); + $rows = $this->row2Mapper->findAll($showColumnIds, $tableId, $limit, $offset, null, $sort, $userId, $customFilters); $this->attachAliasPayloads($rows, $tableColumns); return $rows; } else { @@ -120,13 +121,14 @@ public function findAllByTable(int $tableId, string $userId, ?int $limit = null, * @param string $userId * @param int|null $limit * @param int|null $offset + * @param array $customFilters * @return Row2[] * @throws DoesNotExistException * @throws InternalError * @throws MultipleObjectsReturnedException * @throws PermissionError */ - public function findAllByView(int $viewId, string $userId, ?int $limit = null, ?int $offset = null): array { + public function findAllByView(int $viewId, string $userId, ?int $limit = null, ?int $offset = null, array $customFilters = []): array { try { if ($this->permissionsService->canReadRowsByElementId($viewId, 'view', $userId)) { $view = $this->viewMapper->find($viewId); @@ -139,6 +141,7 @@ public function findAllByView(int $viewId, string $userId, ?int $limit = null, ? $view->getFilterArray(), $view->getSortArray(), $this->resolveFilterUserId($userId, $view), + $customFilters, ); } else { diff --git a/src/shared/components/ncTable/sections/Options.vue b/src/shared/components/ncTable/sections/Options.vue index 7f2ade57bf..d5772f317d 100644 --- a/src/shared/components/ncTable/sections/Options.vue +++ b/src/shared/components/ncTable/sections/Options.vue @@ -44,6 +44,12 @@ {{ t('tables', 'Export filtered rows') }} + + + {{ t('tables', 'Share rows') }} +