diff options
-rw-r--r-- | back-end/src/Endpoints/BookmarkEndpoint.cs | 2 | ||||
-rw-r--r-- | back-end/src/Model/BookmarkStore.cs | 36 | ||||
-rw-r--r-- | ci/config/config.json | 3 | ||||
-rw-r--r-- | ci/taskfile.yaml | 3 | ||||
-rw-r--r-- | front-end/src/components/Bookmarks.vue | 34 |
5 files changed, 71 insertions, 7 deletions
diff --git a/back-end/src/Endpoints/BookmarkEndpoint.cs b/back-end/src/Endpoints/BookmarkEndpoint.cs index b7825d6..ec46097 100644 --- a/back-end/src/Endpoints/BookmarkEndpoint.cs +++ b/back-end/src/Endpoints/BookmarkEndpoint.cs @@ -317,7 +317,7 @@ namespace SimpleBookmark.Endpoints } //Try to update the records - ERRNO result = await Bookmarks.AddBulkAsync(sanitized, entity.Session.UserID, false, entity.EventCancellation); + ERRNO result = await Bookmarks.AddBulkAsync(sanitized, entity.Session.UserID, entity.RequestedTimeUtc, entity.EventCancellation); webm.Result = $"Successfully added {result} of {batch.Length} bookmarks"; webm.Success = true; diff --git a/back-end/src/Model/BookmarkStore.cs b/back-end/src/Model/BookmarkStore.cs index fbd3213..8578976 100644 --- a/back-end/src/Model/BookmarkStore.cs +++ b/back-end/src/Model/BookmarkStore.cs @@ -19,10 +19,13 @@ using System; using System.Linq; using System.Threading; using System.Threading.Tasks; +using System.Collections.Generic; +using VNLib.Utils; using VNLib.Plugins.Extensions.Data; -using VNLib.Plugins.Extensions.Data.Abstractions; using VNLib.Plugins.Extensions.Loading; +using VNLib.Plugins.Extensions.Data.Abstractions; + namespace SimpleBookmark.Model { @@ -113,6 +116,37 @@ namespace SimpleBookmark.Model .ToArray(); } + public async Task<ERRNO> AddBulkAsync(IEnumerable<BookmarkEntry> bookmarks, string userId, DateTimeOffset now, CancellationToken cancellation) + { + //Init new db connection + await using SimpleBookmarkContext context = new(dbOptions.Value); + await context.OpenTransactionAsync(cancellation); + + //Setup clean bookmark instances + bookmarks = bookmarks.Select(b => new BookmarkEntry + { + Id = GetNewRecordId(), //new uuid + UserId = userId, //Set userid + LastModified = now.DateTime, + + //Allow reuse of created time + Created = b.Created, + Description = b.Description, + Name = b.Name, + Tags = b.Tags, + Url = b.Url, + }); + + //Filter out bookmarks that already exist + bookmarks = bookmarks.Where(b => !context.Bookmarks.Any(b2 => b2.UserId == userId && b2.Url == b.Url)); + + //Add bookmarks to db + context.AddRange(bookmarks); + + //Commit transaction + return await context.SaveAndCloseAsync(true, cancellation); + } + private sealed class BookmarkQueryLookup : IDbQueryLookup<BookmarkEntry> { public IQueryable<BookmarkEntry> GetCollectionQueryBuilder(IDbContextHandle context, params string[] constraints) diff --git a/ci/config/config.json b/ci/config/config.json index c012728..4740cd3 100644 --- a/ci/config/config.json +++ b/ci/config/config.json @@ -124,7 +124,8 @@ "hot_reload": false, "reload_delay_sec": 2, "path": "plugins", - "config_dir": "config" + "config_dir": "config", + "assets": "plugins/assets" }, "disabled sys_log": { diff --git a/ci/taskfile.yaml b/ci/taskfile.yaml index 7f67fec..e59e080 100644 --- a/ci/taskfile.yaml +++ b/ci/taskfile.yaml @@ -19,7 +19,10 @@ tasks: #clean out dist dir before building - cmd: powershell -Command "rm -Recurse -Force ./dist" ignore_error: true + #copy setup script for linux + - cmd: powershell -Command "mkdir lib -Force" + ignore_error: true - cmd: powershell -Command "cp setup.sh lib/ -Force" - task: install-plugins diff --git a/front-end/src/components/Bookmarks.vue b/front-end/src/components/Bookmarks.vue index 2a5a6d3..34c31a8 100644 --- a/front-end/src/components/Bookmarks.vue +++ b/front-end/src/components/Bookmarks.vue @@ -5,7 +5,7 @@ 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 } from 'lodash-es'; +import { clone, cloneDeep, join, defaultTo, every, filter, includes, isEmpty, isEqual, first, isString, chunk, map, forEach } from 'lodash-es'; import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue' import { parseNetscapeBookmarkString } from './Boomarks/util.ts'; import type { Bookmark, BookmarkError } from '../store/bookmarks'; @@ -37,13 +37,13 @@ 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 128 characters', maxLength(128)) + maxLength: helpers.withMessage('Name must have less than 100 characters', maxLength(100)) }, 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 128 characters', maxLength(128)) + maxLength: helpers.withMessage('Url must have less than 200 characters', maxLength(200)) }, Description: { maxLength: helpers.withMessage('Description must have less than 512 characters', maxLength(512)) @@ -222,6 +222,7 @@ const upload = (() => { const file = computed(() => first(files.value)); const ignoreErrors = ref(false); + const fixErrors = ref(false); const errors = ref<BookmarkError[]>([]); const progress = ref<string[]>([]); const progressPercent = ref(0); @@ -259,6 +260,23 @@ const upload = (() => { //parse the text into bookmarks 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); + } + }) + } + const chunks = chunk(bms, 20); for(let i = 0; i < chunks.length; i++){ @@ -303,6 +321,7 @@ const upload = (() => { open, cancel, submit, + fixErrors, foundBookmarks, progressPercent, errors, @@ -545,11 +564,18 @@ const upload = (() => { </div> </div> <div class="flex flex-row items-center justify-between my-3 "> - <div> + <div class="flex flex-row gap-4"> <div class="flex items-center"> <input id="ignore-errors" type="checkbox" v-model="upload.ignoreErrors" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded cursor-pointer focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"> <label for="ignore-errors" class="text-sm font-medium ms-2">Ignore Errors</label> </div> + <div class="flex items-center"> + <input id="fix-errors" type="checkbox" v-model="upload.fixErrors" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded cursor-pointer focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"> + <label for="fix-errors" class="text-sm font-medium ms-2">Fix Errors</label> + </div> + </div> + <div> + </div> <button type="submit" class="btn blue"> Upload |