Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
373eef2
chore: Tidy logic sharing between AlbumRoots
artonge May 7, 2025
aabc175
feat: Support setting and getting filters in albums
artonge May 7, 2025
b3c7b4f
test(e2e): Enable place filter test
artonge May 7, 2025
03494e6
chore: Remove legacy files client initialisation
artonge May 7, 2025
aa5dd4a
chore: Use public const to identify metadata
artonge May 15, 2025
452d062
feat: Populate albums based on the set filters
artonge May 15, 2025
de77a12
chore: Simplify filters implementation
artonge May 15, 2025
dc2a1a9
chore: Improve layout of HeaderNavigation to support more content
artonge May 15, 2025
bb24c82
feat: Allow edition of filters in album view
artonge May 15, 2025
fd7ba97
feat: Display filters in album form and album
artonge May 16, 2025
7ee8f21
feat: Return cover picture based on filters
artonge May 16, 2025
9b58191
fix: Parent route computing in HeaderNavigation
artonge May 16, 2025
0f41a2d
feat: Use places in the store to render places filter
artonge May 16, 2025
860c94c
fix: Support filters in shared and public albums
artonge May 16, 2025
6ef4c25
feat: Display filters in public and shared albums
artonge May 16, 2025
95e170c
test: Fix e2e tests after adding filters to albums
artonge May 16, 2025
e6a55c4
fix: Setting filters in album form
artonge May 19, 2025
0e2a03f
feat: Navigate to album upon creating it
artonge May 19, 2025
f4c2c84
test: Add e2e test for filters in albums
artonge May 19, 2025
5b12984
fix: Condition to create album from filters
artonge May 20, 2025
f23e077
test(API): Ensure permissions are respected in the DAV API
artonge May 22, 2025
c7e8abf
fix(albums): Do not offer to remove selection if is coming from filters
artonge May 22, 2025
6b7070a
fix: Psalm, cs and eslint error
artonge May 22, 2025
e8176a7
test: Fix instanciation of AlbumMapper
artonge May 22, 2025
94d2210
chore: Compile assets
artonge May 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat: Use places in the store to render places filter
Take the opportunity to create use functions and share logic between the input and the display

Signed-off-by: Louis Chemineau <[email protected]>
  • Loading branch information
artonge committed May 22, 2025
commit 0f41a2d9ca5345055d0f795079fef0577528d36e
34 changes: 7 additions & 27 deletions src/components/PhotosFilters/PlacesFilterDisplay.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<template>
<div>
<MapMarker /> <b>{{ t('photos', 'Places') }}: </b>
<span v-for="place in selectedOptions" :key="place.label">
<span v-for="place in selectedPlaces" :key="place.label">
<NcChip no-close>
<template #icon>
<img :src="place.previewUrl" class="place__preview">
Expand All @@ -18,43 +18,23 @@
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import MapMarker from 'vue-material-design-icons/MapMarker.vue'

import { generateUrl } from '@nextcloud/router'
import { t } from '@nextcloud/l10n'
import NcChip from '@nextcloud/vue/components/NcChip'

import type { PlacesValueType } from '../../services/PhotosFilters/placesFilter.ts'
import { fetchCollections } from '../../services/collectionFetcher.ts'
import { placesPrefix } from '../../store/places.ts'

type NcSelectPlaceOption = {
label: string
previewUrl?: string
}
import usePlaceFilter from './usePlaceFilter.ts'
import type { PlacesValueType } from '../../services/PhotosFilters/placesFilter.ts';

const props = defineProps<{
value: PlacesValueType
}>()

const loading = ref(true)
const availableOptions = ref<NcSelectPlaceOption[]>([])
const selectedOptions = ref<NcSelectPlaceOption[]>((props.value ?? []).map(place => ({ label: place })))

