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.StatusOK)
return c.SendStatus(fiber.StatusNoContent)
}
func CreateTagOption(c *fiber.Ctx) error {

View File

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

View File

@ -4,14 +4,13 @@
import { cards } from '../../../stores/smallStore';
import api, { processError } from '../../../utils/api';
import status from '../../../utils/status';
import ModalTagTitle from './modal_tag_title.svelte';
export let tag: TagValue;
// export let removeTag: (id: number) => void;
let newValue: string = tag.value;
let newOption: number = tag.option_id;
let projectTag = $projectTags[tag.tag_id];
async function saveTag() {
if (tag.value === newValue && tag.option_id == newOption) return;
// DELETE
@ -54,13 +53,15 @@
async function newTagOption() {
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>
{#if projectTag}
<tr class="tag">
<td class="tag-title">{projectTag.title}</td>
<tr>
<ModalTagTitle projectTag={$projectTags[tag.tag_id]} />
<td>
{#if projectTag.type == 0}
<select bind:value={newOption} on:change={saveTag}>
@ -74,23 +75,5 @@
<input bind:value={newValue} on:blur={saveTag} />
{/if}
</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>
{/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 api, { processError } from '../../../utils/api';
import status from '../../../utils/status';
import AddIcon from '../../icons/addIcon.svelte';
import ModalTag from './modal_tag.svelte';
export let card: Card;
@ -50,19 +51,7 @@
>
<td
><button on:click={addTag}>
<svg
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>
<AddIcon />
</button>
</td>
</tr>

View File

@ -1,56 +1,47 @@
<script lang="ts">
import Menu from '../tuils/menu.svelte';
export let isOpen = false;
export let choices: { id: number; value: string }[] = [];
export let onChoice = (id: number) => {};
export let currentChoice: number = -1;
</script>
{#if isOpen}
<div class="menu">
{#each choices as choice}
<div
class="menu-item"
on:click={() => onChoice(choice.id)}
role="button"
tabindex="0"
on:keypress={(e) => {
if (e.key === 'Enter') {
onChoice(choice.id);
}
}}
>
<span>{choice.value}</span>
{#if currentChoice === choice.id}
<span class="mark"></span>
{/if}
</div>
{/each}
</div>
{/if}
<Menu {isOpen}>
{#each choices as choice}
<div
class="menu-item"
on:click={() => onChoice(choice.id)}
role="button"
tabindex="0"
on:keypress={(e) => {
if (e.key === 'Enter') {
onChoice(choice.id);
}
}}
>
<span>{choice.value}</span>
{#if currentChoice === choice.id}
<span class="mark"></span>
{/if}
</div>
{/each}
</Menu>
<style lang="less">
.menu {
position: absolute;
.menu-item {
display: flex;
flex-direction: column;
background-color: #222;
border: 1px solid #666;
padding: 10px 0;
align-items: center;
justify-content: space-between;
padding: 5px 20px;
.menu-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 5px 20px;
&:hover {
cursor: pointer;
background-color: #333;
}
}
.mark {
margin-left: 20px;
&:hover {
cursor: pointer;
background-color: #333;
}
}
.mark {
margin-left: 20px;
}
</style>

View File

@ -166,6 +166,7 @@
}
input {
cursor: pointer;
padding: 10px;
border-radius: 5px;
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,34 +1,34 @@
import { get, writable } from "svelte/store";
import type { MeTag, TagOption } from "./interfaces";
import api, { processError } from "../utils/api";
import status from "../utils/status";
import { get, writable } from 'svelte/store';
import type { MeTag, TagOption } from './interfaces';
import api, { processError } from '../utils/api';
import status from '../utils/status';
const { subscribe, set, update } = writable({} as { [key: number]: MeTag });
export default {
subscribe,
init: async (projectID: number) : Promise<boolean> => {
const response = await api.get(`/v1/projects/${projectID}/tags`);
subscribe,
init: async (projectID: number): Promise<boolean> => {
const response = await api.get(`/v1/projects/${projectID}/tags`);
if (response.status !== 200) {
processError(response);
return false;
}
if (response.status !== 200) {
processError(response);
return false;
}
const metags: MeTag[] = response.data;
const metags: MeTag[] = response.data;
const tags: { [key: number]: MeTag } = {};
const tags: { [key: number]: MeTag } = {};
metags.forEach((tag: MeTag) => {
if(tag.options === null) tag.options = [];
tags[tag.id] = tag;
});
metags.forEach((tag: MeTag) => {
if (tag.options === null) tag.options = [];
tags[tag.id] = tag;
});
set(tags);
set(tags);
return true;
},
add: async (tag_id: number, value: string) => {
return true;
},
addOption: async (tag_id: number, value: string) => {
const response = await api.post(`/v1/tags/${tag_id}/options`, {
value
});
@ -38,15 +38,66 @@ export default {
return;
}
const option: TagOption = {
id: response.data.id,
tag_id,
value,
};
const option: TagOption = {
id: response.data.id,
tag_id,
value
};
update(tags => {
tags[tag_id].options.push(option);
return tags;
});
},
}
update((tags) => {
tags[tag_id].options.push(option);
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

@ -16,4 +16,17 @@ a {
ul {
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;
}