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
2 changes: 2 additions & 0 deletions lib/Constants/ViewUpdatableParameters.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ enum ViewUpdatableParameters: string {
case SORT = 'sort';
case FILTER = 'filter';
case COLUMN_SETTINGS = 'columns';
case LAYOUT = 'layout';
case VIEW_SETTINGS = 'viewSettings';
}
7 changes: 5 additions & 2 deletions lib/Controller/Api1Controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,7 @@ public function indexViews(int $tableId): DataResponse {
* @param int $tableId Table ID that will hold the view
* @param string $title Title for the view
* @param string|null $emoji Emoji for the view
* @param string|null $layout Layout for the view with 'table', 'tiles', 'gallery' or null
*
* @return DataResponse<Http::STATUS_OK, TablesView, array{}>|DataResponse<Http::STATUS_FORBIDDEN|Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}>
*
Expand All @@ -360,9 +361,9 @@ public function indexViews(int $tableId): DataResponse {
#[CORS]
#[RequirePermission(permission: Application::PERMISSION_MANAGE, type: Application::NODE_TYPE_TABLE, idParam: 'tableId')]
#[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
public function createView(int $tableId, string $title, ?string $emoji): DataResponse {
public function createView(int $tableId, string $title, ?string $emoji, ?string $layout = null): DataResponse {
try {
return new DataResponse($this->viewService->create($title, $emoji, $this->tableService->find($tableId))->jsonSerialize());
return new DataResponse($this->viewService->create($title, $emoji, $this->tableService->find($tableId), null, $layout)->jsonSerialize());
} catch (PermissionError $e) {
$this->logger->warning('A permission error occurred: ' . $e->getMessage(), ['exception' => $e]);
$message = ['message' => $e->getMessage()];
Expand Down Expand Up @@ -418,6 +419,8 @@ public function getView(int $viewId): DataResponse {
* columns?: list<int>,
* columnSettings?: list<array{columnId?: int, order?: int, readonly?: bool, mandatory?: bool}>,
* sort?: list<array{columnId: int, mode: 'ASC'|'DESC'}>,
* layout?: 'table'|'tiles'|'gallery'|null,
* viewSettings?: array{cardBackgroundSource?: int|null, cardTitleSource?: int|null},
* filter?: list<list<array{columnId: int, operator: 'begins-with'|'ends-with'|'contains'|'does-not-contain'|'is-equal'|'is-not-equal'|'is-greater-than'|'is-greater-than-or-equal'|'is-lower-than'|'is-lower-than-or-equal'|'is-empty', value: string|int|float}>>
* } $data fields of the view with their new values
* @return DataResponse<Http::STATUS_OK, TablesView, array{}>|DataResponse<Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND|Http::STATUS_BAD_REQUEST|Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}>
Expand Down
27 changes: 21 additions & 6 deletions lib/Controller/ApiTablesController.php
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ public function createFromScheme(string $title, string $emoji, string $descripti
$view['emoji'],
$table,
$this->userId,
$view['layout'] ?? null,
);

$inputColumnsArray = [];
Expand Down Expand Up @@ -247,12 +248,16 @@ public function createFromScheme(string $title, string $emoji, string $descripti
}, $filters);
}, $view['filter']);

$this->viewService->update($newView->getId(), ViewUpdateInput::fromInputArray(
array_merge($inputColumnsArray, [
'sort' => $newSort,
'filter' => $newFilter,
])
));
$inputData = array_merge($inputColumnsArray, [
'sort' => $newSort,
'filter' => $newFilter,
'layout' => $view['layout'] ?? null,
]);
if (isset($view['viewSettings']) && is_array($view['viewSettings'])) {
$inputData['viewSettings'] = $this->remapViewSettings($view['viewSettings'], $colMap);
}

$this->viewService->update($newView->getId(), ViewUpdateInput::fromInputArray($inputData));
}
$this->db->commit();
return new DataResponse($table->jsonSerialize());
Expand Down Expand Up @@ -281,6 +286,16 @@ public function createFromScheme(string $title, string $emoji, string $descripti
}
}

