edit and delete tags globally in settings
authorPhiTux <redacted>
Sun, 16 Mar 2025 21:36:06 +0000 (22:36 +0100)
committerPhiTux <redacted>
Sun, 16 Mar 2025 21:36:06 +0000 (22:36 +0100)
backend/server/routers/logs.py
backend/server/utils/fileHandling.py
frontend/src/lib/Tag.svelte
frontend/src/lib/TagModal.svelte
frontend/src/routes/+layout.svelte
frontend/src/routes/write/+page.svelte

index 45686c2627d34adc4d398ede50dad744e254ba26..0c54f6facf6fe5931116c2ecacc4c89d7a3bcef0 100644 (file)
@@ -69,7 +69,7 @@ async def saveLog(log: Log, cookie = Depends(users.isLoggedIn)):
         if not found:
             content["days"].append({"day": day, "text": encrypted_text, "date_written": encrypted_date_written})
 
-    if not fileHandling.writeDay(cookie["user_id"], year, month, content):
+    if not fileHandling.writeMonth(cookie["user_id"], year, month, content):
         logger.error(f"Failed to save log for {cookie['user_id']} on {year}-{month:02d}-{day:02d}")
         return {"success": False}
 
@@ -320,7 +320,7 @@ async def uploadFile(day: Annotated[int, Form()], month: Annotated[int, Form()],
         if not found:
             content["days"].append({"day": day, "files": [new_file]})
     
-    if not fileHandling.writeDay(cookie["user_id"], year, month, content):
+    if not fileHandling.writeMonth(cookie["user_id"], year, month, content):
         fileHandling.removeFile(cookie["user_id"], uuid)
         return {"success": False}
 
@@ -343,7 +343,7 @@ async def deleteFile(uuid: str, day: int, month: int, year: int, cookie = Depend
                 if not fileHandling.removeFile(cookie["user_id"], uuid):
                     raise HTTPException(status_code=500, detail="Failed to delete file")
                 dayLog["files"].remove(file)
-                if not fileHandling.writeDay(cookie["user_id"], year, month, content):
+                if not fileHandling.writeMonth(cookie["user_id"], year, month, content):
                     raise HTTPException(status_code=500, detail="Failed to write changes of deleted file!")
                 return {"success": True}
 
@@ -402,7 +402,63 @@ async def saveNewTag(tag: NewTag, cookie = Depends(users.isLoggedIn)):
         return {"success": False}
     else:
         return {"success": True}
+
+
+class EditTag(BaseModel):
+    id: int
+    icon: str
+    name: str
+    color: str
+
+@router.post("/editTag")
+async def editTag(editTag: EditTag, cookie = Depends(users.isLoggedIn)):
+    enc_key = security.get_enc_key(cookie["user_id"], cookie["derived_key"])
+    
+    content:dict = fileHandling.getTags(cookie["user_id"])
+    
+    if not 'tags' in content:
+        raise HTTPException(status_code=500, detail="Tag not found - json error")
+    
+    for tag in content['tags']:
+        if tag['id'] == editTag.id:
+            tag['icon'] = security.encrypt_text(editTag.icon, enc_key)
+            tag['name'] = security.encrypt_text(editTag.name, enc_key)
+            tag['color'] = security.encrypt_text(editTag.color, enc_key)
+            if not fileHandling.writeTags(cookie["user_id"], content):
+                raise HTTPException(status_code=500, detail="Failed to write tag - error writing tags")
+            else:
+                return {"success": True}
+    
+    raise HTTPException(status_code=500, detail="Tag not found - not in tags")
+
+@router.get("/deleteTag")
+async def deleteTag(id: int, cookie = Depends(users.isLoggedIn)):
+    # remove from every log if present
+    for year in fileHandling.get_years(cookie["user_id"]):
+        for month in fileHandling.get_months(cookie["user_id"], year):
+            content:dict = fileHandling.getMonth(cookie["user_id"], year, int(month))
+            if "days" not in content.keys():
+                continue
+            for dayLog in content["days"]:
+                if "tags" in dayLog.keys() and id in dayLog["tags"]:
+                    dayLog["tags"].remove(id)
+            if not fileHandling.writeMonth(cookie["user_id"], year, int(month), content):
+                raise HTTPException(status_code=500, detail="Failed to delete tag - error writing log")
+    
+    # remove from tags
+    content:dict = fileHandling.getTags(cookie["user_id"])
+    if not 'tags' in content:
+        raise HTTPException(status_code=500, detail="Tag not found - json error")
+    
+    for tag in content['tags']:
+        if tag['id'] == id:
+            content['tags'].remove(tag)
+            if not fileHandling.writeTags(cookie["user_id"], content):
+                raise HTTPException(status_code=500, detail="Failed to delete tag - error writing tags")
+            else:
+                return {"success": True}
     
+    raise HTTPException(status_code=500, detail="Tag not found - not in tags")
 
 class AddTagToLog(BaseModel):
     day: int
@@ -432,7 +488,7 @@ async def addTagToLog(data: AddTagToLog, cookie = Depends(users.isLoggedIn)):
     if not dayFound:
         content["days"].append({"day": data.day, "tags": [data.tag_id]})
     
-    if not fileHandling.writeDay(cookie["user_id"], data.year, data.month, content):
+    if not fileHandling.writeMonth(cookie["user_id"], data.year, data.month, content):
         raise HTTPException(status_code=500, detail="Failed to write tag - error writing log")
     return {"success": True}
 
@@ -450,7 +506,7 @@ async def removeTagFromLog(data: AddTagToLog, cookie = Depends(users.isLoggedIn)
         if not data.tag_id in dayLog["tags"]:
             raise HTTPException(status_code=500, detail="Failed to remove tag - not found in log")
         dayLog["tags"].remove(data.tag_id)
-        if not fileHandling.writeDay(cookie["user_id"], data.year, data.month, content):
+        if not fileHandling.writeMonth(cookie["user_id"], data.year, data.month, content):
             raise HTTPException(status_code=500, detail="Failed to remove tag - error writing log")
         return {"success": True}
     
\ No newline at end of file
index a6397e6410de329075768224205c378c28b869ba..1873f62a28c16420b4b5631a10b203cfecd9b5cc 100644 (file)
@@ -51,7 +51,7 @@ def writeUsers(content):
             f.write(json.dumps(content, indent=4))
             return True
         
-def writeDay(user_id, year, month, content):
+def writeMonth(user_id, year, month, content):
     try:
         os.makedirs(os.path.join(settings.data_path, f"{user_id}/{year}"), exist_ok=True)
         f = open(os.path.join(settings.data_path, f"{user_id}/{year}/{month:02d}.json"), "w")
index c25dd0025f6de89c358b9d23b77c98597d273a21..ea25599a9e203d382151a332cb451e7672615ba1 100644 (file)
@@ -1,7 +1,7 @@
 <script>
        import Fa from 'svelte-fa';
        import { faTrash, faPencil, faXmark } from '@fortawesome/free-solid-svg-icons';
-       let { tag, removeTag, isEditable, isRemovable, isDeletable } = $props();
+       let { tag, removeTag, deleteTag, editTag, isEditable, isRemovable, isDeletable } = $props();
 
        let fontColor = $state('#111');
        $effect(() => {
@@ -22,7 +22,7 @@
        <div class="d-flex flex-row">
                <div>{tag.icon} #{tag.name}</div>
                {#if isEditable}
-                       <button class="button btnEdit">
+                       <button onclick={() => editTag(tag.id)} class="button btnEdit">
                                <Fa icon={faPencil} fw />
                        </button>
                {/if}
@@ -32,7 +32,7 @@
                        </button>
                {/if}
                {#if isDeletable}
-                       <button class="button btnRemove">
+                       <button onclick={() => deleteTag(tag.id)} class="button btnRemove">
                                <Fa icon={faTrash} fw />
                        </button>
                {/if}
index a6dfec8eeef57713f1326ba960d9df7fe684be3c..6e8e641877b8ada3785a1e462a86c26827341f77 100644 (file)
@@ -5,18 +5,26 @@
        import Fa from 'svelte-fa';
        import { faTrash } from '@fortawesome/free-solid-svg-icons';
 
-       let { editTag = $bindable(), createTag = false, saveNewTag, isSaving = false } = $props();
+       let {
+               editTag = $bindable(),
+               createTag = false,
+               saveNewTag,
+               saveEditedTag,
+               isSaving = false
+       } = $props();
+
+       let modalElement;
 
        function open() {
                // hide tag picker
                document.querySelector('.tooltip').classList.remove('shown');
 
-               let modal = new bootstrap.Modal(document.getElementById('modalTag'));
+               let modal = new bootstrap.Modal(modalElement);
                modal.show();
        }
 
        function close() {
-               let modal = bootstrap.Modal.getInstance(document.getElementById('modalTag'));
+               let modal = bootstrap.Modal.getInstance(modalElement);
                modal.hide();
        }
 
@@ -34,7 +42,7 @@
        }
 </script>
 
-<div class="modal fade" id="modalTag" tabindex="-1">
+<div bind:this={modalElement} class="modal fade" id="modalTag" tabindex="-1">
        <div class="modal-dialog">
                <div class="modal-content">
                        <div class="modal-header">
                        <div class="modal-footer">
                                <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Schließen</button>
                                <button
-                                       onclick={saveNewTag}
+                                       onclick={() => {
+                                               createTag ? saveNewTag() : saveEditedTag();
+                                       }}
                                        type="button"
                                        class="btn btn-primary"
                                        disabled={!editTag.name || isSaving}
index f98b4b34650574f34e5d61b8f190224c69b020eb..158d716c4b4defdcf84c3fb54adb29bda31b881b 100644 (file)
@@ -1,5 +1,5 @@
 <script>
-       import { fade, blur, slide } from 'svelte/transition';
+       import { blur } from 'svelte/transition';
        import axios from 'axios';
        //import { dev } from '$app/environment';
        import { goto } from '$app/navigation';
        import { API_URL } from '$lib/APIurl.js';
        import trianglify from 'trianglify';
        import { useTrianglify, trianglifyOpacity, autoLoadImages } from '$lib/settingsStore.js';
+       import { tags } from '$lib/tagStore.js';
+       import TagModal from '$lib/TagModal.svelte';
 
        import {
                faRightFromBracket,
                faGlasses,
                faPencil,
-               faSliders
+               faSliders,
+               faTriangleExclamation
        } from '@fortawesome/free-solid-svg-icons';
+       import Tag from '$lib/Tag.svelte';
 
        let { children } = $props();
        let inDuration = 150;
                }
        }
 
+       let settingsModal;
+       function openSettingsModal() {
+               settingsModal = new bootstrap.Modal(document.getElementById('settingsModal'));
+               settingsModal.show();
+       }
+
        $effect(() => {
                if ($trianglifyOpacity) {
                        if (document.querySelector('canvas')) {
                }
        });
 
+       /* Important for development: convenient modal-handling with HMR */
+       if (import.meta.hot) {
+               import.meta.hot.dispose(() => {
+                       document.querySelectorAll('.modal-backdrop').forEach((el) => el.remove());
+               });
+       }
+
        onMount(() => {
                createBackground();
 
                        }, 200);
                });
        });
+
+       let editTagModal;
+       let editTag = $state({});
+       let isSavingEditedTag = $state(false);
+
+       function openTagModal(tagId) {
+               $tags.forEach((tag) => {
+                       if (tag.id === tagId) {
+                               editTag.name = tag.name;
+                               editTag.color = tag.color;
+                               editTag.icon = tag.icon;
+                               editTag.id = tag.id;
+                               return;
+                       }
+               });
+
+               settingsModal.hide();
+               editTagModal.open();
+       }
+
+       let deleteTagId = $state(null);
+       function askDeleteTag(tagId) {
+               if (deleteTagId === tagId) deleteTagId = null;
+               else deleteTagId = tagId;
+       }
+
+       let isDeletingTag = $state(false);
+       function deleteTag(tagId) {
+               if (isDeletingTag) return;
+               isDeletingTag = true;
+
+               axios
+                       .get(API_URL + '/logs/deleteTag', { params: { id: tagId } })
+                       .then((response) => {
+                               if (response.data.success) {
+                                       $tags = $tags.filter((tag) => tag.id !== tagId);
+                               }
+                       })
+                       .catch((error) => {
+                               console.error(error);
+
+                               // show toast
+                               const toast = new bootstrap.Toast(document.getElementById('toastErrorDeleteTag'));
+                               toast.show();
+                       })
+                       .finally(() => {
+                               deleteTagId = null;
+                               isDeletingTag = false;
+                       });
+       }
+
+       function saveEditedTag() {
+               if (isSavingEditedTag) return;
+               isSavingEditedTag = true;
+
+               axios
+                       .post(API_URL + '/logs/editTag', editTag)
+                       .then((response) => {
+                               if (response.data.success) {
+                                       $tags = $tags.map((tag) => {
+                                               if (tag.id === editTag.id) {
+                                                       tag.name = editTag.name;
+                                                       tag.color = editTag.color;
+                                                       tag.icon = editTag.icon;
+                                               }
+                                               return tag;
+                                       });
+
+                                       // show toast
+                                       const toast = new bootstrap.Toast(document.getElementById('toastSuccessEditTag'));
+                                       toast.show();
+                               }
+                       })
+                       .catch((error) => {
+                               console.error(error);
+
+                               // show toast
+                               const toast = new bootstrap.Toast(document.getElementById('toastErrorEditTag'));
+                               toast.show();
+                       })
+                       .finally(() => {
+                               isSavingEditedTag = false;
+                               editTagModal.close();
+                               settingsModal.show();
+                       });
+       }
 </script>
 
 <main class="d-flex flex-column">
                        </div>
 
                        <div class="col-lg-4 col-sm-5 col pe-0 d-flex flex-row justify-content-end">
-                               <button
-                                       class="btn btn-outline-secondary me-2"
-                                       data-bs-toggle="modal"
-                                       data-bs-target="#settingsModal"><Fa icon={faSliders} /></button
+                               <button class="btn btn-outline-secondary me-2" onclick={openSettingsModal}
+                                       ><Fa icon={faSliders} /></button
                                >
                                <button class="btn btn-outline-secondary" onclick={logout}
                                        ><Fa icon={faRightFromBracket} /></button
                {/key}
        </div>
 
+       <TagModal
+               bind:this={editTagModal}
+               createTag={false}
+               bind:editTag
+               isSaving={isSavingEditedTag}
+               {saveEditedTag}
+       />
+
        <!-- Full screen modal -->
        <div class="modal fade" data-bs-backdrop="static" id="settingsModal">
                <div
                                                                        blub <br />
                                                                </div>
 
-                                                               <div id="tags"><h4>Tags</h4></div>
+                                                               <h3 id="tags" class="text-primary">Tags</h3>
+                                                               <div>
+                                                                       Hier können Tags bearbeitet oder auch vollständig aus DailyTxT gelöscht werden.
+                                                                       <div class="d-flex flex-column tagColumn mt-1">
+                                                                               {#each $tags as tag}
+                                                                                       <Tag
+                                                                                               {tag}
+                                                                                               isEditable
+                                                                                               editTag={openTagModal}
+                                                                                               isDeletable
+                                                                                               deleteTag={askDeleteTag}
+                                                                                       />
+                                                                                       {#if deleteTagId === tag.id}
+                                                                                               <div class="alert alert-danger align-items-center" role="alert">
+                                                                                                       <div>
+                                                                                                               <Fa icon={faTriangleExclamation} fw /> <b>Tag dauerhaft löschen?</b> Dies
+                                                                                                               kann einen Moment dauern, da jeder Eintrag nach potenziellen Verlinkungen
+                                                                                                               durchsucht werden muss. Änderungen werden zudem u. U. erst nach einem Neuladen
+                                                                                                               im Browser angezeigt.
+                                                                                                       </div>
+                                                                                                       <!-- svelte-ignore a11y_consider_explicit_label -->
+                                                                                                       <div class="d-flex flex-row mt-2">
+                                                                                                               <button class="btn btn-secondary" onclick={() => (deleteTagId = null)}
+                                                                                                                       >Abbrechen
+                                                                                                               </button>
+                                                                                                               <button
+                                                                                                                       disabled={isDeletingTag}
+                                                                                                                       class="btn btn-danger ms-3"
+                                                                                                                       onclick={() => deleteTag(tag.id)}
+                                                                                                                       >Löschen
+                                                                                                                       {#if isDeletingTag}
+                                                                                                                               <span
+                                                                                                                                       class="spinner-border spinner-border-sm ms-2"
+                                                                                                                                       role="status"
+                                                                                                                                       aria-hidden="true"
+                                                                                                                               ></span>
+                                                                                                                       {/if}
+                                                                                                               </button>
+                                                                                                       </div>
+                                                                                               </div>
+                                                                                       {/if}
+                                                                               {/each}
+                                                                       </div>
+                                                               </div>
 
                                                                <div id="templates"><h4>Vorlagen</h4></div>
 
                        </div>
                </div>
        </div>
+
+       <div class="toast-container position-fixed bottom-0 end-0 p-3">
+               <div
+                       id="toastSuccessEditTag"
+                       class="toast align-items-center text-bg-success"
+                       role="alert"
+                       aria-live="assertive"
+                       aria-atomic="true"
+               >
+                       <div class="d-flex">
+                               <div class="toast-body">Änderungen wurden gespeichert!</div>
+                       </div>
+               </div>
+
+               <div
+                       id="toastErrorEditTag"
+                       class="toast align-items-center text-bg-danger"
+                       role="alert"
+                       aria-live="assertive"
+                       aria-atomic="true"
+               >
+                       <div class="d-flex">
+                               <div class="toast-body">Fehler beim Speichern der Änderungen!</div>
+                       </div>
+               </div>
+
+               <div
+                       id="toastErrorDeleteTag"
+                       class="toast align-items-center text-bg-danger"
+                       role="alert"
+                       aria-live="assertive"
+                       aria-atomic="true"
+               >
+                       <div class="d-flex">
+                               <div class="toast-body">Fehler beim Löschen des Tags!</div>
+                       </div>
+               </div>
+       </div>
 </main>
 
 <style>
+       :global(.tagColumn > span) {
+               width: min-content;
+       }
+
+       .tagColumn {
+               gap: 0.5rem;
+               /* width: min-content; */
+       }
+
        #selectMode:checked {
                border-color: #da880e;
                background-color: #da880e;
index 05e497ecbf1fe55ca5f97521ecb8fb5839c78bc4..c834b8457bf9d8c36b76835dd4cb4a2420598fa2 100644 (file)
                        });
        }
 
-       let editTag = $state({});
+       let newTag = $state({});
        let tagModal;
 
-       function openTagModal(tag) {
-               if (tag === null) {
-                       editTag = {
-                               icon: '',
-                               name: '',
-                               color: '#f57c00'
-                       };
-               } else {
-                       editTag = tag;
-               }
+       function openTagModal() {
+               newTag = {
+                       icon: '',
+                       name: '',
+                       color: '#f57c00'
+               };
 
                tagModal.open();
        }
                isSavingNewTag = true;
                axios
                        .post(API_URL + '/logs/saveNewTag', {
-                               icon: editTag.icon,
-                               name: editTag.name,
-                               color: editTag.color
+                               icon: newTag.icon,
+                               name: newTag.name,
+                               color: newTag.color
                        })
                        .then((response) => {
                                if (response.data.success) {
                                        id="tag-input"
                                        placeholder="Tag..."
                                />
-                               <button
-                                       class="newTagBtn btn btn-outline-secondary ms-2"
-                                       onclick={() => {
-                                               openTagModal(null);
-                                       }}
-                               >
+                               <button class="newTagBtn btn btn-outline-secondary ms-2" onclick={openTagModal}>
                                        <Fa icon={faSquarePlus} fw /> Neu
                                </button>
                        </div>
 
        <TagModal
                bind:this={tagModal}
-               bind:editTag
+               bind:editTag={newTag}
                createTag="true"
                isSaving={isSavingNewTag}
                {saveNewTag}
git clone https://git.99rst.org/PROJECT