Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 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
6b50de6
feat(overview): add grid view preference persistence via user config
moodyjmz May 20, 2026
263da1c
feat(overview): add list view and view mode toggle
moodyjmz May 20, 2026
d21c99a
feat(overview): add Recent section heading, template background, sing…
moodyjmz May 20, 2026
5774639
fix(overview): correct template card sizing and preview URL generation
moodyjmz May 20, 2026
d7da807
fix(overview): guard null userId and rename initial state key
moodyjmz May 20, 2026
4162edc
fix(overview): uniform template card sizing, preview fallback, and mi…
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>
4 changes: 4 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 All @@ -48,6 +51,7 @@
],
],
['name' => 'settings#generateIframeToken', 'url' => 'settings/generateToken/{type}', 'verb' => 'GET'],
['name' => 'settings#setOverviewGridView', 'url' => 'settings/overview/grid_view', 'verb' => 'PUT'],

// Direct Editing: Webview
['name' => 'directView#show', 'url' => '/direct/{token}', 'verb' => 'GET'],
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
61 changes: 61 additions & 0 deletions lib/Controller/OverviewController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?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\IConfig;
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,
private IConfig $config,
private ?string $userId,
) {
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'));
$this->initialState->provideInitialState('overview_config', [
'overview_grid_view' => $this->userId !== null
&& $this->config->getUserValue($this->userId, 'richdocuments', 'overview_grid_view', '0') === '1',
]);

// 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',
]);
}
}
9 changes: 9 additions & 0 deletions lib/Controller/SettingsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,15 @@ public function setPersonalSettings($templateFolder,
return new JSONResponse($response);
}

#[NoAdminRequired]
public function setOverviewGridView(bool $value): JSONResponse {
if ($this->userId === null) {
return new JSONResponse([], Http::STATUS_UNAUTHORIZED);
}
$this->config->setUserValue($this->userId, 'richdocuments', 'overview_grid_view', $value ? '1' : '0');
return new JSONResponse(['message' => 'ok']);
}

/**
* @NoAdminRequired
* @PublicPage
Expand Down
Loading
Loading