private function remapViewSettings(array $viewSettings, array $colMap): array {
foreach (['cardBackgroundSource', 'cardTitleSource'] as $sourceKey) {
if (isset($viewSettings[$sourceKey]) && is_int($viewSettings[$sourceKey]) && $viewSettings[$sourceKey] > 0) {
$viewSettings[$sourceKey] = $colMap[$viewSettings[$sourceKey]] ?? $viewSettings[$sourceKey];
}
}

return $viewSettings;
}

/**
* [api v2] Create a new table and return it
*
Expand Down
6 changes: 3 additions & 3 deletions lib/Controller/ViewController.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,9 @@ public function show(int $id): DataResponse {

#[NoAdminRequired]
#[RequirePermission(permission: Application::PERMISSION_MANAGE, type: Application::NODE_TYPE_TABLE, idParam: 'tableId')]
public function create(int $tableId, string $title, ?string $emoji): DataResponse {
return $this->handleError(function () use ($tableId, $title, $emoji) {
return $this->service->create($title, $emoji, $this->getTable($tableId, true));
public function create(int $tableId, string $title, ?string $emoji, ?string $layout = null): DataResponse {
return $this->handleError(function () use ($tableId, $title, $emoji, $layout) {
return $this->service->create($title, $emoji, $this->getTable($tableId, true), null, $layout);
});
}

Expand Down
17 changes: 17 additions & 0 deletions lib/Db/View.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use OCA\Tables\Model\FilterSet;
use OCA\Tables\Model\Permissions;
use OCA\Tables\Model\SortRuleSet;
use OCA\Tables\Model\ViewSettings;
use OCA\Tables\ResponseDefinitions;
use OCA\Tables\Service\ValueObject\ViewColumnInformation;

Expand Down Expand Up @@ -45,6 +46,10 @@
* @method setEmoji(string $emoji)
* @method getDescription(): string
* @method setDescription(string $description)
* @method getLayout(): ?string
* @method setLayout(?string $layout)
* @method getViewSettings(): ?string
* @method setViewSettings(?string $viewSettings)
* @method getIsShared(): bool
* @method setIsShared(bool $isShared)
* @method getOnSharePermissions(): ?Permissions
Expand Down Expand Up @@ -74,6 +79,8 @@ class View extends EntitySuper implements JsonSerializable {
protected ?string $columns = null; // json
protected ?string $sort = null; // json
protected ?string $filter = null; // json
protected ?string $layout = null;
protected ?string $viewSettings = null; // json

// virtual properties
protected ?bool $isShared = null;
Expand Down Expand Up @@ -171,6 +178,14 @@ public function setFilterArray(array $array):void {
$this->setFilter(\json_encode($array));
}

public function getLayoutNormalized(): string {
return in_array($this->layout, ['tiles', 'gallery'], true) ? $this->layout : 'table';
}

public function getViewSettingsObject(): ViewSettings {
return ViewSettings::createFromInputArray($this->getArray($this->getViewSettings()));
}

private function getSharePermissions(): ?Permissions {
return $this->getOnSharePermissions();
}
Expand Down Expand Up @@ -199,6 +214,8 @@ public function jsonSerialize(): array {
'hasShares' => (bool)$this->hasShares,
'rowsCount' => $this->rowsCount ?: 0,
'ownerDisplayName' => $this->ownerDisplayName,
'layout' => $this->getLayoutNormalized(),
'viewSettings' => $this->getViewSettingsObject()->jsonSerialize(),
];
$serialisedJson['filter'] = $this->getFilterArray();

Expand Down
45 changes: 45 additions & 0 deletions lib/Migration/Version1000Date20260318000000.php

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ideally renamed to later version, since new migration files have been added since this was started

Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Tables\Migration;

use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\DB\Types;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
use Override;

class Version1000Date20260318000000 extends SimpleMigrationStep {

#[Override]
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
$tableName = 'tables_views';
if (!$schema->hasTable($tableName)) {
return null;
}

$table = $schema->getTable($tableName);
if (!$table->hasColumn('layout')) {
$table->addColumn('layout', Types::STRING, [
'notnull' => false,
'length' => 16,
]);
}
if (!$table->hasColumn('view_settings')) {
$table->addColumn('view_settings', \Doctrine\DBAL\Types\Types::JSON, [
'notnull' => false,
]);
}
Comment thread
Rello marked this conversation as resolved.

return $schema;
}
}
61 changes: 61 additions & 0 deletions lib/Model/ViewSettings.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Tables\Model;

use InvalidArgumentException;
use JsonSerializable;

class ViewSettings implements JsonSerializable {
public function __construct(
protected readonly ?int $cardBackgroundSource = null,
protected readonly ?int $cardTitleSource = null,
) {
}

/**
* @param array{cardBackgroundSource?: int|null, cardTitleSource?: int|null} $data
*/
public static function createFromInputArray(array $data): self {
return new self(
cardBackgroundSource: self::nullableIntFromArray($data, 'cardBackgroundSource'),
cardTitleSource: self::nullableIntFromArray($data, 'cardTitleSource'),
);
}

