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: 1 addition & 1 deletion appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Share your tables and views with users and groups within your cloud.
Have a good time and manage whatever you want.

]]></description>
<version>2.2.0</version>
<version>2.2.1</version>
<licence>AGPL-3.0-or-later</licence>
<author>Nextcloud GmbH and Nextcloud contributors</author>
<namespace>Tables</namespace>
Expand Down
1 change: 1 addition & 0 deletions lib/Constants/ViewUpdatableParameters.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ enum ViewUpdatableParameters: string {
case SORT = 'sort';
case FILTER = 'filter';
case COLUMN_SETTINGS = 'columns';
case TECHNICAL_NAME = 'technicalName';
}
4 changes: 4 additions & 0 deletions lib/Db/View.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
* @method setId(int $id)
* @method getTitle(): string
* @method setTitle(string $title)
* @method getTechnicalName(): string
* @method setTechnicalName(?string $technicalName)
* @method getTableId(): int
* @method setTableId(int $tableId)
* @method getColumns(): string
Expand Down Expand Up @@ -64,6 +66,7 @@
*/
class View extends EntitySuper implements JsonSerializable {
protected ?string $title = null;
protected ?string $technicalName = null;
protected ?int $tableId = null;
protected ?string $createdBy = null;
protected ?string $createdAt = null;
Expand Down Expand Up @@ -183,6 +186,7 @@ public function jsonSerialize(): array {
'id' => $this->id,
'tableId' => ($this->tableId || $this->tableId === 0) ? $this->tableId : -1,
'title' => $this->title ?: '',
'technicalName' => $this->technicalName,
'description' => $this->description,
'emoji' => $this->emoji,
'ownership' => $this->ownership ?: '',
Expand Down
84 changes: 84 additions & 0 deletions lib/Migration/Version2011Date20260428000000.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?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\QueryBuilder\IQueryBuilder;
use OCP\DB\Types;
use OCP\IDBConnection;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
use Override;

class Version2011Date20260428000000 extends SimpleMigrationStep {
private IDBConnection $connection;

public function __construct(IDBConnection $connection) {
$this->connection = $connection;
}

#[Override]
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();

if (!$schema->hasTable('tables_views')) {
return null;
}

$table = $schema->getTable('tables_views');
if (!$table->hasColumn('technical_name')) {
$table->addColumn('technical_name', Types::STRING, [
'notnull' => false,
'length' => 200,
]);
}

if (!$table->hasIndex('tables_views_table_tech_name_uq')) {
$table->addUniqueIndex(['table_id', 'technical_name'], 'tables_views_table_tech_name_uq');
}

return $schema;
}

#[Override]
public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
$selectQb = $this->connection->getQueryBuilder();
$selectQb->select('id')
->from('tables_views')
->where(
$selectQb->expr()->orX(
$selectQb->expr()->isNull('technical_name'),
$selectQb->expr()->eq('technical_name', $selectQb->createNamedParameter('')),
)
);

$result = $selectQb->executeQuery();
$updatedCount = 0;

try {
while ($row = $result->fetchAssociative()) {
$viewId = (int)$row['id'];

$updateQb = $this->connection->getQueryBuilder();
$updateQb->update('tables_views')
->set('technical_name', $updateQb->createNamedParameter('view_' . $viewId, IQueryBuilder::PARAM_STR))
->where($updateQb->expr()->eq('id', $updateQb->createNamedParameter($viewId, IQueryBuilder::PARAM_INT)));

$updatedCount += $updateQb->executeStatement();
}
} finally {
$result->closeCursor();
}

$output->info('Version2011Date20260428000000: backfilled technical_name for ' . $updatedCount . ' views.');
}
}
6 changes: 6 additions & 0 deletions lib/Model/ViewUpdateInput.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class ViewUpdateInput {

public function __construct(
protected readonly ?Title $title = null,
protected readonly ?string $technicalName = null,
protected readonly ?string $description = null,
protected readonly ?Emoji $emoji = null,
protected readonly ?ColumnSettings $columnSettings = null,
Expand All @@ -36,6 +37,9 @@ public function updateDetail(): Generator {
if ($this->title) {
yield ViewUpdatableParameters::TITLE => $this->title;
}
if ($this->technicalName !== null) {
yield ViewUpdatableParameters::TECHNICAL_NAME => $this->technicalName;
}
if ($this->description) {
yield ViewUpdatableParameters::DESCRIPTION => $this->description;
}
Expand All @@ -58,6 +62,7 @@ public function updateDetail(): Generator {
* title?: string,
* emoji?: string,
* description?: string,
* technicalName?: string,
* columns?: list<int>,
* columnSettings?: list<array{columnId?: int, order?: int, readonly?: bool, mandatory?: bool}>,
* sort?: list<array{columnId: int, mode: 'ASC'|'DESC'}>,
Expand All @@ -82,6 +87,7 @@ public static function fromInputArray(array $data): self {

return new self(
title: ($data['title'] ?? null) ? new Title($data['title']) : null,
technicalName: $data['technicalName'] ?? null,
description: $data['description'] ?? null,
emoji: ($data['emoji'] ?? null) ? new Emoji($data['emoji']) : null,
columnSettings: ($data['columnSettings'] ?? null) ? ColumnSettings::createViewSettingsFromInputArray($data['columnSettings']) : null,
Expand Down
1 change: 1 addition & 0 deletions lib/ResponseDefinitions.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
* @psalm-type TablesView = array{
* id: int,
* title: string,
* technicalName: string|null,
* emoji: string|null,
* tableId: int,
* ownership: string,
Expand Down
53 changes: 53 additions & 0 deletions lib/Service/ViewService.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use OCA\Tables\Db\Table;
use OCA\Tables\Db\View;
use OCA\Tables\Db\ViewMapper;
use OCA\Tables\Errors\BadRequestError;
use OCA\Tables\Errors\InternalError;
use OCA\Tables\Errors\NotFoundError;
use OCA\Tables\Errors\PermissionError;
Expand Down Expand Up @@ -213,6 +214,9 @@ public function create(string $title, ?string $emoji, Table $table, ?string $use
$item->setLastEditBy($userId);
$item->setCreatedAt($time->format('Y-m-d H:i:s'));
$item->setLastEditAt($time->format('Y-m-d H:i:s'));
if ($item->getTechnicalName() !== null) {
$this->assertTechnicalNameUnique($table->getId(), $item->getTechnicalName());
}
// ownership is not stored with the record, but it might be necessary upon
// further interaction with the view in the running process, as the instance
// is cached now. The ownership is always inherited from the table.
Expand All @@ -223,6 +227,15 @@ public function create(string $title, ?string $emoji, Table $table, ?string $use
$this->logger->error($e->getMessage());
throw new InternalError($e->getMessage());
}
if ($newItem->getTechnicalName() === null || $newItem->getTechnicalName() === '') {
$newItem->setTechnicalName($this->buildDefaultTechnicalName($newItem->getId()));
try {
$newItem = $this->mapper->update($newItem);
} catch (\OCP\DB\Exception $e) {
$this->logger->error($e->getMessage());
throw new InternalError($e->getMessage());
}
}

return $newItem;
}
Expand Down Expand Up @@ -250,6 +263,10 @@ public function update(int $id, ViewUpdateInput $data, ?string $userId = null, b
$this->assertInputColumnsAreValid($view, $userId, $value);
}

if ($parameter === ViewUpdatableParameters::TECHNICAL_NAME) {
$this->assertTechnicalNameUnique($view->getTableId(), $value, $view->getId());
}

if ($value instanceof JsonSerializable) {
$insertableValue = json_encode($value);
}
Expand All @@ -266,6 +283,8 @@ public function update(int $id, ViewUpdateInput $data, ?string $userId = null, b
$this->enhanceView($view, $userId);
}
return $view;
} catch (BadRequestError $e) {
throw $e;
} catch (InvalidArgumentException $e) {
throw $e;
} catch (Exception $e) {
Expand Down Expand Up @@ -603,6 +622,7 @@ public function importView(int $tableId, array $view, string $userId): void {
$item->setTableId($tableId);
$item->setTitle($view['title']);
$item->setEmoji($view['emoji']);
$item->setTechnicalName($view['technicalName'] ?? null);
$item->setCreatedBy($userId);
$item->setCreatedAt($view['createdAt']);
$item->setLastEditBy($userId);
Expand All @@ -613,9 +633,42 @@ public function importView(int $tableId, array $view, string $userId): void {
$item->setFilter(json_encode($view['filter']));
try {
$this->mapper->insert($item);
if ($item->getTechnicalName() === null || $item->getTechnicalName() === '') {
$item->setTechnicalName($this->buildDefaultTechnicalName($item->getId()));
$this->mapper->update($item);
}
} catch (BadRequestError $e) {
throw $e;
} catch (\Exception $e) {
$this->logger->error('userMigrationImport insert error: ' . $e->getMessage());
throw new InternalError('userMigrationImport insert error: ' . $e->getMessage());
}
}

private function buildDefaultTechnicalName(int $viewId): string {
return 'view_' . $viewId;
}

/**
* @throws BadRequestError
* @throws InternalError
*/
private function assertTechnicalNameUnique(int $tableId, string $technicalName, ?int $excludeCurrentViewId = null): void {
try {
$views = $this->mapper->findAll($tableId);
} catch (\OCP\DB\Exception $e) {
$this->logger->error($e->getMessage(), ['exception' => $e]);
throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage());
}

foreach ($views as $view) {
if ($excludeCurrentViewId !== null && $view->getId() === $excludeCurrentViewId) {
continue;
}

if ($view->getTechnicalName() === $technicalName) {
throw new BadRequestError('Technical name must be unique in the table.');
}
}
}
}
5 changes: 5 additions & 0 deletions openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -899,6 +899,7 @@
"required": [
"id",
"title",
"technicalName",
"emoji",
"tableId",
"ownership",
Expand Down Expand Up @@ -926,6 +927,10 @@
"title": {
"type": "string"
},
"technicalName": {
"type": "string",
"nullable": true
},
"emoji": {
"type": "string",
"nullable": true
Expand Down
Loading
Loading