From 1ec354f9fd5fbc442935ca4dbfd52b832792f667 Mon Sep 17 00:00:00 2001 From: Elizabeth Danzberger Date: Fri, 8 May 2026 12:24:14 -0400 Subject: [PATCH 01/29] feat(overview): add office overview page Adds the office overview page to the application bar, and the appropriate route, controller, and template Signed-off-by: Elizabeth Danzberger --- appinfo/info.xml | 8 +++++++ appinfo/routes.php | 3 +++ lib/Controller/OverviewController.php | 34 +++++++++++++++++++++++++++ templates/overview.php | 8 +++++++ 4 files changed, 53 insertions(+) create mode 100644 lib/Controller/OverviewController.php create mode 100644 templates/overview.php diff --git a/appinfo/info.xml b/appinfo/info.xml index 8024e57703..826a042ea2 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -55,4 +55,12 @@ You can also edit your documents off-line with the Collabora Office app from the OCA\Richdocuments\Settings\Personal OCA\Richdocuments\Settings\Section + + + Office + richdocuments.overview.index + app.svg + 10 + + diff --git a/appinfo/routes.php b/appinfo/routes.php index e17267f6ce..6646968513 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -24,6 +24,9 @@ // external api access ['name' => 'document#extAppGetData', 'url' => '/ajax/extapp/data/{fileId}', 'verb' => 'POST'], + // Office overview page + ['name' => 'overview#index', 'url' => '/overview', 'verb' => 'GET'], + // Settings ['name' => 'settings#setPersonalSettings', 'url' => 'ajax/personal.php', 'verb' => 'POST'], ['name' => 'settings#setSettings', 'url' => 'ajax/admin.php', 'verb' => 'POST'], diff --git a/lib/Controller/OverviewController.php b/lib/Controller/OverviewController.php new file mode 100644 index 0000000000..af73fc53fb --- /dev/null +++ b/lib/Controller/OverviewController.php @@ -0,0 +1,34 @@ + +
From f355cc69757f3baf495fb7735b6f8d765c7450ad Mon Sep 17 00:00:00 2001 From: Elizabeth Danzberger Date: Fri, 8 May 2026 13:35:25 -0400 Subject: [PATCH 02/29] feat(overview): office overview vue component Adds the OfficeOverview Vue component and renders it Signed-off-by: Elizabeth Danzberger --- lib/Controller/OverviewController.php | 2 + src/overview.js | 14 ++++++ src/views/OfficeOverview.vue | 70 +++++++++++++++++++++++++++ webpack.js | 1 + 4 files changed, 87 insertions(+) create mode 100644 src/overview.js create mode 100644 src/views/OfficeOverview.vue diff --git a/lib/Controller/OverviewController.php b/lib/Controller/OverviewController.php index af73fc53fb..8ad3893e58 100644 --- a/lib/Controller/OverviewController.php +++ b/lib/Controller/OverviewController.php @@ -13,6 +13,7 @@ use OCP\AppFramework\Http\Attribute\NoCSRFRequired; use OCP\AppFramework\Http\TemplateResponse; use OCP\IRequest; +use OCP\Util; class OverviewController extends Controller { @@ -29,6 +30,7 @@ public function __construct( #[NoAdminRequired] #[NoCSRFRequired] public function index(): TemplateResponse { + Util::addScript('richdocuments', 'richdocuments-overview'); return new TemplateResponse('richdocuments', 'overview'); } } diff --git a/src/overview.js b/src/overview.js new file mode 100644 index 0000000000..e2c8e60f5e --- /dev/null +++ b/src/overview.js @@ -0,0 +1,14 @@ +/** + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import './init-shared.js' +import Vue from 'vue' +import OfficeOverview from './views/OfficeOverview.vue' + +Vue.prototype.t = t +Vue.prototype.n = n + +new Vue({ + render: h => h(OfficeOverview), +}).$mount('#office-overview-vue') diff --git a/src/views/OfficeOverview.vue b/src/views/OfficeOverview.vue new file mode 100644 index 0000000000..f847bd5ef2 --- /dev/null +++ b/src/views/OfficeOverview.vue @@ -0,0 +1,70 @@ + + + + diff --git a/webpack.js b/webpack.js index c2d77b49ae..3ce6d64113 100644 --- a/webpack.js +++ b/webpack.js @@ -13,6 +13,7 @@ webpackConfig.entry = { 'init-viewer': path.join(__dirname, 'src', 'init-viewer.js'), fileActions: path.join(__dirname, 'src', 'file-actions.js'), document: path.join(__dirname, 'src', 'document.js'), + overview: path.join(__dirname, 'src', 'overview.js'), admin: path.join(__dirname, 'src', 'admin.js'), personal: path.join(__dirname, 'src', 'personal.js'), reference: path.join(__dirname, 'src', 'reference.js'), From 534fb8862a7f2d821befedff83bc5f98d83218ad Mon Sep 17 00:00:00 2001 From: Elizabeth Danzberger Date: Fri, 8 May 2026 14:00:57 -0400 Subject: [PATCH 03/29] feat(overview): support keyboard navigation These template response parameters are used to determine where the app navigation and content are, so that keyboard users can jump to them easily Signed-off-by: Elizabeth Danzberger --- lib/Controller/OverviewController.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/Controller/OverviewController.php b/lib/Controller/OverviewController.php index 8ad3893e58..9ced8c2d90 100644 --- a/lib/Controller/OverviewController.php +++ b/lib/Controller/OverviewController.php @@ -31,6 +31,9 @@ public function __construct( #[NoCSRFRequired] public function index(): TemplateResponse { Util::addScript('richdocuments', 'richdocuments-overview'); - return new TemplateResponse('richdocuments', 'overview'); + return new TemplateResponse('richdocuments', 'overview', [ + 'id-app-content' => '#app-content-vue', + 'id-app-navigation' => '#app-navigation-vue', + ]); } } From d06eeebb9f635eb11c6383850f8a6173d8b81481 Mon Sep 17 00:00:00 2001 From: Elizabeth Danzberger Date: Fri, 8 May 2026 14:02:43 -0400 Subject: [PATCH 04/29] fix(overview): update Vue mount point The OfficeOverview Vue component needs to be rendered directly to `#content` else it results in a weird double-margin visual bug. It causes some styles to be applied twice. This is also consistent with other apps. Signed-off-by: Elizabeth Danzberger --- src/overview.js | 2 +- templates/overview.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/overview.js b/src/overview.js index e2c8e60f5e..0b0b79d73d 100644 --- a/src/overview.js +++ b/src/overview.js @@ -11,4 +11,4 @@ Vue.prototype.n = n new Vue({ render: h => h(OfficeOverview), -}).$mount('#office-overview-vue') +}).$mount('#content') diff --git a/templates/overview.php b/templates/overview.php index 37e878717c..0024bd549b 100644 --- a/templates/overview.php +++ b/templates/overview.php @@ -5,4 +5,4 @@ */ ?> -
+
From 98ea252b0b0b9192ae43b91be5335473662a9e94 Mon Sep 17 00:00:00 2001 From: Elizabeth Danzberger Date: Fri, 8 May 2026 14:21:04 -0400 Subject: [PATCH 05/29] feat(overview): re-use office file type icons We had imported material design icons for the office file type icons in the navigation bar, but it might be best to re-use the icons we are using in the rest of the richdocuments app now for consistency. Signed-off-by: Elizabeth Danzberger --- src/overview.js | 1 + src/views/OfficeOverview.vue | 21 ++++++--------------- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/src/overview.js b/src/overview.js index 0b0b79d73d..2eca428705 100644 --- a/src/overview.js +++ b/src/overview.js @@ -3,6 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ import './init-shared.js' +import '../css/filetypes.scss' import Vue from 'vue' import OfficeOverview from './views/OfficeOverview.vue' diff --git a/src/views/OfficeOverview.vue b/src/views/OfficeOverview.vue index f847bd5ef2..44e7620485 100644 --- a/src/views/OfficeOverview.vue +++ b/src/views/OfficeOverview.vue @@ -8,24 +8,21 @@ @@ -38,9 +35,6 @@ import NcAppContent from '@nextcloud/vue/dist/Components/NcAppContent.js' import NcAppNavigation from '@nextcloud/vue/dist/Components/NcAppNavigation.js' import NcAppNavigationItem from '@nextcloud/vue/dist/Components/NcAppNavigationItem.js' import NcContent from '@nextcloud/vue/dist/Components/NcContent.js' -import FileDocumentOutline from 'vue-material-design-icons/FileDocumentOutline.vue' -import FilePowerpointOutline from 'vue-material-design-icons/FilePowerpointOutline.vue' -import FileTableOutline from 'vue-material-design-icons/FileTableOutline.vue' export default { name: 'OfficeOverview', @@ -50,9 +44,6 @@ export default { NcAppNavigation, NcAppNavigationItem, NcContent, - FileDocumentOutline, - FilePowerpointOutline, - FileTableOutline, }, data() { From a9b8914c0d17f343441dce9f05f40c6a6afdaf19 Mon Sep 17 00:00:00 2001 From: Elizabeth Danzberger Date: Fri, 8 May 2026 14:43:12 -0400 Subject: [PATCH 06/29] style(overview): resolve lint errors Signed-off-by: Elizabeth Danzberger --- src/views/OfficeOverview.vue | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/views/OfficeOverview.vue b/src/views/OfficeOverview.vue index 44e7620485..c0e9877341 100644 --- a/src/views/OfficeOverview.vue +++ b/src/views/OfficeOverview.vue @@ -6,20 +6,17 @@ - + + + + + + +
+ +
+
@@ -32,6 +47,11 @@ import NcAppContent from '@nextcloud/vue/dist/Components/NcAppContent.js' import NcAppNavigation from '@nextcloud/vue/dist/Components/NcAppNavigation.js' import NcAppNavigationItem from '@nextcloud/vue/dist/Components/NcAppNavigationItem.js' import NcContent from '@nextcloud/vue/dist/Components/NcContent.js' +import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js' +import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js' +import FileDocumentOutline from 'vue-material-design-icons/FileDocumentOutline.vue' +import OfficeFileEntry from '../components/OfficeFileEntry.vue' +import { getAllOfficeFiles, filterByCategory } from '../services/officeFiles.js' export default { name: 'OfficeOverview', @@ -41,18 +61,72 @@ export default { NcAppNavigation, NcAppNavigationItem, NcContent, + NcEmptyContent, + NcLoadingIcon, + FileDocumentOutline, + OfficeFileEntry, }, data() { return { currentView: 'documents', + allFiles: [], + loading: false, + error: null, } }, + computed: { + files() { + return filterByCategory(this.allFiles, this.currentView) + }, + + emptyMessage() { + const labels = { + documents: t('richdocuments', 'No documents found'), + presentations: t('richdocuments', 'No presentations found'), + spreadsheets: t('richdocuments', 'No spreadsheets found'), + } + + return labels[this.currentView] + }, + }, + + created() { + this.fetchFiles() + }, + methods: { setView(view) { this.currentView = view }, + + async fetchFiles() { + this.loading = true + this.error = null + + try { + this.allFiles = await getAllOfficeFiles() + } catch (e) { + this.error = t('richdocuments', 'Failed to load files') + this.allFiles = [] + } finally { + this.loading = false + } + }, }, } + + From 699418de2c5b05a289f9680fd2c8a634b584ffd4 Mon Sep 17 00:00:00 2001 From: Elizabeth Danzberger Date: Fri, 8 May 2026 17:40:49 -0400 Subject: [PATCH 09/29] fix(overview): use snowflake id Previously, fileid was used, but I learned that it is deprecated and snowflake IDs (node.id) should be used instead Signed-off-by: Elizabeth Danzberger --- src/views/OfficeOverview.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/OfficeOverview.vue b/src/views/OfficeOverview.vue index ceeb12707d..0e4e93fa66 100644 --- a/src/views/OfficeOverview.vue +++ b/src/views/OfficeOverview.vue @@ -35,7 +35,7 @@
From 72a9c0b609f879b8ede96a8bcc3e3c99b7716f81 Mon Sep 17 00:00:00 2001 From: Elizabeth Danzberger Date: Fri, 8 May 2026 17:42:18 -0400 Subject: [PATCH 10/29] fix(overview): dispatch `LoadViewer` event In order for the viewer to be initiated and usable for opening documents, we need to dispatch this event. Previously, `LoadAdditionalScripts` was used, but it actually does not do what is necessary. It is unrelated. Signed-off-by: Elizabeth Danzberger --- lib/Controller/OverviewController.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Controller/OverviewController.php b/lib/Controller/OverviewController.php index 545af2c67b..fa18875612 100644 --- a/lib/Controller/OverviewController.php +++ b/lib/Controller/OverviewController.php @@ -8,11 +8,11 @@ namespace OCA\Richdocuments\Controller; +use OCA\Viewer\Event\LoadViewer; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\Attribute\NoAdminRequired; use OCP\AppFramework\Http\Attribute\NoCSRFRequired; use OCP\AppFramework\Http\TemplateResponse; -use OCP\Collaboration\Resources\LoadAdditionalScriptsEvent; use OCP\EventDispatcher\IEventDispatcher; use OCP\IRequest; use OCP\Server; @@ -36,7 +36,7 @@ public function index(): TemplateResponse { Util::addScript('richdocuments', 'richdocuments-overview'); Server::get(IEventDispatcher::class) - ->dispatchTyped(new LoadAdditionalScriptsEvent()); + ->dispatchTyped(new LoadViewer()); return new TemplateResponse('richdocuments', 'overview', [ 'id-app-content' => '#app-content-vue', From 6738eb7a1584e9c7946efc5d963309720eda120a Mon Sep 17 00:00:00 2001 From: Elizabeth Danzberger Date: Fri, 8 May 2026 17:58:41 -0400 Subject: [PATCH 11/29] refactor(overview): use event dispatcher Switch to using the event dispatcher instead of the service locator pattern. This also fixes a PHP Unit test Signed-off-by: Elizabeth Danzberger --- lib/Controller/OverviewController.php | 5 ++--- tests/lib/Controller/OverviewControllerTest.php | 8 +++++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/Controller/OverviewController.php b/lib/Controller/OverviewController.php index fa18875612..12e6680b1d 100644 --- a/lib/Controller/OverviewController.php +++ b/lib/Controller/OverviewController.php @@ -15,7 +15,6 @@ use OCP\AppFramework\Http\TemplateResponse; use OCP\EventDispatcher\IEventDispatcher; use OCP\IRequest; -use OCP\Server; use OCP\Util; class OverviewController extends Controller { @@ -23,6 +22,7 @@ class OverviewController extends Controller { public function __construct( string $appName, IRequest $request, + private IEventDispatcher $eventDispatcher, ) { parent::__construct($appName, $request); } @@ -35,8 +35,7 @@ public function __construct( public function index(): TemplateResponse { Util::addScript('richdocuments', 'richdocuments-overview'); - Server::get(IEventDispatcher::class) - ->dispatchTyped(new LoadViewer()); + $this->eventDispatcher->dispatchTyped(new LoadViewer()); return new TemplateResponse('richdocuments', 'overview', [ 'id-app-content' => '#app-content-vue', diff --git a/tests/lib/Controller/OverviewControllerTest.php b/tests/lib/Controller/OverviewControllerTest.php index 0eb3f81a85..8d32603234 100644 --- a/tests/lib/Controller/OverviewControllerTest.php +++ b/tests/lib/Controller/OverviewControllerTest.php @@ -10,6 +10,7 @@ use OCA\Richdocuments\Controller\OverviewController; use OCP\AppFramework\Http\TemplateResponse; +use OCP\EventDispatcher\IEventDispatcher; use OCP\IRequest; use Test\TestCase; @@ -17,7 +18,12 @@ class OverviewControllerTest extends TestCase { public function testIndexReturnsTemplateResponse(): void { $request = $this->createMock(IRequest::class); - $controller = new OverviewController('richdocuments', $request); + $eventDispatcher = $this->createMock(IEventDispatcher::class); + $eventDispatcher->expects($this->once()) + ->method('dispatchTyped') + ->with($this->isInstanceOf('OCA\\Viewer\\Event\\LoadViewer')); + + $controller = new OverviewController('richdocuments', $request, $eventDispatcher); $response = $controller->index(); $this->assertInstanceOf(TemplateResponse::class, $response); From 3996a5bff14b3334f1eb4fd67beb208025a09cd9 Mon Sep 17 00:00:00 2001 From: Elizabeth Danzberger Date: Fri, 8 May 2026 18:12:31 -0400 Subject: [PATCH 12/29] test: remove event dispatcher expectation Turns out CI does not load the viewer app, and even stubs won't work; it keeps throwing errors. So, we don't check that the event is dispatched now. We can test this in other ways, naturally, via E2E tests Signed-off-by: Elizabeth Danzberger --- tests/lib/Controller/OverviewControllerTest.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/lib/Controller/OverviewControllerTest.php b/tests/lib/Controller/OverviewControllerTest.php index 8d32603234..0d1b8f4df0 100644 --- a/tests/lib/Controller/OverviewControllerTest.php +++ b/tests/lib/Controller/OverviewControllerTest.php @@ -19,9 +19,6 @@ class OverviewControllerTest extends TestCase { public function testIndexReturnsTemplateResponse(): void { $request = $this->createMock(IRequest::class); $eventDispatcher = $this->createMock(IEventDispatcher::class); - $eventDispatcher->expects($this->once()) - ->method('dispatchTyped') - ->with($this->isInstanceOf('OCA\\Viewer\\Event\\LoadViewer')); $controller = new OverviewController('richdocuments', $request, $eventDispatcher); $response = $controller->index(); From 43d64541e83faca923e8a937cbec4a3a76548f92 Mon Sep 17 00:00:00 2001 From: Elizabeth Danzberger Date: Fri, 8 May 2026 18:28:16 -0400 Subject: [PATCH 13/29] test(overview): check for LoadViewer class before dispatching event Seemed like a lot of extra steps to get the viewer loaded in the CI environment for the PHP tests, so we can just check if the class exists and gracefully skip it. The tests don't need it for now, and in a real scenario or E2E tests it will get dispatched just fine. Signed-off-by: Elizabeth Danzberger --- lib/Controller/OverviewController.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/Controller/OverviewController.php b/lib/Controller/OverviewController.php index 12e6680b1d..4bf8232b67 100644 --- a/lib/Controller/OverviewController.php +++ b/lib/Controller/OverviewController.php @@ -35,7 +35,10 @@ public function __construct( public function index(): TemplateResponse { Util::addScript('richdocuments', 'richdocuments-overview'); - $this->eventDispatcher->dispatchTyped(new LoadViewer()); + // Viewer is pre-installed in production but may not be available in other environments + if (class_exists(LoadViewer::class)) { + $this->eventDispatcher->dispatchTyped(new LoadViewer()); + } return new TemplateResponse('richdocuments', 'overview', [ 'id-app-content' => '#app-content-vue', From 6fe26f0fa0dd9bc3c195a1b57592ee6043573bb8 Mon Sep 17 00:00:00 2001 From: Elizabeth Danzberger Date: Fri, 8 May 2026 19:58:18 -0400 Subject: [PATCH 14/29] feat(overview): display document previews Signed-off-by: Elizabeth Danzberger --- src/components/OfficeFileEntry.vue | 52 ++++++++++++------------------ src/views/OfficeOverview.vue | 6 ++-- 2 files changed, 24 insertions(+), 34 deletions(-) diff --git a/src/components/OfficeFileEntry.vue b/src/components/OfficeFileEntry.vue index 5a76fbb708..71012fe633 100644 --- a/src/components/OfficeFileEntry.vue +++ b/src/components/OfficeFileEntry.vue @@ -7,7 +7,9 @@ class="office-file-entry" @click="openFile"> + + diff --git a/src/components/OfficeFileEntry.vue b/src/components/OfficeFileEntry.vue index 71012fe633..9c946a1cb3 100644 --- a/src/components/OfficeFileEntry.vue +++ b/src/components/OfficeFileEntry.vue @@ -3,31 +3,33 @@ - SPDX-License-Identifier: AGPL-3.0-or-later -->