Skip to content

Commit

Permalink
add tags
Browse files Browse the repository at this point in the history
  • Loading branch information
NyaomiDEV committed Jul 28, 2024
1 parent c4636ad commit 2ec8779
Show file tree
Hide file tree
Showing 9 changed files with 291 additions and 71 deletions.
28 changes: 28 additions & 0 deletions src/components/TagInList.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<script setup lang="ts">
import {
IonItem,
IonLabel,
} from "@ionic/vue";
import { Tag } from '../lib/db/entities/tags';
import TagEdit from "../modals/TagEdit.vue";
import { provide, ref } from "vue";
const props = defineProps<{
tag: Tag
}>();
const tag = props.tag;
const isOpen = ref(false);
provide("isOpen", isOpen);
</script>

<template>
<IonItem button @click="isOpen = true">
<IonLabel>
{{ tag.name }}
</IonLabel>
</IonItem>

<TagEdit :add="false" :tag="tag" />
</template>
3 changes: 2 additions & 1 deletion src/lib/db/entities/journalPosts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ export type JournalPost = UUIDable & {
title: string,
body: string,
cover?: File,
attachments?: Attachment[]
attachments?: Attachment[],
tags?: UUID[] // array of UUIDs
}

export type Attachment = UUIDable & {
Expand Down
38 changes: 20 additions & 18 deletions src/lib/db/entities/tags.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { db } from "..";
import { parseTagFilterQuery } from "../../util/filterQuery";
import { makeUUIDv5 } from "../../util/uuid";
import { UUIDable } from "../types";
import { UUID, UUIDable } from "../types";
import { getSystemUUID } from "./system";

import { getTable as getMembers } from "./members";
import { getTable as getJournalPosts } from "./journalPosts";

export type Tag = UUIDable & {
name: string,
type: "member" | "journal",
color: string
color?: string
}

export function getTable() {
Expand All @@ -26,20 +28,20 @@ export async function newTag(tag: Omit<Tag, keyof UUIDable>) {
});
}

