Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions lib/Controller/RowController.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,29 @@ public function __construct(
parent::__construct(Application::APP_ID, $request);
}

/**
* @param int $tableId ID of the table
* @param string $customFilters JSON-encoded array of filter groups to apply
*/
#[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);
});
}

/**
* @param int $viewId ID of the view
* @param string $customFilters JSON-encoded array of filter groups to apply
*/
#[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);
});
}

Expand Down
8 changes: 6 additions & 2 deletions lib/Db/ColumnMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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')];
Expand All @@ -200,7 +200,11 @@ public function preloadColumns(array $columns, ?array $filters = null, ?array $s
array_push($columnIds, ...array_column($filterGroup, 'columnId'));
}
}

if (count($customFilters) > 0) {
foreach ($customFilters as $filterGroup) {
array_push($columnIds, ...array_column($filterGroup, 'columnId'));
}
}
$this->findAll(array_unique($columnIds));
}

Expand Down
44 changes: 27 additions & 17 deletions lib/Db/Row2Mapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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));
}
}

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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));
Expand Down
9 changes: 6 additions & 3 deletions lib/Service/RowService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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 {
Expand All @@ -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);
Expand All @@ -139,6 +141,7 @@ public function findAllByView(int $viewId, string $userId, ?int $limit = null, ?
$view->getFilterArray(),
$view->getSortArray(),
$this->resolveFilterUserId($userId, $view),
$customFilters,
);

} else {
Expand Down
16 changes: 16 additions & 0 deletions src/modules/main/sections/ElementTitle.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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', {})
},
},
Expand Down
7 changes: 7 additions & 0 deletions src/modules/main/sections/MainWrapper.vue
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ export default {

computed: {
...mapState(useTablesStore, ['activeRowId']),
customFiltered() {
return this.$route.query.customFilters
},
},

watch: {
Expand All @@ -95,6 +98,9 @@ export default {
activeRowId() {
this.reload()
},
customFiltered() {
this.reload(true)
},
},

beforeMount() {
Expand Down Expand Up @@ -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({
Expand Down
29 changes: 28 additions & 1 deletion src/shared/components/ncTable/sections/Options.vue
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@
</template>
{{ t('tables', 'Export filtered rows') }}
</NcActionButton>
<NcActionButton close-after-click @click="shareRows">
<template #icon>
<ShareVariantOutline :size="20" />
</template>
{{ t('tables', 'Share rows') }}
</NcActionButton>
<NcActionButton v-if="config.canDeleteRows" close-after-click @click="deleteSelectedRows">
<template #icon>
<Delete :size="20" />
Expand All @@ -70,10 +76,13 @@ import Plus from 'vue-material-design-icons/Plus.vue'
import Check from 'vue-material-design-icons/CheckboxBlankOutline.vue'
import Delete from 'vue-material-design-icons/TrashCanOutline.vue'
import TrayArrowDown from 'vue-material-design-icons/TrayArrowDown.vue'
import ShareVariantOutline from 'vue-material-design-icons/ShareVariantOutline.vue'
import viewportHelper from '../../../mixins/viewportHelper.js'
import copyToClipboard from '../../../mixins/copyToClipboard.js'
import SearchForm from '../partials/SearchForm.vue'
import PaginationBlock from './PaginationBlock.vue'
import { translate as t, translatePlural as n } from '@nextcloud/l10n'
import { TYPE_META_ID } from '../../../constants.ts'

export default {
name: 'Options',
Expand All @@ -87,10 +96,11 @@ export default {
Check,
Delete,
TrayArrowDown,
ShareVariantOutline,
PaginationBlock,
},

mixins: [viewportHelper],
mixins: [viewportHelper, copyToClipboard],

props: {
selectedRows: {
Expand Down Expand Up @@ -193,6 +203,23 @@ export default {
deselectAllRows() {
emit('tables:selected-rows:deselect', { elementId: this.elementId, isView: this.isView })
},
async shareRows() {
await this.$router.push({
path: this.$route.path,
query: {
...this.$route.query,
customFilters: JSON.stringify([[

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TBH I don't like that we're sending potentially large json string to the server as a query parameter. Any better ideas for that?

a. use POST + body
b. use QUERY + body
c. ...?

{
columnId: TYPE_META_ID,
operator: 'is-equal',
value: this.selectedRows,
},
]]),
},
})

this.copyToClipboard(document.location.href, false)
},
},
}
</script>
Expand Down
6 changes: 3 additions & 3 deletions src/store/data.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.'))
Expand Down
28 changes: 26 additions & 2 deletions tests/integration/features/APIv2.feature
Original file line number Diff line number Diff line change
Expand Up @@ -561,8 +561,8 @@ Feature: APIv2
| t2 | table | read |
And user "participant1-v2" shares the Context "c1" to "user" "participant2-v2" with permissions "read,create,update,delete,manage"
When user "participant2-v2" transfers the Context "c1" to "participant3-v2"
Then the reported status is "403"
Then the reported status is "403"

@api2 @contexts @contexts-ownership
Scenario: Transfer an inaccessible context
Given table "Table 1 via api v2" with emoji "👋" exists for user "participant1-v2" as "t1" via v2
Expand All @@ -584,6 +584,30 @@ Feature: APIv2
Then the reported status is "404"


@api2 @rows @custom-filters
Scenario: Fetch rows with customFilters parameter containing array values
Given table "Tasks" with emoji "📋" exists for user "participant1-v2" as "tasks" via v2
Then column "status" exists with following properties
| type | selection |
| mandatory | 1 |
And column "title" exists with following properties
| type | text |
| subtype | line |
| mandatory | 1 |
And row exists with following values
| status | Task A |
And row exists with following values
| status | Task B |
And row exists with following values
| status | Task C |
When user "participant1-v2" fetches rows for table "tasks" with customFilters containing array values
Then the reported status is "200"
And the response contains at least the following rows
| title |
| Task A |
| Task B |
| Task C |

@api1 @rows
Scenario: Create and modify usergroup row via v1
Given table "Usergroup row check" with emoji "👋" exists for user "participant1-v2" as "base1" via v2
Expand Down
Loading
Loading