Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
1ec354f
feat(overview): add office overview page
elzody May 8, 2026
f355cc6
feat(overview): office overview vue component
elzody May 8, 2026
534fb88
feat(overview): support keyboard navigation
elzody May 8, 2026
d06eeeb
fix(overview): update Vue mount point
elzody May 8, 2026
98ea252
feat(overview): re-use office file type icons
elzody May 8, 2026
a9b8914
style(overview): resolve lint errors
elzody May 8, 2026
5d190e4
test: add overview page tests
elzody May 8, 2026
8d32dfb
feat(overview): display documents in overview
elzody May 8, 2026
699418d
fix(overview): use snowflake id
elzody May 8, 2026
72a9c0b
fix(overview): dispatch `LoadViewer` event
elzody May 8, 2026
6738eb7
refactor(overview): use event dispatcher
elzody May 8, 2026
3996a5b
test: remove event dispatcher expectation
elzody May 8, 2026
43d6454
test(overview): check for LoadViewer class before dispatching event
elzody May 8, 2026
6fe26f0
feat(overview): display document previews
elzody May 8, 2026
7ef85be
fix(overview): do not display txt files
elzody May 13, 2026
c2ea4a6
feat(overview): file card component
elzody May 13, 2026
7a6e70a
feat(overview): get rid of extra component
elzody May 13, 2026
496f21b
test(overview): add more tests
elzody May 13, 2026
ca4140c
fix(overview): do not display previews if disabled
elzody May 14, 2026
bd2e37f
feat(overview): use file type icons when previews disabled
elzody May 14, 2026
c3a7951
style: code formatting
elzody May 14, 2026
29807c3
test(overview): preview tests
elzody May 14, 2026
5e74110
test(overview): organize unit tests
elzody May 14, 2026
fafb678
fix(overview): left-align card name and subname
elzody May 14, 2026
189d0f0
feat(overview): add diagram support
elzody May 14, 2026
bfdd271
feat(overview): search bar
elzody May 14, 2026
6092bf2
test(overview): adds test for search bar
elzody May 14, 2026
31e596e
fix(overview): ensure cursor pointer for slotted content
elzody May 15, 2026
bf0db90
feat(overview): use dav search
elzody May 15, 2026
221afaf
feat(overview): sort files by recent edit, favourites first
moodyjmz May 20, 2026
bf0d730
feat(overview): add templates service using NC Files API
moodyjmz May 20, 2026
a35cf9f
refactor(overview): remove hardcoded OFFICE_MIME_FILTERS, accept MIME…
moodyjmz May 20, 2026
33949fd
feat(overview): derive nav categories from template providers, use ba…
moodyjmz May 20, 2026
a75304e
feat(overview): add TemplateSection component with blank and template…
moodyjmz May 20, 2026
5ac61e8
feat(overview): wire TemplateSection into OfficeOverview with create …
moodyjmz May 20, 2026
41f9850
fix(overview): correct nav category names, icons, dialog title, activ…
moodyjmz May 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,12 @@ You can also edit your documents off-line with the Collabora Office app from the
<personal>OCA\Richdocuments\Settings\Personal</personal>
<personal-section>OCA\Richdocuments\Settings\Section</personal-section>
</settings>
<navigations>
<navigation>
<name>Office</name>
<route>richdocuments.overview.index</route>
<icon>app.svg</icon>
<order>10</order>
</navigation>
</navigations>
</info>
3 changes: 3 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down
158 changes: 158 additions & 0 deletions cypress/e2e/overview.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

const CATEGORY_FILES = [
{
category: 'Documents',
emptyMessage: 'No documents found',
fixture: 'document.odt',
mimeType: 'application/vnd.oasis.opendocument.text',
},
{
category: 'Presentations',
emptyMessage: 'No presentations found',
fixture: 'presentation.odp',
mimeType: 'application/vnd.oasis.opendocument.presentation',
},
{
category: 'Spreadsheets',
emptyMessage: 'No spreadsheets found',
fixture: 'spreadsheet.ods',
mimeType: 'application/vnd.oasis.opendocument.spreadsheet',
},
{
category: 'Diagrams',
emptyMessage: 'No diagrams found',
fixture: 'drawing.odg',
mimeType: 'application/vnd.oasis.opendocument.graphics',
},
]

