Skip to content

Commit

Permalink
feat: Timeline Filters (#3284)
Browse files Browse the repository at this point in the history
* added timeline event filters

* updated empty timeline text

* simplify icons/labels for event types

* added missing translations

* cloned sort improvements to explore page

* added filter indicator

* lint

* removed lint warning

* add top margin to "no events found" text

Co-authored-by: Kuchenpirat <[email protected]>

* fixed reversed sort icons

Co-authored-by: Kuchenpirat <[email protected]>

* fixed sort dir on timeline filter

* sync checkbox state with preferences state

---------

Co-authored-by: Kuchenpirat <[email protected]>
  • Loading branch information
michael-genson and Kuchenpirat authored Mar 12, 2024
1 parent e83fa89 commit 0a34473
Show file tree
Hide file tree
Showing 7 changed files with 139 additions and 27 deletions.
8 changes: 6 additions & 2 deletions frontend/components/Domain/Recipe/RecipeExplorerPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,16 @@
<v-list>
<v-list-item @click="toggleOrderDirection()">
<v-icon left>
{{ $globals.icons.sort }}
{{
state.orderDirection === "asc" ?
$globals.icons.sortDescending : $globals.icons.sortAscending
}}
</v-icon>
<v-list-item-title>
{{ state.orderDirection === "asc" ? "Sort Descending" : "Sort Ascending" }}
{{ state.orderDirection === "asc" ? $tc("general.sort-descending") : $tc("general.sort-ascending") }}
</v-list-item-title>
</v-list-item>
<v-divider />
<v-list-item
v-for="v in sortable"
:key="v.name"
Expand Down
94 changes: 85 additions & 9 deletions frontend/components/Domain/Recipe/RecipeTimeline.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,53 @@
<v-row class="my-0 mx-7">
<v-spacer />
<v-col class="text-right">
<v-btn fab small color="info" @click="reverseSort">
<v-icon> {{ preferences.orderDirection === "asc" ? $globals.icons.sortCalendarAscending : $globals.icons.sortCalendarDescending }} </v-icon>
</v-btn>
<!-- Filters -->
<v-menu offset-y bottom left nudge-bottom="3" :close-on-content-click="false">
<template #activator="{ on, attrs }">
<v-badge :content="filterBadgeCount" :value="filterBadgeCount" bordered overlap>
<v-btn fab small color="info" v-bind="attrs" v-on="on">
<v-icon> {{ $globals.icons.filter }} </v-icon>
</v-btn>
</v-badge>
</template>
<v-card>
<v-list>
<v-list-item @click="reverseSort">
<v-icon left>
{{
preferences.orderDirection === "asc" ?
$globals.icons.sortCalendarDescending : $globals.icons.sortCalendarAscending
}}
</v-icon>
<v-list-item-title>
{{ preferences.orderDirection === "asc" ? $tc("general.sort-descending") : $tc("general.sort-ascending") }}
</v-list-item-title>
</v-list-item>
<v-divider />
<v-list-item class="pa-0">
<v-list class="py-0" style="width: 100%;">
<v-list-item
v-for="option, idx in eventTypeFilterState"
:key="idx"
>
<v-checkbox
:input-value="option.checked"
readonly
@click="toggleEventTypeOption(option.value)"
>
<template #label>
<v-icon left>
{{ option.icon }}
</v-icon>
{{ option.label }}
</template>
</v-checkbox>
</v-list-item>
</v-list>
</v-list-item>
</v-list>
</v-card>
</v-menu>
</v-col>
</v-row>
<v-divider class="mx-2"/>
Expand All @@ -29,9 +73,9 @@
/>
</v-timeline>
</div>
<v-card v-else-if="!loading">
<v-card v-else-if="!loading" class="mt-2">
<v-card-title class="justify-center pa-9">
{{ $t("recipe.timeline-is-empty") }}
{{ $t("recipe.timeline-no-events-found-try-adjusting-filters") }}
</v-card-title>
</v-card>
<div v-if="loading" class="mb-3 text-center">
Expand All @@ -41,14 +85,15 @@
</template>

<script lang="ts">
import { defineComponent, onMounted, ref, useAsync, useContext } from "@nuxtjs/composition-api";
import { computed, defineComponent, onMounted, ref, useAsync, useContext } from "@nuxtjs/composition-api";
import { useThrottleFn, whenever } from "@vueuse/core";
import RecipeTimelineItem from "./RecipeTimelineItem.vue"
import { useTimelinePreferences } from "~/composables/use-users/preferences";
import { useTimelineEventTypes } from "~/composables/recipes/use-recipe-timeline-events";
import { useAsyncKey } from "~/composables/use-utils";
import { alert } from "~/composables/use-toast";
import { useUserApi } from "~/composables/api";
import { Recipe, RecipeTimelineEventOut, RecipeTimelineEventUpdate } from "~/lib/api/types/recipe"
import { Recipe, RecipeTimelineEventOut, RecipeTimelineEventUpdate, TimelineEventType } from "~/lib/api/types/recipe";
export default defineComponent({
components: { RecipeTimelineItem },
Expand Down Expand Up @@ -76,6 +121,7 @@ export default defineComponent({
const api = useUserApi();
const { i18n } = useContext();
const preferences = useTimelinePreferences();
const { eventTypeOptions } = useTimelineEventTypes();
const loading = ref(true);
const ready = ref(false);
Expand All @@ -85,6 +131,15 @@ export default defineComponent({
const timelineEvents = ref([] as RecipeTimelineEventOut[]);
const recipes = new Map<string, Recipe>();
const filterBadgeCount = computed(() => eventTypeOptions.value.length - preferences.value.types.length);
const eventTypeFilterState = computed(() => {
return eventTypeOptions.value.map(option => {
return {
...option,
checked: preferences.value.types.includes(option.value),
}
});
});
interface ScrollEvent extends Event {
target: HTMLInputElement;
Expand Down Expand Up @@ -112,7 +167,7 @@ export default defineComponent({
}
);
// Sorting
// Preferences
function reverseSort() {
if (loading.value) {
return;
Expand All @@ -122,6 +177,21 @@ export default defineComponent({
initializeTimelineEvents();
}
function toggleEventTypeOption(option: TimelineEventType) {
if (loading.value) {
return;
}
const index = preferences.value.types.indexOf(option);
if (index === -1) {
preferences.value.types.push(option);
} else {
preferences.value.types.splice(index, 1);
}
initializeTimelineEvents();
}
// Timeline Actions
async function updateTimelineEvent(index: number) {
const event = timelineEvents.value[index]
Expand Down Expand Up @@ -179,8 +249,11 @@ export default defineComponent({
async function scrollTimelineEvents() {
const orderBy = "timestamp";
const orderDirection = preferences.value.orderDirection === "asc" ? "asc" : "desc";
// eslint-disable-next-line quotes
const eventTypeValue = `["${preferences.value.types.join('", "')}"]`;
const queryFilter = `(${props.queryFilter}) AND eventType IN ${eventTypeValue}`
const response = await api.recipes.getAllTimelineEvents(page.value, perPage, { orderBy, orderDirection, queryFilter: props.queryFilter });
const response = await api.recipes.getAllTimelineEvents(page.value, perPage, { orderBy, orderDirection, queryFilter });
page.value += 1;
if (!response?.data) {
return;
Expand Down Expand Up @@ -256,11 +329,14 @@ export default defineComponent({
return {
deleteTimelineEvent,
filterBadgeCount,
loading,
onScroll,
preferences,
eventTypeFilterState,
recipes,
reverseSort,
toggleEventTypeOption,
timelineEvents,
updateTimelineEvent,
};
Expand Down
21 changes: 6 additions & 15 deletions frontend/components/Domain/Recipe/RecipeTimelineItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ import { computed, defineComponent, ref, useContext, useRoute } from "@nuxtjs/co
import RecipeCardMobile from "./RecipeCardMobile.vue";
import RecipeTimelineContextMenu from "./RecipeTimelineContextMenu.vue";
import { useStaticRoutes } from "~/composables/api";
import { useTimelineEventTypes } from "~/composables/recipes/use-recipe-timeline-events";
import { Recipe, RecipeTimelineEventOut } from "~/lib/api/types/recipe"
import UserAvatar from "~/components/Domain/User/UserAvatar.vue";
import SafeMarkdown from "~/components/global/SafeMarkdown.vue";
Expand All @@ -124,6 +125,7 @@ export default defineComponent({
setup(props) {
const { $auth, $globals, $vuetify } = useContext();
const { recipeTimelineEventImage } = useStaticRoutes();
const { eventTypeOptions } = useTimelineEventTypes();
const timelineEvents = ref([] as RecipeTimelineEventOut[]);
const route = useRoute();
Expand Down Expand Up @@ -164,21 +166,10 @@ export default defineComponent({
}
})
const icon = computed( () => {
switch (props.event.eventType) {
case "comment":
return $globals.icons.commentTextMultiple;
case "info":
return $globals.icons.informationVariant;
case "system":
return $globals.icons.cog;
default:
return $globals.icons.informationVariant;
};
})
const icon = computed(() => {
const option = eventTypeOptions.value.find((option) => option.value === props.event.eventType);
return option ? option.icon : $globals.icons.informationVariant;
});
const hideImage = ref(false);
const eventImageUrl = computed<string>( () => {
Expand Down
35 changes: 35 additions & 0 deletions frontend/composables/recipes/use-recipe-timeline-events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { computed, useContext } from "@nuxtjs/composition-api";
import { TimelineEventType } from "~/lib/api/types/recipe";

export interface TimelineEventTypeData {
value: TimelineEventType;
label: string;
icon: string;
}

export const useTimelineEventTypes = () => {
const { $globals, i18n } = useContext();
const eventTypeOptions = computed<TimelineEventTypeData[]>(() => {
return [
{
value: "comment",
label: i18n.tc("recipe.comment"),
icon: $globals.icons.commentTextMultiple,
},
{
value: "info",
label: i18n.tc("settings.theme.info"),
icon: $globals.icons.informationVariant,
},
{
value: "system",
label: i18n.tc("general.system"),
icon: $globals.icons.cog,
},
];
});

return {
eventTypeOptions,
}
}
3 changes: 3 additions & 0 deletions frontend/composables/use-users/preferences.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Ref, useContext } from "@nuxtjs/composition-api";
import { useLocalStorage } from "@vueuse/core";
import { TimelineEventType } from "~/lib/api/types/recipe";

export interface UserPrintPreferences {
imagePosition: string;
Expand Down Expand Up @@ -28,6 +29,7 @@ export interface UserShoppingListPreferences {

export interface UserTimelinePreferences {
orderDirection: string;
types: TimelineEventType[];
}

export function useUserPrintPreferences(): Ref<UserPrintPreferences> {
Expand Down Expand Up @@ -87,6 +89,7 @@ export function useTimelinePreferences(): Ref<UserTimelinePreferences> {
"timeline-preferences",
{
orderDirection: "asc",
types: ["info", "system", "comment"] as TimelineEventType[],
},
{ mergeDefaults: true }
// we cast to a Ref because by default it will return an optional type ref
Expand Down
4 changes: 4 additions & 0 deletions frontend/lang/messages/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -147,12 +147,15 @@
"show-all": "Show All",
"shuffle": "Shuffle",
"sort": "Sort",
"sort-ascending": "Sort Ascending",
"sort-descending": "Sort Descending",
"sort-alphabetically": "Alphabetical",
"status": "Status",
"subject": "Subject",
"submit": "Submit",
"success-count": "Success: {count}",
"sunday": "Sunday",
"system": "System",
"templates": "Templates:",
"test": "Test",
"themes": "Themes",
Expand Down Expand Up @@ -518,6 +521,7 @@
"edit-timeline-event": "Edit Timeline Event",
"timeline": "Timeline",
"timeline-is-empty": "Nothing on the timeline yet. Try making this recipe!",
"timeline-no-events-found-try-adjusting-filters": "No events found. Try adjusting your search filters.",
"group-global-timeline": "{groupName} Global Timeline",
"open-timeline": "Open Timeline",
"made-this": "I Made This",
Expand Down
1 change: 0 additions & 1 deletion frontend/pages/login.vue
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,6 @@ export default defineComponent({
const allowSignup = computed(() => appInfo.value?.allowSignup || false);
const allowOidc = computed(() => appInfo.value?.enableOidc || false);
const oidcRedirect = computed(() => appInfo.value?.oidcRedirect || false);
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
const oidcProviderName = computed(() => appInfo.value?.oidcProviderName || "OAuth")
whenever(
Expand Down

0 comments on commit 0a34473

Please sign in to comment.