Skip to content

Commit

Permalink
feat(files): Implement files list filters for name, modified time and…
Browse files Browse the repository at this point in the history
… type

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
  • Loading branch information
susnux committed Jun 7, 2024
1 parent 12d7634 commit 337ba7c
Show file tree
Hide file tree
Showing 5 changed files with 521 additions and 0 deletions.
59 changes: 59 additions & 0 deletions apps/files/src/components/FilesListFilter/FilesListFilter.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<template>
<NcActions force-menu
:type="isActive ? 'primary' : 'tertiary'"
:menu-name="filterName">
<template #icon>
<slot name="icon" />
</template>
<template v-if="isActive">
<NcActionButton class="files-list-filter__clear-button" close-after-click @click="$emit('reset-filter')">
{{ t('files', 'Clear filter') }}
</NcActionButton>
<NcActionSeparator />
</template>
<slot />
</NcActions>
</template>

<script lang="ts">
import { translate as t } from '@nextcloud/l10n'
import { defineComponent } from 'vue'
import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
import NcActionSeparator from '@nextcloud/vue/dist/Components/NcActionSeparator.js'
export default defineComponent({
name: 'FilesListFilter',
components: {
NcActions,
NcActionButton,
NcActionSeparator,
},
props: {
isActive: {
type: Boolean,
required: true,
},
filterName: {
type: String,
required: true,
},
},
emits: ['reset-filter'],
methods: {
t,
},
})
</script>

<style scoped>
.files-list-filter__clear-button :deep(.action-button__text) {
color: var(--color-error-text);
}
</style>
98 changes: 98 additions & 0 deletions apps/files/src/components/FilesListFilter/FilesListFilterName.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<template>
<NcTextField :value.sync="query"
:label="t('files', 'Filename')"
show-trailing-button
:trailing-button-label="t('files', 'Clear filter')"
@trailing-button-click="resetFilter" />
</template>

<script lang="ts">
import type { Node } from '@nextcloud/files'
import { subscribe, unsubscribe } from '@nextcloud/event-bus'
import { translate as t } from '@nextcloud/l10n'
import { defineComponent } from 'vue'
import debounce from 'debounce'
import useFilesFilter from '../../composables/useFilesFilter.ts'
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
export default defineComponent({
name: 'FilesListFilterName',
components: {
NcTextField,
},
props: {
},
setup() {
return {
...useFilesFilter(),
}
},
data() {
return {
query: '',
}
},
computed: {
debouncedSetFilter() {
return debounce(this.setFilter, 200)
},
},
watch: {
query(newValue: string) {
if (!newValue) {
// Remove filter if no query is set
this.deleteFilter('files-filter-name')
} else {
this.debouncedSetFilter()
}
},
},
mounted() {
subscribe('nextcloud:unified-search.search', this.onSearch)
subscribe('nextcloud:unified-search.reset', this.resetFilter)
},
beforeDestroy() {
this.deleteFilter('files-filter-name')
unsubscribe('nextcloud:unified-search.search', this.onSearch)
unsubscribe('nextcloud:unified-search.reset', this.resetFilter)
},
methods: {
t,
setFilter() {
this.addFilter({
id: 'files-filter-name',
filter: (node: Node) => {
if (!this.query) {
return true
}
const query = this.query.toLowerCase()
return (node.attributes.displayName ?? node.basename).toLowerCase().includes(query)
},
})
},
onSearch({ query }: { query: string }) {
this.query = query ?? ''
},
resetFilter() {
this.query = ''
},
},
})
</script>
154 changes: 154 additions & 0 deletions apps/files/src/components/FilesListFilter/FilesListFilterTime.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
<template>
<FilesListFilter :is-active="isActive"
:filter-name="label"
@reset-filter="resetFilter">
<template #icon>
<NcIconSvgWrapper :path="mdiCalendarRange" />
</template>
<NcActionButton v-for="preset of timePresets"
:key="preset.id"
type="radio"
close-after-click
:model-value.sync="selectedOption"
:value="preset.id">
{{ preset.label }}
</NcActionButton>
<!-- TODO: Custom time range -->
</FilesListFilter>
</template>

<script lang="ts">
import type { Node } from '@nextcloud/files'
import { mdiCalendarRange } from '@mdi/js'
import { translate as t } from '@nextcloud/l10n'
import { defineComponent } from 'vue'
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
import FilesListFilter from './FilesListFilter.vue'
import useFilesFilter from '../../composables/useFilesFilter'
const startOfToday = () => (new Date()).setHours(0, 0, 0, 0)
const timePresets = [
{
id: 'today',
label: t('files', 'Today'),
filter: (time: number) => time > startOfToday(),
},
{
id: 'last-7',
label: t('files', 'Last 7 days'),
filter: (time: number) => time > (startOfToday() - (7 * 24 * 60 * 60 * 1000)),
},
{
id: 'last-30',
label: t('files', 'Last 30 days'),
filter: (time: number) => time > (startOfToday() - (30 * 24 * 60 * 60 * 1000)),
},
{
id: 'this-year',
label: t('files', 'This year ({year})', { year: (new Date()).getFullYear() }),
filter: (time: number) => time > (new Date(startOfToday())).setMonth(0, 1),
},
{
id: 'last-year',
label: t('files', 'Last year ({year})', { year: (new Date()).getFullYear() - 1 }),
filter: (time: number) => (time > (new Date(startOfToday())).setFullYear((new Date()).getFullYear() - 1, 0, 1)) && (time < (new Date(startOfToday())).setMonth(0, 1)),
},
] as const
export default defineComponent({
components: {
FilesListFilter,
NcActionButton,
NcIconSvgWrapper,
},
props: {
},
setup() {
return {
...useFilesFilter(),
timePresets,
// icons used in template
mdiCalendarRange,
}
},
data() {
return {
selectedOption: null as (typeof timePresets)[number]['id'] | null,
timeRangeEnd: null as number | null,
timeRangeStart: null as number | null,
}
},
computed: {
/**
* Is the filter currently active
*/
isActive() {
return this.selectedOption !== null
},
currentPreset() {
return timePresets.find(({ id }) => id === this.selectedOption) ?? null
},
label() {
if (this.currentPreset) {
return this.currentPreset.label
}
return t('files', 'Modified')
},
},
watch: {
selectedOption() {
if (this.selectedOption === null) {
this.deleteFilter('files-filter-time')
} else {
const preset = this.currentPreset
this.addFilter({
id: 'files-filter-time',
filter: (node: Node) => {
if (!node.mtime) {
return false
}
const mtime = node.mtime.getTime()
if (preset) {
return preset.filter(mtime)
} else {
return (!this.timeRangeStart || this.timeRangeStart < mtime) && (!this.timeRangeEnd || this.timeRangeEnd > mtime)
}
},
})
}
},
},
methods: {
t,
resetFilter() {
this.selectedOption = null
this.timeRangeEnd = null
this.timeRangeStart = null
},
},
})
</script>

<style scoped lang="scss">
.files-list-filter-time {
&__clear-button :deep(.action-button__text) {
color: var(--color-error-text);
}
}
</style>
Loading

0 comments on commit 337ba7c

Please sign in to comment.