Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 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
1b32003
feat(overview): add All / Mine / Shared with me filter chips
moodyjmz May 20, 2026
167a957
fix(overview): give filter buttons pill shape via --button-radius ove…
moodyjmz May 20, 2026
56cb465
feat(overview): open files via OCA.Viewer on single click
moodyjmz May 20, 2026
305fdd0
fix(overview): correct min-version to 34 to preserve NC34 compatibility
moodyjmz May 20, 2026
b708ccc
fix(overview): use mime-type map for category names, fix chip variant…
moodyjmz May 20, 2026
b8a6dbb
docs(overview): add Office Overview section to README
moodyjmz May 20, 2026
8ae7aa6
style(overview): match template section background to app navigation
moodyjmz May 20, 2026
7154dff
fix(overview): fix preview error fallback for file grid cards
moodyjmz May 20, 2026
627c544
perf(overview): hoist mime maps to computed; fix l10n bug in new-file…
moodyjmz May 20, 2026
7ca5fa7
fix(overview): add aria-live for filter results; add Viewer fallback
moodyjmz May 20, 2026
5d5581e
fix(template-section): remove hardcoded blue fallback from background
moodyjmz May 20, 2026
36e571f
chore(officeFiles): document unpaginated DAV SEARCH limitation
moodyjmz May 20, 2026
d8f4138
feat(overview): cap recent files at 50; link to Files app for more
moodyjmz May 20, 2026
118562f
fix(overview): use correct Files app route path for recent view
moodyjmz May 20, 2026
c4f4e27
feat(overview): pass search query when forwarding to Files app
moodyjmz May 20, 2026
20cb3ca
fix(overview): remove mimeBasenames to avoid introducing new l10n str…
moodyjmz May 20, 2026
446bedc
fix(overview): escape XML values in buildOfficeMimeSearch
moodyjmz May 20, 2026
af1c8f4
fix(overview): replace v-html with NcIconSvgWrapper for inline SVG icons
moodyjmz May 20, 2026
ac64bf2
fix(overview): use FileDocumentOutline as grid preview fallback; add …
moodyjmz May 20, 2026
550897c
chore(overview): fix trailing comma lint warnings
moodyjmz May 21, 2026
6ad06ac
fix(overview): fix stylelint issues in new files
moodyjmz May 21, 2026
06a893d
fix(overview): update OverviewControllerTest for IConfig and userId p…
moodyjmz May 21, 2026
1a06ad3
fix(info): revert min-version to 35
moodyjmz May 21, 2026
bfd4fef
fix(overview): default grid view to enabled
moodyjmz May 21, 2026
a5b432e
fix(templates): set hasPreview on Collabora templates
moodyjmz May 21, 2026
7e2a169
fix(template-section): use 16/9 aspect ratio for presentation cards
moodyjmz May 21, 2026
055b255
fix(viewer): only register Viewer handler when Collabora server URL i…
moodyjmz May 21, 2026
c4cdf4b
fix(e2e): reset doc_format before overview tests with files
moodyjmz May 22, 2026
54dc46f
fix(e2e): scroll file card into view before asserting visibility
moodyjmz May 22, 2026
cbf953f
feat(overview): scroll navigation for template row
marcoambrosini May 22, 2026
700538b
feat(overview): document-type themed gradient background
marcoambrosini May 22, 2026
d47715f
fix(overview): template card visual polish
marcoambrosini May 22, 2026
1600766
feat(overview): move search into app navigation
marcoambrosini May 22, 2026
c22b58d
feat(overview): mime icon and restyled gallery file cards
marcoambrosini May 22, 2026
a7b5045
fix(templates): accept docx/xlsx/pptx MIME types in template picker
moodyjmz May 22, 2026
2c97c6f
fix(overview): use templateId/templateType and open file after creation
moodyjmz May 22, 2026
0aa1dcd
Merge branch 'feat/overview-pr6-design' into feat/overview-pr6-open-file
moodyjmz May 22, 2026
5b0e1cd
fix(overview): surface server error message on failed template creation
moodyjmz May 22, 2026
ba6ef58
test(overview): add e2e tests for create-from-template flow
moodyjmz May 22, 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
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ Nextcloud Office supports dozens of document formats including DOC, DOCX, PPT, P

