aboutsummaryrefslogtreecommitdiff
path: root/front-end/src/components/Bookmarks.vue
diff options
context:
space:
mode:
authorLibravatar vnugent <public@vaughnnugent.com>2024-02-25 01:11:06 -0500
committerLibravatar vnugent <public@vaughnnugent.com>2024-02-25 01:11:06 -0500
commitbd3a7a25792b837c5f28c7580adf132abc6f35e7 (patch)
tree2a3ec046f8f76f115e648f2bc6d1576cfa0a6c6f /front-end/src/components/Bookmarks.vue
parent52645b724834e669788a45edb9d135f243432540 (diff)
Squashed commit of the following:
commit 069f81fc3c87c437eceff756ddca7a4c1b58044d Author: vnugent <public@vaughnnugent.com> Date: Sat Feb 24 22:33:34 2024 -0500 feat: #3 setup mode, admin signup, fixes, and contianerize! commit 97ffede9eb312fca0257afa06969d47a12703f3b Author: vnugent <public@vaughnnugent.com> Date: Mon Feb 19 22:26:03 2024 -0500 feat: new account setup and invitation links commit 1c8f59bc0a1b25ce5013b0f1fc7fa73c0de415d6 Author: vnugent <public@vaughnnugent.com> Date: Thu Feb 15 16:49:59 2024 -0500 feat: update packages, drag/drop link, and fix some button padding
Diffstat (limited to 'front-end/src/components/Bookmarks.vue')
-rw-r--r--front-end/src/components/Bookmarks.vue77
1 files changed, 56 insertions, 21 deletions
diff --git a/front-end/src/components/Bookmarks.vue b/front-end/src/components/Bookmarks.vue
index 93ddd73..cc3cd6a 100644
--- a/front-end/src/components/Bookmarks.vue
+++ b/front-end/src/components/Bookmarks.vue
@@ -5,10 +5,10 @@ import { get, set, formatTimeAgo, useToggle, useTimestamp, useFileDialog, asyncC
import { useVuelidate } from '@vuelidate/core';
import { required, maxLength, minLength, helpers } from '@vuelidate/validators';
import { apiCall, useConfirm, useGeneralToaster, useVuelidateWrapper, useWait } from '@vnuge/vnlib.browser';
-import { clone, cloneDeep, join, defaultTo, every, filter, includes, isEmpty, isEqual, first, isString, chunk, map, forEach } from 'lodash-es';
+import { clone, cloneDeep, join, defaultTo, every, filter, includes, isEmpty, isEqual, first, isString, chunk, map, forEach, isNil } from 'lodash-es';
import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue'
import { parseNetscapeBookmarkString } from './Boomarks/util.ts';
-import type { Bookmark, BookmarkError } from '../store/bookmarks';
+import type { BatchUploadResult, Bookmark, BookmarkError } from '../store/bookmarks';
import AddOrUpdateForm from './Boomarks/AddOrUpdateForm.vue';
const Dialog = defineAsyncComponent(() => import('./global/Dialog.vue'));
@@ -27,7 +27,7 @@ const { copy } = useClipboard()
//Refresh on page load
store.bookmarks.refresh();
-const safeNameRegex = /^[a-zA-Z0-9_\-\|\. ]*$/;
+const safeNameRegex = /^[a-zA-Z0-9_\-\|\., ]*$/;
const safeUrlRegex = /^(https?|ftp):\/\/[^\s/$.?#].[^\s]*$/;
const safeTagRegex = /^[a-zA-Z0-9-_]*$/;
@@ -38,19 +38,19 @@ const addOrEditValidator = (buffer: Ref<Partial<Bookmark>>) => {
required: helpers.withMessage('Name cannot be empty', required),
safeName: helpers.withMessage('Bookmark name contains illegal characters', (value: string) => safeNameRegex.test(value)),
minLength: helpers.withMessage('Name must be at least 1 characters', minLength(1)),
- maxLength: helpers.withMessage('Name must have less than 100 characters', maxLength(100))
+ maxLength: helpers.withMessage('Name must have less than 200 characters', maxLength(200))
},
Url: {
required: helpers.withMessage('Url cannot be empty', required),
safeUrl: helpers.withMessage('Url contains illegal characters or is not a valid URL', (value: string) => safeUrlRegex.test(value)),
minLength: helpers.withMessage('Url must be at least 1 characters', minLength(1)),
- maxLength: helpers.withMessage('Url must have less than 200 characters', maxLength(200))
+ maxLength: helpers.withMessage('Url must have less than 300 characters', maxLength(300))
},
Description: {
- maxLength: helpers.withMessage('Description must have less than 512 characters', maxLength(512))
+ maxLength: helpers.withMessage('Description must have less than 500 characters', maxLength(500))
},
Tags: {
- maxLength: helpers.withMessage('Tags must have less than 32 characters', (tags: string[]) => every(tags, tag => tag.length < 32)),
+ maxLength: helpers.withMessage('Tags must have less than 64 characters', (tags: string[]) => every(tags, tag => tag.length < 64)),
safeTag: helpers.withMessage('Tags contains illegal characters', (tags: string[]) => every(tags, tag => safeTagRegex.test(tag)))
}
}));
@@ -99,6 +99,7 @@ const bmDelete = async (bookmark: Bookmark) => {
const isTagSelected = (tag: string, currentTags: MaybeRef<string[]>) => includes(get(currentTags), tag);
const execSearch = () => store.bookmarks.query = get(localSearch);
+const clearTags = () => store.bookmarks.tags = [];
const percentToWith = (percent: number) => ({ width: `${percent}%` });
const printErroMessage = (error: BookmarkError) => {
const errorMessages = map(error.errors, e=> e.message);
@@ -262,22 +263,39 @@ const upload = (() => {
const bms = get(foundBookmarks);
if(get(fixErrors)){
- //try to fix names
- forEach(bms, bm => {
- //If the name is not safe, replace all illegal characters
- if(!safeNameRegex.test(bm.Name)){
- bm.Name = bm.Name.replace(/[^a-zA-Z0-9_\-\|\. ]/g, ' ');
- }
- })
//truncate name length
forEach(bms, bm => {
if(bm.Name.length > 100){
- bm.Name = bm.Name.substring(0, 100);
+ bm.Name = bm.Name.substring(0, 199);
+ }
+
+ //Replace illegal characters from name strings
+ bm.Name = bm.Name.replace(/[^a-zA-Z0-9_\-\|\., ]/g, ' ');
+
+ if(!isNil(bm.Description)){
+ //truncate description
+ if (bm.Description.length > 500) {
+ bm.Description = bm.Description.substring(0, 499);
+ }
+
+ bm.Description = bm.Description.replace(/[^\x00-\x7F]/g, ''); //only allow utf-8 characters
}
+
+ //Try to remove illegal chars from tags
+ bm.Tags = map(bm.Tags, tag => tag.replace(/[^a-zA-Z0-9\-]/g, ''));
})
}
+ forEach(bms, bm => {
+ //Remove any empty tags
+ bm.Tags = filter(bm.Tags, tag => tag?.length > 0);
+
+ if(isEmpty(bm.Tags)){
+ (bm.Tags as any) = null;
+ }
+ })
+
const chunks = chunk(bms, 20);
for(let i = 0; i < chunks.length; i++){
@@ -290,9 +308,18 @@ const upload = (() => {
//See if an error occured
if(!isString(result) && 'invalid' in result){
+ const { message, invalid } = result as BatchUploadResult;
+
//add errors to the error list
- errors.value.push(...result.invalid);
+ errors.value.push(...invalid);
isError = true;
+
+ if(message){
+ toaster.error({
+ title: `Batch ${i} upload failed due to an error`,
+ text: message
+ })
+ }
}
if(isError){
@@ -382,14 +409,14 @@ const upload = (() => {
<MenuItems class="absolute z-10 bg-white divide-y divide-gray-100 rounded-b shadow right-2 lg:left-0 min-w-32 lg:end-0 dark:bg-gray-700">
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200" aria-labelledby="dropdownDefaultButton">
<!-- Use the `active` state to conditionally style the active item. -->
- <MenuItem as="template" v-slot="{ active }">
+ <MenuItem as="template" v-slot="{ }">
<li>
<button @click="add.open()" class="block w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">
Manual
</button>
</li>
</MenuItem>
- <MenuItem as="template" v-slot="{ active }">
+ <MenuItem as="template" v-slot="{ }">
<li>
<button @click="upload.open()" class="block w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">
Upload html
@@ -402,7 +429,7 @@ const upload = (() => {
</Menu>
</div>
</div>
- <div class="grid flex-auto grid-cols-4 gap-8 mt-4 max-w-[60rem] mx-auto w-full">
+ <div class="grid flex-auto grid-cols-4 gap-8 sm:mt-4 max-w-[60rem] mx-auto w-full">
<div class="col-span-4 lg:col-span-3">
@@ -414,7 +441,7 @@ const upload = (() => {
<span class="sr-only">Loading...</span>
</div>
- <div class="mx-auto mt-2">
+ <div class="mx-auto sm:mt-2">
<div class="grid h-full grid-cols-1 gap-1 leading-tight md:leading-normal">
<div v-for="bm in bookmarks" :key="bm.Id" :id="join(['bm', bm.Id], '-')" class="w-full p-1">
@@ -476,7 +503,15 @@ const upload = (() => {
</div>
</div>
<div class="hidden lg:block">
- <div class="mt-10">
+ <div class="h-10">
+ <div class="ml-12">
+ <button :disabled="isEmpty(selectedTags)" @click="clearTags()"
+ class="text-sm font-bold text-gray-600 duration-75 ease-linear disabled:opacity-0 hover:underline">
+ Clear Tags
+ </button>
+ </div>
+ </div>
+ <div class="mt-1">
<ul class="grid grid-cols-2">
<li v-for="tag in tags" :key="tag" class="text-sm">
<span