From 6cb7da37824d02a1898d08d0f9495c77fde4dd1d Mon Sep 17 00:00:00 2001 From: vnugent Date: Sat, 20 Jan 2024 23:49:29 -0500 Subject: inital commit --- front-end/src/store/bookmarks.ts | 249 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 249 insertions(+) create mode 100644 front-end/src/store/bookmarks.ts (limited to 'front-end/src/store/bookmarks.ts') diff --git a/front-end/src/store/bookmarks.ts b/front-end/src/store/bookmarks.ts new file mode 100644 index 0000000..a49937b --- /dev/null +++ b/front-end/src/store/bookmarks.ts @@ -0,0 +1,249 @@ +// Copyright (C) 2024 Vaughn Nugent +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import 'pinia' +import { MaybeRef, shallowRef, watch, computed, Ref, ref } from 'vue'; +import { apiCall, useAxios, WebMessage } from '@vnuge/vnlib.browser'; +import { useToggle, get, set, useOffsetPagination, watchDebounced, syncRef } from '@vueuse/core'; +import { PiniaPluginContext, PiniaPlugin, storeToRefs } from 'pinia' +import { isArray, join, map, split, sortBy } from 'lodash-es'; +import { useQuery } from './index'; + +export interface Bookmark{ + readonly Id: string + Name: string + Url: string + Tags: string[] + Description: string + Created: string + LastModified: string +} + +export interface BatchUploadResult{ + readonly invalid: BookmarkError[] +} + +export interface BookmarkError{ + readonly subject: Bookmark + readonly errors: Array<{ + readonly message: string + readonly property: string + }> +} + +export interface BookmarkApi{ + list: (page: number, limit: number, search: BookmarkSearch) => Promise + add: (bookmark: Bookmark) => Promise + addMany: (bookmarks: Bookmark[], failOnValidationError: boolean) => Promise + set: (bookmark: Bookmark) => Promise + getTags: () => Promise + delete: (bookmark: Bookmark | Bookmark[]) => Promise + count: () => Promise +} + +export interface BookmarkSearch{ + query: string | undefined | null + tags: string[] +} + +declare module 'pinia' { + export interface PiniaCustomProperties { + bookmarks:{ + api: BookmarkApi + query: string + tags: string[] + allTags: string[] + list: Bookmark[] + pages: ReturnType + refresh: () => void + } + } +} + +const useBookmarkApi = (endpoint: MaybeRef): BookmarkApi => { + + const axios = useAxios(null) + + const listBookmarks = async (page: number, limit: number, search: BookmarkSearch) => { + const query = get(search.query) + const tagQuery = join(get(search.tags), ' ') + + const params = new URLSearchParams() + params.append('page', (page - 1).toString()) + params.append('limit', limit.toString()) + params.append('t', tagQuery) + + //Add query if defined + if(query){ + params.append('q', query) + } + + const { data } = await axios.get(`${get(endpoint)}?${params.toString()}`) + return data; + } + + const addBookmark = async (bookmark: Bookmark) => { + const { data } = await axios.post(`${get(endpoint)}`, bookmark) + data.getResultOrThrow(); + } + + const setBookmark = async (bookmark: Bookmark) => { + const { data } = await axios.patch>(`${get(endpoint)}`, bookmark) + data.getResultOrThrow(); + } + + const deleteBookmark = async (bookmark: Bookmark | Bookmark[]) => { + if(isArray(bookmark)){ + //Delete multiple bookmarks with comma separated ids + const bookmarIds = join(map(bookmark, b => b.Id), ',') + const { data } = await axios.delete>(`${get(endpoint)}?ids=${bookmarIds}`) + data.getResultOrThrow(); + } + else { + //Delete a single bookmark + const { data } = await axios.delete>(`${get(endpoint)}?id=${bookmark.Id}`) + data.getResultOrThrow(); + } + } + + const getItemsCount = async () => { + const { data } = await axios.get>(`${get(endpoint)}?count=true`) + return data.getResultOrThrow(); + } + + const getTags = async () => { + const { data } = await axios.get(`${get(endpoint)}?getTags=true`) + return sortBy(data); + } + + const addMany = async (bookmarks: Bookmark[], failOnValidationError: boolean): Promise => { + let params = '' + + if(failOnValidationError){ + params = '?failOnInvalid=true' + } + + //Exec request, ignore a validation error on a 20x response + const { data } = await axios.put>(`${get(endpoint)}${params}`, bookmarks); + return data.result; + } + + return { + list: listBookmarks, + add: addBookmark, + set: setBookmark, + delete: deleteBookmark, + count: getItemsCount, + addMany, + getTags + } +} + +const urlPagiation = (p: Ref, l: Ref) => { + const page = useQuery('page') + const limit = useQuery('limit') + + const currentPage = computed({ + get: () => page.value ? parseInt(page.value) : 1, + set: (value) => set(page, value.toString()) + }) + + const currentPageSize = computed({ + get: () => limit.value ? parseInt(limit.value) : 20, + set: (value) => set(limit, value.toString()) + }) + + //Sync current page and limit with the provided refs + syncRef(currentPage, p, { immediate: true }) + syncRef(currentPageSize, l, { immediate: true }) +} + +const searchQuery = (search: Ref, tags: Ref) => { + + const query = useQuery('q') + const tagQuery = useQuery('t') + + const currentTags = computed({ + get: () => split(tagQuery.value, ' '), + set: (value) => set(tagQuery, join(value, ' ')) + }) + + //Sync current page and limit with the provided refs + syncRef(query, search, { immediate: true }) + syncRef(currentTags, tags, { immediate: true }) +} + +export const bookmarkPlugin = (bookmarkEndpoint: MaybeRef): PiniaPlugin => { + + return ({ store }: PiniaPluginContext) => { + + const { loggedIn } = storeToRefs(store) + const [onRefresh, refresh] = useToggle() + + const totalBookmarks = shallowRef(0) + const bookmarks = shallowRef() + const allTags = shallowRef([]) + + const pages = useOffsetPagination({ page: 1, pageSize: 20 }) + const { currentPage, currentPageSize } = pages; + //Sync url query params with the pagination + urlPagiation(currentPage, currentPageSize) + + //sync search query and tags + const query = ref(null) + const tags = ref([]) + searchQuery(query, tags) + + //Init api + const bookmarkApi = useBookmarkApi(bookmarkEndpoint) + + watch([loggedIn, onRefresh], ([ li ]) => { + if(!li){ + //Clear the bookmarks + set(totalBookmarks, 0) + set(bookmarks, []) + return + } + + //Update the total bookmarks + apiCall(async () => totalBookmarks.value = await bookmarkApi.count()) + apiCall(async () => allTags.value = await bookmarkApi.getTags()) + }) + + //Watch for serach query changes + watchDebounced([currentPage, currentPageSize, totalBookmarks, tags, query, allTags], + ([ page, pageSize, _, tags, query ]) => { + apiCall(async () => bookmarks.value = await bookmarkApi.list(page, pageSize, { tags, query })) + }, { debounce: 10 }) + + //Watch for page changes and scroll to top + watch([currentPage], () => window.scrollTo({ top: 0, behavior: 'smooth' })) + + //reset current page when tags change or query changes + watch([tags, query], () => set(currentPage, 1)) + + return { + bookmarks:{ + list: bookmarks, + pages, + refresh, + query, + tags, + allTags, + api: bookmarkApi + } + } + } +} \ No newline at end of file -- cgit