Skip to content

Commit 06a91c9

Browse files
committed
feat: add possibility generate links for concrete rows
Signed-off-by: Kostiantyn Miakshyn <molodchick@gmail.com>
1 parent 8d7f665 commit 06a91c9

10 files changed

Lines changed: 151 additions & 34 deletions

File tree

lib/Controller/RowController.php

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,19 +28,29 @@ public function __construct(
2828
parent::__construct(Application::APP_ID, $request);
2929
}
3030

31+
/**
32+
* @param int $tableId ID of the table
33+
* @param string $customFilters JSON-encoded array of filter groups to apply
34+
*/
3135
#[NoAdminRequired]
3236
#[RequirePermission(permission: Application::PERMISSION_READ, type: Application::NODE_TYPE_TABLE, idParam: 'tableId')]
33-
public function index(int $tableId): DataResponse {
34-
return $this->handleError(function () use ($tableId) {
35-
return $this->service->findAllByTable($tableId, $this->userId);
37+
public function index(int $tableId, string $customFilters = ''): DataResponse {
38+
return $this->handleError(function () use ($tableId, $customFilters) {
39+
$customFilters = json_decode($customFilters, true) ?? [];
40+
return $this->service->findAllByTable($tableId, $this->userId, customFilters: $customFilters);
3641
});
3742
}
3843

44+
/**
45+
* @param int $viewId ID of the view
46+
* @param string $customFilters JSON-encoded array of filter groups to apply
47+
*/
3948
#[NoAdminRequired]
4049
#[RequirePermission(permission: Application::PERMISSION_READ, type: Application::NODE_TYPE_VIEW, idParam: 'viewId')]
41-
public function indexView(int $viewId): DataResponse {
42-
return $this->handleError(function () use ($viewId) {
43-
return $this->service->findAllByView($viewId, $this->userId);
50+
public function indexView(int $viewId, string $customFilters = ''): DataResponse {
51+
return $this->handleError(function () use ($viewId, $customFilters) {
52+
$customFilters = json_decode($customFilters, true) ?? [];
53+
return $this->service->findAllByView($viewId, $this->userId, customFilters: $customFilters);
4454
});
4555
}
4656

lib/Db/ColumnMapper.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ public function countColumns(int $tableId): int {
190190
* This method efficiently loads column data for a given set of columns, filters, and sorts
191191
* by fetching all required data in a single database operation.
192192
*/
193-
public function preloadColumns(array $columns, ?array $filters = null, ?array $sorts = null): void {
193+
public function preloadColumns(array $columns, ?array $filters = null, ?array $sorts = null, array $customFilters = []): void {
194194
$columnIds = $columns;
195195
if (!is_null($sorts) && count($sorts) > 0) {
196196
$columnIds = [...$columns, ...array_column($sorts, 'columnId')];
@@ -200,7 +200,11 @@ public function preloadColumns(array $columns, ?array $filters = null, ?array $s
200200
array_push($columnIds, ...array_column($filterGroup, 'columnId'));
201201
}
202202
}
203-
203+
if (count($customFilters) > 0) {
204+
foreach ($customFilters as $filterGroup) {
205+
array_push($columnIds, ...array_column($filterGroup, 'columnId'));
206+
}
207+
}
204208
$this->findAll(array_unique($columnIds));
205209
}
206210

lib/Db/Row2Mapper.php

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -132,15 +132,15 @@ public function setUserId(string $userId): void {
132132
* @return int[]
133133
* @throws InternalError
134134
*/
135-
private function getWantedRowIds(string $userId, int $tableId, ?array $filter = null, ?array $sort = null, ?int $limit = null, ?int $offset = null): array {
135+
private function getWantedRowIds(string $userId, int $tableId, ?array $filter = null, ?array $sort = null, ?int $limit = null, ?int $offset = null, array $customFilters = []): array {
136136
$qb = $this->db->getQueryBuilder();
137137

138138
$qb->select('sleeves.id')
139139
->from('tables_row_sleeves', 'sleeves')
140140
->where($qb->expr()->eq('table_id', $qb->createNamedParameter($tableId, IQueryBuilder::PARAM_INT)));
141141

142-
if ($filter) {
143-
$this->addFilterToQuery($qb, $filter, $userId);
142+
if ($filter || $customFilters) {
143+
$this->addFilterToQuery($qb, $filter ?? [], $customFilters, $userId);
144144
}
145145

146146
$this->addSortQueryForMultipleSleeveFinder($qb, 'sleeves', $sort);
@@ -172,14 +172,15 @@ private function getWantedRowIds(string $userId, int $tableId, ?array $filter =
172172
* @param array|null $filter
173173
* @param array|null $sort
174174
* @param string|null $userId
175+
* @param array $customFilters
175176
* @return Row2[]
176177
* @throws InternalError
177178
*/
178-
public function findAll(array $showColumnIds, int $tableId, ?int $limit = null, ?int $offset = null, ?array $filter = null, ?array $sort = null, ?string $userId = null): array {
179+
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 {
179180
try {
180181
$this->columnMapper->preloadColumns($showColumnIds, $filter, $sort);
181182

182-
$wantedRowIdsArray = $this->getWantedRowIds($userId, $tableId, $filter, $sort, $limit, $offset);
183+
$wantedRowIdsArray = $this->getWantedRowIds($userId, $tableId, $filter, $sort, $limit, $offset, $customFilters);
183184

184185
// Get rows without SQL sorting
185186
$rows = $this->getRows($wantedRowIdsArray, $showColumnIds);
@@ -273,16 +274,22 @@ private function getRowsChunk(array $rowIds, array $columnIds): array {
273274
/**
274275
* @throws InternalError
275276
*/
276-
private function addFilterToQuery(IQueryBuilder $qb, array $filters, string $userId): void {
277-
// TODO move this into service
278-
$this->replacePlaceholderValues($filters, $userId);
279-
277+
private function addFilterToQuery(IQueryBuilder $qb, array $filters, array $customFilters, string $userId): void {
278+
$conditions = [];
280279
if (count($filters) > 0) {
281-
$qb->andWhere(
282-
$qb->expr()->orX(
283-
...$this->getFilterGroups($qb, $filters)
284-
)
285-
);
280+
$this->replacePlaceholderValues($filters, $userId);
281+
$conditions[] = $qb->expr()->orX(...$this->getFilterGroups($qb, $filters));
282+
}
283+
284+
if (count($customFilters) > 0) {
285+
$this->replacePlaceholderValues($customFilters, $userId);
286+
$conditions[] = $qb->expr()->orX(...$this->getFilterGroups($qb, $customFilters));
287+
}
288+
289+
if (count($conditions) == 1) {
290+
$qb->andWhere($conditions[0]);
291+
} elseif (count($conditions) > 1) {
292+
$qb->andWhere($qb->expr()->andX(...$conditions));
286293
}
287294
}
288295

@@ -350,7 +357,7 @@ private function addSortQueryForMultipleSleeveFinder(IQueryBuilder $qb, string $
350357
private function replacePlaceholderValues(array &$filters, string $userId): void {
351358
foreach ($filters as &$filterGroup) {
352359
foreach ($filterGroup as &$filter) {
353-
if (str_starts_with($filter['value'], '@')) {
360+
if (is_string($filter['value']) && str_starts_with($filter['value'], '@')) {
354361
$columnId = (int)($filter['columnId'] ?? 0);
355362
$column = $columnId > 0 ? $this->columnMapper->find($columnId) : null;
356363
$filter['value'] = $this->columnsHelper->resolveSearchValue($filter['value'], $userId, $column);
@@ -593,14 +600,14 @@ private function getFilterExpression(IQueryBuilder $qb, Column $column, string $
593600
/**
594601
* @throws InternalError
595602
*/
596-
private function getMetaFilterExpression(IQueryBuilder $qb, int $columnId, string $operator, string $value): IQueryBuilder {
603+
private function getMetaFilterExpression(IQueryBuilder $qb, int $columnId, string $operator, string|array $value): IQueryBuilder {
597604
$qb2 = $this->db->getQueryBuilder();
598605
$qb2->select('id');
599606
$qb2->from('tables_row_sleeves');
600607

601608
switch ($columnId) {
602609
case Column::TYPE_META_ID:
603-
$qb2->where($this->getSqlOperator($operator, $qb, 'id', (int)$value, IQueryBuilder::PARAM_INT));
610+
$qb2->where($this->getSqlOperator($operator, $qb, 'id', (array)$value, IQueryBuilder::PARAM_INT_ARRAY));
604611
break;
605612
case Column::TYPE_META_CREATED_BY:
606613
$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
638645
case 'does-not-contain':
639646
return $qb->expr()->notLike($columnName, $qb->createNamedParameter('%' . $this->db->escapeLikeParameter($value) . '%', $paramType));
640647
case 'is-equal':
648+
if (is_array($value)) {
649+
return $qb->expr()->in($columnName, $qb->createNamedParameter($value, $paramType));
650+
}
641651
return $qb->expr()->eq($columnName, $qb->createNamedParameter($value, $paramType));
642652
case 'is-not-equal':
643653
return $qb->expr()->neq($columnName, $qb->createNamedParameter($value, $paramType));

lib/Service/RowService.php

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,12 @@ public function formatRowsForPublicShare(array $rows): array {
9090
* @param string $userId
9191
* @param ?int $limit
9292
* @param ?int $offset
93+
* @param array $customFilters
9394
* @return Row2[]
9495
* @throws InternalError
9596
* @throws PermissionError
9697
*/
97-
public function findAllByTable(int $tableId, string $userId, ?int $limit = null, ?int $offset = null): array {
98+
public function findAllByTable(int $tableId, string $userId, ?int $limit = null, ?int $offset = null, array $customFilters = []): array {
9899
try {
99100
if ($this->permissionsService->canReadRowsByElementId($tableId, 'table', $userId)) {
100101
$tableColumns = $this->columnMapper->findAllByTable($tableId);
@@ -103,7 +104,7 @@ public function findAllByTable(int $tableId, string $userId, ?int $limit = null,
103104
$table = $this->tableMapper->find($tableId);
104105
$sort = $table->getSortArray() ?: null;
105106

106-
$rows = $this->row2Mapper->findAll($showColumnIds, $tableId, $limit, $offset, null, $sort, $userId);
107+
$rows = $this->row2Mapper->findAll($showColumnIds, $tableId, $limit, $offset, null, $sort, $userId, $customFilters);
107108
$this->attachAliasPayloads($rows, $tableColumns);
108109
return $rows;
109110
} else {
@@ -120,13 +121,14 @@ public function findAllByTable(int $tableId, string $userId, ?int $limit = null,
120121
* @param string $userId
121122
* @param int|null $limit
122123
* @param int|null $offset
124+
* @param array $customFilters
123125
* @return Row2[]
124126
* @throws DoesNotExistException
125127
* @throws InternalError
126128
* @throws MultipleObjectsReturnedException
127129
* @throws PermissionError
128130
*/
129-
public function findAllByView(int $viewId, string $userId, ?int $limit = null, ?int $offset = null): array {
131+
public function findAllByView(int $viewId, string $userId, ?int $limit = null, ?int $offset = null, array $customFilters = []): array {
130132
try {
131133
if ($this->permissionsService->canReadRowsByElementId($viewId, 'view', $userId)) {
132134
$view = $this->viewMapper->find($viewId);
@@ -139,6 +141,7 @@ public function findAllByView(int $viewId, string $userId, ?int $limit = null, ?
139141
$view->getFilterArray(),
140142
$view->getSortArray(),
141143
$this->resolveFilterUserId($userId, $view),
144+
$customFilters,
142145
);
143146

144147
$viewColumns = $this->columnMapper->findAll($view->getColumnIds());

src/modules/main/sections/ElementTitle.vue

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,12 +74,28 @@ export default {
7474
},
7575
7676
isViewSettingSet() {
77+
if (this.$route.query.customFilters?.length > 0) {
78+
return true
79+
}
80+
7781
return !(!this.viewSetting || ((!this.viewSetting.hiddenColumns || this.viewSetting.hiddenColumns.length === 0) && (!this.viewSetting.sorting) && (!this.viewSetting.filter || this.viewSetting.filter.length === 0)))
7882
},
7983
},
8084
8185
methods: {
8286
resetLocalAdjustments() {
87+
if (this.$route.query.customFilters?.length > 0) {
88+
this.$router.push({
89+
path: this.$route.path,
90+
query: {
91+
...this.$route.query,
92+
customFilters: undefined,
93+
},
94+
})
95+
96+
return
97+
}
98+
8399
this.$emit('update:viewSetting', {})
84100
},
85101
},

src/modules/main/sections/MainWrapper.vue

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@ export default {
8686
8787
computed: {
8888
...mapState(useTablesStore, ['activeRowId']),
89+
customFiltered() {
90+
return this.$route.query.customFilters
91+
},
8992
},
9093
9194
watch: {
@@ -95,6 +98,9 @@ export default {
9598
activeRowId() {
9699
this.reload()
97100
},
101+
customFiltered() {
102+
this.reload(true)
103+
},
98104
},
99105
100106
beforeMount() {
@@ -183,6 +189,7 @@ export default {
183189
await this.loadRowsFromBE({
184190
viewId: this.isView ? this.element.id : null,
185191
tableId: !this.isView ? this.element.id : null,
192+
customFilters: this.$route.query.customFilters,
186193
})
187194
} else {
188195
await this.removeRows({

src/shared/components/ncTable/sections/Options.vue

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@
4444
</template>
4545
{{ t('tables', 'Export filtered rows') }}
4646
</NcActionButton>
47+
<NcActionButton close-after-click @click="shareRows">
48+
<template #icon>
49+
<ShareVariantOutline :size="20" />
50+
</template>
51+
{{ t('tables', 'Share rows') }}
52+
</NcActionButton>
4753
<NcActionButton v-if="config.canDeleteRows" close-after-click @click="deleteSelectedRows">
4854
<template #icon>
4955
<Delete :size="20" />
@@ -70,10 +76,13 @@ import Plus from 'vue-material-design-icons/Plus.vue'
7076
import Check from 'vue-material-design-icons/CheckboxBlankOutline.vue'
7177
import Delete from 'vue-material-design-icons/TrashCanOutline.vue'
7278
import TrayArrowDown from 'vue-material-design-icons/TrayArrowDown.vue'
79+
import ShareVariantOutline from 'vue-material-design-icons/ShareVariantOutline.vue'
7380
import viewportHelper from '../../../mixins/viewportHelper.js'
81+
import copyToClipboard from '../../../mixins/copyToClipboard.js'
7482
import SearchForm from '../partials/SearchForm.vue'
7583
import PaginationBlock from './PaginationBlock.vue'
7684
import { translate as t, translatePlural as n } from '@nextcloud/l10n'
85+
import { TYPE_META_ID } from '../../../constants.ts'
7786
7887
export default {
7988
name: 'Options',
@@ -87,10 +96,11 @@ export default {
8796
Check,
8897
Delete,
8998
TrayArrowDown,
99+
ShareVariantOutline,
90100
PaginationBlock,
91101
},
92102
93-
mixins: [viewportHelper],
103+
mixins: [viewportHelper, copyToClipboard],
94104
95105
props: {
96106
selectedRows: {
@@ -193,6 +203,23 @@ export default {
193203
deselectAllRows() {
194204
emit('tables:selected-rows:deselect', { elementId: this.elementId, isView: this.isView })
195205
},
206+
async shareRows() {
207+
await this.$router.push({
208+
path: this.$route.path,
209+
query: {
210+
...this.$route.query,
211+
customFilters: JSON.stringify([[
212+
{
213+
columnId: TYPE_META_ID,
214+
operator: 'is-equal',
215+
value: this.selectedRows,
216+
},
217+
]]),
218+
},
219+
})
220+
221+
this.copyToClipboard(document.location.href, false)
222+
},
196223
},
197224
}
198225
</script>

src/store/data.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -233,16 +233,16 @@ export const useDataStore = defineStore('data', {
233233
},
234234

235235
// ROWS
236-
async loadRowsFromBE({ tableId, viewId }) {
236+
async loadRowsFromBE({ tableId, viewId, customFilters = null }) {
237237
const stateId = genStateKey(!!(viewId), viewId ?? tableId)
238238
this.loading[stateId] = true
239239
let res = null
240240

241241
try {
242242
if (viewId) {
243-
res = await axios.get(generateUrl('/apps/tables/row/view/' + viewId))
243+
res = await axios.get(generateUrl('/apps/tables/row/view/' + viewId), { params: { customFilters } })
244244
} else {
245-
res = await axios.get(generateUrl('/apps/tables/row/table/' + tableId))
245+
res = await axios.get(generateUrl('/apps/tables/row/table/' + tableId), { params: { customFilters } })
246246
}
247247
} catch (e) {
248248
displayError(e, t('tables', 'Could not load rows.'))

tests/integration/features/APIv2.feature

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -561,8 +561,8 @@ Feature: APIv2
561561
| t2 | table | read |
562562
And user "participant1-v2" shares the Context "c1" to "user" "participant2-v2" with permissions "read,create,update,delete,manage"
563563
When user "participant2-v2" transfers the Context "c1" to "participant3-v2"
564-
Then the reported status is "403"
565-
564+
Then the reported status is "403"
565+
566566
@api2 @contexts @contexts-ownership
567567
Scenario: Transfer an inaccessible context
568568
Given table "Table 1 via api v2" with emoji "👋" exists for user "participant1-v2" as "t1" via v2
@@ -584,6 +584,30 @@ Feature: APIv2
584584
Then the reported status is "404"
585585

586586

587+
@api2 @rows @custom-filters
588+
Scenario: Fetch rows with customFilters parameter containing array values
589+
Given table "Tasks" with emoji "📋" exists for user "participant1-v2" as "tasks" via v2
590+
Then column "status" exists with following properties
591+
| type | selection |
592+
| mandatory | 1 |
593+
And column "title" exists with following properties
594+
| type | text |
595+
| subtype | line |
596+
| mandatory | 1 |
597+
And row exists with following values
598+
| status | Task A |
599+
And row exists with following values
600+
| status | Task B |
601+
And row exists with following values
602+
| status | Task C |
603+
When user "participant1-v2" fetches rows for table "tasks" with customFilters containing array values
604+
Then the reported status is "200"
605+
And the response contains at least the following rows
606+
| title |
607+
| Task A |
608+
| Task B |
609+
| Task C |
610+
587611
@api1 @rows
588612
Scenario: Create and modify usergroup row via v1
589613
Given table "Usergroup row check" with emoji "👋" exists for user "participant1-v2" as "base1" via v2

0 commit comments

Comments
 (0)