Skip to content

Commit 3da238b

Browse files
committed
fixup! feat(files): add Home view
Signed-off-by: skjnldsv <[email protected]>
1 parent acd9b89 commit 3da238b

File tree

5 files changed

+74
-60
lines changed

5 files changed

+74
-60
lines changed

apps/files/src/actions/openInFilesAction.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
33
* SPDX-License-Identifier: AGPL-3.0-or-later
44
*/
5+
import type { Node, View } from '@nextcloud/files'
56
import { translate as t } from '@nextcloud/l10n'
6-
import { type Node, FileType, FileAction, DefaultType } from '@nextcloud/files'
7+
import { DefaultType, FileAction, FileType } from '@nextcloud/files'
78

89
/**
910
* TODO: Move away from a redirect and handle
@@ -14,7 +15,7 @@ export const action = new FileAction({
1415
displayName: () => t('files', 'Open in Files'),
1516
iconSvgInline: () => '',
1617

17-
enabled: (nodes, view) => view.id === 'recent',
18+
enabled: (nodes, view: View) => ['home', 'recent'].includes(view.id),
1819

1920
async exec(node: Node) {
2021
let dir = node.dirname

apps/files/src/services/RecommendedFiles.ts

Lines changed: 40 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -3,75 +3,63 @@
33
* SPDX-License-Identifier: AGPL-3.0-or-later
44
*/
55
import type { ContentsWithRoot } from '@nextcloud/files'
6+
import type { FileStat, ResponseDataDetailed } from 'webdav'
67

78
import { CancelablePromise } from 'cancelable-promise'
8-
import { File, Folder, Permission, } from '@nextcloud/files'
9-
import { generateOcsUrl } from '@nextcloud/router'
9+
import { File, Folder, Permission } from '@nextcloud/files'
1010
import { getCurrentUser } from '@nextcloud/auth'
11-
import { getRemoteURL, getRootPath } from '@nextcloud/files/dav'
12-
import axios from '@nextcloud/axios'
11+
import { getDefaultPropfind, getRemoteURL, registerDavProperty, resultToNode } from '@nextcloud/files/dav'
12+
import { client } from './WebdavClient'
13+
import logger from '../logger'
14+
import { getCapabilities } from '@nextcloud/capabilities'
15+
import { getContents as getRecentContents } from './Recent'
1316

