Merge pull request #58562 from nextcloud/feat/1699/recent-files-mark-recently-created

feat: set creation_time on file creation and render recently created icon
This commit is contained in:
Stephan Orbaugh 2026-03-03 16:08:07 +01:00 committed by GitHub
commit f6c79c0d33
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 119 additions and 8 deletions

View file

@ -27,6 +27,8 @@ class Capabilities implements ICapability {
'dav' => [
'chunking' => '1.0',
'public_shares_chunking' => true,
'search_supports_creation_time' => true,
'search_supports_upload_time' => true,
]
];
if ($this->config->getSystemValueBool('bulkupload.enabled', true)) {

View file

@ -86,6 +86,7 @@ class FileSearchBackend implements ISearchBackend {
new SearchPropertyDefinition('{DAV:}displayname', true, true, true),
new SearchPropertyDefinition('{DAV:}getcontenttype', true, true, true),
new SearchPropertyDefinition('{DAV:}getlastmodified', true, true, true, SearchPropertyDefinition::DATATYPE_DATETIME),
new SearchPropertyDefinition('{DAV:}creationdate', true, true, true, SearchPropertyDefinition::DATATYPE_DATETIME),
new SearchPropertyDefinition('{http://nextcloud.org/ns}upload_time', true, true, true, SearchPropertyDefinition::DATATYPE_DATETIME),
new SearchPropertyDefinition(FilesPlugin::SIZE_PROPERTYNAME, true, true, true, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER),
new SearchPropertyDefinition(TagsPlugin::FAVORITE_PROPERTYNAME, true, true, true, SearchPropertyDefinition::DATATYPE_BOOLEAN),
@ -299,6 +300,8 @@ class FileSearchBackend implements ISearchBackend {
return $node->getName();
case '{DAV:}getlastmodified':
return $node->getLastModified();
case '{DAV:}creationdate':
return $node->getNode()->getCreationTime();
case '{http://nextcloud.org/ns}upload_time':
return $node->getNode()->getUploadTime();
case FilesPlugin::SIZE_PROPERTYNAME:
@ -461,6 +464,8 @@ class FileSearchBackend implements ISearchBackend {
return 'mimetype';
case '{DAV:}getlastmodified':
return 'mtime';
case '{DAV:}creationdate':
return 'creation_time';
case '{http://nextcloud.org/ns}upload_time':
return 'upload_time';
case FilesPlugin::SIZE_PROPERTYNAME:

View file

@ -42,6 +42,11 @@
<FavoriteIcon v-once />
</span>
<!-- Recently created icon -->
<span v-else-if="isRecentView && isRecentlyCreated" class="files-list__row-icon-recently-created">
<RecentlyCreatedIcon v-once />
</span>
<component
:is="fileOverlay"
v-if="fileOverlay"
@ -71,6 +76,7 @@ import PlayCircleIcon from 'vue-material-design-icons/PlayCircle.vue'
import TagIcon from 'vue-material-design-icons/Tag.vue'
import CollectivesIcon from './CollectivesIcon.vue'
import FavoriteIcon from './FavoriteIcon.vue'
import RecentlyCreatedIcon from './RecentlyCreatedIcon.vue'
import { usePreviewImage } from '../../composables/usePreviewImage.ts'
import logger from '../../logger.ts'
import { isLivePhoto } from '../../services/LivePhotos.ts'
@ -91,6 +97,7 @@ export default defineComponent({
LinkIcon,
NetworkIcon,
TagIcon,
RecentlyCreatedIcon,
},
props: {
@ -138,6 +145,21 @@ export default defineComponent({
return this.source.attributes.favorite === 1
},
isRecentlyCreated(): boolean {
if (!this.source.crtime) {
return false
}
const oneDayAgo = new Date()
oneDayAgo.setDate(oneDayAgo.getDate() - 1)
return this.source.crtime > oneDayAgo
},
isRecentView(): boolean {
return this.$route?.params?.view === 'recent'
},
userConfig(): UserConfig {
return this.userConfigStore.userConfig
},

View file

@ -0,0 +1,71 @@
<!--
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<NcIconSvgWrapper class="recently-created-marker-icon" :name="t('files', 'Recently created')" :path="mdiPlus" />
</template>
<script lang="ts">
import { mdiPlus } from '@mdi/js'
import { t } from '@nextcloud/l10n'
import { defineComponent } from 'vue'
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
/**
* A recently created icon to be used for overlaying recently created entries like the file preview / icon
* It has a stroke around the icon to ensure enough contrast for accessibility.
*
* If the background has a hover state you might want to also apply it to the stroke like this:
* ```scss
* .parent:hover :deep(.recently-created-marker-icon svg path) {
* stroke: var(--color-background-hover);
* }
* ```
*/
export default defineComponent({
name: 'RecentlyCreatedIcon',
components: {
NcIconSvgWrapper,
},
setup() {
return {
mdiPlus,
}
},
methods: {
t,
},
})
</script>
<style lang="scss" scoped>
.recently-created-marker-icon {
color: var(--color-element-success);
// Override NcIconSvgWrapper defaults (clickable area)
min-width: unset !important;
min-height: unset !important;
:deep() {
svg {
// We added a stroke for a11y so we must increase the size to include the stroke
width: 20px !important;
height: 20px !important;
// Override NcIconSvgWrapper defaults of 20px
max-width: unset !important;
max-height: unset !important;
// Show a border around the icon for better contrast
path {
stroke: var(--color-main-background);
stroke-width: 8px;
stroke-linejoin: round;
paint-order: stroke;
}
}
}
}
</style>

View file

@ -743,7 +743,7 @@ export default defineComponent({
& > span {
justify-content: flex-start;
&:not(.files-list__row-icon-favorite) svg {
&:not(.files-list__row-icon-favorite):not(.files-list__row-icon-recently-created) svg {
width: var(--icon-preview-size);
height: var(--icon-preview-size);
}
@ -791,7 +791,8 @@ export default defineComponent({
}
}
&-favorite {
&-favorite,
&-recently-created {
position: absolute;
top: 0px;
inset-inline-end: -10px;
@ -993,8 +994,9 @@ export default defineComponent({
}
}
// Star icon in the top right
.files-list__row-icon-favorite {
// Icon in the top right
.files-list__row-icon-favorite,
.files-list__row-icon-recently-created {
position: absolute;
top: 0;
inset-inline-end: 0;

4
dist/files-main.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -250,6 +250,12 @@ class Cache implements ICache {
* @throws \RuntimeException
*/
public function put($file, array $data) {
// do not carry over creation_time to file versions, as each new version would otherwise
// create a filecache_extended entry with the same creation_time as the original file
if (str_starts_with($file, 'files_versions/')) {
unset($data['creation_time']);
}
if (($id = $this->getId($file)) > -1) {
$this->update($id, $data);
return $id;

View file

@ -153,7 +153,7 @@ class QuerySearchHelper {
$requestedFields = $this->searchBuilder->extractRequestedFields($searchQuery->getSearchOperation());
$joinExtendedCache = in_array('upload_time', $requestedFields);
$joinExtendedCache = in_array('creation_time', $requestedFields) || in_array('upload_time', $requestedFields);
$query = $builder->selectFileCache('file', $joinExtendedCache);

View file

@ -64,6 +64,7 @@ class SearchBuilder {
'share_with' => 'string',
'share_type' => 'integer',
'owner' => 'string',
'creation_time' => 'integer',
'upload_time' => 'integer',
];
@ -258,6 +259,7 @@ class SearchBuilder {
'share_with' => ['eq'],
'share_type' => ['eq'],
'owner' => ['eq'],
'creation_time' => ['eq', 'gt', 'lt', 'gte', 'lte'],
'upload_time' => ['eq', 'gt', 'lt', 'gte', 'lte'],
];

View file

@ -175,6 +175,7 @@ class Folder extends Node implements IFolder {
throw new NotPermittedException('Could not create path "' . $fullPath . '"');
}
$node = new File($this->root, $this->view, $fullPath, null, $this);
$this->view->putFileInfo($fullPath, ['creation_time' => time()]);
$this->sendHooks(['postWrite', 'postCreate'], [$node]);
return $node;
}