From c3fac2cc9233cea7b435987ca755fb74a3e87541 Mon Sep 17 00:00:00 2001 From: ITExilion Date: Thu, 22 Jan 2026 17:27:16 +0000 Subject: [PATCH] feat(views): add view grouping functionality - Add view_group column to database (migration) - Update View model with viewGroup property - Add viewGroup to updatable parameters in ViewService - Add group input field in ViewSettings modal - Display grouped views in navigation with collapsible sections --- lib/Db/View.php | 5 +- .../Version001000Date20250122000000.php | 40 ++++++++++ lib/Service/ViewService.php | 2 +- src/modules/modals/ViewSettings.vue | 14 ++++ .../partials/NavigationTableItem.vue | 76 +++++++++++++++++-- 5 files changed, 130 insertions(+), 7 deletions(-) create mode 100644 lib/Migration/Version001000Date20250122000000.php diff --git a/lib/Db/View.php b/lib/Db/View.php index 224f9b347f..02060e488c 100644 --- a/lib/Db/View.php +++ b/lib/Db/View.php @@ -35,7 +35,8 @@ * @method setCreatedAt(string $createdAt) * @method getFilter(): string * @method setFilter(string $filter) - * @method getLastEditBy(): string + * @method getViewGroup(): ?string + * @method setViewGroup(?string $viewGroup) * @method setLastEditBy(string $lastEditBy) * @method getLastEditAt(): string * @method setLastEditAt(string $lastEditAt) @@ -72,6 +73,7 @@ class View extends EntitySuper implements JsonSerializable { protected ?string $columns = null; // json protected ?string $sort = null; // json protected ?string $filter = null; // json + protected ?string $viewGroup = null; // virtual properties protected ?bool $isShared = null; @@ -207,6 +209,7 @@ public function jsonSerialize(): array { 'ownerDisplayName' => $this->ownerDisplayName, ]; $serialisedJson['filter'] = $this->getFilterArray(); + $serialisedJson['viewGroup'] = $this->viewGroup; return $serialisedJson; } diff --git a/lib/Migration/Version001000Date20250122000000.php b/lib/Migration/Version001000Date20250122000000.php new file mode 100644 index 0000000000..a1aa86b175 --- /dev/null +++ b/lib/Migration/Version001000Date20250122000000.php @@ -0,0 +1,40 @@ +hasTable('tables_views')) { + $table = $schema->getTable('tables_views'); + if (!$table->hasColumn('view_group')) { + $table->addColumn('view_group', Types::STRING, [ + 'notnull' => false, + 'default' => null, + 'length' => 255, + ]); + } + } + + return $schema; + } +} diff --git a/lib/Service/ViewService.php b/lib/Service/ViewService.php index 42fde22ace..f9a324d38f 100644 --- a/lib/Service/ViewService.php +++ b/lib/Service/ViewService.php @@ -262,7 +262,7 @@ public function update(int $id, array $data, ?string $userId = null, bool $skipT throw new PermissionError('PermissionError: can not update view with id ' . $id); } - $updatableParameter = ['title', 'emoji', 'description', 'sort', 'filter', 'columns', 'columnSettings']; + $updatableParameter = ['title', 'emoji', 'description', 'sort', 'filter', 'columns', 'columnSettings', 'viewGroup']; foreach ($data as $key => $value) { if (!in_array($key, $updatableParameter)) { diff --git a/src/modules/modals/ViewSettings.vue b/src/modules/modals/ViewSettings.vue index fef14d5f89..6849af26c9 100644 --- a/src/modules/modals/ViewSettings.vue +++ b/src/modules/modals/ViewSettings.vue @@ -35,6 +35,16 @@ +
+
+ {{ t('tables', 'Group') }} +
+
+ +
+
@@ -129,6 +139,7 @@ export default { open: false, title: '', description: '', + viewGroup: '', icon: '', errorTitle: false, selectedColumns: [], @@ -294,6 +305,7 @@ export default { title: this.title, description: this.description, emoji: this.icon, + viewGroup: this.viewGroup, } const res = await this.insertNewView({ data }) if (res) { @@ -316,6 +328,7 @@ export default { title: this.title, description: this.description, emoji: this.icon, + viewGroup: this.viewGroup, columnSettings: JSON.stringify(newColumnSettings), }, } @@ -344,6 +357,7 @@ export default { this.title = this.mutableView.title ?? '' this.description = this.mutableView.description ?? '' this.icon = this.mutableView.emoji ?? this.loadEmoji() + this.viewGroup = this.mutableView.viewGroup ?? '' this.errorTitle = false this.selectedColumns = this.mutableView.columnSettings ? this.mutableView.columnSettings.map(item => item.columnId) : null this.allColumns = [] diff --git a/src/modules/navigation/partials/NavigationTableItem.vue b/src/modules/navigation/partials/NavigationTableItem.vue index 9d01cf4f93..754e70630c 100644 --- a/src/modules/navigation/partials/NavigationTableItem.vue +++ b/src/modules/navigation/partials/NavigationTableItem.vue @@ -127,10 +127,23 @@ -
    - -
+
    + + + +
  • +
    + {{ collapsedGroups[groupName] ? "▶" : "▼" }} + {{ groupName }} + ({{ views.length }}) +
    +
      + +
    +
  • +
@@ -205,6 +218,7 @@ export default { data() { return { isParentOfActiveView: false, + collapsedGroups: {}, } }, @@ -222,7 +236,25 @@ export default { hasViews() { return this.getViews.length > 0 }, - }, + groupedViews() { + const groups = {} + const ungrouped = [] + this.getViews.forEach(view => { + if (view.viewGroup) { + if (!groups[view.viewGroup]) { + groups[view.viewGroup] = [] + } + groups[view.viewGroup].push(view) + } else { + ungrouped.push(view) + } + }) + return { groups, ungrouped } + }, + hasGroups() { + return Object.keys(this.groupedViews.groups).length > 0 + }, + }, watch: { activeView() { if (!this.isParentOfActiveView && this.activeView?.tableId === this.table?.id) { @@ -238,6 +270,9 @@ export default { methods: { ...mapActions(useTablesStore, ['favoriteTable', 'removeFavoriteTable', 'updateTable']), emit, + toggleGroup(groupName) { + this.$set(this.collapsedGroups, groupName, !this.collapsedGroups[groupName]) + }, deleteTable() { emit('tables:table:delete', this.table) }, @@ -341,4 +376,35 @@ export default { display: inline; } } +.view-group { + list-style: none; + padding: 0; + margin: 0; +} +.view-group-header { + display: flex; + align-items: center; + padding: 8px 8px 8px 32px; + cursor: pointer; + font-weight: 500; + color: var(--color-text-maxcontrast); + &:hover { + background-color: var(--color-background-hover); + } +} +.view-group-icon { + width: 16px; + margin-right: 8px; + font-size: 10px; +} +.view-group-name { + flex: 1; +} +.view-group-count { + font-size: 12px; + color: var(--color-text-lighter); +} +.view-group-items { + padding-left: 16px; +}