Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(NoteManager): Improve filtering by tags in note search #1510

Merged
merged 10 commits into from
Nov 19, 2024
Next Next commit
feat(NoteManager): Improve filtering by tags in note search
  • Loading branch information
rexy712 committed Nov 10, 2024
commit b37339fd249852906bb6da984139fd5be7bc9a41
4 changes: 4 additions & 0 deletions client/src/fa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import {
faLocationDot,
faLock,
faMagnifyingGlass,
faMinus,
faMinusSquare,
faNoteSticky,
faPaintBrush,
Expand All @@ -74,6 +75,7 @@ import {
faUserTag,
faUsers,
faVideo,
faX,
} from "@fortawesome/free-solid-svg-icons";

export function loadFontAwesome(): void {
Expand Down Expand Up @@ -123,6 +125,7 @@ export function loadFontAwesome(): void {
faLocationDot,
faLock,
faMagnifyingGlass,
faMinus,
faMinusSquare,
faNoteSticky,
faPaintBrush,
Expand Down Expand Up @@ -153,6 +156,7 @@ export function loadFontAwesome(): void {
faVideo,
faWindowClose,
faWindowRestore,
faX,
);

dom.watch();
Expand Down
7 changes: 6 additions & 1 deletion client/src/game/systems/notes/types.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import type { ApiNote } from "../../../apiTypes";
import type { DistributiveOmit } from "../../../core/types";

export type NoteTag = {
name: string;
colour: string;
};

export type ClientNote = DistributiveOmit<ApiNote, "tags"> & {
tags: { name: string; colour: string }[];
tags: NoteTag[];
};

export enum NoteManagerMode {
Expand Down
150 changes: 126 additions & 24 deletions client/src/game/ui/notes/NoteList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ import { mostReadable } from "../../../core/utils";
import { coreStore } from "../../../store/core";
import { locationStore } from "../../../store/location";
import { noteState } from "../../systems/notes/state";
import { NoteManagerMode, type ClientNote } from "../../systems/notes/types";
import { NoteManagerMode, type ClientNote, type NoteTag } from "../../systems/notes/types";
import { popoutNote } from "../../systems/notes/ui";
import { propertiesState } from "../../systems/properties/state";
import { locationSettingsState } from "../../systems/settings/location/state";

import TagAutoCompleteSearch from "./TagAutoCompleteSearch.vue";

const emit = defineEmits<(e: "mode", mode: NoteManagerMode) => void>();

const noteTypes = ["global", "local"] as const;
Expand All @@ -29,8 +31,10 @@ const searchFilters = reactive({
});

const searchBar = ref<HTMLInputElement | null>(null);
const searchTags = ref<NoteTag[]>([]);
const searchFilter = ref("");
const showSearchFilters = ref(false);
const showTagSearch = ref(false);
const searchPage = ref(1);

const shapeFiltered = computed(() => noteState.reactive.shapeFilter !== undefined);
Expand All @@ -45,6 +49,17 @@ const shapeName = computed(() => {
// searchBar.value?.focus();
// });


const availableTags = computed(() => {
const tagList = new Map<string, NoteTag>();
for (const [_, note] of noteState.reactive.notes) {
for (const tag of note.tags) {
tagList.set(tag.name, tag);
}
}
return Array.from(tagList, ([name, value]) => value).sort((a, b) => a.name.localeCompare(b.name));
});

const noteArray = computed(() => {
let it: Iterable<DeepReadonly<ClientNote>> = noteState.reactive.notes.values();
if (!searchFilters.includeArchivedLocations) {
Expand All @@ -56,6 +71,16 @@ const noteArray = computed(() => {
}));
return Array.from(it2);
});

function containsSearchTags(note: typeof noteArray.value[number]): boolean {
for (const tag of searchTags.value) {
if (!note.tags.some((t) => t.name === tag.name)) {
return false;
}
}
return true;
}

const filteredNotes = computed(() => {
const sf = searchFilter.value.trim().toLowerCase();
const searchLocal = selectedNoteTypes.value === "local";
Expand All @@ -80,6 +105,10 @@ const filteredNotes = computed(() => {
}
}

if (!containsSearchTags(note)) {
continue;
}

if (sf.length === 0) {
notes.push(note);
continue;
Expand Down Expand Up @@ -110,6 +139,15 @@ const visibleNotes = computed(() => {
};
});

function toggleTagInSearch(tag: NoteTag): void {
const tempArray = searchTags.value.filter((x) => x.name !== tag.name);
if (tempArray.length === searchTags.value.length) {
searchTags.value.push(tag);
} else {
searchTags.value = tempArray;
}
}

function editNote(noteId: string): void {
noteState.mutableReactive.currentNote = noteId;
emit("mode", NoteManagerMode.Edit);
Expand All @@ -125,7 +163,7 @@ function clearShapeFilter(): void {
<div>NOTES {{ shapeName ? `for ${shapeName}` : "" }}</div>
</header>
<div id="notes-search" :class="shapeFiltered ? 'disabled' : ''">
<div>
<div id="search-bar">
<ToggleGroup
v-show="!shapeFiltered"
id="kind-selector"
Expand All @@ -135,7 +173,6 @@ function clearShapeFilter(): void {
active-color="rgba(173, 216, 230, 0.5)"
/>
<font-awesome-icon icon="magnifying-glass" @click="searchBar?.focus()" />
<div v-if="shapeName" class="shape-name" @click="clearShapeFilter">{{ shapeName }}</div>
<input ref="searchBar" v-model="searchFilter" type="text" placeholder="search through your notes.." />
<div v-show="showSearchFilters" id="search-filter">
<fieldset>
Expand All @@ -144,10 +181,6 @@ function clearShapeFilter(): void {
<input id="note-search-title" v-model="searchFilters.title" type="checkbox" />
<label for="note-search-title">title</label>
</div>
<div>
<input id="note-search-tags" v-model="searchFilters.tags" type="checkbox" />
<label for="note-search-tags">tags</label>
</div>
<div>
<input id="note-search-text" v-model="searchFilters.text" type="checkbox" />
<label for="note-search-text">text</label>
Expand Down Expand Up @@ -211,6 +244,27 @@ function clearShapeFilter(): void {
/>
</div>
</div>
<div id="search-filters">
<span style="padding-right:1rem;">Filters:</span>
<div id="filter-bubbles">
<div v-if="shapeName" class="shape-name tag-bubble removable" @click="clearShapeFilter">{{ shapeName }}</div>
<div
v-for="tag of searchTags"
:key="tag.name"
:style="{ color: mostReadable(tag.colour), backgroundColor: tag.colour }"
class="tag-bubble removable"
@click="toggleTagInSearch(tag)"
>
{{ tag.name }}
</div>
</div>
<div id="add-filters">
<TagAutoCompleteSearch v-show="showTagSearch" id="tag-search-bar" placeholder="Search Tags..." :options="availableTags" @picked="toggleTagInSearch" />
<font-awesome-icon v-if="showTagSearch" id="tag-search-show" icon="minus" title="Hide Tag Search" @click="showTagSearch = false" />
<font-awesome-icon v-else id="tag-search-hide" icon="plus" title="Show Tag Search" @click="showTagSearch = true" />
</div>
</div>

</div>
<template v-if="visibleNotes.notes.length === 0">
<div id="no-notes">
Expand Down Expand Up @@ -251,6 +305,9 @@ function clearShapeFilter(): void {
v-for="tag of note.tags"
:key="tag.name"
:style="{ color: mostReadable(tag.colour), backgroundColor: tag.colour }"
class="tag-bubble"
:title='"Toggle \"" + tag.name + "\" filter"'
@click="toggleTagInSearch(tag)"
>
{{ tag.name }}
</div>
Expand Down Expand Up @@ -293,7 +350,7 @@ header {
margin: 1rem 0;
position: relative;

> div {
> #search-bar {
position: relative;
display: flex;
align-items: center;
Expand All @@ -311,16 +368,6 @@ header {
margin-left: 1rem;
}

> .shape-name {
margin-left: 0.5rem;
font-weight: bold;

&:hover {
text-decoration: line-through;
cursor: pointer;
}
}

> input {
padding: 0.5rem 1rem;
flex-grow: 1;
Expand Down Expand Up @@ -365,6 +412,52 @@ header {
}
}
}
> #search-filters {
margin: 0 1rem 0;
display: flex;
flex-direction: row;
align-items: center;
min-height: 2.7rem;
border-bottom: solid 2px black;

> #filter-bubbles {
flex: 2 0 0;
display: flex;
flex-direction: row;
align-items: center;
flex-wrap: wrap;
row-gap: 0.5rem;
height: 100%;
padding: 0.25rem;

> .shape-name {
font-weight: bold;
border: solid 2px black;
}


> div {
flex: 0 0 auto;
}
}
> #add-filters {
display: flex;
flex-direction: row;
align-items: center;
height: 100%;

> #tag-search-bar {
height: 1.5rem;
min-width: 8rem;
}

> #tag-search-show,
> #tag-search-hide {
margin: 0 0.5rem;
}
}
}

}

#no-notes {
Expand Down Expand Up @@ -412,12 +505,6 @@ header {

.note-tags {
display: flex;

> div {
padding: 0.25rem 0.5rem;
border-radius: 0.5rem;
margin-right: 0.5rem;
}
}

.note-actions {
Expand All @@ -440,6 +527,21 @@ header {
}
}
}
.tag-bubble {
padding: 0.25rem 0.5rem;
border-radius: 0.5rem;
margin-right: 0.5rem;
}

.tag-bubble.is-active,
.tag-bubble:hover {
filter: brightness(85%);
cursor: pointer;
}

.removable:hover {
text-decoration: line-through;
}

footer {
display: flex;
Expand Down
Loading