Edit and remove projectTags
This commit is contained in:
parent
bc566280a6
commit
b182c831a3
|
@ -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 {
|
||||
|
|
|
@ -65,9 +65,6 @@
|
|||
{/if}
|
||||
|
||||
<style lang="less">
|
||||
.content {
|
||||
}
|
||||
|
||||
.modal {
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
position: fixed;
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -166,6 +166,7 @@
|
|||
}
|
||||
|
||||
input {
|
||||
cursor: pointer;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
background-color: transparent;
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
Loading…
Reference in New Issue