fetchCollections(placesPrefix)
.then(places => {
availableOptions.value = places.map(place => ({
label: place.basename,
previewUrl: generateUrl(`/apps/photos/api/v1/preview/${place.attributes['last-photo']}?x=${64}&y=${64}`),
}))

selectedOptions.value = (props.value ?? [])
.map(place => availableOptions.value.find(option => option.label === place))
.filter(place => place !== undefined)
const emit = defineEmits<{
(event: 'update:value', value: PlacesValueType): void
}>()

loading.value = false
})
const { selectedPlaces } = usePlaceFilter(props, emit)
</script>
<style lang="scss" scoped>
.place__preview {
Expand Down
62 changes: 23 additions & 39 deletions src/components/PhotosFilters/PlacesFilterInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,38 @@
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<NcSelect v-model="selectedOptions"
<NcSelect v-model="selectedPlaces"
class="places-select"
:options="availableOptions"
:options="availablePlacesWithoutSelections"
:aria-label-combobox="t('photos', 'Select places')"
:placeholder="t('photos', 'Select places')"
:loading="loading"
:loading="loadingCollections"
:multiple="true">
<template #option="option">
<NcListItemIcon :name="option.label"
:is-no-user="true"
:url="option.previewUrl" />
</template>
<template #selected-option="option">
<NcChip no-close>
<template #icon>
<img :src="option.previewUrl" class="place__preview">
</template>
<template #default>
{{ option.label }}
</template>
</NcChip>
</template>
</NcSelect>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue'

import NcSelect from '@nextcloud/vue/components/NcSelect'
import NcChip from '@nextcloud/vue/components/NcChip'
import NcListItemIcon from '@nextcloud/vue/components/NcListItemIcon'
import { generateUrl } from '@nextcloud/router'
import { t } from '@nextcloud/l10n'

import type { PlacesValueType } from '../../services/PhotosFilters/placesFilter.ts'
import { fetchCollections } from '../../services/collectionFetcher.ts'
import { placesPrefix } from '../../store/places.ts'

type NcSelectPlaceOption = {
label: string
previewUrl: string
}
import usePlaceFilter from './usePlaceFilter.ts'
import type { PlacesValueType } from '../../services/PhotosFilters/placesFilter.ts';

const props = defineProps<{
value: PlacesValueType
Expand All @@ -42,34 +44,16 @@ const emit = defineEmits<{
(event: 'update:value', value: PlacesValueType): void
}>()

const loading = ref(true)
const availableOptions = ref<NcSelectPlaceOption[]>([])
const selectedOptions = ref<NcSelectPlaceOption[]>([])

watch(selectedOptions, (newSelectedOptionValue) => {
if (newSelectedOptionValue.length === 0) {
emit('update:value', undefined)
} else {
emit('update:value', newSelectedOptionValue.map(place => place.label))
}
})

fetchCollections(placesPrefix)
.then(places => {
availableOptions.value = places.map(place => ({
label: place.basename,
previewUrl: generateUrl(`/apps/photos/api/v1/preview/${place.attributes['last-photo']}?x=${64}&y=${64}`),
}))

selectedOptions.value = (props.value ?? [])
.map(place => availableOptions.value.find(option => option.label === place))
.filter(place => place !== undefined)

loading.value = false
})
const { availablePlacesWithoutSelections, loadingCollections, selectedPlaces } = usePlaceFilter(props, emit)
</script>
<style lang="scss" scoped>
.places-select {
margin-bottom: 0 !important;
}

.place__preview {
width: 20px;
height: 20px;
border-radius: 100%;
}
</style>
69 changes: 69 additions & 0 deletions src/components/PhotosFilters/usePlaceFilter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { computed, ref, watch } from 'vue'

import { generateUrl } from '@nextcloud/router'

import { placesPrefix } from '../../store/places.ts'
import useFetchCollections from '../../mixins/useFetchCollections.ts'
import store from '../../store/index.ts'
import type { PlacesValueType } from '../../services/PhotosFilters/placesFilter.ts'

type NcSelectPlaceOption = {
label: string
previewUrl?: string
}

export default function(props: Readonly<{ value: PlacesValueType }>, emit: (event: 'update:value', args: PlacesValueType) => void,) {
const { fetchCollections, loadingCollections } = useFetchCollections()

const availablePlaces = ref<NcSelectPlaceOption[]>([])

const selectedPlaces = computed<NcSelectPlaceOption[]>({
get() {
return (props.value ?? []).map(placeId => {
const place = store.getters.getPlace(placeId)

return {
label: place?.displayname ?? placeId,
previewUrl: place ? generateUrl(`/apps/photos/api/v1/preview/${place.attributes['last-photo']}?x=${64}&y=${64}`) : undefined,
}
})
},
set(newSelectedPlacesValue) {
if (newSelectedPlacesValue.length === 0) {
emit('update:value', undefined)
} else {
emit('update:value', newSelectedPlacesValue.map(place => place.label))
}
}
})

const availablePlacesWithoutSelections = computed(() => availablePlaces.value.filter((option) => !selectedPlaces.value.includes(option)))

watch(
() => store.getters.places,
(value) => {
availablePlaces.value = Object.values(value).map((place) => {
return {
label: place.displayname,
previewUrl: generateUrl(`/apps/photos/api/v1/preview/${place.attributes['last-photo']}?x=${64}&y=${64}`),
}
})
},
{ immediate: true },
)

if (Object.values(store.getters.places).length === 0) {
fetchCollections(placesPrefix)
}

return {
availablePlacesWithoutSelections,
selectedPlaces,
loadingCollections,
}
}
33 changes: 33 additions & 0 deletions src/mixins/useAbortController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { computed, onUnmounted, ref } from 'vue'

import router from '../router'

export default function() {
const abortController = ref(new AbortController())

const abortSignal = computed(() => abortController.value.signal)

function abortPendingRequest() {
abortController.value.abort()
abortController.value = new AbortController()
}

onUnmounted(() => {
abortController.value.abort()
})

router.beforeEach((from, to, next) => {
abortPendingRequest()
next()
})

return {
abortSignal,
abortPendingRequest,
}
}
49 changes: 49 additions & 0 deletions src/mixins/useFetchCollections.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import type { WebDAVClient } from 'webdav'
import { ref } from 'vue'

import { fetchCollections, type Collection } from '../services/collectionFetcher.js'
import logger from '../services/logger.js'
import store from '../store'
import { davClient } from '../services/DavClient.ts'
import useAbortController from './useAbortController.ts'

export default function() {
const errorFetchingCollections = ref(null as null|number|Error|unknown)
const loadingCollections = ref(false)
const { abortSignal } = useAbortController()

async function _fetchCollections(collectionHome: string, extraProps: string[] = [], client: WebDAVClient = davClient): Promise<Collection[]> {
if (loadingCollections.value) {
return []
}

try {
loadingCollections.value = true
errorFetchingCollections.value = null

const collections = await fetchCollections(collectionHome, { signal: abortSignal.value }, extraProps, client)

store.dispatch('addCollections', { collections })

return collections
} catch (error) {
if (error.response?.status === 404) {
errorFetchingCollections.value = 404
} else {
errorFetchingCollections.value = error
}
logger.error('Error fetching collections:', { error })
} finally {
loadingCollections.value = false
}

return []
}

return { fetchCollections: _fetchCollections, errorFetchingCollections, loadingCollections }
}
5 changes: 4 additions & 1 deletion src/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ export type PhotosStore = {
getPublicAlbumName(publicAlbumName: string): string,
getSharedAlbum(sharedAlbumName: string): Album
getSharedAlbumFiles(sharedAlbumName: string): string[]
getPlaceName(sharedAlbumName: string): string,
getPlace(sharedAlbumName: string): Collection|undefined
getPlaceFiles(sharedAlbumName: string): string[]
getSharedAlbumName(sharedAlbumName: string): string,
tagId(name: string): string,
}
Expand Down Expand Up @@ -93,6 +96,6 @@ const photosStore = new Store({
],

strict: process.env.NODE_ENV !== 'production',
})
}) as PhotosStore

export default photosStore