Skip to content

Commit 9618dca

Browse files
fix(dav): add missing search_supports_creation_time and search_supports_upload_time to Capabilities return type
Signed-off-by: Cristian Scheid <cristianscheid@gmail.com>
1 parent 70e9e2f commit 9618dca

File tree

11 files changed

+146
-7
lines changed

11 files changed

+146
-7
lines changed

apps/dav/lib/Capabilities.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,13 @@ public function __construct(IConfig $config, IAvailabilityCoordinator $coordinat
3838
}
3939

4040
/**
41-
* @return array{dav: array{chunking: string, bulkupload?: string, absence-supported?: bool}}
41+
* @return array{dav: array{chunking: string, search_supports_creation_time: bool, search_supports_upload_time: bool, bulkupload?: string, absence-supported?: bool}}
4242
*/
4343
public function getCapabilities() {
4444
$capabilities = [
4545
'dav' => [
4646
'chunking' => '1.0',
47+
'search_supports_creation_time' => true,
4748
'search_supports_upload_time' => true,
4849
]
4950
];

apps/dav/lib/Files/FileSearchBackend.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ public function getPropertyDefinitionsForScope(string $href, ?string $path): arr
105105
new SearchPropertyDefinition('{DAV:}displayname', true, true, true),
106106
new SearchPropertyDefinition('{DAV:}getcontenttype', true, true, true),
107107
new SearchPropertyDefinition('{DAV:}getlastmodified', true, true, true, SearchPropertyDefinition::DATATYPE_DATETIME),
108+
new SearchPropertyDefinition('{DAV:}creationdate', true, true, true, SearchPropertyDefinition::DATATYPE_DATETIME),
108109
new SearchPropertyDefinition('{http://nextcloud.org/ns}upload_time', true, true, true, SearchPropertyDefinition::DATATYPE_DATETIME),
109110
new SearchPropertyDefinition(FilesPlugin::SIZE_PROPERTYNAME, true, true, true, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER),
110111
new SearchPropertyDefinition(TagsPlugin::FAVORITE_PROPERTYNAME, true, true, true, SearchPropertyDefinition::DATATYPE_BOOLEAN),
@@ -318,6 +319,8 @@ private function getSearchResultProperty(SearchResult $result, SearchPropertyDef
318319
return $node->getName();
319320
case '{DAV:}getlastmodified':
320321
return $node->getLastModified();
322+
case '{DAV:}creationdate':
323+
return $node->getNode()->getCreationTime();
321324
case '{http://nextcloud.org/ns}upload_time':
322325
return $node->getNode()->getUploadTime();
323326
case FilesPlugin::SIZE_PROPERTYNAME:
@@ -474,6 +477,8 @@ private function mapPropertyNameToColumn(SearchPropertyDefinition $property) {
474477
return 'mimetype';
475478
case '{DAV:}getlastmodified':
476479
return 'mtime';
480+
case '{DAV:}creationdate':
481+
return 'creation_time';
477482
case '{http://nextcloud.org/ns}upload_time':
478483
return 'upload_time';
479484
case FilesPlugin::SIZE_PROPERTYNAME:

apps/dav/openapi.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,20 @@
2929
"dav": {
3030
"type": "object",
3131
"required": [
32-
"chunking"
32+
"chunking",
33+
"search_supports_creation_time",
34+
"search_supports_upload_time"
3335
],
3436
"properties": {
3537
"chunking": {
3638
"type": "string"
3739
},
40+
"search_supports_creation_time": {
41+
"type": "boolean"
42+
},
43+
"search_supports_upload_time": {
44+
"type": "boolean"
45+
},
3846
"bulkupload": {
3947
"type": "string"
4048
},

apps/dav/tests/unit/CapabilitiesTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ public function testGetCapabilities(): void {
4646
$expected = [
4747
'dav' => [
4848
'chunking' => '1.0',
49+
'search_supports_creation_time' => true,
50+
'search_supports_upload_time' => true,
4951
],
5052
];
5153
$this->assertSame($expected, $capabilities->getCapabilities());
@@ -65,6 +67,8 @@ public function testGetCapabilitiesWithBulkUpload(): void {
6567
$expected = [
6668
'dav' => [
6769
'chunking' => '1.0',
70+
'search_supports_creation_time' => true,
71+
'search_supports_upload_time' => true,
6872
'bulkupload' => '1.0',
6973
],
7074
];
@@ -85,6 +89,8 @@ public function testGetCapabilitiesWithAbsence(): void {
8589
$expected = [
8690
'dav' => [
8791
'chunking' => '1.0',
92+
'search_supports_creation_time' => true,
93+
'search_supports_upload_time' => true,
8894
'absence-supported' => true,
8995
],
9096
];

apps/files/src/components/FileEntry/FileEntryPreview.vue

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@
4949
<FavoriteIcon v-once />
5050
</span>
5151

52+
<!-- Recently created icon -->
53+
<span v-else-if="isRecentView && isRecentlyCreated" class="files-list__row-icon-recently-created">
54+
<RecentlyCreatedIcon v-once />
55+
</span>
56+
5257
<OverlayIcon :is="fileOverlay"
5358
v-if="fileOverlay"
5459
class="files-list__row-icon-overlay files-list__row-icon-overlay--file" />
@@ -79,6 +84,7 @@ import PlayCircleIcon from 'vue-material-design-icons/PlayCircle.vue'
7984
import { useUserConfigStore } from '../../store/userconfig.ts'
8085
import CollectivesIcon from './CollectivesIcon.vue'
8186
import FavoriteIcon from './FavoriteIcon.vue'
87+
import RecentlyCreatedIcon from './RecentlyCreatedIcon.vue'
8288
import { isLivePhoto } from '../../services/LivePhotos'
8389
8490
export default Vue.extend({
@@ -96,6 +102,7 @@ export default Vue.extend({
96102
LinkIcon,
97103
NetworkIcon,
98104
TagIcon,
105+
RecentlyCreatedIcon,
99106
},
100107
101108
props: {
@@ -134,6 +141,29 @@ export default Vue.extend({
134141
return this.source.attributes.favorite === 1
135142
},
136143
144+
isRecentlyCreated(): boolean {
145+
if (this.source.attributes.upload_time) {
146+
return false
147+
}
148+
149+
const creationDate = this.source.attributes.creationdate
150+
? new Date(this.source.attributes.creationdate)
151+
: null
152+
153+
if (!creationDate) {
154+
return false
155+
}
156+
157+
const oneDayAgo = new Date()
158+
oneDayAgo.setDate(oneDayAgo.getDate() - 1)
159+
160+
return creationDate > oneDayAgo
161+
},
162+
163+
isRecentView(): boolean {
164+
return this.$route?.params?.view === 'recent'
165+
},
166+
137167
userConfig(): UserConfig {
138168
return this.userConfigStore.userConfig
139169
},
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
<template>
6+
<NcIconSvgWrapper class="recently-created-marker-icon" :name="t('files', 'Recently created')" :svg="PlusSvg" />
7+
</template>
8+
9+
<script lang="ts">
10+
import PlusSvg from '@mdi/svg/svg/plus.svg?raw'
11+
import { translate as t } from '@nextcloud/l10n'
12+
import { defineComponent } from 'vue'
13+
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
14+
15+
/**
16+
* A recently created icon to be used for overlaying recently created entries like the file preview / icon
17+
* It has a stroke around the icon to ensure enough contrast for accessibility.
18+
*
19+
* If the background has a hover state you might want to also apply it to the stroke like this:
20+
* ```scss
21+
* .parent:hover :deep(.favorite-marker-icon svg path) {
22+
* stroke: var(--color-background-hover);
23+
* }
24+
* ```
25+
*/
26+
export default defineComponent({
27+
name: 'RecentlyCreatedIcon',
28+
components: {
29+
NcIconSvgWrapper,
30+
},
31+
32+
data() {
33+
return {
34+
PlusSvg,
35+
}
36+
},
37+
38+
async mounted() {
39+
await this.$nextTick()
40+
// MDI default viewBox is "0 0 24 24" but we add a stroke of 10px so we must adjust it
41+
const el = this.$el.querySelector('svg')
42+
el?.setAttribute?.('viewBox', '-4 -4 30 30')
43+
},
44+
45+
methods: {
46+
t,
47+
},
48+
})
49+
</script>
50+
51+
<style lang="scss" scoped>
52+
.recently-created-marker-icon {
53+
color: var(--color-element-success);
54+
// Override NcIconSvgWrapper defaults (clickable area)
55+
min-width: unset !important;
56+
min-height: unset !important;
57+
58+
:deep() {
59+
svg {
60+
// We added a stroke for a11y so we must increase the size to include the stroke
61+
width: 20px !important;
62+
height: 20px !important;
63+
64+
// Override NcIconSvgWrapper defaults of 20px
65+
max-width: unset !important;
66+
max-height: unset !important;
67+
68+
// Show a border around the icon for better contrast
69+
path {
70+
stroke: var(--color-main-background);
71+
stroke-width: 8px;
72+
stroke-linejoin: round;
73+
paint-order: stroke;
74+
}
75+
}
76+
}
77+
}
78+
</style>

apps/files/src/components/FilesListVirtual.vue

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -577,7 +577,7 @@ export default defineComponent({
577577
& > span {
578578
justify-content: flex-start;
579579
580-
&:not(.files-list__row-icon-favorite) svg {
580+
&:not(.files-list__row-icon-favorite):not(.files-list__row-icon-recently-created) svg {
581581
width: var(--icon-preview-size);
582582
height: var(--icon-preview-size);
583583
}
@@ -609,7 +609,8 @@ export default defineComponent({
609609
}
610610
}
611611
612-
&-favorite {
612+
&-favorite,
613+
&-recently-created {
613614
position: absolute;
614615
top: 0px;
615616
right: -10px;
@@ -801,8 +802,9 @@ tbody.files-list__tbody.files-list__tbody--grid {
801802
}
802803
}
803804
804-
// Star icon in the top right
805-
.files-list__row-icon-favorite {
805+
// Icon in the top right
806+
.files-list__row-icon-favorite,
807+
.files-list__row-icon-recently-created {
806808
position: absolute;
807809
top: 0;
808810
right: 0;

lib/private/Files/Cache/Cache.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,12 @@ public function getFolderContentsById($fileId) {
264264
* @throws \RuntimeException
265265
*/
266266
public function put($file, array $data) {
267+
// do not carry over creation_time to file versions, as each new version would otherwise
268+
// create a filecache_extended entry with the same creation_time as the original file
269+
if (str_starts_with($file, 'files_versions/')) {
270+
unset($data['creation_time']);
271+
}
272+
267273
if (($id = $this->getId($file)) > -1) {
268274
$this->update($id, $data);
269275
return $id;

lib/private/Files/Cache/QuerySearchHelper.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ public function searchInCaches(ISearchQuery $searchQuery, array $caches): array
175175

176176
$requestedFields = $this->searchBuilder->extractRequestedFields($searchQuery->getSearchOperation());
177177

178-
$joinExtendedCache = in_array('upload_time', $requestedFields);
178+
$joinExtendedCache = in_array('creation_time', $requestedFields) || in_array('upload_time', $requestedFields);
179179

180180
$query = $builder->selectFileCache('file', $joinExtendedCache);
181181

lib/private/Files/Cache/SearchBuilder.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ class SearchBuilder {
8484
'share_with' => 'string',
8585
'share_type' => 'integer',
8686
'owner' => 'string',
87+
'creation_time' => 'integer',
8788
'upload_time' => 'integer',
8889
];
8990

@@ -278,6 +279,7 @@ private function validateComparison(ISearchComparison $operator) {
278279
'share_with' => ['eq'],
279280
'share_type' => ['eq'],
280281
'owner' => ['eq'],
282+
'creation_time' => ['eq', 'gt', 'lt', 'gte', 'lte'],
281283
'upload_time' => ['eq', 'gt', 'lt', 'gte', 'lte'],
282284
];
283285

0 commit comments

Comments
 (0)