From 518d549a1eec64e9c9162a65850c5e2ff6b6cf1e Mon Sep 17 00:00:00 2001 From: taiebot <13300652+taiebot@users.noreply.github.com> Date: Thu, 1 Oct 2026 21:05:00 +0200 Subject: [PATCH] Feat app card layout Add a new view for app. Card layout --------- Assisted-by: ClaudeCode:claude-sonnet-5-5 Signed-off-by: taiebot <13300652+taiebot@users.noreply.github.com> --- lib/Controller/ContextController.php | 8 +- lib/Db/Context.php | 7 +- lib/Db/ContextMapper.php | 9 +- .../Version2301Date20260930000000.php | 42 +++ lib/ResponseDefinitions.php | 1 + lib/Service/ContextService.php | 8 +- .../main/sections/ContextResourceCards.vue | 254 ++++++++++++++++++ src/modules/modals/CreateContext.vue | 26 +- src/modules/modals/EditContext.vue | 23 +- src/pages/Context.vue | 106 +++++++- src/store/store.js | 5 + tests/unit/Service/ContextServiceTest.php | 68 +++++ 12 files changed, 526 insertions(+), 31 deletions(-) create mode 100644 lib/Migration/Version2301Date20260930000000.php create mode 100644 src/modules/main/sections/ContextResourceCards.vue diff --git a/lib/Controller/ContextController.php b/lib/Controller/ContextController.php index e87f8ca440..68b9b26c85 100644 --- a/lib/Controller/ContextController.php +++ b/lib/Controller/ContextController.php @@ -100,6 +100,7 @@ public function show(int $contextId): DataResponse { * @param string $iconName Material design icon name of the context * @param string $description Descriptive text of the context * @psalm-param list $nodes optional nodes to be connected to this context + * @param bool $cardViewEnabled Show the resources of the context as cards instead of stacked * * @return DataResponse|DataResponse * @@ -108,7 +109,7 @@ public function show(int $contextId): DataResponse { * 403: lacking permissions on a resource */ #[NoAdminRequired] - public function create(string $name, string $iconName, string $description = '', array $nodes = []): DataResponse { + public function create(string $name, string $iconName, string $description = '', array $nodes = [], bool $cardViewEnabled = false): DataResponse { try { if (!$this->isValidIcon($iconName)) { return new DataResponse(['message' => 'Invalid icon name'], Http::STATUS_BAD_REQUEST); @@ -120,6 +121,7 @@ public function create(string $name, string $iconName, string $description = '', $this->sanitizeInputNodes($nodes), $this->userId, 0, + $cardViewEnabled, )->jsonSerialize()); } catch (Exception $e) { return $this->handleError($e); @@ -138,6 +140,7 @@ public function create(string $name, string $iconName, string $description = '', * @param ?string $iconName provide this parameter to set a new icon * @param ?string $description provide this parameter to set a new description * @param ?array{id: int, type: int, permissions: int, order: int} $nodes provide this parameter to set a new list of nodes. + * @param ?bool $cardViewEnabled provide this parameter to show (or stop showing) the resources as cards * * @return DataResponse|DataResponse * @@ -149,7 +152,7 @@ public function create(string $name, string $iconName, string $description = '', * @CanManageContext */ #[NoAdminRequired] - public function update(int $contextId, ?string $name, ?string $iconName, ?string $description, ?array $nodes): DataResponse { + public function update(int $contextId, ?string $name, ?string $iconName, ?string $description, ?array $nodes, ?bool $cardViewEnabled = null): DataResponse { try { if ($iconName !== null && !$this->isValidIcon($iconName)) { return new DataResponse(['message' => 'Invalid icon name'], Http::STATUS_BAD_REQUEST); @@ -162,6 +165,7 @@ public function update(int $contextId, ?string $name, ?string $iconName, ?string $iconName, $description, $nodes, + $cardViewEnabled, )->jsonSerialize()); } catch (Exception|MultipleObjectsReturnedException $e) { return $this->handleError($e); diff --git a/lib/Db/Context.php b/lib/Db/Context.php index 3fa14f56b5..7be131ce1d 100644 --- a/lib/Db/Context.php +++ b/lib/Db/Context.php @@ -21,6 +21,8 @@ * @method setOwnerId(string $value): void * @method getOwnerType(): int * @method setOwnerType(int $value): void + * @method getCardViewEnabled(): ?bool + * @method setCardViewEnabled(?bool $value): void * * @method getSharing(): array * @method setSharing(array $value): void @@ -35,6 +37,7 @@ class Context extends EntitySuper implements JsonSerializable { protected ?string $description = null; protected ?string $ownerId = null; protected ?int $ownerType = null; + protected ?bool $cardViewEnabled = null; // virtual properties protected ?array $sharing = null; @@ -46,6 +49,7 @@ class Context extends EntitySuper implements JsonSerializable { public function __construct() { $this->addType('id', 'integer'); $this->addType('owner_type', 'integer'); + $this->addType('cardViewEnabled', 'boolean'); } public function jsonSerialize(): array { @@ -56,7 +60,8 @@ public function jsonSerialize(): array { 'iconName' => $this->getIcon(), 'description' => $this->getDescription(), 'owner' => $this->getOwnerId(), - 'ownerType' => $this->getOwnerType() + 'ownerType' => $this->getOwnerType(), + 'cardViewEnabled' => (bool)$this->getCardViewEnabled(), ]; // extended data diff --git a/lib/Db/ContextMapper.php b/lib/Db/ContextMapper.php index 0da199335e..9001e2100b 100644 --- a/lib/Db/ContextMapper.php +++ b/lib/Db/ContextMapper.php @@ -84,6 +84,7 @@ protected function formatResultRows(array $rows, ?string $userId) { 'description' => $rows[0]['description'], 'owner_id' => $rows[0]['owner_id'], 'owner_type' => $rows[0]['owner_type'], + 'card_view_enabled' => $rows[0]['card_view_enabled'] ?? null, ]; $formatted['sharing'] = array_reduce($rows, function (array $carry, array $item) use ($userId) { @@ -103,7 +104,9 @@ protected function formatResultRows(array $rows, ?string $userId) { 'display_mode_default' => (int)$item['display_mode_default'], ]; if ($userId !== null) { - $item['display_mode'] ??= $item['display_mode_default']; + if ($item['display_mode'] === null) { + $item['display_mode'] = $item['display_mode_default']; + } $carry[$item['share_id']]['display_mode'] = (int)$item['display_mode']; } return $carry; @@ -128,7 +131,9 @@ protected function formatResultRows(array $rows, ?string $userId) { // empty Context return $carry; } - $carry[$item['page_id']] ??= ['content' => []]; + if (!isset($carry[$item['page_id']])) { + $carry[$item['page_id']] = ['content' => []]; + } $carry[$item['page_id']]['id'] = (int)$item['page_id']; $carry[$item['page_id']]['page_type'] = $item['page_type']; if ($item['node_rel_id'] !== null) { diff --git a/lib/Migration/Version2301Date20260930000000.php b/lib/Migration/Version2301Date20260930000000.php new file mode 100644 index 0000000000..32bf2c13ec --- /dev/null +++ b/lib/Migration/Version2301Date20260930000000.php @@ -0,0 +1,42 @@ +hasTable('tables_contexts_context')) { + return null; + } + + $contexts = $schema->getTable('tables_contexts_context'); + + if (!$contexts->hasColumn('card_view_enabled')) { + $contexts->addColumn('card_view_enabled', Types::BOOLEAN, [ + 'notnull' => false, + 'default' => false, + 'comment' => 'Show the application resources as cards instead of stacked', + ]); + } + + return $schema; + } +} diff --git a/lib/ResponseDefinitions.php b/lib/ResponseDefinitions.php index c935d72524..f4287898eb 100644 --- a/lib/ResponseDefinitions.php +++ b/lib/ResponseDefinitions.php @@ -233,6 +233,7 @@ * description: string, * owner: string, * ownerType: int, + * cardViewEnabled: bool, * } * * @psalm-type TablesContextNavigation = array{ diff --git a/lib/Service/ContextService.php b/lib/Service/ContextService.php index edfd7d9a6e..383e20d2d0 100644 --- a/lib/Service/ContextService.php +++ b/lib/Service/ContextService.php @@ -132,13 +132,14 @@ public function findById(int $id, ?string $userId): Context { * @psalm-param list $nodes * @throws Exception|PermissionError|InvalidArgumentException */ - public function create(string $name, string $iconName, string $description, array $nodes, string $ownerId, int $ownerType): Context { + public function create(string $name, string $iconName, string $description, array $nodes, string $ownerId, int $ownerType, bool $cardViewEnabled = false): Context { $context = new Context(); $context->setName(trim($name)); $context->setIcon(trim($iconName)); $context->setDescription(trim($description)); $context->setOwnerId($ownerId); $context->setOwnerType($ownerType); + $context->setCardViewEnabled($cardViewEnabled); $this->atomic(function () use ($context, $nodes): void { $this->contextMapper->insert($context); @@ -159,7 +160,7 @@ public function create(string $name, string $iconName, string $description, arra * @throws DoesNotExistException * @throws PermissionError|MultipleObjectsReturnedException */ - public function update(int $contextId, string $userId, ?string $name, ?string $iconName, ?string $description, ?array $nodes): Context { + public function update(int $contextId, string $userId, ?string $name, ?string $iconName, ?string $description, ?array $nodes, ?bool $cardViewEnabled = null): Context { $context = $this->contextMapper->findById($contextId, $userId); if ($name !== null) { @@ -171,6 +172,9 @@ public function update(int $contextId, string $userId, ?string $name, ?string $i if ($description !== null) { $context->setDescription(trim($description)); } + if ($cardViewEnabled !== null) { + $context->setCardViewEnabled($cardViewEnabled); + } $hasUpdatedNodeInformation = false; if ($nodes !== null) { diff --git a/src/modules/main/sections/ContextResourceCards.vue b/src/modules/main/sections/ContextResourceCards.vue new file mode 100644 index 0000000000..afa3c14ce1 --- /dev/null +++ b/src/modules/main/sections/ContextResourceCards.vue @@ -0,0 +1,254 @@ + + + + + + diff --git a/src/modules/modals/CreateContext.vue b/src/modules/modals/CreateContext.vue index 9b56275a70..85558d5c39 100644 --- a/src/modules/modals/CreateContext.vue +++ b/src/modules/modals/CreateContext.vue @@ -44,14 +44,22 @@
- + {{ t('tables', 'Show in app list') }} - +
+
+ + {{ t('tables', 'Show resources as cards') }} + + +
@@ -64,7 +72,7 @@