aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorskjnldsv <skjnldsv@protonmail.com>2025-06-27 16:12:10 +0200
committerskjnldsv <skjnldsv@protonmail.com>2025-06-27 16:12:10 +0200
commit331e61d5b9b28f4d0da568695962b8db6bfa884c (patch)
treece8bafbf6298f3398f11f0b6f85c17904f8babd2
parentacd9b89861eabf53ce10154edefc480423810bd2 (diff)
downloadnextcloud-server-feat/files-home-view.tar.gz
nextcloud-server-feat/files-home-view.zip
fixup! feat(files): add Home viewfeat/files-home-view
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
-rw-r--r--apps/files/src/actions/openInFilesAction.ts5
-rw-r--r--apps/files/src/services/RecommendedFiles.ts92
-rw-r--r--apps/files/src/views/FilesHeaderHomeSearch.vue4
-rw-r--r--apps/files/src/views/FilesList.vue7
-rw-r--r--apps/files/src/views/home.ts19
5 files changed, 68 insertions, 59 deletions
diff --git a/apps/files/src/actions/openInFilesAction.ts b/apps/files/src/actions/openInFilesAction.ts
index 10e19e7eace..b478b99b60d 100644
--- a/apps/files/src/actions/openInFilesAction.ts
+++ b/apps/files/src/actions/openInFilesAction.ts
@@ -2,8 +2,9 @@
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
+import type { Node, View } from '@nextcloud/files'
import { translate as t } from '@nextcloud/l10n'
-import { type Node, FileType, FileAction, DefaultType } from '@nextcloud/files'
+import { DefaultType, FileAction, FileType } from '@nextcloud/files'
/**
* TODO: Move away from a redirect and handle
@@ -14,7 +15,7 @@ export const action = new FileAction({
displayName: () => t('files', 'Open in Files'),
iconSvgInline: () => '',
- enabled: (nodes, view) => view.id === 'recent',
+ enabled: (nodes, view: View) => ['home', 'recent'].includes(view.id),
async exec(node: Node) {
let dir = node.dirname
diff --git a/apps/files/src/services/RecommendedFiles.ts b/apps/files/src/services/RecommendedFiles.ts
index 6c81d83bc58..f76bdda8200 100644
--- a/apps/files/src/services/RecommendedFiles.ts
+++ b/apps/files/src/services/RecommendedFiles.ts
@@ -3,75 +3,63 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { ContentsWithRoot } from '@nextcloud/files'
+import type { FileStat, ResponseDataDetailed } from 'webdav'
import { CancelablePromise } from 'cancelable-promise'
-import { File, Folder, Permission, } from '@nextcloud/files'
-import { generateOcsUrl } from '@nextcloud/router'
+import { File, Folder, Permission } from '@nextcloud/files'
import { getCurrentUser } from '@nextcloud/auth'
-import { getRemoteURL, getRootPath } from '@nextcloud/files/dav'
-import axios from '@nextcloud/axios'
+import { getDefaultPropfind, getRemoteURL, registerDavProperty, resultToNode } from '@nextcloud/files/dav'
+import { client } from './WebdavClient'
+import logger from '../logger'
+import { getCapabilities } from '@nextcloud/capabilities'
+import { getContents as getRecentContents } from './Recent'
-import { getContents as getDefaultContents } from './Files'
-
-type RecommendedFiles = {
- 'id': string
- 'timestamp': number
- 'name': string
- 'directory': string
- 'extension': string
- 'mimeType': string
- 'hasPreview': boolean
- 'reason': string
-}
-
-type RecommendedFilesResponse = {
- 'recommendations': RecommendedFiles[]
+// Check if the recommendations capability is enabled
+// If not, we'll just use recent files
+const isRecommendationEnabled = getCapabilities()?.recommendations?.enabled === true
+if (isRecommendationEnabled) {
+ registerDavProperty('nc:recommendation-reason', { nc: 'http://nextcloud.org/ns' })
+ registerDavProperty('nc:recommendation-reason-label', { nc: 'http://nextcloud.org/ns' })
}
-const fetchRecommendedFiles = (controller: AbortController): Promise<RecommendedFilesResponse> => {
- const url = generateOcsUrl('apps/recommendations/api/v1/recommendations/always')
-
- return axios.get(url, {
- signal: controller.signal,
- headers: {
- 'OCS-APIRequest': 'true',
- 'Content-Type': 'application/json',
- },
- }).then(resp => resp.data.ocs.data as RecommendedFilesResponse)
-}
-
-export const getContents = (path = '/'): CancelablePromise<ContentsWithRoot> => {
- if (path !== '/') {
- return getDefaultContents(path)
+export const getContents = (): CancelablePromise<ContentsWithRoot> => {
+ if (!isRecommendationEnabled) {
+ logger.debug('Recommendations capability is not enabled, falling back to recent files')
+ return getRecentContents()
}
const controller = new AbortController()
- return new CancelablePromise(async (resolve, reject, cancel) => {
- cancel(() => controller.abort())
+ const propfindPayload = getDefaultPropfind()
+
+ return new CancelablePromise(async (resolve, reject, onCancel) => {
+ onCancel(() => controller.abort())
+
+ const root = `/recommendations/${getCurrentUser()?.uid}`
try {
- const { recommendations } = await fetchRecommendedFiles(controller)
+ const contentsResponse = await client.getDirectoryContents(root, {
+ details: true,
+ data: propfindPayload,
+ includeSelf: false,
+ signal: controller.signal,
+ }) as ResponseDataDetailed<FileStat[]>
+ const contents = contentsResponse.data
resolve({
folder: new Folder({
id: 0,
- source: `${getRemoteURL()}${getRootPath()}`,
- root: getRootPath(),
+ source: `${getRemoteURL()}${root}`,
+ root,
owner: getCurrentUser()?.uid || null,
permissions: Permission.READ,
}),
- contents: recommendations.map((rec) => {
- const Node = rec.mimeType === 'httpd/unix-directory' ? Folder : File
- return new Node({
- id: parseInt(rec.id),
- source: `${getRemoteURL()}/${getRootPath()}/${rec.directory}/${rec.name}`.replace(/\/\//g, '/'),
- root: getRootPath(),
- mime: rec.mimeType,
- mtime: new Date(rec.timestamp * 1000),
- owner: getCurrentUser()?.uid || null,
- permissions: Permission.READ,
- attributes: rec,
- })
- }),
+ contents: contents.map((result) => {
+ try {
+ return resultToNode(result, root)
+ } catch (error) {
+ logger.error(`Invalid node detected '${result.basename}'`, { error })
+ return null
+ }
+ }).filter(Boolean) as File[],
})
} catch (error) {
reject(error)
diff --git a/apps/files/src/views/FilesHeaderHomeSearch.vue b/apps/files/src/views/FilesHeaderHomeSearch.vue
index 39bd9ce1dbb..431a6f7b4e9 100644
--- a/apps/files/src/views/FilesHeaderHomeSearch.vue
+++ b/apps/files/src/views/FilesHeaderHomeSearch.vue
@@ -56,7 +56,7 @@ export default defineComponent({
<style lang="scss">
// Align everything in the middle
.files-list__header-home-search-wrapper,
-.files-list__filters {
+.files-content__home .files-list__filters {
display: flex !important;
max-width: var(--breakpoint-mobile) !important;
height: auto !important;
@@ -71,7 +71,7 @@ export default defineComponent({
}
// Align the filters with the search input for the Home view
-.files-list__filters {
+.files-content__home .files-list__filters {
padding-block: calc(var(--default-grid-baseline, 4px) * 2) !important;
}
</style>
diff --git a/apps/files/src/views/FilesList.vue b/apps/files/src/views/FilesList.vue
index 60791a2b527..a7311f3c546 100644
--- a/apps/files/src/views/FilesList.vue
+++ b/apps/files/src/views/FilesList.vue
@@ -3,7 +3,9 @@
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
- <NcAppContent :page-heading="pageHeading" data-cy-files-content>
+ <NcAppContent :class="['files-content', `files-content__${currentView?.id}`]"
+ :page-heading="pageHeading"
+ data-cy-files-content>
<div class="files-list__header" :class="{ 'files-list__header--public': isPublic }">
<!-- Current folder breadcrumbs -->
<BreadCrumbs :path="directory" @reload="fetchContent">
@@ -89,6 +91,7 @@
:current-view="currentView"
:header="header" />
</div>
+
<!-- Empty due to error -->
<NcEmptyContent v-if="error" :name="error" data-cy-files-content-error>
<template #action>
@@ -103,10 +106,12 @@
<IconAlertCircleOutline />
</template>
</NcEmptyContent>
+
<!-- Custom empty view -->
<div v-else-if="currentView?.emptyView" class="files-list__empty-view-wrapper">
<div ref="customEmptyView" />
</div>
+
<!-- Default empty directory view -->
<NcEmptyContent v-else
:name="currentView?.emptyTitle || t('files', 'No files in here')"
diff --git a/apps/files/src/views/home.ts b/apps/files/src/views/home.ts
index ddc70561f72..82de46609c7 100644
--- a/apps/files/src/views/home.ts
+++ b/apps/files/src/views/home.ts
@@ -2,12 +2,12 @@
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
-import type { ComponentPublicInstance, VueConstructor } from 'vue'
+import type { VueConstructor } from 'vue'
import { translate as t } from '@nextcloud/l10n'
import HomeSvg from '@mdi/svg/svg/home.svg?raw'
import { getContents } from '../services/RecommendedFiles'
-import { Folder, getNavigation, Header, registerFileListHeaders, View } from '@nextcloud/files'
+import { Column, Folder, getNavigation, Header, registerFileListHeaders, View } from '@nextcloud/files'
import Vue from 'vue'
export const registerHomeView = () => {
@@ -19,7 +19,22 @@ export const registerHomeView = () => {
icon: HomeSvg,
order: -50,
+ defaultSortKey: 'mtime',
+
getContents,
+
+ columns: [
+ new Column({
+ id: 'recommendation-reason',
+ title: t('files', 'Reason'),
+ render(node) {
+ let reason = node.attributes?.['recommendation-reason-label'] || t('files', 'Suggestion')
+ const span = document.createElement('span')
+ span.textContent = reason
+ return span
+ },
+ }),
+ ],
}))
let FilesHeaderHomeSearch: VueConstructor