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.StatusNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.SendStatus(fiber.StatusOK)
|
return c.SendStatus(fiber.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateTagOption(c *fiber.Ctx) error {
|
func CreateTagOption(c *fiber.Ctx) error {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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 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>
|
||||||
|
|
|
@ -1,56 +1,47 @@
|
||||||
<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"
|
on:click={() => onChoice(choice.id)}
|
||||||
on:click={() => onChoice(choice.id)}
|
role="button"
|
||||||
role="button"
|
tabindex="0"
|
||||||
tabindex="0"
|
on:keypress={(e) => {
|
||||||
on:keypress={(e) => {
|
if (e.key === 'Enter') {
|
||||||
if (e.key === 'Enter') {
|
onChoice(choice.id);
|
||||||
onChoice(choice.id);
|
}
|
||||||
}
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<span>{choice.value}</span>
|
||||||
<span>{choice.value}</span>
|
{#if currentChoice === choice.id}
|
||||||
{#if currentChoice === choice.id}
|
<span class="mark"> ✓ </span>
|
||||||
<span class="mark"> ✓ </span>
|
{/if}
|
||||||
{/if}
|
</div>
|
||||||
</div>
|
{/each}
|
||||||
{/each}
|
</Menu>
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style lang="less">
|
<style lang="less">
|
||||||
.menu {
|
.menu-item {
|
||||||
position: absolute;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
align-items: center;
|
||||||
background-color: #222;
|
justify-content: space-between;
|
||||||
border: 1px solid #666;
|
padding: 5px 20px;
|
||||||
padding: 10px 0;
|
|
||||||
|
|
||||||
.menu-item {
|
&:hover {
|
||||||
display: flex;
|
cursor: pointer;
|
||||||
align-items: center;
|
background-color: #333;
|
||||||
justify-content: space-between;
|
|
||||||
padding: 5px 20px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
background-color: #333;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mark {
|
|
||||||
margin-left: 20px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mark {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -166,6 +166,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
input {
|
input {
|
||||||
|
cursor: pointer;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
background-color: transparent;
|
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 { 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) {
|
||||||
processError(response);
|
processError(response);
|
||||||
return false;
|
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) => {
|
metags.forEach((tag: MeTag) => {
|
||||||
if(tag.options === null) tag.options = [];
|
if (tag.options === null) tag.options = [];
|
||||||
tags[tag.id] = tag;
|
tags[tag.id] = tag;
|
||||||
});
|
});
|
||||||
|
|
||||||
set(tags);
|
set(tags);
|
||||||
|
|
||||||
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
|
||||||
});
|
});
|
||||||
|
@ -38,15 +38,66 @@ export default {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
@ -16,4 +16,17 @@ 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;
|
||||||
}
|
}
|
Loading…
Reference in New Issue