Merge pull request #6367 from Ocelot-Social-Community/alternate-solution-filter-order-post-types

feat(webapp): alternative solution for filter and order posts
This commit is contained in:
Moriz Wahl 2023-06-05 20:23:45 +02:00 committed by GitHub
commit befc37f304
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 377 additions and 65 deletions

View File

@ -158,7 +158,7 @@ describe('Filter Posts', () => {
})
describe('order events by event start descending', () => {
it('finds the events orderd accordingly', async () => {
it('finds the events ordered accordingly', async () => {
const {
data: { Post: result },
} = await query({
@ -180,7 +180,7 @@ describe('Filter Posts', () => {
})
describe('order events by event start ascending', () => {
it('finds the events orderd accordingly', async () => {
it('finds the events ordered accordingly', async () => {
const {
data: { Post: result },
} = await query({

View File

@ -12,12 +12,12 @@
<li class="item item-save-topics">
<labeled-button filled :label="$t('actions.save')" icon="save" @click="saveCategories" />
</li>
<hr />
<ds-space margin="base" />
</template>
<template #filter-list>
<div class="category-filter-list">
<hr />
<ds-space margin="small" />
<base-button
v-for="category in categories"
:key="category.id"

View File

@ -0,0 +1,57 @@
<template>
<filter-menu-section class="order-by-filter" :title="sectionTitle" :divider="false">
<template #filter-list>
<li class="item">
<labeled-button
icon="check"
:label="$t('filter-menu.ended.all.label')"
:filled="!eventsEnded"
:title="$t('filter-menu.ended.all.hint')"
@click="toggleEventsEnded"
data-test="all-button"
/>
</li>
<li class="item">
<labeled-button
icon="calendar"
:label="$t('filter-menu.ended.onlyEnded.label')"
:filled="eventsEnded"
:title="$t('filter-menu.ended.onlyEnded.hint')"
@click="toggleEventsEnded"
data-test="not-ended-button"
/>
</li>
</template>
</filter-menu-section>
</template>
<script>
import { mapGetters, mapMutations } from 'vuex'
import FilterMenuSection from '~/components/FilterMenu/FilterMenuSection'
import LabeledButton from '~/components/_new/generic/LabeledButton/LabeledButton'
export default {
name: 'EventsByFilter',
components: {
FilterMenuSection,
LabeledButton,
},
computed: {
...mapGetters({
postFilter: 'posts/filter',
}),
sectionTitle() {
// return $t('filter-menu.eventsEnded')
return null
},
eventsEnded() {
return !!this.postFilter.eventStart_gte
},
},
methods: {
...mapMutations({
toggleEventsEnded: 'posts/TOGGLE_EVENTS_ENDED',
}),
},
}
</script>

View File

@ -15,6 +15,7 @@ describe('FilterMenu.vue', () => {
const getters = {
'posts/isActive': () => false,
'posts/filteredPostTypes': () => [],
'posts/orderBy': () => 'createdAt_desc',
}

View File

@ -6,6 +6,10 @@
<post-type-filter />
<categories-filter v-if="categoriesActive" @showFilterMenu="$emit('showFilterMenu')" />
</div>
<div v-if="eventSetInPostTypeFilter" class="filter-menu-options">
<h2 class="title">{{ $t('filter-menu.eventsBy') }}</h2>
<events-by-filter />
</div>
<div class="filter-menu-options">
<h2 class="title">{{ $t('filter-menu.order-by') }}</h2>
<order-by-filter />
@ -14,6 +18,8 @@
</template>
<script>
import { mapGetters } from 'vuex'
import EventsByFilter from './EventsByFilter'
import PostTypeFilter from './PostTypeFilter'
import FollowingFilter from './FollowingFilter'
import OrderByFilter from './OrderByFilter'
@ -21,6 +27,7 @@ import CategoriesFilter from './CategoriesFilter'
export default {
components: {
EventsByFilter,
FollowingFilter,
OrderByFilter,
CategoriesFilter,
@ -31,6 +38,14 @@ export default {
categoriesActive: this.$env.CATEGORIES_ACTIVE,
}
},
computed: {
...mapGetters({
filteredPostTypes: 'posts/filteredPostTypes',
}),
eventSetInPostTypeFilter() {
return this.filteredPostTypes.includes('Event')
},
},
}
</script>

View File

@ -44,13 +44,12 @@ export default {
</script>
<style lang="scss">
.my-filter-button-selected {
padding-right: 30px;
margin-top: 4px;
padding-right: 36px;
}
.base-button.filter-remove {
position: relative;
margin-left: -31px;
margin-left: -37px;
top: -5px;
margin-right: 8px;
}

View File

@ -11,6 +11,8 @@ describe('OrderByFilter', () => {
'posts/TOGGLE_ORDER': jest.fn(),
}
const getters = {
'posts/filteredPostTypes': () => [],
'posts/orderedByCreationDate': () => true,
'posts/orderBy': () => 'createdAt_desc',
}

View File

@ -1,23 +1,23 @@
<template>
<filter-menu-section :divider="false" class="order-by-filter">
<filter-menu-section class="order-by-filter" :title="sectionTitle" :divider="false">
<template #filter-list>
<li class="item">
<labeled-button
icon="sort-amount-asc"
:label="$t('filter-menu.order.newest.label')"
:filled="orderBy === 'createdAt_desc'"
:title="$t('filter-menu.order.newest.hint')"
@click="toggleOrder('createdAt_desc')"
:label="buttonLabel('desc')"
:filled="orderBy === orderedDesc"
:title="buttonTitle('desc')"
@click="toggleOrder(orderedDesc)"
data-test="newest-button"
/>
</li>
<li class="item">
<labeled-button
icon="sort-amount-desc"
:label="$t('filter-menu.order.oldest.label')"
:filled="orderBy === 'createdAt_asc'"
:title="$t('filter-menu.order.oldest.hint')"
@click="toggleOrder('createdAt_asc')"
:label="buttonLabel('asc')"
:filled="orderBy === orderedAsc"
:title="buttonTitle('asc')"
@click="toggleOrder(orderedAsc)"
data-test="oldest-button"
/>
</li>
@ -38,13 +38,56 @@ export default {
},
computed: {
...mapGetters({
filteredPostTypes: 'posts/filteredPostTypes',
orderBy: 'posts/orderBy',
}),
orderedByCreationDate() {
return !this.filteredPostTypes.includes('Event')
},
orderedAsc() {
return this.orderedByCreationDate ? 'createdAt_asc' : 'eventStart_desc'
},
orderedDesc() {
return this.orderedByCreationDate ? 'createdAt_desc' : 'eventStart_asc'
},
sectionTitle() {
return this.orderedByCreationDate
? this.$t('filter-menu.creationDate')
: this.$t('filter-menu.startDate')
},
},
methods: {
...mapMutations({
toggleOrder: 'posts/TOGGLE_ORDER',
}),
buttonLabel(buttonType) {
switch (buttonType) {
case 'asc':
return this.orderedByCreationDate
? this.$t('filter-menu.order.oldest.label')
: this.$t('filter-menu.order.last.label')
case 'desc':
return this.orderedByCreationDate
? this.$t('filter-menu.order.newest.label')
: this.$t('filter-menu.order.next.label')
default:
return ''
}
},
buttonTitle(buttonType) {
switch (buttonType) {
case 'asc':
return this.orderedByCreationDate
? this.$t('filter-menu.order.oldest.hint')
: this.$t('filter-menu.order.last.hint')
case 'desc':
return this.orderedByCreationDate
? this.$t('filter-menu.order.newest.hint')
: this.$t('filter-menu.order.next.hint')
default:
return ''
}
},
},
}
</script>

View File

@ -5,22 +5,31 @@
class="following-filter"
>
<template #filter-follower>
<li class="item all-item">
<labeled-button
icon="check"
:label="$t('filter-menu.all')"
:filled="filteredPostTypes.length === 0"
:title="$t('filter-menu.all')"
@click="togglePostType(null)"
/>
</li>
<li class="item article-item">
<labeled-button
icon="book"
:label="$t('filter-menu.article')"
:filled="articleSet"
:filled="filteredPostTypes.includes('Article')"
:title="$t('filter-menu.article')"
@click="toggleFilterPostType('Article')"
@click="togglePostType('Article')"
/>
</li>
<li class="item event-item">
<labeled-button
icon="calendar"
:label="$t('filter-menu.events')"
:filled="eventSet"
:title="$t('filter-menu.events')"
@click="toggleFilterPostType('Event')"
:label="$t('filter-menu.event')"
:filled="filteredPostTypes.includes('Event')"
:title="$t('filter-menu.event')"
@click="togglePostType('Event')"
/>
</li>
</template>
@ -41,18 +50,11 @@ export default {
computed: {
...mapGetters({
filteredPostTypes: 'posts/filteredPostTypes',
currentUser: 'auth/user',
}),
articleSet() {
return this.filteredPostTypes.includes('Article')
},
eventSet() {
return this.filteredPostTypes.includes('Event')
},
},
methods: {
...mapMutations({
toggleFilterPostType: 'posts/TOGGLE_POST_TYPE',
togglePostType: 'posts/TOGGLE_POST_TYPE',
}),
},
}

View File

@ -272,7 +272,9 @@
"myFriends": "Nutzer denen ich folge",
"myGroups": "Aus meinen Gruppen",
"myTopics": "Meine Themen",
"noFilter": "Beiträge filtern"
"noFilter": "Inhalt filtern",
"onlyArticles": "Nur Beiträge",
"onlyEvents": "Nur Veranstaltungen"
},
"filterMyGroups": "Beiträge in meinen Gruppen",
"inappropriatePicture": "Dieses Bild kann für einige Menschen unangemessen sein.",
@ -378,31 +380,53 @@
},
"filter-menu": {
"all": "Alle",
"article": "Artikel",
"article": "Beitrag",
"categories": "Themen",
"creationDate": "Erstellungszeitpunkt",
"deleteFilter": "Filter löschen",
"emotions": "Emotionen",
"events": "Veranstaltungen",
"ended": {
"all": {
"hint": "Zeige alle, auch beendete",
"label": "Alle"
},
"onlyEnded": {
"hint": "Zeige nur noch nicht beendete",
"label": "Nicht beendete"
}
},
"event": "Veranstaltung",
"eventsBy": "Veranstaltungen zeige ...",
"eventsEnded": "Beendet",
"filter-by": "Filtern nach ...",
"following": "Nutzer denen ich folge",
"languages": "Sprachen",
"my-groups": "Meinen Gruppen",
"order": {
"last": {
"hint": "Sortiere die Letzten nach vorn",
"label": "Letzte zuerst"
},
"newest": {
"hint": "Sortiere die Neuesten nach vorn",
"label": "Neueste zuerst"
},
"next": {
"hint": "Sortiere die Nächsten nach vorn",
"label": "Nächste zuerst"
},
"oldest": {
"hint": "Sortiere die Ältesten nach vorn",
"label": "Älteste zuerst"
}
},
"order-by": "Sortieren nach ...",
"post-type": "Beitrags-Typ",
"post-type": "Inhaltstyp",
"save": {
"error": "Themen konnten nicht gespeichert werden!",
"success": "Themen gespeichert!"
}
},
"startDate": "Anfangszeitpunkt"
},
"followButton": {
"follow": "Folgen",

View File

@ -272,7 +272,9 @@
"myFriends": "Users I follow",
"myGroups": "By my groups",
"myTopics": "My topics",
"noFilter": "Filter posts"
"noFilter": "Filter content",
"onlyArticles": "Only articles",
"onlyEvents": "Only events"
},
"filterMyGroups": "Contributions in my groups",
"inappropriatePicture": "This image may be inappropriate for some people.",
@ -380,29 +382,51 @@
"all": "All",
"article": "Article",
"categories": "Topics",
"creationDate": "Creation date",
"deleteFilter": "Delete filter",
"emotions": "Emotions",
"events": "Events",
"ended": {
"all": {
"hint": "Show all, also ended",
"label": "All"
},
"onlyEnded": {
"hint": "Show only not ended",
"label": "Not ended"
}
},
"event": "Event",
"eventsBy": "Events show ...",
"eventsEnded": "Ended",
"filter-by": "Filter by ...",
"following": "Users I follow",
"languages": "Languages",
"my-groups": "My groups",
"order": {
"last": {
"hint": "Sort posts by the last first",
"label": "Last first"
},
"newest": {
"hint": "Sort posts by the newest first",
"label": "Newest first"
},
"next": {
"hint": "Sort posts by the next first",
"label": "Next first"
},
"oldest": {
"hint": "Sort posts by the oldest first",
"label": "Oldest first"
}
},
"order-by": "Order by ...",
"post-type": "Post type",
"post-type": "Content type",
"save": {
"error": "Failed saving topic settings!",
"success": "Topics saved!"
}
},
"startDate": "Start date"
},
"followButton": {
"follow": "Follow",

View File

@ -28,6 +28,10 @@ describe('PostIndex', () => {
store = new Vuex.Store({
getters: {
'posts/filter': () => ({}),
'posts/filteredPostTypes': () => [],
'posts/articleSetInPostTypeFilter': () => false,
'posts/eventSetInPostTypeFilter': () => false,
'posts/eventsEnded': () => '',
'posts/orderBy': () => 'createdAt_desc',
'auth/user': () => {
return { id: 'u23' }

View File

@ -25,6 +25,7 @@
<base-button
class="my-filter-button"
v-if="
!postsFilter['postType_in'] &&
!postsFilter['categories_some'] &&
!postsFilter['author'] &&
!postsFilter['postsInMyGroups']
@ -38,6 +39,22 @@
<base-icon class="my-filter-button" :name="filterButtonIcon"></base-icon>
</base-button>
<header-button
v-if="filteredPostTypes.includes('Article')"
:title="$t('contribution.filterMasonryGrid.onlyArticles')"
:clickButton="openFilterMenu"
:titleRemove="$t('filter-menu.deleteFilter')"
:clickRemove="resetPostType"
/>
<header-button
v-if="filteredPostTypes.includes('Event')"
:title="$t('contribution.filterMasonryGrid.onlyEvents')"
:clickButton="openFilterMenu"
:titleRemove="$t('filter-menu.deleteFilter')"
:clickRemove="resetPostType"
/>
<header-button
v-if="postsFilter['categories_some']"
:title="$t('contribution.filterMasonryGrid.myTopics')"
@ -62,7 +79,7 @@
:clickRemove="resetByGroups"
/>
<div id="my-filter" v-if="showFilter">
<div @mouseleave="showFilter = false">
<div @mouseleave="mouseLeaveFilterMenu">
<filter-menu-component @showFilterMenu="showFilterMenu" />
</div>
</div>
@ -120,6 +137,7 @@
<script>
import postListActions from '~/mixins/postListActions'
import mobile from '~/mixins/mobile'
import DonationInfo from '~/components/DonationInfo/DonationInfo.vue'
import HashtagsFilter from '~/components/HashtagsFilter/HashtagsFilter.vue'
import HcEmpty from '~/components/Empty/Empty'
@ -134,7 +152,6 @@ import UpdateQuery from '~/components/utils/UpdateQuery'
import FilterMenuComponent from '~/components/FilterMenu/FilterMenuComponent'
import { SHOW_CONTENT_FILTER_MASONRY_GRID } from '~/constants/filter.js'
import { POST_ADD_BUTTON_POSITION_TOP } from '~/constants/posts.js'
import mobile from '~/mixins/mobile'
export default {
components: {
@ -154,12 +171,13 @@ export default {
hideByScroll: false,
revScrollpos: 0,
showFilter: false,
developerNoAutoClosingFilterMenu: false, // stops automatic closing of filter menu for developer purposes: default is 'false'
showDonations: false,
goal: 15000,
progress: 7000,
posts: [],
hasMore: true,
// Initialize your apollo data
// initialize your apollo data
offset: 0,
pageSize: 12,
hashtag,
@ -170,14 +188,12 @@ export default {
},
computed: {
...mapGetters({
filteredPostTypes: 'posts/filteredPostTypes',
postsFilter: 'posts/filter',
orderBy: 'posts/orderBy',
}),
filterButtonIcon() {
if (Object.keys(this.postsFilter).length === 0) {
return this.showFilter ? 'angle-up' : 'angle-down'
}
return 'close'
return this.showFilter ? 'angle-up' : 'angle-down'
},
finalFilters() {
let filter = this.postsFilter
@ -197,6 +213,11 @@ export default {
},
},
watchQuery: ['hashtag'],
watch: {
postsFilter() {
this.resetPostList()
},
},
mounted() {
if (this.categoryId) {
this.resetCategories()
@ -207,6 +228,7 @@ export default {
},
methods: {
...mapMutations({
resetPostType: 'posts/RESET_POST_TYPE',
resetByFollowed: 'posts/TOGGLE_FILTER_BY_FOLLOWED',
resetByGroups: 'posts/TOGGLE_FILTER_BY_MY_GROUPS',
resetCategories: 'posts/RESET_CATEGORIES',
@ -215,6 +237,10 @@ export default {
openFilterMenu() {
this.showFilter = !this.showFilter
},
mouseLeaveFilterMenu() {
if (this.developerNoAutoClosingFilterMenu) return
this.showFilter = false
},
showFilterMenu(e) {
if (!e || (!e.target.closest('#my-filter') && !e.target.closest('.my-filter-button'))) {
if (!this.showFilter) return

View File

@ -63,12 +63,6 @@ export const mutations = {
if (isEmpty(get(filter, 'categories_some.id_in'))) delete filter.categories_some
state.filter = filter
},
TOGGLE_POST_TYPE(state, postType) {
const filter = clone(state.filter)
update(filter, 'postType_in', (postTypes) => xor(postTypes, [postType]))
if (isEmpty(get(filter, 'postType_in'))) delete filter.postType_in
state.filter = filter
},
TOGGLE_LANGUAGE(state, languageCode) {
const filter = clone(state.filter)
update(filter, 'language_in', (languageCodes) => xor(languageCodes, [languageCode]))
@ -81,6 +75,42 @@ export const mutations = {
if (isEmpty(get(filter, 'emotions_some.emotion_in'))) delete filter.emotions_some
state.filter = filter
},
RESET_POST_TYPE(state) {
const filter = clone(state.filter)
delete filter.eventStart_gte
delete filter.postType_in
state.order = 'createdAt_desc'
state.filter = filter
},
TOGGLE_POST_TYPE(state, postType) {
const filter = clone(state.filter)
if (postType && !(filter.postType_in && filter.postType_in.includes(postType))) {
filter.postType_in = [postType]
if (postType === 'Event') {
filter.eventStart_gte = new Date()
state.order = 'eventStart_asc'
} else {
delete filter.eventStart_gte
state.order = 'createdAt_desc'
}
} else {
delete filter.eventStart_gte
delete filter.postType_in
state.order = 'createdAt_desc'
}
state.filter = filter
},
TOGGLE_EVENTS_ENDED(state) {
const filter = clone(state.filter)
if (filter.eventStart_gte) {
delete filter.eventStart_gte
} else {
if (filter.postType_in && filter.postType_in.includes('Event')) {
filter.eventStart_gte = new Date()
}
}
state.filter = filter
},
TOGGLE_ORDER(state, value) {
state.order = value
},

View File

@ -233,25 +233,49 @@ describe('mutations', () => {
}
})
it('creates post type filter if empty', () => {
it('creates post type filter if empty, sets event start filter and event start order', () => {
state = { filter: {} }
expect(testMutation('Event')).toEqual({ postType_in: ['Event'] })
expect(testMutation('Event')).toEqual({
postType_in: ['Event'],
eventStart_gte: expect.any(Date),
})
expect(getters.orderBy(state)).toEqual('eventStart_asc')
})
it('adds post type not present', () => {
state = { filter: { postType_in: ['Event'] } }
expect(testMutation('Article')).toEqual({ postType_in: ['Event', 'Article'] })
it('changes post type if present, resets filter event start and order', () => {
state = {
filter: {
postType_in: ['Event'],
eventStart_gte: new Date(),
},
order: 'eventStart_asc',
}
expect(testMutation('Article')).toEqual({ postType_in: ['Article'] })
expect(getters.orderBy(state)).toEqual('createdAt_desc')
})
it('removes category id if present', () => {
state = { filter: { postType_in: ['Event', 'Article'] } }
const result = testMutation('Event')
expect(result).toEqual({ postType_in: ['Article'] })
})
it('removes category filter if empty', () => {
state = { filter: { postType_in: ['Event'] } }
it('removes post type filter if same post type is present and sets order', () => {
state = {
filter: {
postType_in: ['Event'],
eventStart_gte: new Date(),
},
order: 'eventStart_asc',
}
expect(testMutation('Event')).toEqual({})
expect(getters.orderBy(state)).toEqual('createdAt_desc')
})
it('removes post type filter if called with null', () => {
state = {
filter: {
postType_in: ['Event'],
eventStart_gte: new Date(),
},
order: 'eventStart_asc',
}
expect(testMutation(null)).toEqual({})
expect(getters.orderBy(state)).toEqual('createdAt_desc')
})
it('does not get in the way of other filters', () => {
@ -265,6 +289,67 @@ describe('mutations', () => {
})
})
describe('RESET_POST_TYPE', () => {
beforeEach(() => {
testMutation = () => {
mutations.RESET_POST_TYPE(state)
return getters.filter(state)
}
})
it('resets the post type filter, event start and order', () => {
state = {
filter: {
postType_in: ['Event'],
eventStart_gte: new Date(),
},
order: 'eventStart_asc',
}
expect(testMutation()).toEqual({})
expect(getters.orderBy(state)).toEqual('createdAt_desc')
})
})
describe('TOGGLE_EVENTS_ENDED', () => {
beforeEach(() => {
testMutation = (postType) => {
mutations.TOGGLE_EVENTS_ENDED(state, postType)
return getters.filter(state)
}
})
it('does not set events ended when post type is not Event', () => {
state = {
filter: {},
}
expect(testMutation()).toEqual({})
})
it('sets events ended when post type is Event', () => {
state = {
filter: {
postType_in: ['Event'],
},
}
expect(testMutation()).toEqual({
postType_in: ['Event'],
eventStart_gte: expect.any(Date),
})
})
it('unsets events ended when set', () => {
state = {
filter: {
postType_in: ['Event'],
eventStart_gte: new Date(),
},
}
expect(testMutation()).toEqual({
postType_in: ['Event'],
})
})
})
describe('TOGGLE_FILTER_BY_FOLLOWED', () => {
beforeEach(() => {
testMutation = (userId) => {