public function getCardBackgroundSource(): ?int {
return $this->cardBackgroundSource;
}

public function getCardTitleSource(): ?int {
return $this->cardTitleSource;
}

/**
* @return array{cardBackgroundSource: int|null, cardTitleSource: int|null}
*/
public function jsonSerialize(): array {
return [
'cardBackgroundSource' => $this->cardBackgroundSource,
'cardTitleSource' => $this->cardTitleSource,
];
}

private static function nullableIntFromArray(array $data, string $key): ?int {
if (!array_key_exists($key, $data) || $data[$key] === null) {
return null;
}

if (!is_int($data[$key])) {
throw new InvalidArgumentException('Invalid ' . $key . ' value.');
}

return $data[$key];
}
}
62 changes: 61 additions & 1 deletion lib/Model/ViewUpdateInput.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ public function __construct(
protected readonly ?ColumnSettings $columnSettings = null,
protected readonly ?FilterSet $filterSet = null,
protected readonly ?SortRuleSet $sortRuleSet = null,
protected readonly ?string $layout = null,
protected readonly ?ViewSettings $viewSettings = null,
) {
}

Expand All @@ -51,6 +53,12 @@ public function updateDetail(): Generator {
if ($this->sortRuleSet) {
yield ViewUpdatableParameters::SORT => $this->sortRuleSet;
}
if ($this->layout !== null) {
yield ViewUpdatableParameters::LAYOUT => $this->layout;
}
if ($this->viewSettings !== null) {
yield ViewUpdatableParameters::VIEW_SETTINGS => $this->viewSettings;
}
}