Nextcloud Office is based on the Collabora Online Development Edition (CODE) and is available free and under heavy development, adding features and improvements all the time! Enterprise users have access to the more stable, scalable Collabora Online Enterprise based version through a Nextcloud support subscription.

### Office Overview

The app registers an **Office** entry in the Nextcloud navigation bar. The overview page gives you a single place to:

- Browse recent documents, spreadsheets, presentations, and diagrams — with filter chips for *All*, *Mine* (owned by you), and *Shared with me*.
- Search within the active category.
- Create a new file from a blank document or from one of your personal templates.
- Switch between grid and list view (preference is persisted per user).
- Open any listed file directly in the editor with a single click.

## Installation

Nextcloud Office is built on Collabora Online which requires a dedicated service running next to the Nextcloud webserver stack. There are several ways to run the coolwsd service. For full details, see the related section in the admin manual https://docs.nextcloud.com/server/latest/admin_manual/office/index.html
Expand Down
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
254 changes: 254 additions & 0 deletions cypress/e2e/overview.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
/**
* 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.nextcloudTestingAppConfigSet('richdocuments', 'doc_format', '')
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)
.scrollIntoView()
.should('be.visible')

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

cy.get('.app-navigation-search input[type="search"]')
.should('have.attr', 'aria-label', `Search ${category}`)
})

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('.app-navigation-search input[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('.app-navigation-search input[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('.app-navigation-search input[type="search"]').type('xyz123noresults')

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

cy.get('.app-navigation-search input[type="search"]').should('have.value', '')
cy.contains('.file-card__name', second.fixture).should('be.visible')
})
})

describe('create from template', function() {
let randUser

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

beforeEach(function() {
cy.login(randUser)
cy.visit('/apps/richdocuments/overview')
cy.contains('.app-navigation-entry', 'Documents').click()
})

it('Opens the create dialog with pre-filled filename when clicking Blank', function() {
cy.contains('.template-card__name', 'Blank')
.closest('.template-card')
.click()

cy.get('[role="dialog"]').should('be.visible')
cy.get('[role="dialog"] input[type="text"]').invoke('val').should('match', /\.\w+$/)
})

it('Creates a blank file and navigates to it', function() {
cy.intercept('POST', /templates\/create/).as('createFile')

cy.contains('.template-card__name', 'Blank')
.closest('.template-card')
.click()

cy.get('[role="dialog"]').within(() => {
cy.contains('button', 'Create').click()
})

cy.wait('@createFile').then(({ request, response }) => {
expect(request.body).to.have.property('templatePath', '')
expect(response.statusCode).to.equal(200)
})

// After successful creation the page navigates away from the overview
cy.location('pathname').should('not.include', 'overview')
})

it('Shows an error message when creation fails', function() {
cy.intercept('POST', /templates\/create/, {
statusCode: 403,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
ocs: {
meta: { status: 'failure', statuscode: 403, message: 'File already exists' },
data: {},
},
}),
}).as('createFail')

cy.contains('.template-card__name', 'Blank')
.closest('.template-card')
.click()

cy.get('[role="dialog"]').within(() => {
cy.contains('button', 'Create').click()
})

cy.wait('@createFail')

// Dialog stays open and shows the server error message
cy.get('[role="dialog"]', { timeout: 8000 }).should('contain.text', 'exists')
})

it('Uses templateId and templateType from the template when clicking a non-blank template', function() {
cy.get('.template-section__list .template-card').then($cards => {
const nonBlank = $cards.filter((_, el) => !el.querySelector('.template-card__name')?.textContent.includes('Blank'))
if (nonBlank.length === 0) {
this.skip()
return
}

cy.intercept('POST', /templates\/create/).as('createFromTemplate')

cy.wrap(nonBlank.first()).click()

cy.get('[role="dialog"]').within(() => {
cy.contains('button', 'Create').click()
})

cy.wait('@createFromTemplate').then(({ request }) => {
expect(request.body.templatePath).to.not.equal('')
expect(request.body.templateType).to.not.equal('user_system')
})
})
})
})
})
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', '1') === '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