diff --git a/.scoper-production-dependencies b/.scoper-production-dependencies
index 9d95f3ede6..2c54d256d7 100644
--- a/.scoper-production-dependencies
+++ b/.scoper-production-dependencies
@@ -4,3 +4,5 @@ maennchen/zipstream-php
markbaker/complex
markbaker/matrix
psr/simple-cache
+symfony/uid
+symfony/polyfill-uuid
diff --git a/appinfo/info.xml b/appinfo/info.xml
index 4f05ced258..94e609d38b 100644
--- a/appinfo/info.xml
+++ b/appinfo/info.xml
@@ -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.
]]>
- 2.2.0
+ 2.2.1-dev.0
AGPL-3.0-or-later
Nextcloud GmbH and Nextcloud contributors
Tables
diff --git a/composer.json b/composer.json
index 28c2f55eca..1ca69df5ca 100644
--- a/composer.json
+++ b/composer.json
@@ -39,7 +39,7 @@
"lint": "find . -name \\*.php -not -path './vendor/*' -not -path './build/*' -print0 | xargs -0 -n1 php -l",
"cs:check": "php-cs-fixer fix --dry-run --diff",
"cs:fix": "php-cs-fixer fix",
- "psalm": "./vendor/bin/psalm.phar --show-info=false --no-cache",
+ "psalm": "@php ./vendor/bin/psalm.phar --show-info=false --no-cache",
"psalm:update-baseline": "./vendor/bin/psalm.phar --update-baseline",
"psalm:fix": "./vendor/bin/psalm.phar --no-cache --alter --issues=InvalidReturnType,InvalidNullableReturnType,MismatchingDocblockParamType,MismatchingDocblockReturnType,MissingParamType,InvalidFalsableReturnType",
"psalm:fix:dry": "./vendor/bin/psalm.phar --no-cache --alter --issues=InvalidReturnType,InvalidNullableReturnType,MismatchingDocblockParamType,MismatchingDocblockReturnType,MissingParamType,InvalidFalsableReturnType --dry-run",
@@ -65,6 +65,7 @@
"require": {
"phpoffice/phpspreadsheet": "^5.1",
"ext-json": "*",
- "bamarni/composer-bin-plugin": "^1.9.1"
+ "bamarni/composer-bin-plugin": "^1.9.1",
+ "symfony/uid": "^6.4"
}
}
diff --git a/composer.lock b/composer.lock
index 8a83391b95..3ce0d96fff 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "46861b4f8ab5cf979b0b581545a099c5",
+ "content-hash": "a23ff2aecc750aa400068e3f79dc35b8",
"packages": [
{
"name": "bamarni/composer-bin-plugin",
@@ -486,6 +486,167 @@
"source": "https://github.com/php-fig/simple-cache/tree/3.0.0"
},
"time": "2021-10-29T13:26:27+00:00"
+ },
+ {
+ "name": "symfony/polyfill-uuid",
+ "version": "v1.37.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-uuid.git",
+ "reference": "26dfec253c4cf3e51b541b52ddf7e42cb0908e94"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/26dfec253c4cf3e51b541b52ddf7e42cb0908e94",
+ "reference": "26dfec253c4cf3e51b541b52ddf7e42cb0908e94",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "provide": {
+ "ext-uuid": "*"
+ },
+ "suggest": {
+ "ext-uuid": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Uuid\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Grégoire Pineau",
+ "email": "lyrixx@lyrixx.info"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for uuid functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "uuid"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-uuid/tree/v1.37.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-04-10T16:19:22+00:00"
+ },
+ {
+ "name": "symfony/uid",
+ "version": "v6.4.32",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/uid.git",
+ "reference": "6b973c385f00341b246f697d82dc01a09107acdd"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/uid/zipball/6b973c385f00341b246f697d82dc01a09107acdd",
+ "reference": "6b973c385f00341b246f697d82dc01a09107acdd",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/polyfill-uuid": "^1.15"
+ },
+ "require-dev": {
+ "symfony/console": "^5.4|^6.0|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Uid\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Grégoire Pineau",
+ "email": "lyrixx@lyrixx.info"
+ },
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides an object-oriented API to generate and represent UIDs",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "UID",
+ "ulid",
+ "uuid"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/uid/tree/v6.4.32"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-12-23T15:07:59+00:00"
}
],
"packages-dev": [
diff --git a/lib/Analytics/AnalyticsDatasource.php b/lib/Analytics/AnalyticsDatasource.php
index 170f35ddc8..9963c9f8d6 100644
--- a/lib/Analytics/AnalyticsDatasource.php
+++ b/lib/Analytics/AnalyticsDatasource.php
@@ -298,9 +298,9 @@ private function formatSelectionValue(Column $column, mixed $value): mixed {
return '';
}
- foreach ($column->getSelectionOptionsArray() as $option) {
- if ((int)$option['id'] === (int)$value) {
- return $option['label'];
+ foreach ($column->getSelectionOptionsCollection() as $option) {
+ if ($option->key() === (int)$value) {
+ return $option->label();
}
}
@@ -318,9 +318,9 @@ private function isMultiSelection(Column $column): bool {
private function getSelectionLabels(Column $column, array $values): array {
$labels = [];
foreach ($values as $value) {
- foreach ($column->getSelectionOptionsArray() as $option) {
- if ((int)$option['id'] === (int)$value) {
- $labels[] = $option['label'];
+ foreach ($column->getSelectionOptionsCollection() as $option) {
+ if ($option->key() === (int)$value) {
+ $labels[] = $option->label();
break;
}
}
diff --git a/lib/Controller/ApiTablesController.php b/lib/Controller/ApiTablesController.php
index 7ad79e84de..b94a07d622 100644
--- a/lib/Controller/ApiTablesController.php
+++ b/lib/Controller/ApiTablesController.php
@@ -22,6 +22,7 @@
use OCA\Tables\Service\ColumnService;
use OCA\Tables\Service\TableService;
use OCA\Tables\Service\ViewService;
+use OCA\Tables\Vendor\Symfony\Component\Uid\Uuid;
use OCP\App\IAppManager;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
@@ -153,6 +154,9 @@ public function createFromScheme(string $title, string $emoji, string $descripti
$table = $this->service->create($title, 'custom', $emoji, $description);
$colMap = [];
foreach ($columns as $column) {
+ if (isset($column['uuid']) && !Uuid::isValid($column['uuid'])) {
+ throw new \InvalidArgumentException('Invalid UUID provided');
+ }
$col = $this->columnService->create(
$this->userId,
$table->getId(),
@@ -182,7 +186,8 @@ public function createFromScheme(string $title, string $emoji, string $descripti
usergroupSelectGroups: $column['usergroupSelectGroups'],
usergroupSelectTeams: $column['usergroupSelectTeams'],
showUserStatus: $column['showUserStatus'],
- customSettings: empty($column['customSettings']) ? null : json_encode($column['customSettings'])
+ customSettings: empty($column['customSettings']) ? null : json_encode($column['customSettings']),
+ uuid: $column['uuid'] ?? null,
)
);
$colMap[$column['id']] = $col->getId();
diff --git a/lib/Db/Column.php b/lib/Db/Column.php
index 52698e607f..0952c0183d 100644
--- a/lib/Db/Column.php
+++ b/lib/Db/Column.php
@@ -10,8 +10,10 @@
use JsonSerializable;
use OCA\Tables\Constants\ColumnType;
use OCA\Tables\Dto\Column as ColumnDto;
+use OCA\Tables\Model\SelectionOptions;
use OCA\Tables\ResponseDefinitions;
use OCA\Tables\Service\ValueObject\ViewColumnInformation;
+use OCA\Tables\Vendor\Symfony\Component\Uid\Uuid;
use ValueError;
/**
@@ -19,6 +21,8 @@
*
* @psalm-import-type TablesColumn from ResponseDefinitions
*
+ * @method string|null getUuid()
+ * @method setUuid(?string $uuid)
* @method getTitle(): string
* @method setTitle(string $title)
* @method getTableId(): int
@@ -58,14 +62,12 @@
* @method setTextDefault(?string $textDefault)
* @method getTextAllowedPattern(): string
* @method setTextAllowedPattern(?string $textAllowedPattern)
- * @method getTextAllowedPattern(): ?string
* @method getTextMaxLength(): int
* @method setTextMaxLength(?int $textMaxLength)
* @method getTextUnique(): bool
* @method setTextUnique(?bool $textUnique)
- * @method getSelectionOptions(): string
- * @method getSelectionDefault(): string
- * @method setSelectionOptions(?string $selectionOptionsArray)
+ * @method string getSelectionOptions()
+ * @method setSelectionOptions(?string $selectionOptions)
* @method setSelectionDefault(?string $selectionDefault)
* @method getSelectionDefault(): ?string
* @method getDatetimeDefault(): string
@@ -118,6 +120,7 @@ class Column extends EntitySuper implements JsonSerializable {
public const RELATION_TARGET_ID = 'targetId';
public const RELATION_LABEL_COLUMN = 'labelColumn';
+ protected ?string $uuid = null;
protected ?string $title = null;
protected ?int $tableId = null;
protected ?string $createdBy = null;
@@ -169,6 +172,7 @@ class Column extends EntitySuper implements JsonSerializable {
public function __construct() {
$this->addType('id', 'integer');
+ $this->addType('uuid', 'string');
$this->addType('tableId', 'integer');
$this->addType('mandatory', 'boolean');
@@ -190,6 +194,8 @@ public function __construct() {
$this->addType('showUserStatus', 'boolean');
$this->addType('customSettings', 'string');
+
+ $this->addType('selectionOptions', 'string');
}
public static function isValidMetaTypeId(int $metaTypeId): bool {
@@ -202,8 +208,41 @@ public static function isValidMetaTypeId(int $metaTypeId): bool {
], true);
}
+ public function setter(string $name, array $args): void {
+ if ($name === 'uuid') {
+ $this->setOrAssignUuid((string)$args[0]);
+ return;
+ }
+
+ parent::setter($name, $args);
+ }
+
+ private function assignUuid(): void {
+ if ($this->uuid !== null) {
+ throw new \RuntimeException('This column already has a UUID, they are immutable');
+ }
+ $this->applyUuid(Uuid::v7()->toRfc4122());
+ }
+
+ private function setOrAssignUuid(?string $uuid): void {
+ if ($this->uuid !== null) {
+ throw new \RuntimeException('This column already has a UUID, they are immutable');
+ }
+ if ($uuid === null) {
+ $this->assignUuid();
+ return;
+ }
+ $this->applyUuid($uuid);
+ }
+
+ private function applyUuid(string $uuid): void {
+ $this->uuid = $uuid;
+ $this->markFieldUpdated('uuid');
+ }
+
public static function fromDto(ColumnDto $data): self {
$column = new self();
+ $column->setOrAssignUuid($data->getUuid());
$column->setTitle($data->getTitle());
$column->setType($data->getType());
$column->setSubtype($data->getSubtype() ?? '');
@@ -219,8 +258,7 @@ public static function fromDto(ColumnDto $data): self {
$column->setNumberDecimals($data->getNumberDecimals());
$column->setNumberPrefix($data->getNumberPrefix() ?? '');
$column->setNumberSuffix($data->getNumberSuffix() ?? '');
- $column->setSelectionOptions($data->getSelectionOptions());
- $column->setSelectionDefault($data->getSelectionDefault());
+ $column->setSelectionOptionsCollection(SelectionOptions::createFromInputJsonString($data->getSelectionOptions() ?? '[]', $data->getSelectionDefault()));
$column->setDatetimeDefault($data->getDatetimeDefault());
$column->setUsergroupDefault($data->getUsergroupDefault());
$column->setUsergroupMultipleItems($data->getUsergroupMultipleItems());
@@ -246,18 +284,17 @@ public function setUsergroupDefaultArray(array $array):void {
$this->setUsergroup($json);
}
- public function getSelectionOptionsArray(): array {
- $options = $this->getSelectionOptions();
- if ($options !== '' && $options !== null && $options !== 'null') {
- return \json_decode($options, true);
- } else {
- return [];
- }
+ public function getSelectionOptionsCollection(): SelectionOptions {
+ return SelectionOptions::createFromInputJsonString($this->getSelectionOptions() ?? '[]', $this->getSelectionDefault());
}
- public function setSelectionOptionsArray(array $array):void {
- $json = \json_encode($array);
- $this->setSelectionOptions($json);
+ public function setSelectionOptionsCollection(SelectionOptions $selectionOptions): void {
+ $this->setSelectionOptions(json_encode($selectionOptions->jsonSerialize()));
+ $this->setSelectionDefault($selectionOptions->defaultSerialized());
+ }
+
+ public function getSelectionOptionsArray(): ?array {
+ return $this->getSelectionOptionsCollection()->jsonSerialize();
}
/**
@@ -266,6 +303,7 @@ public function setSelectionOptionsArray(array $array):void {
public function jsonSerialize(): array {
return [
'id' => $this->id,
+ 'uuid' => $this->uuid,
'tableId' => $this->tableId,
'title' => $this->title,
'createdBy' => $this->createdBy,
diff --git a/lib/Dto/Column.php b/lib/Dto/Column.php
index 5a321f1aee..e4032b377e 100644
--- a/lib/Dto/Column.php
+++ b/lib/Dto/Column.php
@@ -34,6 +34,7 @@ public function __construct(
private ?bool $usergroupSelectTeams = null,
private ?bool $showUserStatus = null,
private ?string $customSettings = null,
+ private ?string $uuid = null,
) {
}
@@ -69,9 +70,14 @@ public static function createFromArray(array $data): self {
usergroupSelectTeams: $data['usergroupSelectTeams'] ?? null,
showUserStatus: $data['showUserStatus'] ?? null,
customSettings: $customSettings,
+ uuid: $data['uuid'] ?? null,
);
}
+ public function getUuid(): ?string {
+ return $this->uuid;
+ }
+
public function getType(): ?string {
return $this->type;
}
diff --git a/lib/Migration/Version2020Date20260513185340.php b/lib/Migration/Version2020Date20260513185340.php
new file mode 100644
index 0000000000..b594268a52
--- /dev/null
+++ b/lib/Migration/Version2020Date20260513185340.php
@@ -0,0 +1,101 @@
+hasTable(self::TARGET_TABLE)) {
+ return null;
+ }
+
+ $columnsTable = $schema->getTable(self::TARGET_TABLE);
+ if (!$columnsTable->hasColumn(self::COL_UUID)) {
+ $columnsTable->addColumn(self::COL_UUID, Types::STRING, [
+ 'notnull' => false,
+ 'default' => null,
+ 'length' => 36,
+ 'comment' => 'UUIDv7 identifier to support structural updates across instances',
+ ]);
+ }
+ if (!$columnsTable->hasUniqueConstraint(self::INDEX_NAME)) {
+ $columnsTable->addUniqueIndex(['table_id', self::COL_UUID], self::INDEX_NAME);
+ }
+
+ return $schema;
+ }
+
+ #[Override]
+ public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
+
+ $qbUpdate = $this->db->getQueryBuilder();
+ $qbUpdate->update(self::TARGET_TABLE)
+ ->set(self::COL_UUID, $qbUpdate->createParameter('columnUuid'))
+ ->where($qbUpdate->expr()->eq(self::COL_ID, $qbUpdate->createParameter('columnLocalId')));
+
+ $qbSelect = $this->db->getQueryBuilder();
+ $qbSelect->select(self::COL_ID)
+ ->from(self::TARGET_TABLE);
+ $select = $qbSelect->executeQuery();
+
+ $writeBatches = 250;
+ $updates = 0;
+
+ try {
+ $this->db->beginTransaction();
+ while (($columnId = $select->fetchOne()) !== false) {
+ $qbUpdate->setParameters(
+ [
+ 'columnLocalId' => (int)$columnId,
+ 'columnUuid' => Uuid::v7()->toRfc4122(),
+ ],
+ [
+ Types::INTEGER,
+ Types::STRING,
+ ]
+ );
+ $qbUpdate->executeStatement();
+ $updates++;
+ if ($updates % $writeBatches === 0) {
+ $this->db->commit();
+ $this->db->beginTransaction();
+ }
+ }
+ $this->db->commit();
+ } catch (\Exception $e) {
+ $this->db->rollBack();
+ throw $e;
+ }
+
+ $select->closeCursor();
+ }
+}
diff --git a/lib/Model/SelectionOption.php b/lib/Model/SelectionOption.php
new file mode 100644
index 0000000000..d85aa94420
--- /dev/null
+++ b/lib/Model/SelectionOption.php
@@ -0,0 +1,51 @@
+key;
+ }
+
+ public function label(): string {
+ return $this->label;
+ }
+
+ #[\Override]
+ public function jsonSerialize(): array {
+ return [
+ 'id' => $this->key,
+ 'label' => $this->label,
+ ];
+ }
+}
diff --git a/lib/Model/SelectionOptions.php b/lib/Model/SelectionOptions.php
new file mode 100644
index 0000000000..e196c54059
--- /dev/null
+++ b/lib/Model/SelectionOptions.php
@@ -0,0 +1,138 @@
+
+ */
+class SelectionOptions implements JsonSerializable, Iterator {
+
+ public function __construct(
+ /** @var SelectionOption[]|null */
+ private ?array $selectionOptions,
+ private mixed $default,
+ ) {
+ if ($this->selectionOptions !== null) {
+ // `check` subtype has options set to null
+ foreach ($this->selectionOptions as $selectionOption) {
+ if (!$selectionOption instanceof SelectionOption) {
+ throw new \InvalidArgumentException('Provided selectionOption must be an instance of SelectionOption');
+ }
+ }
+ }
+
+ if (is_string($this->default)) {
+ $this->default = \json_decode($this->default(), true);
+ }
+
+ if (is_int($this->default)) {
+ $this->applyIntDefault();
+ } elseif (is_bool($this->default)) {
+ // does not need any further handling
+ return;
+ } elseif (is_array($this->default)) {
+ $this->applyArrayDefault();
+ }
+ }
+
+ private function applyIntDefault(): void {
+ // default value targets a specific key
+ foreach ($this->selectionOptions as $selectionOption) {
+ if ($selectionOption->key() === $this->default) {
+ return;
+ }
+ }
+ // if the default is not available anymore, we pragmatically unset it.
+ $this->default = null;
+ }
+
+ private function applyArrayDefault(): void {
+ $confirmedOptions = [];
+ foreach ($this->default as $defaultOption) {
+ $normalizedDefaultOption = (int)$defaultOption;
+ foreach ($this->selectionOptions as $selectionOption) {
+ if ($selectionOption->key() === $normalizedDefaultOption) {
+ $confirmedOptions[] = $normalizedDefaultOption;
+ continue 2;
+ }
+ // if the default is not available anymore, we pragmatically ignore it.
+ }
+ }
+ $this->default = $confirmedOptions;
+ }
+
+ public static function createFromInputArray(?array $data, null|bool|int|string $default): self {
+ if ($data !== null) {
+ $selectionOptions = [];
+ foreach ($data as $inputSelectionOption) {
+ $selectionOptions[] = SelectionOption::createFromInputArray($inputSelectionOption);
+ }
+ }
+ // `check` subtype has null as options
+ return new self($selectionOptions ?? null, $default);
+ }
+
+ public static function createFromInputJsonString(?string $data, null|bool|int|string $default): self {
+ if ($data !== null && $data !== 'null') {
+ $inputArray = \json_decode($data === '' ? '[]' : $data, true);
+ if (!is_array($inputArray)) {
+ throw new \InvalidArgumentException('Provided selectionOption is not a valid JSON string');
+ }
+ } else {
+ // `check` subtype has "null" as options
+ $inputArray = null;
+ }
+ return self::createFromInputArray($inputArray, $default);
+ }
+
+ public function default(): mixed {
+ return $this->default;
+ }
+
+ public function defaultSerialized(): string {
+ return \json_encode($this->default());
+ }
+
+ #[\Override]
+ public function jsonSerialize(): ?array {
+ if ($this->selectionOptions === null) {
+ return null;
+ }
+ return array_map(static fn (SelectionOption $so) => $so->jsonSerialize(), $this->selectionOptions);
+ }
+
+ #[\Override]
+ public function current(): SelectionOption {
+ return current($this->selectionOptions);
+ }
+
+ #[\Override]
+ public function next(): void {
+ next($this->selectionOptions);
+ }
+
+ #[\Override]
+ public function key(): ?int {
+ return key($this->selectionOptions);
+ }
+
+ #[\Override]
+ public function valid(): bool {
+ return $this->key() !== null;
+ }
+
+ #[\Override]
+ public function rewind(): void {
+ reset($this->selectionOptions);
+ }
+}
diff --git a/lib/ResponseDefinitions.php b/lib/ResponseDefinitions.php
index 1dd4d5f6a8..c006a46ccb 100644
--- a/lib/ResponseDefinitions.php
+++ b/lib/ResponseDefinitions.php
@@ -117,6 +117,7 @@
*
* @psalm-type TablesColumn = array{
* id: int,
+ * uuid: string,
* title: string,
* tableId: int,
* createdBy: string,
diff --git a/lib/Service/ColumnService.php b/lib/Service/ColumnService.php
index 1f7322e8f1..92916900de 100644
--- a/lib/Service/ColumnService.php
+++ b/lib/Service/ColumnService.php
@@ -21,10 +21,12 @@
use OCA\Tables\Errors\NotFoundError;
use OCA\Tables\Errors\PermissionError;
use OCA\Tables\Helper\UserHelper;
+use OCA\Tables\Model\SelectionOptions;
use OCA\Tables\ResponseDefinitions;
use OCA\Tables\Service\ValueObject\Title;
use OCA\Tables\Service\ValueObject\ViewColumnInformation;
use OCA\Tables\Validation\ColumnDtoValidator;
+use OCA\Tables\Vendor\Symfony\Component\Uid\Uuid;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\IL10N;
@@ -74,6 +76,7 @@ public function __construct(
}
/**
+ * @return Column[]
* @throws InternalError
* @throws PermissionError
*/
@@ -141,7 +144,7 @@ public function findAllByManagedView(View $view, string $userId): array {
/**
* @param int $viewId
* @param string|null $userId
- * @return array
+ * @return Column[]
* @throws NotFoundError
* @throws PermissionError
* @throws InternalError
@@ -391,11 +394,10 @@ public function update(
$item->setNumberMin($columnDto->getNumberMin());
$item->setNumberMax($columnDto->getNumberMax());
$item->setNumberDecimals($columnDto->getNumberDecimals());
- if ($columnDto->getSelectionOptions() !== null) {
- $item->setSelectionOptions($columnDto->getSelectionOptions());
- }
- if ($columnDto->getSelectionDefault() !== null) {
- $item->setSelectionDefault($columnDto->getSelectionDefault());
+ if ($columnDto->getSelectionOptions() !== null || $columnDto->getSelectionDefault() !== null) {
+ $item->setSelectionOptionsCollection(SelectionOptions::createFromInputJsonString(
+ $columnDto->getSelectionOptions(), $columnDto->getSelectionDefault())
+ );
}
$item->setDatetimeDefault($columnDto->getDatetimeDefault());
@@ -672,6 +674,15 @@ private function enhanceColumns(?array $columns, ?View $view = null): array {
*/
public function importColumn(Table $table, array $column): int {
$item = new Column();
+ if (isset($column['uuid'])) {
+ $uuid = (string)$column['uuid'];
+ if ($uuid === '') {
+ $uuid = null;
+ } elseif (!Uuid::isValid($uuid)) {
+ throw new \InvalidArgumentException('Invalid UUID provided');
+ }
+ }
+ $item->setUuid($uuid ?? null);
$item->setTableId($table->getId());
$item->setTitle($column['title']);
$item->setCreatedBy($table->getOwnership());
@@ -692,8 +703,7 @@ public function importColumn(Table $table, array $column): int {
$item->setTextAllowedPattern($column['textAllowedPattern']);
$item->setTextMaxLength($column['textMaxLength']);
$item->setTextUnique($column['textUnique']);
- $item->setSelectionOptions(json_encode($column['selectionOptions']));
- $item->setSelectionDefault($column['selectionDefault']);
+ $item->setSelectionOptionsCollection(SelectionOptions::createFromInputArray($column['selectionOptions'], $column['selectionDefault']));
$item->setDatetimeDefault($column['datetimeDefault']);
$item->setUsergroupDefault(json_encode($column['usergroupDefault']));
$item->setUsergroupMultipleItems($column['usergroupMultipleItems']);
diff --git a/lib/Service/ColumnTypes/SelectionBusiness.php b/lib/Service/ColumnTypes/SelectionBusiness.php
index 3fd6f19c76..423478d6ec 100644
--- a/lib/Service/ColumnTypes/SelectionBusiness.php
+++ b/lib/Service/ColumnTypes/SelectionBusiness.php
@@ -22,9 +22,9 @@ public function parseValue($value, Column $column): string {
return '';
}
- foreach ($column->getSelectionOptionsArray() as $option) {
- if ($option['id'] === $intValue) {
- return json_encode((string)$option['id']);
+ foreach ($column->getSelectionOptionsCollection() as $option) {
+ if ($option->key() === $intValue) {
+ return json_encode((string)$option->key());
}
}
@@ -32,9 +32,9 @@ public function parseValue($value, Column $column): string {
}
public function parseDisplayValue($value, Column $column): string {
- foreach ($column->getSelectionOptionsArray() as $option) {
- if ($option['label'] === $value) {
- return json_encode($option['id']);
+ foreach ($column->getSelectionOptionsCollection() as $option) {
+ if ($option->label() === $value) {
+ return json_encode($option->key());
}
}
@@ -46,7 +46,7 @@ public function parseDisplayValue($value, Column $column): string {
* @param Column $column
* @return bool
*/
- public function canBeParsed($value, Column $column): bool {
+ public function canBeParsed(mixed $value, Column $column): bool {
if ($value === null) {
return true;
}
@@ -56,8 +56,8 @@ public function canBeParsed($value, Column $column): bool {
return false;
}
- foreach ($column->getSelectionOptionsArray() as $option) {
- if ($option['id'] === $intValue) {
+ foreach ($column->getSelectionOptionsCollection() as $option) {
+ if ($option->key() === $intValue) {
return true;
}
}
@@ -70,8 +70,8 @@ public function canBeParsedDisplayValue($value, Column $column): bool {
return true;
}
- foreach ($column->getSelectionOptionsArray() as $option) {
- if ($option['label'] === $value) {
+ foreach ($column->getSelectionOptionsCollection() as $option) {
+ if ($option->label() === $value) {
return true;
}
}
diff --git a/lib/Service/ColumnTypes/SelectionMultiBusiness.php b/lib/Service/ColumnTypes/SelectionMultiBusiness.php
index b8799a6613..edf7b6c813 100644
--- a/lib/Service/ColumnTypes/SelectionMultiBusiness.php
+++ b/lib/Service/ColumnTypes/SelectionMultiBusiness.php
@@ -8,10 +8,11 @@
namespace OCA\Tables\Service\ColumnTypes;
use OCA\Tables\Db\Column;
+use OCA\Tables\Model\SelectionOptions;
class SelectionMultiBusiness extends SuperBusiness {
- private array $options = [];
+ private SelectionOptions $options;
/**
* @param mixed $value (array|string|null)
@@ -23,7 +24,7 @@ public function parseValue($value, Column $column): string {
return json_encode([]);
}
- $this->options = $column->getSelectionOptionsArray();
+ $this->options = $column->getSelectionOptionsCollection();
$wasString = false;
if (is_string($value)) {
@@ -48,20 +49,18 @@ public function parseValue($value, Column $column): string {
* @param int|string|null $value int assume as option ID, string assumes a label
* @return int|null return always the option ID or null
*/
- private function getOptionIdForValue($value): ?int {
+ private function getOptionIdForValue(mixed $value): ?int {
if ($value === null) {
return null;
}
foreach ($this->options as $option) {
if (is_int($value)) {
- if ($option['id'] === $value) {
- return $option['id'];
- }
- } else {
- if ($option['label'] === $value) {
- return $option['id'];
+ if ($option->key() === $value) {
+ return $option->key();
}
+ } elseif ($option->label() === $value) {
+ return $option->key();
}
}
return null;
@@ -77,7 +76,7 @@ public function canBeParsed($value, Column $column): bool {
return true;
}
- $this->options = $column->getSelectionOptionsArray();
+ $this->options = $column->getSelectionOptionsCollection();
$wasString = false;
if (is_string($value)) {
diff --git a/openapi.json b/openapi.json
index b02bee3c14..76f66a1680 100644
--- a/openapi.json
+++ b/openapi.json
@@ -72,6 +72,7 @@
"type": "object",
"required": [
"id",
+ "uuid",
"title",
"tableId",
"createdBy",
@@ -112,6 +113,9 @@
"type": "integer",
"format": "int64"
},
+ "uuid": {
+ "type": "string"
+ },
"title": {
"type": "string"
},
diff --git a/src/types/openapi/openapi.ts b/src/types/openapi/openapi.ts
index e913176189..2d9ecbf65c 100644
--- a/src/types/openapi/openapi.ts
+++ b/src/types/openapi/openapi.ts
@@ -932,6 +932,7 @@ export type components = {
readonly Column: {
/** Format: int64 */
readonly id: number;
+ readonly uuid: string;
readonly title: string;
/** Format: int64 */
readonly tableId: number;
diff --git a/tests/unit/Service/ColumnTypes/SelectionBusinessTest.php b/tests/unit/Service/ColumnTypes/SelectionBusinessTest.php
index babf703852..7fa416c7dd 100644
--- a/tests/unit/Service/ColumnTypes/SelectionBusinessTest.php
+++ b/tests/unit/Service/ColumnTypes/SelectionBusinessTest.php
@@ -8,6 +8,8 @@
namespace OCA\Tables\Service\ColumnTypes;
use OCA\Tables\Db\Column;
+use OCA\Tables\Model\SelectionOption;
+use OCA\Tables\Model\SelectionOptions;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
@@ -21,14 +23,16 @@ public function setUp(): void {
$this->logger = $this->createMock(LoggerInterface::class);
$this->selectionBusiness = new SelectionBusiness($this->logger);
+ $selectionOptions = new SelectionOptions([
+ new SelectionOption(1, 'Option 1'),
+ new SelectionOption(2, 'Option 2'),
+ new SelectionOption(3, 'Option 3'),
+ new SelectionOption(4, '1'),
+ ], null);
+
$this->column = $this->createMock(Column::class);
- $this->column->method('getSelectionOptionsArray')
- ->willReturn([
- ['id' => 1, 'label' => 'Option 1'],
- ['id' => 2, 'label' => 'Option 2'],
- ['id' => 3, 'label' => 'Option 3'],
- ['id' => 4, 'label' => '1'],
- ]);
+ $this->column->method('getSelectionOptionsCollection')
+ ->willReturn($selectionOptions);
}
public function parseValueProvider(): array {