From 2905e6271a5ff211f320c4925be02100895958b2 Mon Sep 17 00:00:00 2001 From: "Enjeck C." Date: Thu, 28 May 2026 05:29:54 +0100 Subject: [PATCH] fix: Avoid saving whitespace titles Signed-off-by: Enjeck C. --- lib/Command/AddTable.php | 3 +- lib/Command/RenameTable.php | 3 +- lib/Controller/Api1Controller.php | 14 +- lib/Controller/ApiColumnsController.php | 211 ++++++++++--------- lib/Controller/ApiTablesController.php | 14 +- lib/Service/ColumnService.php | 36 +++- lib/Service/TableService.php | 5 + lib/Service/ValueObject/Title.php | 11 + lib/Validation/ColumnDtoValidator.php | 15 +- openapi.json | 264 ++++++++++++++++++++++++ package-lock.json | 149 +++++++++++++ src/modules/modals/CreateColumn.vue | 4 +- src/modules/modals/CreateTable.vue | 4 +- src/modules/modals/EditColumn.vue | 4 +- src/modules/modals/EditTable.vue | 4 +- src/types/openapi/openapi.ts | 118 +++++++++++ 16 files changed, 751 insertions(+), 108 deletions(-) diff --git a/lib/Command/AddTable.php b/lib/Command/AddTable.php index 0a3a2d12c1..944a7eb7c9 100644 --- a/lib/Command/AddTable.php +++ b/lib/Command/AddTable.php @@ -7,6 +7,7 @@ namespace OCA\Tables\Command; +use InvalidArgumentException; use OCA\Tables\Errors\InternalError; use OCA\Tables\Errors\PermissionError; use OCA\Tables\Service\TableService; @@ -78,7 +79,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int unset($arr['rowsCount']); unset($arr['ownerDisplayName']); $output->writeln(json_encode($arr, JSON_PRETTY_PRINT)); - } catch (InternalError|PermissionError|Exception $e) { + } catch (InternalError|PermissionError|Exception|InvalidArgumentException $e) { $output->writeln('Error occurred: ' . $e->getMessage()); $this->logger->warning('Following error occurred during executing occ command "' . self::class . '"', ['exception' => $e]); return 1; diff --git a/lib/Command/RenameTable.php b/lib/Command/RenameTable.php index 8f656cd86f..d525d65da4 100644 --- a/lib/Command/RenameTable.php +++ b/lib/Command/RenameTable.php @@ -7,6 +7,7 @@ namespace OCA\Tables\Command; +use InvalidArgumentException; use OCA\Tables\Errors\InternalError; use OCA\Tables\Service\TableService; use Psr\Log\LoggerInterface; @@ -84,7 +85,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int unset($arr['rowsCount']); unset($arr['ownerDisplayName']); $output->writeln(json_encode($arr, JSON_PRETTY_PRINT)); - } catch (InternalError $e) { + } catch (InternalError|InvalidArgumentException $e) { $output->writeln('Error occurred: ' . $e->getMessage()); $this->logger->warning('Following error occurred during executing occ command "' . self::class . '"', ['exception' => $e]); return 1; diff --git a/lib/Controller/Api1Controller.php b/lib/Controller/Api1Controller.php index 9eccdd274a..8d8a9278a3 100644 --- a/lib/Controller/Api1Controller.php +++ b/lib/Controller/Api1Controller.php @@ -131,9 +131,10 @@ public function index(): DataResponse { * @param string|null $emoji Emoji for the table * @param string $template Template to use if wanted * - * @return DataResponse|DataResponse + * @return DataResponse|DataResponse * * 200: Tables returned + * 400: Invalid request data */ #[NoAdminRequired] #[NoCSRFRequired] @@ -142,6 +143,10 @@ public function index(): DataResponse { public function createTable(string $title, ?string $emoji, string $template = 'custom'): DataResponse { try { return new DataResponse($this->tableService->create($title, $template, $emoji)->jsonSerialize()); + } catch (InvalidArgumentException $e) { + $this->logger->warning('An invalid request occurred: ' . $e->getMessage(), ['exception' => $e]); + $message = ['message' => $e->getMessage()]; + return new DataResponse($message, Http::STATUS_BAD_REQUEST); } catch (InternalError|Exception $e) { $this->logger->error('An internal error or exception occurred: ' . $e->getMessage(), ['exception' => $e]); $message = ['message' => $e->getMessage()]; @@ -232,9 +237,10 @@ public function getTable(int $tableId): DataResponse { * @param string|null $title New table title * @param string|null $emoji New table emoji * @param bool $archived Whether the table is archived - * @return DataResponse|DataResponse + * @return DataResponse|DataResponse * * 200: Tables returned + * 400: Invalid request data * 403: No permissions * 404: Not found */ @@ -246,6 +252,10 @@ public function getTable(int $tableId): DataResponse { public function updateTable(int $tableId, ?string $title = null, ?string $emoji = null, ?bool $archived = false): DataResponse { try { return new DataResponse($this->tableService->update($tableId, $title, $emoji, null, $archived, $this->userId)->jsonSerialize()); + } catch (InvalidArgumentException $e) { + $this->logger->warning('An invalid request occurred: ' . $e->getMessage(), ['exception' => $e]); + $message = ['message' => $e->getMessage()]; + return new DataResponse($message, Http::STATUS_BAD_REQUEST); } catch (PermissionError $e) { $this->logger->warning('A permission error occurred: ' . $e->getMessage(), ['exception' => $e]); $message = ['message' => $e->getMessage()]; diff --git a/lib/Controller/ApiColumnsController.php b/lib/Controller/ApiColumnsController.php index fc73b40688..eb4a0c5c60 100644 --- a/lib/Controller/ApiColumnsController.php +++ b/lib/Controller/ApiColumnsController.php @@ -130,11 +130,12 @@ public function show(int $id): DataResponse { * * * @return DataResponse|DataResponse|DataResponse * * * 200: Column created + * 400: Invalid request data * 403: No permission * 404: Not found * @throws InternalError @@ -147,26 +148,30 @@ public function show(int $id): DataResponse { public function createNumberColumn(int $baseNodeId, string $title, ?float $numberDefault, ?int $numberDecimals, ?string $numberPrefix, ?string $numberSuffix, ?float $numberMin, ?float $numberMax, ?string $subtype = null, ?string $description = null, ?array $selectedViewIds = [], bool $mandatory = false, string $baseNodeType = 'table', array $customSettings = []): DataResponse { $tableId = $baseNodeType === 'table' ? $baseNodeId : null; $viewId = $baseNodeType === 'view' ? $baseNodeId : null; - $column = $this->service->create( - $this->userId, - $tableId, - $viewId, - new ColumnDto( - title: $title, - type: ColumnType::NUMBER->value, - subtype: $subtype, - mandatory: $mandatory, - description: $description, - numberDefault: $numberDefault, - numberMin: $numberMin, - numberMax: $numberMax, - numberDecimals: $numberDecimals, - numberPrefix: $numberPrefix, - numberSuffix: $numberSuffix, - customSettings: json_encode($customSettings), - ), - $selectedViewIds - ); + try { + $column = $this->service->create( + $this->userId, + $tableId, + $viewId, + new ColumnDto( + title: $title, + type: ColumnType::NUMBER->value, + subtype: $subtype, + mandatory: $mandatory, + description: $description, + numberDefault: $numberDefault, + numberMin: $numberMin, + numberMax: $numberMax, + numberDecimals: $numberDecimals, + numberPrefix: $numberPrefix, + numberSuffix: $numberSuffix, + customSettings: json_encode($customSettings), + ), + $selectedViewIds + ); + } catch (BadRequestError $e) { + return $this->handleBadRequestError($e); + } return new DataResponse($column->jsonSerialize()); } @@ -193,11 +198,12 @@ public function createNumberColumn(int $baseNodeId, string $title, ?float $numbe * @param array $customSettings Custom settings for the * column * @return DataResponse|DataResponse|DataResponse * * * 200: Column created + * 400: Invalid request data * 403: No permission * 404: Not found * @throws InternalError @@ -210,24 +216,28 @@ public function createNumberColumn(int $baseNodeId, string $title, ?float $numbe public function createTextColumn(int $baseNodeId, string $title, ?string $textDefault, ?string $textAllowedPattern, ?int $textMaxLength, ?bool $textUnique = false, ?string $subtype = null, ?string $description = null, ?array $selectedViewIds = [], bool $mandatory = false, string $baseNodeType = 'table', array $customSettings = []): DataResponse { $tableId = $baseNodeType === 'table' ? $baseNodeId : null; $viewId = $baseNodeType === 'view' ? $baseNodeId : null; - $column = $this->service->create( - $this->userId, - $tableId, - $viewId, - new ColumnDto( - title: $title, - type: ColumnType::TEXT->value, - subtype: $subtype, - mandatory: $mandatory, - description: $description, - textDefault: $textDefault, - textAllowedPattern: $textAllowedPattern, - textMaxLength: $textMaxLength, - textUnique: $textUnique, - customSettings: json_encode($customSettings), - ), - $selectedViewIds - ); + try { + $column = $this->service->create( + $this->userId, + $tableId, + $viewId, + new ColumnDto( + title: $title, + type: ColumnType::TEXT->value, + subtype: $subtype, + mandatory: $mandatory, + description: $description, + textDefault: $textDefault, + textAllowedPattern: $textAllowedPattern, + textMaxLength: $textMaxLength, + textUnique: $textUnique, + customSettings: json_encode($customSettings), + ), + $selectedViewIds + ); + } catch (BadRequestError $e) { + return $this->handleBadRequestError($e); + } return new DataResponse($column->jsonSerialize()); } @@ -256,11 +266,12 @@ public function createTextColumn(int $baseNodeId, string $title, ?string $textDe * * * @return DataResponse|DataResponse|DataResponse * * * 200: Column created + * 400: Invalid request data * 403: No permission * 404: Not found * @throws InternalError @@ -273,22 +284,26 @@ public function createTextColumn(int $baseNodeId, string $title, ?string $textDe public function createSelectionColumn(int $baseNodeId, string $title, string $selectionOptions, ?string $selectionDefault, ?string $subtype = null, ?string $description = null, ?array $selectedViewIds = [], bool $mandatory = false, string $baseNodeType = 'table', array $customSettings = []): DataResponse { $tableId = $baseNodeType === 'table' ? $baseNodeId : null; $viewId = $baseNodeType === 'view' ? $baseNodeId : null; - $column = $this->service->create( - $this->userId, - $tableId, - $viewId, - new ColumnDto( - title: $title, - type: ColumnType::SELECTION->value, - subtype: $subtype, - mandatory: $mandatory, - description: $description, - selectionOptions: $selectionOptions, - selectionDefault: $selectionDefault, - customSettings: json_encode($customSettings), - ), - $selectedViewIds - ); + try { + $column = $this->service->create( + $this->userId, + $tableId, + $viewId, + new ColumnDto( + title: $title, + type: ColumnType::SELECTION->value, + subtype: $subtype, + mandatory: $mandatory, + description: $description, + selectionOptions: $selectionOptions, + selectionDefault: $selectionDefault, + customSettings: json_encode($customSettings), + ), + $selectedViewIds + ); + } catch (BadRequestError $e) { + return $this->handleBadRequestError($e); + } return new DataResponse($column->jsonSerialize()); } @@ -314,11 +329,12 @@ public function createSelectionColumn(int $baseNodeId, string $title, string $se * * * @return DataResponse|DataResponse|DataResponse * * * 200: Column created + * 400: Invalid request data * 403: No permission * 404: Not found * @throws InternalError @@ -331,21 +347,25 @@ public function createSelectionColumn(int $baseNodeId, string $title, string $se public function createDatetimeColumn(int $baseNodeId, string $title, ?string $datetimeDefault, ?string $subtype = null, ?string $description = null, ?array $selectedViewIds = [], bool $mandatory = false, string $baseNodeType = 'table', array $customSettings = []): DataResponse { $tableId = $baseNodeType === 'table' ? $baseNodeId : null; $viewId = $baseNodeType === 'view' ? $baseNodeId : null; - $column = $this->service->create( - $this->userId, - $tableId, - $viewId, - new ColumnDto( - title: $title, - type: ColumnType::DATETIME->value, - subtype: $subtype, - mandatory: $mandatory, - description: $description, - datetimeDefault: $datetimeDefault, - customSettings: json_encode($customSettings), - ), - $selectedViewIds - ); + try { + $column = $this->service->create( + $this->userId, + $tableId, + $viewId, + new ColumnDto( + title: $title, + type: ColumnType::DATETIME->value, + subtype: $subtype, + mandatory: $mandatory, + description: $description, + datetimeDefault: $datetimeDefault, + customSettings: json_encode($customSettings), + ), + $selectedViewIds + ); + } catch (BadRequestError $e) { + return $this->handleBadRequestError($e); + } return new DataResponse($column->jsonSerialize()); } @@ -374,11 +394,12 @@ public function createDatetimeColumn(int $baseNodeId, string $title, ?string $da * * * @return DataResponse|DataResponse|DataResponse * * * 200: Column created + * 400: Invalid request data * 403: No permission * 404: Not found * @throws InternalError @@ -391,25 +412,29 @@ public function createDatetimeColumn(int $baseNodeId, string $title, ?string $da public function createUsergroupColumn(int $baseNodeId, string $title, ?string $usergroupDefault, ?bool $usergroupMultipleItems = null, ?bool $usergroupSelectUsers = null, ?bool $usergroupSelectGroups = null, ?bool $usergroupSelectTeams = null, ?bool $showUserStatus = null, ?string $description = null, ?array $selectedViewIds = [], bool $mandatory = false, string $baseNodeType = 'table', array $customSettings = []): DataResponse { $tableId = $baseNodeType === 'table' ? $baseNodeId : null; $viewId = $baseNodeType === 'view' ? $baseNodeId : null; - $column = $this->service->create( - $this->userId, - $tableId, - $viewId, - new ColumnDto( - title: $title, - type: ColumnType::PEOPLE->value, - mandatory: $mandatory, - description: $description, - usergroupDefault: $usergroupDefault, - usergroupMultipleItems: $usergroupMultipleItems, - usergroupSelectUsers: $usergroupSelectUsers, - usergroupSelectGroups: $usergroupSelectGroups, - usergroupSelectTeams: $usergroupSelectTeams, - showUserStatus: $showUserStatus, - customSettings: json_encode($customSettings), - ), - $selectedViewIds - ); + try { + $column = $this->service->create( + $this->userId, + $tableId, + $viewId, + new ColumnDto( + title: $title, + type: ColumnType::PEOPLE->value, + mandatory: $mandatory, + description: $description, + usergroupDefault: $usergroupDefault, + usergroupMultipleItems: $usergroupMultipleItems, + usergroupSelectUsers: $usergroupSelectUsers, + usergroupSelectGroups: $usergroupSelectGroups, + usergroupSelectTeams: $usergroupSelectTeams, + showUserStatus: $showUserStatus, + customSettings: json_encode($customSettings), + ), + $selectedViewIds + ); + } catch (BadRequestError $e) { + return $this->handleBadRequestError($e); + } return new DataResponse($column->jsonSerialize()); } } diff --git a/lib/Controller/ApiTablesController.php b/lib/Controller/ApiTablesController.php index 7510929b37..7ad79e84de 100644 --- a/lib/Controller/ApiTablesController.php +++ b/lib/Controller/ApiTablesController.php @@ -10,6 +10,7 @@ use Exception; use OCA\Tables\AppInfo\Application; use OCA\Tables\Dto\Column as ColumnDto; +use OCA\Tables\Errors\BadRequestError; use OCA\Tables\Errors\InternalError; use OCA\Tables\Errors\NotFoundError; use OCA\Tables\Errors\PermissionError; @@ -263,6 +264,13 @@ public function createFromScheme(string $title, string $emoji, string $descripti } $this->logger->warning('An invalid request occurred: ' . $e->getMessage(), ['exception' => $e]); return new DataResponse(['message' => $e->getMessage()], Http::STATUS_BAD_REQUEST); + } catch (BadRequestError $e) { + try { + $this->db->rollBack(); + } catch (\OCP\DB\Exception $re) { + return $this->handleError($re); + } + return $this->handleBadRequestError($e); } catch (InternalError|Exception $e) { try { $this->db->rollBack(); @@ -281,14 +289,18 @@ public function createFromScheme(string $title, string $emoji, string $descripti * @param string|null $description Description for the table * @param string $template Template to use if wanted * - * @return DataResponse|DataResponse + * @return DataResponse|DataResponse * * 200: Tables returned + * 400: Invalid request data */ #[NoAdminRequired] public function create(string $title, ?string $emoji, ?string $description, string $template = 'custom'): DataResponse { try { return new DataResponse($this->service->create($title, $template, $emoji, $description)->jsonSerialize()); + } catch (\InvalidArgumentException $e) { + $this->logger->warning('An invalid request occurred: ' . $e->getMessage(), ['exception' => $e]); + return new DataResponse(['message' => $e->getMessage()], Http::STATUS_BAD_REQUEST); } catch (InternalError|Exception $e) { return $this->handleError($e); } diff --git a/lib/Service/ColumnService.php b/lib/Service/ColumnService.php index fe56029974..b3ff394e14 100644 --- a/lib/Service/ColumnService.php +++ b/lib/Service/ColumnService.php @@ -22,6 +22,7 @@ use OCA\Tables\Errors\PermissionError; use OCA\Tables\Helper\UserHelper; use OCA\Tables\ResponseDefinitions; +use OCA\Tables\Service\ValueObject\Title; use OCA\Tables\Service\ValueObject\ViewColumnInformation; use OCA\Tables\Validation\ColumnDtoValidator; use OCP\AppFramework\Db\DoesNotExistException; @@ -224,6 +225,7 @@ public function find(int $id, ?string $userId = null): Column { * @param array $selectedViewIds * @return Column * + * @throws BadRequestError * @throws InternalError * @throws PermissionError|NotFoundError */ @@ -237,7 +239,12 @@ public function create( if (ColumnType::tryFrom($columnDto->getType()) === null) { throw new BadRequestError('Column type ' . $columnDto->getType() . ' does not exist.'); } - $this->columnDtoValidator->validate($columnDto); + $this->columnDtoValidator->validate($columnDto, true); + $columnTitle = $this->normalizeTitle($columnDto->getTitle(), true); + if ($columnTitle === null) { + throw new BadRequestError('Title is missing.'); + } + // security if ($viewId) { try { @@ -281,7 +288,7 @@ public function create( // Add number to title to avoid duplicate $columns = $this->mapper->findAllByTable($table->getId()); $i = 1; - $newTitle = $columnDto->getTitle(); + $newTitle = $columnTitle; while (true) { $found = false; foreach ($columns as $column) { @@ -293,7 +300,7 @@ public function create( if (!$found) { break; } - $newTitle = $columnDto->getTitle() . ' (' . $i . ')'; + $newTitle = $columnTitle . ' (' . $i . ')'; $i++; } @@ -339,6 +346,7 @@ public function create( * @param string|null $userId * @param ColumnDto $columnDto * @return Column + * @throws BadRequestError * @throws InternalError */ public function update( @@ -355,9 +363,10 @@ public function update( throw new PermissionError('update column id = ' . $columnId . ' is not allowed.'); } $this->columnDtoValidator->validate($columnDto); + $title = $this->normalizeTitle($columnDto->getTitle(), false); - if ($columnDto->getTitle() !== null) { - $item->setTitle($columnDto->getTitle()); + if ($title !== null) { + $item->setTitle($title); } if ($columnDto->getType() !== null) { $item->setType($columnDto->getType()); @@ -404,6 +413,8 @@ public function update( $this->updateMetadata($item, $userId); return $this->enhanceColumn($this->mapper->update($item)); + } catch (BadRequestError $e) { + throw $e; } catch (Exception $e) { $this->logger->error($e->getMessage()); throw new InternalError($e->getMessage()); @@ -440,6 +451,21 @@ private function validateCustomSettings(?string $customSettings): void { } } + private function normalizeTitle(?string $title, bool $required): ?string { + if ($title === null) { + if ($required) { + throw new BadRequestError('Title is missing.'); + } + return null; + } + + try { + return (string)new Title($title); + } catch (\InvalidArgumentException $e) { + throw new BadRequestError($e->getMessage(), 0, $e); + } + } + private function updateMetadata(Column $column, ?string $userId, bool $setCreateData = false): void { if ($userId) { $column->setLastEditBy($userId); diff --git a/lib/Service/TableService.php b/lib/Service/TableService.php index 726ee1c73e..388036b315 100644 --- a/lib/Service/TableService.php +++ b/lib/Service/TableService.php @@ -26,6 +26,7 @@ use OCA\Tables\Model\SortRuleSet; use OCA\Tables\Model\TableScheme; use OCA\Tables\ResponseDefinitions; +use OCA\Tables\Service\ValueObject\Title; use OCP\App\IAppManager; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\MultipleObjectsReturnedException; @@ -271,10 +272,12 @@ public function find(int $id, bool $skipTableEnhancement = false, ?string $userI * @param string|null $userId * @return Table * @throws InternalError + * @throws \InvalidArgumentException * @noinspection DuplicatedCode */ public function create(string $title, string $template, ?string $emoji, ?string $description = '', ?string $userId = null): Table { $userId = $this->permissionsService->preCheckUserId($userId, false); // we can assume that the $userId is set + $title = (string)new Title($title); $time = new DateTime(); $item = new Table(); @@ -475,6 +478,7 @@ public function delete(int $id, ?string $userId = null): Table { * @throws InternalError * @throws NotFoundError * @throws PermissionError + * @throws \InvalidArgumentException */ public function update(int $id, ?string $title, ?string $emoji, ?string $description, ?bool $archived = null, ?string $userId = null, ?ColumnSettings $columnSettings = null, ?SortRuleSet $sort = null): Table { $userId = $this->permissionsService->preCheckUserId($userId); @@ -497,6 +501,7 @@ public function update(int $id, ?string $title, ?string $emoji, ?string $descrip $changes = new ChangeSet($table); $time = new DateTime(); if ($title !== null) { + $title = (string)new Title($title); $table->setTitle($title); } if ($emoji !== null) { diff --git a/lib/Service/ValueObject/Title.php b/lib/Service/ValueObject/Title.php index a7f842e61a..3a21f76fd4 100644 --- a/lib/Service/ValueObject/Title.php +++ b/lib/Service/ValueObject/Title.php @@ -15,6 +15,12 @@ class Title implements Stringable { public function __construct( protected string $title, ) { + $this->title = $this->normalize($this->title); + + if ($this->title === '') { + throw new \InvalidArgumentException('Title is missing.'); + } + if (strlen($this->title) > 200) { throw new \InvalidArgumentException('Title exceed maximum length of 200 bytes'); } @@ -23,4 +29,9 @@ public function __construct( public function __toString(): string { return $this->title; } + + private function normalize(string $title): string { + $normalizedTitle = preg_replace('/^[\s\p{Z}]+|[\s\p{Z}]+$/u', '', $title); + return $normalizedTitle ?? trim($title); + } } diff --git a/lib/Validation/ColumnDtoValidator.php b/lib/Validation/ColumnDtoValidator.php index c0cb8f58e2..03282ffe7a 100644 --- a/lib/Validation/ColumnDtoValidator.php +++ b/lib/Validation/ColumnDtoValidator.php @@ -9,12 +9,25 @@ use OCA\Tables\Dto\Column as ColumnDto; use OCA\Tables\Errors\BadRequestError; +use OCA\Tables\Service\ValueObject\Title; class ColumnDtoValidator { /** * @throws BadRequestError */ - public function validate(ColumnDto $columnDto): void { + public function validate(ColumnDto $columnDto, bool $requiresTitle = false): void { + $title = $columnDto->getTitle(); + if ($requiresTitle && $title === null) { + throw new BadRequestError('Title is missing.'); + } + if ($title !== null) { + try { + new Title($title); + } catch (\InvalidArgumentException $e) { + throw new BadRequestError($e->getMessage(), 0, $e); + } + } + $textMaxLength = $columnDto->getTextMaxLength(); if ($textMaxLength !== null && $textMaxLength < 0) { throw new BadRequestError('Maximum text length must be greater than or equal to 0.'); diff --git a/openapi.json b/openapi.json index 6a9442764f..1b05204d98 100644 --- a/openapi.json +++ b/openapi.json @@ -1225,6 +1225,24 @@ } } }, + "400": { + "description": "Invalid request data", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + }, "500": { "description": "", "content": { @@ -1329,6 +1347,24 @@ } } }, + "400": { + "description": "Invalid request data", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + }, "403": { "description": "No permissions", "content": { @@ -6639,6 +6675,44 @@ } } }, + "400": { + "description": "Invalid request data", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, "500": { "description": "", "content": { @@ -8836,6 +8910,44 @@ } } }, + "400": { + "description": "Invalid request data", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, "403": { "description": "No permission", "content": { @@ -9140,6 +9252,44 @@ } } }, + "400": { + "description": "Invalid request data", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, "403": { "description": "No permission", "content": { @@ -9432,6 +9582,44 @@ } } }, + "400": { + "description": "Invalid request data", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, "403": { "description": "No permission", "content": { @@ -9723,6 +9911,44 @@ } } }, + "400": { + "description": "Invalid request data", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, "403": { "description": "No permission", "content": { @@ -10024,6 +10250,44 @@ } } }, + "400": { + "description": "Invalid request data", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, "403": { "description": "No permission", "content": { diff --git a/package-lock.json b/package-lock.json index ce85db5bda..352e89e9eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3275,6 +3275,17 @@ "@vue/shared": "3.5.34" } }, + "node_modules/@nextcloud/dialogs/node_modules/@vue/devtools-api": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz", + "integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@vue/devtools-kit": "^7.7.9" + } + }, "node_modules/@nextcloud/dialogs/node_modules/@vue/devtools-shared": { "version": "8.1.2", "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.1.2.tgz", @@ -3476,6 +3487,29 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/@nextcloud/dialogs/node_modules/pinia": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz", + "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@vue/devtools-api": "^7.7.7" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.5.0", + "vue": "^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/@nextcloud/dialogs/node_modules/readdirp": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", @@ -6169,6 +6203,34 @@ "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", "license": "MIT" }, + "node_modules/@vue/devtools-kit": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz", + "integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@vue/devtools-shared": "^7.7.9", + "birpc": "^2.3.0", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz", + "integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "rfdc": "^1.4.1" + } + }, "node_modules/@vue/eslint-config-typescript": { "version": "13.0.0", "resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-13.0.0.tgz", @@ -8004,6 +8066,23 @@ "dev": true, "license": "MIT" }, + "node_modules/copy-anything": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", + "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "is-what": "^5.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/core-js": { "version": "3.37.0", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.37.0.tgz", @@ -11686,6 +11765,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-what": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", + "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -13359,6 +13452,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -14068,6 +14169,14 @@ "dev": true, "license": "MIT" }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -15597,6 +15706,21 @@ } } }, + "node_modules/rollup-plugin-license/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/rollup-plugin-node-externals": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/rollup-plugin-node-externals/-/rollup-plugin-node-externals-8.1.1.tgz", @@ -16061,6 +16185,17 @@ "spdx-ranges": "^2.0.0" } }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "license": "BSD-3-Clause", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/split-ca": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", @@ -16798,6 +16933,20 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/superjson": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", + "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "copy-anything": "^4" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", diff --git a/src/modules/modals/CreateColumn.vue b/src/modules/modals/CreateColumn.vue index 91c70d8a17..d5ead08dad 100644 --- a/src/modules/modals/CreateColumn.vue +++ b/src/modules/modals/CreateColumn.vue @@ -289,7 +289,8 @@ export default { return str.charAt(0).toUpperCase() + str.slice(1) }, async actionConfirm() { - if (!this.column.title) { + const title = this.column.title?.trim() ?? '' + if (!title) { showInfo(t('tables', 'Please insert a title for the new column.')) this.titleMissingError = true } else if (this.column.customSettings?.width @@ -301,6 +302,7 @@ export default { showInfo(t('tables', 'You need to select a type for the new column.')) this.typeMissingError = true } else { + this.column.title = title this.$emit('save', this.prepareSubmitData()) if (this.isCustomSave) { this.reset() diff --git a/src/modules/modals/CreateTable.vue b/src/modules/modals/CreateTable.vue index 0e5e7845db..8e245de829 100644 --- a/src/modules/modals/CreateTable.vue +++ b/src/modules/modals/CreateTable.vue @@ -184,10 +184,12 @@ export default { this.actionCancel() return } - if (this.title === '') { + const title = this.title.trim() + if (title === '') { showError(t('tables', 'Cannot create new table. Title is missing.')) this.errorTitle = true } else { + this.title = title const newTableId = await this.sendNewTableToBE(this.templateChoice) if (newTableId) { await this.$router.push('/table/' + newTableId) diff --git a/src/modules/modals/EditColumn.vue b/src/modules/modals/EditColumn.vue index e5981514a4..465cf50e7a 100644 --- a/src/modules/modals/EditColumn.vue +++ b/src/modules/modals/EditColumn.vue @@ -165,11 +165,13 @@ export default { this.$emit('close') }, async saveColumn() { - if (this.editColumn.title === '') { + const title = this.editColumn.title?.trim() ?? '' + if (title === '') { showError(t('tables', 'Cannot update column. Title is missing.')) this.editErrorTitle = true return } + this.editColumn.title = title if (this.editColumn.customSettings?.width && (this.editColumn.customSettings?.width < COLUMN_WIDTH_MIN || this.editColumn.customSettings?.width > COLUMN_WIDTH_MAX)) { diff --git a/src/modules/modals/EditTable.vue b/src/modules/modals/EditTable.vue index 455db33c69..ee2cbeee84 100644 --- a/src/modules/modals/EditTable.vue +++ b/src/modules/modals/EditTable.vue @@ -170,10 +170,12 @@ export default { this.open = false }, async submit() { - if (this.title === '') { + const title = this.title.trim() + if (title === '') { showError(t('tables', 'Cannot update table. Title is missing.')) this.errorTitle = true } else { + this.title = title const res = await this.updateTable({ id: this.tableId, data: { title: this.title, emoji: this.icon, description: this.localTable.description, columnSettings: this.localColumnSettings, sort: this.localSortRules } }) if (res) { showSuccess(t('tables', 'Updated table "{emoji}{table}".', { emoji: this.icon ? this.icon + ' ' : '', table: this.title })) diff --git a/src/types/openapi/openapi.ts b/src/types/openapi/openapi.ts index 0419b6bda4..ae8deca03c 100644 --- a/src/types/openapi/openapi.ts +++ b/src/types/openapi/openapi.ts @@ -1298,6 +1298,17 @@ export interface operations { readonly "application/json": components["schemas"]["Table"]; }; }; + /** @description Invalid request data */ + readonly 400: { + headers: { + readonly [name: string]: unknown; + }; + content: { + readonly "application/json": { + readonly message: string; + }; + }; + }; /** @description Current user is not logged in */ readonly 401: { headers: { @@ -1428,6 +1439,17 @@ export interface operations { readonly "application/json": components["schemas"]["Table"]; }; }; + /** @description Invalid request data */ + readonly 400: { + headers: { + readonly [name: string]: unknown; + }; + content: { + readonly "application/json": { + readonly message: string; + }; + }; + }; /** @description Current user is not logged in */ readonly 401: { headers: { @@ -4371,6 +4393,22 @@ export interface operations { }; }; }; + /** @description Invalid request data */ + readonly 400: { + headers: { + readonly [name: string]: unknown; + }; + content: { + readonly "application/json": { + readonly ocs: { + readonly meta: components["schemas"]["OCSMeta"]; + readonly data: { + readonly message: string; + }; + }; + }; + }; + }; /** @description Current user is not logged in */ readonly 401: { headers: { @@ -5338,6 +5376,22 @@ export interface operations { }; }; }; + /** @description Invalid request data */ + readonly 400: { + headers: { + readonly [name: string]: unknown; + }; + content: { + readonly "application/json": { + readonly ocs: { + readonly meta: components["schemas"]["OCSMeta"]; + readonly data: { + readonly message: string; + }; + }; + }; + }; + }; /** @description Current user is not logged in */ readonly 401: { headers: { @@ -5488,6 +5542,22 @@ export interface operations { }; }; }; + /** @description Invalid request data */ + readonly 400: { + headers: { + readonly [name: string]: unknown; + }; + content: { + readonly "application/json": { + readonly ocs: { + readonly meta: components["schemas"]["OCSMeta"]; + readonly data: { + readonly message: string; + }; + }; + }; + }; + }; /** @description Current user is not logged in */ readonly 401: { headers: { @@ -5628,6 +5698,22 @@ export interface operations { }; }; }; + /** @description Invalid request data */ + readonly 400: { + headers: { + readonly [name: string]: unknown; + }; + content: { + readonly "application/json": { + readonly ocs: { + readonly meta: components["schemas"]["OCSMeta"]; + readonly data: { + readonly message: string; + }; + }; + }; + }; + }; /** @description Current user is not logged in */ readonly 401: { headers: { @@ -5769,6 +5855,22 @@ export interface operations { }; }; }; + /** @description Invalid request data */ + readonly 400: { + headers: { + readonly [name: string]: unknown; + }; + content: { + readonly "application/json": { + readonly ocs: { + readonly meta: components["schemas"]["OCSMeta"]; + readonly data: { + readonly message: string; + }; + }; + }; + }; + }; /** @description Current user is not logged in */ readonly 401: { headers: { @@ -5926,6 +6028,22 @@ export interface operations { }; }; }; + /** @description Invalid request data */ + readonly 400: { + headers: { + readonly [name: string]: unknown; + }; + content: { + readonly "application/json": { + readonly ocs: { + readonly meta: components["schemas"]["OCSMeta"]; + readonly data: { + readonly message: string; + }; + }; + }; + }; + }; /** @description Current user is not logged in */ readonly 401: { headers: {