Edit and remove projectTags

This commit is contained in:
Brieuc Dubois 2024-01-02 17:17:16 +01:00
parent bc566280a6
commit b182c831a3
10 changed files with 324 additions and 114 deletions

View File

@ -102,7 +102,7 @@ func UpdateTag(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusNotFound) return c.SendStatus(fiber.StatusNotFound)
} }
return c.SendStatus(fiber.StatusOK) return c.SendStatus(fiber.StatusNoContent)
} }
func CreateTagOption(c *fiber.Ctx) error { func CreateTagOption(c *fiber.Ctx) error {

View File

@ -65,9 +65,6 @@
{/if} {/if}
<style lang="less"> <style lang="less">
.content {
}
.modal { .modal {
background-color: rgba(0, 0, 0, 0.8); background-color: rgba(0, 0, 0, 0.8);
position: fixed; position: fixed;

View File

@ -4,14 +4,13 @@
import { cards } from '../../../stores/smallStore'; import { cards } from '../../../stores/smallStore';
import api, { processError } from '../../../utils/api'; import api, { processError } from '../../../utils/api';
import status from '../../../utils/status'; import status from '../../../utils/status';
import ModalTagTitle from './modal_tag_title.svelte';
export let tag: TagValue; export let tag: TagValue;
// export let removeTag: (id: number) => void; // export let removeTag: (id: number) => void;
let newValue: string = tag.value; let newValue: string = tag.value;
let newOption: number = tag.option_id; let newOption: number = tag.option_id;
let projectTag = $projectTags[tag.tag_id];
async function saveTag() { async function saveTag() {
if (tag.value === newValue && tag.option_id == newOption) return; if (tag.value === newValue && tag.option_id == newOption) return;
// DELETE // DELETE
@ -54,13 +53,15 @@
async function newTagOption() { async function newTagOption() {
const value = prompt('New option value'); const value = prompt('New option value');
if (value) projectTags.add(tag.tag_id, value); if (value) projectTags.addOption(tag.tag_id, value);
} }
const projectTag = $projectTags[tag.tag_id];
</script> </script>
{#if projectTag} {#if projectTag}
<tr class="tag"> <tr>
<td class="tag-title">{projectTag.title}</td> <ModalTagTitle projectTag={$projectTags[tag.tag_id]} />
<td> <td>
{#if projectTag.type == 0} {#if projectTag.type == 0}
<select bind:value={newOption} on:change={saveTag}> <select bind:value={newOption} on:change={saveTag}>
@ -74,23 +75,5 @@
<input bind:value={newValue} on:blur={saveTag} /> <input bind:value={newValue} on:blur={saveTag} />
{/if} {/if}
</td> </td>
<!-- <td>
<button on:click={() => removeTag(tag.tag_id)} class="remove-tag-button">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="white"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</td> -->
</tr> </tr>
{/if} {/if}

View File

@ -0,0 +1,146 @@
<script lang="ts">
import { tick } from 'svelte';
import type { MeTag } from '../../../stores/interfaces';
import Menu from '../../tuils/menu.svelte';
import projectTags from '../../../stores/projectTags';
import { toastAlert } from '../../../utils/toasts';
export let projectTag: MeTag;
let askConfirm: boolean = false;
let titleInput: HTMLInputElement;
let isMenuOpen: boolean = false;
let lastTitle: string = projectTag.title;
async function openMenu() {
isMenuOpen = !isMenuOpen;
await tick();
if (isMenuOpen && titleInput) titleInput.focus();
}
async function saveProjectTag() {
if (projectTag.title === lastTitle) return;
if (projectTag.title === '') {
toastAlert('Tag title cannot be empty');
return;
}
await projectTags.update(projectTag);
lastTitle = projectTag.title;
}
</script>
<td>
<div
class="title"
on:click={openMenu}
tabindex="0"
role="button"
on:keydown={(e) => {
if (e.key === 'Enter') {
openMenu();
}
}}
>
{projectTag.title}
</div>
<Menu
bind:isOpen={isMenuOpen}
onLeave={() => {
askConfirm = false;
projectTag.title = lastTitle;
}}
>
<div class="menu-items">
<input
bind:this={titleInput}
bind:value={projectTag.title}
on:blur={saveProjectTag}
on:keydown={(e) => {
if (e.key === 'Enter') {
saveProjectTag();
}
}}
/>
<button>Type: </button>
{#if askConfirm}
<div class="askconfirm">
<span>Confirm?</span>
<div>
<button on:click={() => {}}>✓</button>
<button on:click={() => (askConfirm = false)}>✗</button>
</div>
</div>
{:else}
<button on:click={() => (askConfirm = true)}>Delete</button>
{/if}
</div>
</Menu>
</td>
<style lang="less">
.title {
padding: 3px 5px;
margin: 2px;
border-radius: 5px;
&:hover {
background-color: #fff2;
cursor: pointer;
}
}
input {
margin-bottom: 5px;
width: 70%;
background-color: inherit;
color: inherit;
border: 1px solid #666;
padding: 3px 5px;
font-size: inherit;
border-radius: 5px;
}
button {
padding: 3px 5px;
text-align: left;
width: 80%;
}
.menu-items {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.askconfirm {
width: 75%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
text-align: center;
div {
display: flex;
flex-direction: row;
align-items: center;
button {
margin-left: 5px;
}
:first-child {
background-color: #0f02;
}
:last-child {
background-color: #f002;
}
}
}
</style>

View File

@ -2,6 +2,7 @@
import type { Card } from '../../../stores/interfaces'; import type { Card } from '../../../stores/interfaces';
import api, { processError } from '../../../utils/api'; import api, { processError } from '../../../utils/api';
import status from '../../../utils/status'; import status from '../../../utils/status';
import AddIcon from '../../icons/addIcon.svelte';
import ModalTag from './modal_tag.svelte'; import ModalTag from './modal_tag.svelte';
export let card: Card; export let card: Card;
@ -50,19 +51,7 @@
> >
<td <td
><button on:click={addTag}> ><button on:click={addTag}>
<svg <AddIcon />
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
stroke="white"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
</button> </button>
</td> </td>
</tr> </tr>

View File

@ -1,12 +1,13 @@
<script lang="ts"> <script lang="ts">
import Menu from '../tuils/menu.svelte';
export let isOpen = false; export let isOpen = false;
export let choices: { id: number; value: string }[] = []; export let choices: { id: number; value: string }[] = [];
export let onChoice = (id: number) => {}; export let onChoice = (id: number) => {};
export let currentChoice: number = -1; export let currentChoice: number = -1;
</script> </script>
{#if isOpen} <Menu {isOpen}>
<div class="menu">
{#each choices as choice} {#each choices as choice}
<div <div
class="menu-item" class="menu-item"
@ -25,18 +26,9 @@
{/if} {/if}
</div> </div>
{/each} {/each}
</div> </Menu>
{/if}
<style lang="less"> <style lang="less">
.menu {
position: absolute;
display: flex;
flex-direction: column;
background-color: #222;
border: 1px solid #666;
padding: 10px 0;
.menu-item { .menu-item {
display: flex; display: flex;
align-items: center; align-items: center;
@ -52,5 +44,4 @@
.mark { .mark {
margin-left: 20px; margin-left: 20px;
} }
}
</style> </style>

View File

@ -166,6 +166,7 @@
} }
input { input {
cursor: pointer;
padding: 10px; padding: 10px;
border-radius: 5px; border-radius: 5px;
background-color: transparent; background-color: transparent;

View File

@ -0,0 +1,39 @@
<script lang="ts">
import { onDestroy, onMount } from 'svelte';
export let isOpen: boolean;
let menuElement: HTMLElement;
export let onLeave: () => void = () => {};
function handleFocus(event: FocusEvent) {
if (isOpen && !menuElement.contains(event.target as Node) && menuElement !== event.target) {
isOpen = false;
onLeave();
}
}
onMount(() => {
window.addEventListener('focus', handleFocus, true);
});
onDestroy(() => {
window.removeEventListener('focus', handleFocus, true);
});
</script>
{#if isOpen}
<div class="menu" bind:this={menuElement}>
<slot />
</div>
{/if}
<style lang="less">
.menu {
position: absolute;
display: flex;
flex-direction: column;
background-color: #222;
border: 1px solid #666;
padding: 10px 0;
}
</style>

View File

@ -1,13 +1,13 @@
import { get, writable } from "svelte/store"; import { get, writable } from 'svelte/store';
import type { MeTag, TagOption } from "./interfaces"; import type { MeTag, TagOption } from './interfaces';
import api, { processError } from "../utils/api"; import api, { processError } from '../utils/api';
import status from "../utils/status"; import status from '../utils/status';
const { subscribe, set, update } = writable({} as { [key: number]: MeTag }); const { subscribe, set, update } = writable({} as { [key: number]: MeTag });
export default { export default {
subscribe, subscribe,
init: async (projectID: number) : Promise<boolean> => { init: async (projectID: number): Promise<boolean> => {
const response = await api.get(`/v1/projects/${projectID}/tags`); const response = await api.get(`/v1/projects/${projectID}/tags`);
if (response.status !== 200) { if (response.status !== 200) {
@ -20,7 +20,7 @@ export default {
const tags: { [key: number]: MeTag } = {}; const tags: { [key: number]: MeTag } = {};
metags.forEach((tag: MeTag) => { metags.forEach((tag: MeTag) => {
if(tag.options === null) tag.options = []; if (tag.options === null) tag.options = [];
tags[tag.id] = tag; tags[tag.id] = tag;
}); });
@ -28,7 +28,7 @@ export default {
return true; return true;
}, },
add: async (tag_id: number, value: string) => { addOption: async (tag_id: number, value: string) => {
const response = await api.post(`/v1/tags/${tag_id}/options`, { const response = await api.post(`/v1/tags/${tag_id}/options`, {
value value
}); });
@ -41,12 +41,63 @@ export default {
const option: TagOption = { const option: TagOption = {
id: response.data.id, id: response.data.id,
tag_id, tag_id,
value, value
}; };
update(tags => { update((tags) => {
tags[tag_id].options.push(option); tags[tag_id].options.push(option);
return tags; return tags;
}); });
}, },
} delete: async (tag_id: number) => {
const response = await api.delete(`/v1/tags/${tag_id}`);
if (response.status !== status.NoContent) {
processError(response, 'Failed to delete tag');
return;
}
update((tags) => {
delete tags[tag_id];
return tags;
});
},
update: async (tag: MeTag) => {
const response = await api.put(`/v1/tags/${tag.id}`, tag);
if (response.status !== status.NoContent) {
processError(response, 'Failed to update tag');
return;
}
update((tags) => {
tags[tag.id] = tag;
return tags;
});
},
add: async (ProjectId: number, title: string, type: number) => {
const response = await api.post(`/v1/tags`, {
project_id: ProjectId,
title,
type
});
if (response.status !== status.Created) {
processError(response, 'Failed to create tag');
return;
}
const tag: MeTag = {
id: response.data.id,
project_id: ProjectId,
title,
type,
options: []
};
update((tags) => {
tags[tag.id] = tag;
return tags;
});
}
};

View File

@ -17,3 +17,16 @@ a {
ul { ul {
list-style: none; list-style: none;
} }
button:not(.real) {
border: none;
background-color: inherit;
color: inherit;
font-size: inherit;
cursor: pointer;
border-radius: 3px;
}
button:not(.real):hover {
background-color: #fff2;
}