/**
Expand All @@ -61,11 +69,13 @@ public function updateDetail(): Generator {
* columns?: list<int>,
* columnSettings?: list<array{columnId?: int, order?: int, readonly?: bool, mandatory?: bool}>,
* sort?: list<array{columnId: int, mode: 'ASC'|'DESC'}>,
* layout?: 'table'|'tiles'|'gallery'|null,
* viewSettings?: array{cardBackgroundSource?: int|null, cardTitleSource?: int|null}|string,
* filter?: list<list<array{columnId: int, operator: 'begins-with'|'ends-with'|'contains'|'does-not-contain'|'is-equal'|'is-not-equal'|'is-greater-than'|'is-greater-than-or-equal'|'is-lower-than'|'is-lower-than-or-equal'|'is-empty', value: string|int|float}>>
* } $data
*/
public static function fromInputArray(array $data): self {
$data = self::transformJsonToArrayInPayload($data, ['columnSettings', 'filter', 'sort']);
$data = self::transformJsonToArrayInPayload($data, ['columnSettings', 'filter', 'sort', 'viewSettings']);

if (isset($data['columns']) && !isset($data['columnSettings'])) {
$logger = Server::get(LoggerInterface::class);
Expand All @@ -80,16 +90,66 @@ public static function fromInputArray(array $data): self {
$data['columnSettings'] = $value;
}

$layout = self::normalizeLayout($data['layout'] ?? null);
$viewSettings = self::createViewSettingsFromInputData($data);

return new self(
title: ($data['title'] ?? null) ? new Title($data['title']) : null,
description: $data['description'] ?? null,
emoji: ($data['emoji'] ?? null) ? new Emoji($data['emoji']) : null,
columnSettings: ($data['columnSettings'] ?? null) ? ColumnSettings::createViewSettingsFromInputArray($data['columnSettings']) : null,
filterSet: ($data['filter'] ?? null) ? FilterSet::createFromInputArray($data['filter']) : null,
sortRuleSet: ($data['sort'] ?? null) ? SortRuleSet::createFromInputArray($data['sort']) : null,
layout: $layout,
viewSettings: $viewSettings,
);
}

private static function createViewSettingsFromInputData(array $data): ?ViewSettings {
if (array_key_exists('viewSettings', $data)) {
if ($data['viewSettings'] === null) {
return new ViewSettings();
}
if (!is_array($data['viewSettings'])) {
throw new \InvalidArgumentException('Invalid viewSettings value.');
}
return ViewSettings::createFromInputArray($data['viewSettings']);
}

$legacyKeys = ['cardBackgroundSource', 'cardTitleSource'];
$hasLegacySettings = false;
foreach ($legacyKeys as $legacyKey) {
if (array_key_exists($legacyKey, $data)) {
$hasLegacySettings = true;
break;
}
}
if (!$hasLegacySettings) {
return null;
}

return ViewSettings::createFromInputArray([
'cardBackgroundSource' => $data['cardBackgroundSource'] ?? null,
'cardTitleSource' => $data['cardTitleSource'] ?? null,
]);
}

private static function normalizeLayout(mixed $layout): ?string {
if ($layout === null || $layout === '') {
return null;
}

if (!is_string($layout)) {
throw new \InvalidArgumentException('Invalid layout value.');
}

if (!in_array($layout, ['table', 'tiles', 'gallery'], true)) {
throw new \InvalidArgumentException('Invalid layout value.');
}

return $layout;
}

protected static function transformJsonToArrayInPayload(array $input, array $keys): array {
$output = $input;
foreach ($keys as $targetKey) {
Expand Down
2 changes: 2 additions & 0 deletions lib/ResponseDefinitions.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
* columnSettings:list<array{columnId: int, order: int, readonly: bool}>,
* sort: list<array{columnId: int, mode: 'ASC'|'DESC'}>,
* filter: list<list<array{columnId: int, operator: 'begins-with'|'ends-with'|'contains'|'does-not-contain'|'is-equal'|'is-not-equal'|'is-greater-than'|'is-greater-than-or-equal'|'is-lower-than'|'is-lower-than-or-equal'|'is-empty', value: string|int|float}>>,
* layout: 'table'|'tiles'|'gallery',
* viewSettings: array{cardBackgroundSource: int|null, cardTitleSource: int|null},
* isShared: bool,
* favorite: bool,
* onSharePermissions: ?array{
Expand Down
2 changes: 1 addition & 1 deletion lib/Service/TableTemplateService.php
Original file line number Diff line number Diff line change
Expand Up @@ -826,7 +826,7 @@ private function createRow(Table $table, array $values): void {
private function createView(Table $table, array $data): void {
try {
$inputData = ViewUpdateInput::fromInputArray($data);
$view = $this->viewService->create($data['title'], $data['emoji'], $table);
$view = $this->viewService->create($data['title'], $data['emoji'], $table, null, $data['layout'] ?? null);
$this->viewService->update($view->getId(), $inputData);
} catch (PermissionError $e) {
$this->logger->warning('Cannot create view, permission denied: ' . $e->getMessage());
Expand Down
Loading
Loading