export async function getTagFromName(name: string){
return await getTable().get({ name });
export async function removeTag(uuid: UUID){
const tag = await getTable().get(uuid);
if(tag?.type === "member"){
getMembers().toCollection().modify(member => {
member.tags = member.tags?.filter(tag => tag !== uuid)
});
} else if(tag?.type === "journal") {
getJournalPosts().toCollection().modify(journalPost => {
journalPost.tags = journalPost.tags?.filter(tag => tag !== uuid)
});
}
await getTable().delete(uuid);
}

export async function getTagsFromFilterQuery(filterQuery: string){
const parsed = await parseTagFilterQuery(filterQuery);

return getTable().where("name").startsWithIgnoreCase(parsed.query).filter(x => {

if (parsed.type) {
if (x.type !== parsed.type.toLowerCase())
return false;
}

return true;
}).toArray();
}
export async function getTagFromName(name: string){
return await getTable().get({ name });
}
10 changes: 7 additions & 3 deletions src/lib/db/liveQueries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { from, useObservable } from "@vueuse/rxjs";
import { liveQuery } from "dexie";
import { Ref, ref, watch } from "vue";
import { Member, getMembersFromFilterQuery, getTable as getMembersTable } from "./entities/members";
import { Tag, getTagsFromFilterQuery, getTable as getTagsTable } from "./entities/tags";
import { Tag, getTable as getTagsTable } from "./entities/tags";

export function getFilteredMembers(search: Ref<string>){
const members = ref<Member[]>();
Expand All @@ -17,14 +17,18 @@ export function getFilteredMembers(search: Ref<string>){
return members;
}

export function getFilteredTags(search: Ref<string>) {
export function getFilteredTags(search: Ref<string>, type: Ref<string>) {
const tags = ref<Tag[]>();

watch([
search,
type,
useObservable(from(liveQuery(() => getTagsTable().toArray())))
], async () => {
tags.value = await getTagsFromFilterQuery(search.value);
if(search.value.length == 0)
tags.value = await getTagsTable().filter(x => x.type === type.value).toArray();
else
tags.value = await getTagsTable().where("name").startsWithIgnoreCase(search.value).filter(x => x.type === type.value).toArray();
}, { immediate: true });

return tags;
Expand Down
43 changes: 0 additions & 43 deletions src/lib/util/filterQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,46 +96,3 @@ export async function parseMemberFilterQuery(search: string): Promise<MemberFilt
result.query = queryTokens.filter(Boolean).join(" ");
return result;
}


export function parseTagFilterQuery(search: string): TagFilterQuery {
const tokens = search.split(" ");

const queryTokens: string[] = [];

const result: TagFilterQuery = {
query: ""
};

for (const token of tokens) {
switch (token.charAt(0)) {
case "@":
const tokenParts = token.slice(1).split(":");
switch (tokenParts[0].toLowerCase()) {
case "type":
switch (tokenParts[1]?.toLowerCase()){
case "journal":
result.type = "journal";
break;
case "member":
result.type = "member";
break;
default:
queryTokens.push(token);
break;
}
break;
default:
queryTokens.push(token);
break;
}
break;
default:
queryTokens.push(token);
break;
}
}

result.query = queryTokens.filter(Boolean).join(" ");
return result;
}
2 changes: 1 addition & 1 deletion src/modals/MemberEdit.vue
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
isEditing.value = false;
} else {
await newMember(memberWithoutUUID);
modalController.dismiss(null, "added");
await modalController.dismiss(null, "added");
}
} else {
isEditing.value = true;
Expand Down
179 changes: 179 additions & 0 deletions src/modals/TagEdit.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
<script setup lang="ts">
import {
IonContent,
IonHeader,
IonToolbar,
IonTitle,
IonButton,
IonIcon,
IonList,
IonInput,
IonFab,
IonFabButton,
IonLabel,
IonItem,
modalController,
IonModal,
IonButtons,
IonSegment,
IonSegmentButton
} from "@ionic/vue";
import Color from "../components/Color.vue";
import {
saveOutline as saveIOS,
chevronBack as backIOS,
trashBinOutline as trashIOS
} from "ionicons/icons";
import saveMD from "@material-design-icons/svg/outlined/save.svg";
import backMD from "@material-design-icons/svg/outlined/arrow_back.svg";
import trashMD from "@material-design-icons/svg/outlined/delete.svg";
import { Tag, getTable, newTag, removeTag } from '../lib/db/entities/tags';
import { Ref, inject, ref } from "vue";
import { addMaterialColors, unsetMaterialColors } from "../lib/theme";
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
const isOpen = inject<Ref<boolean>>("isOpen");
const props = defineProps<{
tag?: PartialBy<Tag, "uuid">,
add: boolean,
edit?: boolean
}>();
const tag = ref({
name: props.tag?.name || "",
type: props.tag?.type || "member",
...props.tag || {}
} as PartialBy<Tag, "uuid">);
function dismiss(){
if(isOpen) {
isOpen.value = false;
tag.value = {
name: props.tag?.name || "",
type: props.tag?.type || "member",
...props.tag || {}
};
}
}
async function save(){
const { uuid, ...tagWithoutUUID } = tag.value;
if(!props.add){
await getTable().update(tag.value.uuid, tagWithoutUUID);
// update tag in props, since it's reactive
for(const prop in tag.value)
props.tag![prop] = tag.value[prop];
try{
await modalController.dismiss(undefined, "modified");
}catch(_){}
// catch an error because the type might get changed, causing the parent to be removed from DOM
// however it's safe for us to ignore
} else {
await newTag(tagWithoutUUID);
await modalController.dismiss(undefined, "added");
}
}
async function deleteTag(){
await removeTag(tag.value!.uuid!);
await modalController.dismiss(undefined, "deleted");
}
const self = ref();
function setAccent() {
if(tag.value.color && tag.value.color !== "#000000"){
addMaterialColors(tag.value.color, self.value.$el);
} else {
unsetMaterialColors(self.value.$el);
}
}
</script>

<template>
<IonModal ref="self" :isOpen @willPresent="setAccent" @didDismiss="dismiss">
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonButton shape="round" fill="clear" @click="dismiss">
<IonIcon slot="icon-only" :md="backMD" :ios="backIOS"></IonIcon>
</IonButton>
</IonButtons>
<IonTitle>{{ tag.type === "member" ? $t("options:tagManagement.edit.header.member") : $t("options:tagManagement.edit.header.journal") }}</IonTitle>
</IonToolbar>
</IonHeader>

<IonContent>
<IonList inset>
<IonItem lines="none">
<IonInput mode="md" fill="outline" :label="$t('options:tagManagement.edit.name')" labelPlacement="floating" v-model="tag.name" />
</IonItem>

<IonItem button lines="none">
<Color v-model="tag.color" @update:model-value="setAccent">
<IonLabel>
{{ $t("options:tagManagement.edit.color") }}
</IonLabel>
</Color>
</IonItem>

<IonItem lines="none">
<IonLabel>
<h3 class="centered-text">{{ $t("options:tagManagement.edit.type.header") }}</h3>
<IonSegment class="segment-alt" v-model="tag.type">
<IonSegmentButton value="member">
<IonLabel>
{{ $t("options:tagManagement.edit.type.member") }}
</IonLabel>
</IonSegmentButton>
<IonSegmentButton value="journal">
<IonLabel>
{{ $t("options:tagManagement.edit.type.journal") }}
</IonLabel>
</IonSegmentButton>
</IonSegment>
</IonLabel>
</IonItem>

<IonItem button lines="none" v-if="tag.uuid" @click="deleteTag">
<IonIcon :ios="trashIOS" :md="trashMD" slot="start" aria-hidden="true" />
<IonLabel>
<h3>{{ $t("options:tagManagement.edit.actions.delete.title") }}</h3>
<p>{{ $t("options:tagManagement.edit.actions.delete.desc") }}</p>
</IonLabel>
</IonItem>
</IonList>

<IonFab slot="fixed" vertical="bottom" horizontal="end">
<IonFabButton @click="save" v-if="tag.name.length > 0">
<IonIcon :ios="saveIOS" :md="saveMD" />
</IonFabButton>
</IonFab>
</IonContent>
</IonModal>
</template>

<style scoped>
ion-modal {
--width: 100%;
--height: 100%;
}
ion-content {
--padding-bottom: 80px;
}
ion-input, ion-textarea {
margin-top: 16px;
}
ion-item {
margin-bottom: 16px;
}
</style>
Loading

0 comments on commit 2ec8779

Please sign in to comment.