From 01d96196509a1763cbda577f9d015b9a73ce3145 Mon Sep 17 00:00:00 2001 From: Rello Date: Wed, 18 Mar 2026 12:50:28 +0100 Subject: [PATCH 01/31] Add persistent per-view layouts Signed-off-by: Rello --- cypress/e2e/view.cy.js | 30 +++ lib/Constants/ViewUpdatableParameters.php | 1 + lib/Controller/Api1Controller.php | 5 +- lib/Controller/ApiTablesController.php | 2 + lib/Controller/ViewController.php | 6 +- lib/Db/View.php | 8 + .../Version1000Date20260318000000.php | 40 ++++ lib/Model/ViewUpdateInput.php | 25 +++ lib/ResponseDefinitions.php | 1 + lib/Service/TableTemplateService.php | 2 +- lib/Service/ViewService.php | 4 +- src/modules/main/sections/MainWrapper.vue | 4 +- src/modules/modals/ViewSettings.vue | 28 ++- .../ncTable/sections/CustomTable.vue | 185 +++++++++++++++--- tests/unit/Db/ViewLayoutTest.php | 65 ++++++ 15 files changed, 375 insertions(+), 31 deletions(-) create mode 100644 lib/Migration/Version1000Date20260318000000.php create mode 100644 tests/unit/Db/ViewLayoutTest.php diff --git a/cypress/e2e/view.cy.js b/cypress/e2e/view.cy.js index 9e267a1507..13b855dba1 100644 --- a/cypress/e2e/view.cy.js +++ b/cypress/e2e/view.cy.js @@ -7,6 +7,7 @@ const firstTitle = 'Test view' const secondTitle = 'Test view 2' const thirdTitle = 'Test view 3' const fourthTitle = 'Test view 4' +const fifthTitle = 'Gallery view' describe('Interact with views', () => { @@ -59,6 +60,7 @@ describe('Interact with views', () => { cy.get('[data-cy="navigationTableItem"]').contains(secondTitle).should('not.exist') cy.get('[data-cy="navigationTableItem"]').contains(thirdTitle).should('not.exist') cy.get('[data-cy="navigationTableItem"]').contains(fourthTitle).should('not.exist') + cy.get('[data-cy="navigationTableItem"]').contains(fifthTitle).should('not.exist') }) it('Create view and insert rows in the view', () => { @@ -156,6 +158,34 @@ describe('Interact with views', () => { // cy.get('[data-cy="editRowSaveButton"]').contains('Save').click() }) + + + it('Persists the configured gallery layout for a view', () => { + cy.loadTable('View test table') + cy.get('[data-cy="customTableAction"] button').click() + cy.get('[data-cy="dataTableCreateViewBtn"]').contains('Create view').click({ force: true }) + cy.get('[data-cy="viewSettingsDialogTitleInput"]').type(fifthTitle) + cy.get('[data-cy="viewLayoutGallery"]').click({ force: true }) + + cy.intercept({ method: 'POST', url: '**/apps/tables/view' }).as('createView') + cy.intercept({ method: 'PUT', url: '**/apps/tables/view/*' }).as('updateView') + cy.get('[data-cy="modifyViewBtn"]').contains('Create View').click() + cy.wait('@createView').its('request.body').should('include', { layout: 'gallery' }) + cy.wait('@updateView').its('request.body.data').should('include', { layout: 'gallery' }) + + cy.get('[data-cy="customTableRow"]').should('not.exist') + cy.get('[data-cy="galleryLayoutBody"]').should('have.length.at.least', 1) + cy.get('[data-cy="galleryMetadataItem"]').contains('title').should('not.exist') + cy.get('[data-cy="galleryMetadataItem"]').contains('selection').should('exist') + cy.get('[data-cy="viewSettingsDialog"]').should('not.exist') + cy.contains('.options', 'Gallery').should('not.exist') + + cy.reload() + cy.loadView(fifthTitle) + cy.get('[data-cy="galleryLayoutBody"]').should('have.length.at.least', 1) + cy.get('[data-cy="customTableRow"]').should('not.exist') + }) + it('Create view and delete rows in the view', () => { cy.loadTable('View test table') diff --git a/lib/Constants/ViewUpdatableParameters.php b/lib/Constants/ViewUpdatableParameters.php index f991af4525..56bedd065a 100644 --- a/lib/Constants/ViewUpdatableParameters.php +++ b/lib/Constants/ViewUpdatableParameters.php @@ -16,4 +16,5 @@ enum ViewUpdatableParameters: string { case SORT = 'sort'; case FILTER = 'filter'; case COLUMN_SETTINGS = 'columns'; + case LAYOUT = 'layout'; } diff --git a/lib/Controller/Api1Controller.php b/lib/Controller/Api1Controller.php index 0db4b1eb8b..8663b74a26 100644 --- a/lib/Controller/Api1Controller.php +++ b/lib/Controller/Api1Controller.php @@ -346,9 +346,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()]; @@ -404,6 +404,7 @@ public function getView(int $viewId): DataResponse { * columns?: list, * columnSettings?: list, * sort?: list, + * layout?: 'table'|'tiles'|'gallery'|null, * filter?: list> * } $data fields of the view with their new values * @return DataResponse|DataResponse diff --git a/lib/Controller/ApiTablesController.php b/lib/Controller/ApiTablesController.php index 3ccc8115f3..4d9e309301 100644 --- a/lib/Controller/ApiTablesController.php +++ b/lib/Controller/ApiTablesController.php @@ -181,6 +181,7 @@ public function createFromScheme(string $title, string $emoji, string $descripti $view['emoji'], $table, $this->userId, + $view['layout'] ?? null, ); $inputColumnsArray = []; @@ -218,6 +219,7 @@ public function createFromScheme(string $title, string $emoji, string $descripti array_merge($inputColumnsArray, [ 'sort' => $newSort, 'filter' => $newFilter, + 'layout' => $view['layout'] ?? null, ]) )); } diff --git a/lib/Controller/ViewController.php b/lib/Controller/ViewController.php index 2828dc4021..e8f6a9205e 100644 --- a/lib/Controller/ViewController.php +++ b/lib/Controller/ViewController.php @@ -78,9 +78,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); }); } diff --git a/lib/Db/View.php b/lib/Db/View.php index e3d87a9e63..93ed00b05e 100644 --- a/lib/Db/View.php +++ b/lib/Db/View.php @@ -45,6 +45,8 @@ * @method setEmoji(string $emoji) * @method getDescription(): string * @method setDescription(string $description) + * @method getLayout(): ?string + * @method setLayout(?string $layout) * @method getIsShared(): bool * @method setIsShared(bool $isShared) * @method getOnSharePermissions(): ?Permissions @@ -74,6 +76,7 @@ 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; // virtual properties protected ?bool $isShared = null; @@ -171,6 +174,10 @@ 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'; + } + private function getSharePermissions(): ?Permissions { return $this->getOnSharePermissions(); } @@ -199,6 +206,7 @@ public function jsonSerialize(): array { 'hasShares' => (bool)$this->hasShares, 'rowsCount' => $this->rowsCount ?: 0, 'ownerDisplayName' => $this->ownerDisplayName, + 'layout' => $this->getLayoutNormalized(), ]; $serialisedJson['filter'] = $this->getFilterArray(); diff --git a/lib/Migration/Version1000Date20260318000000.php b/lib/Migration/Version1000Date20260318000000.php new file mode 100644 index 0000000000..82dec0be4e --- /dev/null +++ b/lib/Migration/Version1000Date20260318000000.php @@ -0,0 +1,40 @@ +hasTable($tableName)) { + return null; + } + + $table = $schema->getTable($tableName); + if (!$table->hasColumn('layout')) { + $table->addColumn('layout', Types::STRING, [ + 'notnull' => false, + 'length' => 16, + ]); + } + + return $schema; + } +} diff --git a/lib/Model/ViewUpdateInput.php b/lib/Model/ViewUpdateInput.php index 5c1972f1d5..cfbf116e33 100644 --- a/lib/Model/ViewUpdateInput.php +++ b/lib/Model/ViewUpdateInput.php @@ -29,6 +29,7 @@ public function __construct( protected readonly ?ColumnSettings $columnSettings = null, protected readonly ?FilterSet $filterSet = null, protected readonly ?SortRuleSet $sortRuleSet = null, + protected readonly ?string $layout = null, ) { } @@ -51,6 +52,9 @@ public function updateDetail(): Generator { if ($this->sortRuleSet) { yield ViewUpdatableParameters::SORT => $this->sortRuleSet; } + if ($this->layout !== null) { + yield ViewUpdatableParameters::LAYOUT => $this->layout; + } } /** @@ -61,6 +65,7 @@ public function updateDetail(): Generator { * columns?: list, * columnSettings?: list, * sort?: list, + * layout?: 'table'|'tiles'|'gallery'|null, * filter?: list> * } $data */ @@ -80,6 +85,8 @@ public static function fromInputArray(array $data): self { $data['columnSettings'] = $value; } + $layout = self::normalizeLayout($data['layout'] ?? null); + return new self( title: $data['title'] ? new Title($data['title']) : null, description: $data['description'] ?? null, @@ -87,9 +94,27 @@ public static function fromInputArray(array $data): self { columnSettings: $data['columnSettings'] ? ColumnSettings::createFromInputArray($data['columnSettings']) : null, filterSet: $data['filter'] ? FilterSet::createFromInputArray($data['filter']) : null, sortRuleSet: $data['sort'] ? SortRuleSet::createFromInputArray($data['sort']) : null, + layout: $layout, ); } + + 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) { diff --git a/lib/ResponseDefinitions.php b/lib/ResponseDefinitions.php index 4ab08bdcd0..38eaeda627 100644 --- a/lib/ResponseDefinitions.php +++ b/lib/ResponseDefinitions.php @@ -29,6 +29,7 @@ * columnSettings:list, * sort: list, * filter: list>, + * layout: 'table'|'tiles'|'gallery', * isShared: bool, * favorite: bool, * onSharePermissions: ?array{ diff --git a/lib/Service/TableTemplateService.php b/lib/Service/TableTemplateService.php index c11fac37bf..6e2e38c6e3 100644 --- a/lib/Service/TableTemplateService.php +++ b/lib/Service/TableTemplateService.php @@ -856,7 +856,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()); diff --git a/lib/Service/ViewService.php b/lib/Service/ViewService.php index cc05fc5b81..2431424bf0 100644 --- a/lib/Service/ViewService.php +++ b/lib/Service/ViewService.php @@ -193,7 +193,7 @@ public function findSharedViewsWithMe(?string $userId = null): array { * @throws InternalError * @throws PermissionError */ - public function create(string $title, ?string $emoji, Table $table, ?string $userId = null): View { + public function create(string $title, ?string $emoji, Table $table, ?string $userId = null, ?string $layout = null): View { /** @var string $userId */ $userId = $this->permissionsService->preCheckUserId($userId, false); // $userId is set @@ -209,6 +209,7 @@ public function create(string $title, ?string $emoji, Table $table, ?string $use $item->setEmoji($emoji); } $item->setDescription(''); + $item->setLayout(in_array($layout, ['tiles', 'gallery'], true) ? $layout : null); $item->setTableId($table->getId()); $item->setCreatedBy($userId); $item->setLastEditBy($userId); @@ -610,6 +611,7 @@ public function importView(int $tableId, array $view, string $userId): void { $item->setColumns(json_encode($view['columnSettings'])); $item->setSort(json_encode($view['sort'])); $item->setFilter(json_encode($view['filter'])); + $item->setLayout(in_array($view['layout'] ?? null, ['tiles', 'gallery'], true) ? $view['layout'] : null); try { $this->mapper->insert($item); } catch (\Exception $e) { diff --git a/src/modules/main/sections/MainWrapper.vue b/src/modules/main/sections/MainWrapper.vue index bbd8099484..b0b53593aa 100644 --- a/src/modules/main/sections/MainWrapper.vue +++ b/src/modules/main/sections/MainWrapper.vue @@ -132,7 +132,9 @@ export default { // Since we show one page at a time, no need keep other tables in the store this.clearState() - this.viewSetting = {} + this.viewSetting = { + layout: this.isView ? (this.element?.layout ?? 'table') : 'table', + } if (this.isView && this.element?.sort?.length) { this.viewSetting.presetSorting = [...this.element.sort] } diff --git a/src/modules/modals/ViewSettings.vue b/src/modules/modals/ViewSettings.vue index fef14d5f89..61bbdbcf19 100644 --- a/src/modules/modals/ViewSettings.vue +++ b/src/modules/modals/ViewSettings.vue @@ -53,6 +53,20 @@ :generated-filters="viewSetting ? generatedView.filter : null" :columns="allColumns" /> + + +
+ + {{ t('tables', 'Table') }} + + + {{ t('tables', 'Tile') }} + + + {{ t('tables', 'Gallery') }} + +
+
@@ -461,6 +525,91 @@ export default { } } +.card-layout { + display: grid; + gap: 16px; + padding-inline: calc(var(--default-grid-baseline) * 2); + padding-top: 8px; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); +} + +.layout-card { + padding: 0; + border: 1px solid var(--color-border-dark); + border-radius: var(--border-radius-large); + overflow: hidden; + background: var(--color-main-background); + text-align: start; + cursor: pointer; +} + +.layout-card__image-wrapper { + position: relative; + aspect-ratio: 1 / 1; + background: var(--color-background-dark); +} + +.layout-card__image { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + +.layout-card__no-image { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + color: var(--color-text-maxcontrast); +} + +.layout-card__title-banner { + position: absolute; + left: 0; + right: 0; + bottom: 0; + padding: 12px; + background: rgba(0,0,0,0.4); + color: #fff; + text-align: center; + font-weight: 600; +} + +.layout-card__body { + padding: 12px; +} + +.layout-card__title { + font-weight: 600; + margin-bottom: 8px; +} + +.layout-card__metadata { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: 6px; +} + +.layout-card__metadata li { + display: flex; + flex-direction: column; + gap: 2px; +} + +.layout-card__metadata-label { + font-size: 12px; + color: var(--color-text-maxcontrast); +} + +.layout-card__metadata-value { + white-space: normal; + word-break: break-word; +} + :deep(table) { position: relative; border-collapse: collapse; @@ -472,7 +621,6 @@ export default { * { border: none; } - // white-space: nowrap; td, th { padding-inline-end: 8px; @@ -505,7 +653,7 @@ export default { th { vertical-align: middle; color: var(--color-text-maxcontrast); - box-shadow: inset 0 -1px 0 var(--color-border); // use box-shadow instead of border to be compatible with sticky heads + box-shadow: inset 0 -1px 0 var(--color-border); background-color: var(--color-main-background-translucent); z-index: 5; } @@ -513,7 +661,6 @@ export default { } tbody { - td { text-align: start; vertical-align: middle; @@ -537,7 +684,6 @@ export default { background-color: inherit; } - // viewer integration .editor-wrapper { min-width: 100px; overflow-y: auto; @@ -559,7 +705,6 @@ export default { } } - // inline editing .inline-editing-container { position: relative; width: 100%; @@ -603,15 +748,12 @@ export default { } tr>td.sticky:last-child { - // visibility: hidden; opacity: 0; } tr:hover>td:last-child { - // visibility: visible; opacity: 1; } - } .table-row-leave-active { @@ -627,5 +769,4 @@ export default { margin-bottom: 0 !important; transform: translateX(-1rem); } - diff --git a/tests/unit/Db/ViewLayoutTest.php b/tests/unit/Db/ViewLayoutTest.php new file mode 100644 index 0000000000..e7c486d536 --- /dev/null +++ b/tests/unit/Db/ViewLayoutTest.php @@ -0,0 +1,65 @@ +getConnection()->createSchema(); + $table = $schema->getTable($this->getConnection()->getPrefix() . 'tables_views'); + + $this->assertTrue($table->hasColumn('layout')); + $this->assertSame(16, $table->getColumn('layout')->getLength()); + } + + public function testViewUpdateInputAcceptsValidLayout(): void { + $input = ViewUpdateInput::fromInputArray(['layout' => 'tiles']); + $updates = iterator_to_array($input->updateDetail()); + + $this->assertSame('tiles', $updates[ViewUpdatableParameters::LAYOUT]); + } + + public function testViewUpdateInputDefaultsMissingOrTableLayoutToNull(): void { + $missing = iterator_to_array(ViewUpdateInput::fromInputArray([])->updateDetail()); + $table = iterator_to_array(ViewUpdateInput::fromInputArray(['layout' => 'table'])->updateDetail()); + + $this->assertArrayNotHasKey(ViewUpdatableParameters::LAYOUT, $missing); + $this->assertArrayNotHasKey(ViewUpdatableParameters::LAYOUT, $table); + } + + public function testViewUpdateInputRejectsInvalidLayout(): void { + $this->expectException(InvalidArgumentException::class); + ViewUpdateInput::fromInputArray(['layout' => 'masonry']); + } + + public function testViewSerializationNormalizesLayout(): void { + $defaultView = new View(); + $defaultView->setTitle('Default'); + $defaultView->setColumns('[]'); + $defaultView->setSort('[]'); + $defaultView->setFilter('[]'); + $defaultView->setLayout(null); + + $galleryView = new View(); + $galleryView->setTitle('Gallery'); + $galleryView->setColumns('[]'); + $galleryView->setSort('[]'); + $galleryView->setFilter('[]'); + $galleryView->setLayout('gallery'); + + $this->assertSame('table', $defaultView->jsonSerialize()['layout']); + $this->assertSame('gallery', $galleryView->jsonSerialize()['layout']); + } +} From 19fa7e5a0bdbcca5fc2eba4d4c19a94617987de4 Mon Sep 17 00:00:00 2001 From: Rello Date: Wed, 18 Mar 2026 13:06:49 +0100 Subject: [PATCH 02/31] Fix layout review feedback Signed-off-by: Rello --- src/shared/components/ncTable/sections/CustomTable.vue | 5 ++--- tests/unit/Db/ViewLayoutTest.php | 10 +++++----- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/shared/components/ncTable/sections/CustomTable.vue b/src/shared/components/ncTable/sections/CustomTable.vue index 280d73d3b1..f36d372758 100644 --- a/src/shared/components/ncTable/sections/CustomTable.vue +++ b/src/shared/components/ncTable/sections/CustomTable.vue @@ -566,8 +566,7 @@ export default { .layout-card__title-banner { position: absolute; - left: 0; - right: 0; + inset-inline: 0; bottom: 0; padding: 12px; background: rgba(0,0,0,0.4); @@ -607,7 +606,7 @@ export default { .layout-card__metadata-value { white-space: normal; - word-break: break-word; + overflow-wrap: anywhere; } :deep(table) { diff --git a/tests/unit/Db/ViewLayoutTest.php b/tests/unit/Db/ViewLayoutTest.php index e7c486d536..27ef576274 100644 --- a/tests/unit/Db/ViewLayoutTest.php +++ b/tests/unit/Db/ViewLayoutTest.php @@ -9,11 +9,11 @@ namespace OCA\Tables\Tests\Unit\Db; +use InvalidArgumentException; use OCA\Tables\Constants\ViewUpdatableParameters; use OCA\Tables\Db\View; use OCA\Tables\Model\ViewUpdateInput; use OCA\Tables\Tests\Unit\Database\DatabaseTestCase; -use InvalidArgumentException; class ViewLayoutTest extends DatabaseTestCase { public function testMigrationAddsLayoutColumn(): void { @@ -25,15 +25,15 @@ public function testMigrationAddsLayoutColumn(): void { } public function testViewUpdateInputAcceptsValidLayout(): void { - $input = ViewUpdateInput::fromInputArray(['layout' => 'tiles']); + $input = ViewUpdateInput::fromInputArray(['title' => 'Layout view', 'layout' => 'tiles']); $updates = iterator_to_array($input->updateDetail()); $this->assertSame('tiles', $updates[ViewUpdatableParameters::LAYOUT]); } public function testViewUpdateInputDefaultsMissingOrTableLayoutToNull(): void { - $missing = iterator_to_array(ViewUpdateInput::fromInputArray([])->updateDetail()); - $table = iterator_to_array(ViewUpdateInput::fromInputArray(['layout' => 'table'])->updateDetail()); + $missing = iterator_to_array(ViewUpdateInput::fromInputArray(['title' => 'Missing layout'])->updateDetail()); + $table = iterator_to_array(ViewUpdateInput::fromInputArray(['title' => 'Table layout', 'layout' => 'table'])->updateDetail()); $this->assertArrayNotHasKey(ViewUpdatableParameters::LAYOUT, $missing); $this->assertArrayNotHasKey(ViewUpdatableParameters::LAYOUT, $table); @@ -41,7 +41,7 @@ public function testViewUpdateInputDefaultsMissingOrTableLayoutToNull(): void { public function testViewUpdateInputRejectsInvalidLayout(): void { $this->expectException(InvalidArgumentException::class); - ViewUpdateInput::fromInputArray(['layout' => 'masonry']); + ViewUpdateInput::fromInputArray(['title' => 'Invalid layout', 'layout' => 'masonry']); } public function testViewSerializationNormalizesLayout(): void { From 23edbaf9125dc386af91661731dd45b0e6fbb688 Mon Sep 17 00:00:00 2001 From: Rello Date: Thu, 19 Mar 2026 21:11:49 +0100 Subject: [PATCH 03/31] Refactor value assignment for JsonSerializable Signed-off-by: Rello --- lib/Service/ViewService.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/Service/ViewService.php b/lib/Service/ViewService.php index 2431424bf0..bb0535be3d 100644 --- a/lib/Service/ViewService.php +++ b/lib/Service/ViewService.php @@ -252,12 +252,12 @@ public function update(int $id, ViewUpdateInput $data, ?string $userId = null, b $this->assertInputColumnsAreValid($view, $userId, $value); } - if ($value instanceof JsonSerializable) { - $insertableValue = json_encode($value); - } + $insertableValue = $value instanceof JsonSerializable + ? json_encode($value) + : $value; $setterMethod = 'set' . ucfirst($parameter->value); - $view->$setterMethod($insertableValue ?? $value); + $view->$setterMethod($insertableValue); } $time = new DateTime(); From aea4c66de0b9e8120ec12b5395fe841b2251ef6e Mon Sep 17 00:00:00 2001 From: Rello Date: Thu, 19 Mar 2026 21:13:20 +0100 Subject: [PATCH 04/31] Refactor layout options to use a table format Signed-off-by: Rello --- src/modules/modals/ViewSettings.vue | 52 +++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/src/modules/modals/ViewSettings.vue b/src/modules/modals/ViewSettings.vue index 61bbdbcf19..804aa2c6de 100644 --- a/src/modules/modals/ViewSettings.vue +++ b/src/modules/modals/ViewSettings.vue @@ -55,17 +55,26 @@ -
- - {{ t('tables', 'Table') }} - - - {{ t('tables', 'Tile') }} - - - {{ t('tables', 'Gallery') }} - -
+ + + + + + + + + + + + + +
@@ -397,9 +406,24 @@ export default { } .layout-options { - display: flex; - flex-direction: column; - gap: 8px; + width: 100%; + table-layout: fixed; + border-collapse: separate; + border-spacing: 12px 8px; +} + +.layout-options td { + text-align: center; + vertical-align: middle; +} + +.layout-options__selection :deep(.checkbox-radio-switch) { + display: inline-flex; +} + +.layout-options__title td { + font-weight: 600; + color: var(--color-text-maxcontrast); } .sticky { From 0cb66deeba9630ab60845676f8d3dfa61c9db92e Mon Sep 17 00:00:00 2001 From: Rello Date: Thu, 19 Mar 2026 21:14:31 +0100 Subject: [PATCH 05/31] Add dynamic class binding for card layout Signed-off-by: Rello --- src/pages/View.vue | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/pages/View.vue b/src/pages/View.vue index e470492def..e1e70f6e2c 100644 --- a/src/pages/View.vue +++ b/src/pages/View.vue @@ -3,7 +3,7 @@ - SPDX-License-Identifier: AGPL-3.0-or-later -->