describe('Office overview page', function() {
describe('without files', function() {
let randUser

before(function() {
cy.createRandomUser().then(user => {
randUser = user
})
})

beforeEach(function() {
cy.login(randUser)
cy.visit('/apps/richdocuments/overview')
})

it('Shows the navigation sidebar with appropriate entries', function() {
CATEGORY_FILES.forEach(({ category }) => {
cy.contains('.app-navigation-entry', category).should('exist')
})
})

it('Highlights the active navigation item and shows empty state on click', function() {
CATEGORY_FILES.forEach(({ category, emptyMessage }) => {
cy.contains('.app-navigation-entry', category).click()
cy.contains('.app-navigation-entry', category)
.should('have.class', 'active')
cy.get('.empty-content')
.should('be.visible')
.and('contain', emptyMessage)
})
})
})

describe('with files', function() {
let randUser

before(function() {
cy.createRandomUser().then(user => {
randUser = user
cy.login(user)

CATEGORY_FILES.forEach(({ fixture, mimeType }) => {
cy.uploadFile(user, fixture, mimeType, `/${fixture}`)
})

cy.createFolder(user, 'subfolder').then(() => {
CATEGORY_FILES.forEach(({ fixture, mimeType }) => {
cy.uploadFile(user, fixture, mimeType, `/subfolder/${fixture}`)
})
})
})
})

beforeEach(function() {
cy.login(randUser)
cy.visit('/apps/richdocuments/overview', {
onBeforeLoad(win) {
cy.spy(win, 'postMessage').as('postMessage')
},
})
})

CATEGORY_FILES.forEach(({ category, fixture }) => {
it(`Shows ${category} file cards in the correct category`, function() {
cy.contains('.app-navigation-entry', category).click()

cy.contains('.file-card__name', fixture)
.should('be.visible')

cy.get('.file-card__preview img')
.should('exist')

cy.get('.input-field__label')
.should('contain', `Search ${category.toLowerCase()}`)
})

it(`Opens the viewer when clicking a ${category} file card`, function() {
cy.contains('.app-navigation-entry', category).click()
cy.contains('.file-card', fixture).click()

cy.waitForViewer()
cy.waitForCollabora()

cy.closeDocument()
})
})

it('Shows file cards for files in subdirectories', function() {
CATEGORY_FILES.forEach(({ category }) => {
cy.contains('.app-navigation-entry', category).click()
cy.get('.file-card').should('have.length.at.least', 2)
})
})

it('Filters file cards by search query', function() {
const { category, fixture } = CATEGORY_FILES[0]
const stem = fixture.split('.')[0]

cy.contains('.app-navigation-entry', category).click()

cy.get('.office-overview__search [type="search"]').type(stem)
cy.contains('.file-card__name', fixture).should('be.visible')
})

it('Shows empty state when search matches nothing', function() {
const { category } = CATEGORY_FILES[0]

cy.contains('.app-navigation-entry', category).click()

cy.get('.office-overview__search [type="search"]').type('xyz123noresults')
cy.get('.empty-content').should('be.visible')
})

it('Resets search when switching categories', function() {
const [first, second] = CATEGORY_FILES

cy.contains('.app-navigation-entry', first.category).click()
cy.get('.office-overview__search [type="search"]').type('xyz123noresults')

cy.contains('.app-navigation-entry', second.category).click()

cy.get('.office-overview__search [type="search"]').should('have.value', '')
cy.contains('.file-card__name', second.fixture).should('be.visible')
})
})
})
2 changes: 1 addition & 1 deletion cypress/e2e/templates.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ describe('Global templates', function() {
.scrollIntoView()

cy.intercept('DELETE', '**/richdocuments/template/*').as('templateDeleteRequest')
cy.get('.template-btn[data-cy-template-btn-name="systemtemplate"]').click()
cy.get('.file-card[data-cy-template-btn-name="systemtemplate"]').click()

cy.wait('@templateDeleteRequest').then(({ response }) => {
expect(response.statusCode).to.equal(204)
Expand Down
54 changes: 54 additions & 0 deletions lib/Controller/OverviewController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

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\AppFramework\Services\IInitialState;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IPreview;
use OCP\IRequest;
use OCP\Util;

class OverviewController extends Controller {

public function __construct(
string $appName,
IRequest $request,
private IEventDispatcher $eventDispatcher,
private IInitialState $initialState,
private IPreview $preview,
) {
parent::__construct($appName, $request);
}

/**
* @return TemplateResponse
*/
#[NoAdminRequired]
#[NoCSRFRequired]
public function index(): TemplateResponse {
Util::addScript('richdocuments', 'richdocuments-overview');

$this->initialState->provideInitialState('previewEnabled', $this->preview->isMimeSupported('application/vnd.oasis.opendocument.text'));

// 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',
'id-app-navigation' => '#app-navigation-vue',
]);
}
}
Loading
Loading