14-
import { getContents as getDefaultContents } from './Files'
15-
16-
type RecommendedFiles = {
17-
'id': string
18-
'timestamp': number
19-
'name': string
20-
'directory': string
21-
'extension': string
22-
'mimeType': string
23-
'hasPreview': boolean
24-
'reason': string
25-
}
26-
27-
type RecommendedFilesResponse = {
28-
'recommendations': RecommendedFiles[]
17+
// Check if the recommendations capability is enabled
18+
// If not, we'll just use recent files
19+
const isRecommendationEnabled = getCapabilities()?.recommendations?.enabled === true
20+
if (isRecommendationEnabled) {
21+
registerDavProperty('nc:recommendation-reason', { nc: 'http://nextcloud.org/ns' })
22+
registerDavProperty('nc:recommendation-reason-label', { nc: 'http://nextcloud.org/ns' })
2923
}
3024

31-
const fetchRecommendedFiles = (controller: AbortController): Promise<RecommendedFilesResponse> => {
32-
const url = generateOcsUrl('apps/recommendations/api/v1/recommendations/always')
33-
34-
return axios.get(url, {
35-
signal: controller.signal,
36-
headers: {
37-
'OCS-APIRequest': 'true',
38-
'Content-Type': 'application/json',
39-
},
40-
}).then(resp => resp.data.ocs.data as RecommendedFilesResponse)
41-
}
42-
43-
export const getContents = (path = '/'): CancelablePromise<ContentsWithRoot> => {
44-
if (path !== '/') {
45-
return getDefaultContents(path)
25+
export const getContents = (): CancelablePromise<ContentsWithRoot> => {
26+
if (!isRecommendationEnabled) {
27+
logger.debug('Recommendations capability is not enabled, falling back to recent files')
28+
return getRecentContents()
4629
}
4730

4831
const controller = new AbortController()
49-
return new CancelablePromise(async (resolve, reject, cancel) => {
50-
cancel(() => controller.abort())
32+
const propfindPayload = getDefaultPropfind()
33+
34+
return new CancelablePromise(async (resolve, reject, onCancel) => {
35+
onCancel(() => controller.abort())
36+
37+
const root = `/recommendations/${getCurrentUser()?.uid}`
5138
try {
52-
const { recommendations } = await fetchRecommendedFiles(controller)
39+
const contentsResponse = await client.getDirectoryContents(root, {
40+
details: true,
41+
data: propfindPayload,
42+
includeSelf: false,
43+
signal: controller.signal,
44+
}) as ResponseDataDetailed<FileStat[]>
5345

46+
const contents = contentsResponse.data
5447
resolve({
5548
folder: new Folder({
5649
id: 0,
57-
source: `${getRemoteURL()}${getRootPath()}`,
58-
root: getRootPath(),
50+
source: `${getRemoteURL()}${root}`,
51+
root,
5952
owner: getCurrentUser()?.uid || null,
6053
permissions: Permission.READ,
6154
}),
62-
contents: recommendations.map((rec) => {
63-
const Node = rec.mimeType === 'httpd/unix-directory' ? Folder : File
64-
return new Node({
65-
id: parseInt(rec.id),
66-
source: `${getRemoteURL()}/${getRootPath()}/${rec.directory}/${rec.name}`.replace(/\/\//g, '/'),
67-
root: getRootPath(),
68-
mime: rec.mimeType,
69-
mtime: new Date(rec.timestamp * 1000),
70-
owner: getCurrentUser()?.uid || null,
71-
permissions: Permission.READ,
72-
attributes: rec,
73-
})
74-
}),
55+
contents: contents.map((result) => {
56+
try {
57+
return resultToNode(result, root)
58+
} catch (error) {
59+
logger.error(`Invalid node detected '${result.basename}'`, { error })
60+
return null
61+
}
62+
}).filter(Boolean) as File[],
7563
})
7664
} catch (error) {
7765
reject(error)

apps/files/src/views/FilesHeaderHomeSearch.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export default defineComponent({
5656
<style lang="scss">
5757
// Align everything in the middle
5858
.files-list__header-home-search-wrapper,
59-
.files-list__filters {
59+
.files-content__home .files-list__filters {
6060
display: flex !important;
6161
max-width: var(--breakpoint-mobile) !important;
6262
height: auto !important;
@@ -71,7 +71,7 @@ export default defineComponent({
7171
}
7272
7373
// Align the filters with the search input for the Home view
74-
.files-list__filters {
74+
.files-content__home .files-list__filters {
7575
padding-block: calc(var(--default-grid-baseline, 4px) * 2) !important;
7676
}
7777
</style>

apps/files/src/views/FilesList.vue

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
- SPDX-License-Identifier: AGPL-3.0-or-later
44
-->
55
<template>
6-
<NcAppContent :page-heading="pageHeading" data-cy-files-content>
6+
<NcAppContent :class="['files-content', `files-content__${currentView?.id}`]"
7+
:page-heading="pageHeading"
8+
data-cy-files-content>
79
<div class="files-list__header" :class="{ 'files-list__header--public': isPublic }">
810
<!-- Current folder breadcrumbs -->
911
<BreadCrumbs :path="directory" @reload="fetchContent">
@@ -89,6 +91,7 @@
8991
:current-view="currentView"
9092
:header="header" />
9193
</div>
94+
9295
<!-- Empty due to error -->
9396
<NcEmptyContent v-if="error" :name="error" data-cy-files-content-error>
9497
<template #action>
@@ -103,10 +106,12 @@
103106
<IconAlertCircleOutline />
104107
</template>
105108
</NcEmptyContent>
109+
106110
<!-- Custom empty view -->
107111
<div v-else-if="currentView?.emptyView" class="files-list__empty-view-wrapper">
108112
<div ref="customEmptyView" />
109113
</div>
114+
110115
<!-- Default empty directory view -->
111116
<NcEmptyContent v-else
112117
:name="currentView?.emptyTitle || t('files', 'No files in here')"

apps/files/src/views/home.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
33
* SPDX-License-Identifier: AGPL-3.0-or-later
44
*/
5-
import type { ComponentPublicInstance, VueConstructor } from 'vue'
6-
import { translate as t } from '@nextcloud/l10n'
5+
import type { VueConstructor } from 'vue'
6+
import { getCanonicalLocale, getLanguage, translate as t } from '@nextcloud/l10n'
77
import HomeSvg from '@mdi/svg/svg/home.svg?raw'
88

99
import { getContents } from '../services/RecommendedFiles'
10-
import { Folder, getNavigation, Header, registerFileListHeaders, View } from '@nextcloud/files'
10+
import { Column, Folder, getNavigation, Header, registerFileListHeaders, View } from '@nextcloud/files'
1111
import Vue from 'vue'
1212

1313
export const registerHomeView = () => {
@@ -19,7 +19,27 @@ export const registerHomeView = () => {
1919
icon: HomeSvg,
2020
order: -50,
2121

22+
defaultSortKey: 'mtime',
23+
2224
getContents,
25+
26+
columns: [
27+
new Column({
28+
id: 'recommendation-reason',
29+
title: t('files', 'Reason'),
30+
sort(a, b) {
31+
const aReason = a.attributes?.['recommendation-reason-label'] || t('files', 'Suggestion')
32+
const bReason = b.attributes?.['recommendation-reason-label'] || t('files', 'Suggestion')
33+
return aReason.localeCompare(bReason, [getLanguage(), getCanonicalLocale()], { numeric: true, usage: 'sort' })
34+
},
35+
render(node) {
36+
const reason = node.attributes?.['recommendation-reason-label'] || t('files', 'Suggestion')
37+
const span = document.createElement('span')
38+
span.textContent = reason
39+
return span
40+
},
41+
}),
42+
],
2343
}))
2444

2545
let FilesHeaderHomeSearch: VueConstructor

0 commit comments

Comments
 (0)