Tag selection
This commit is contained in:
parent
b182c831a3
commit
29ab752170
|
@ -18,10 +18,15 @@ func GetCardTags(cardID int, projectID int) ([]types.FullCardTag, error) {
|
||||||
projectID = card.ProjectID
|
projectID = card.ProjectID
|
||||||
}
|
}
|
||||||
|
|
||||||
rows, err := db.Query(`SELECT t.id, t.title, t.type, COALESCE(ct.option_id, -1), COALESCE(ct.value, '')
|
// rows, err := db.Query(`SELECT t.id, t.title, t.type, COALESCE(ct.option_id, -1), COALESCE(ct.value, '')
|
||||||
FROM tags t
|
// FROM tags t
|
||||||
LEFT JOIN cardtags ct ON ct.tag_id = t.id AND ct.card_id = ?
|
// LEFT JOIN cardtags ct ON ct.tag_id = t.id AND ct.card_id = ?
|
||||||
WHERE t.project_id = ?
|
// WHERE t.project_id = ?
|
||||||
|
// `, cardID, projectID)
|
||||||
|
rows, err := db.Query(`SELECT t.id, t.title, t.type, ct.option_id, ct.value
|
||||||
|
FROM cardtags ct
|
||||||
|
INNER JOIN tags t ON t.id = ct.tag_id
|
||||||
|
WHERE ct.card_id = ? AND t.project_id = ?
|
||||||
`, cardID, projectID)
|
`, cardID, projectID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -20,7 +20,7 @@ export async function newCardApi(projectId: number, tags: TagValue[]): Promise<C
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: id,
|
id: id,
|
||||||
projectId: projectId,
|
project_id: projectId,
|
||||||
title: 'Untitled',
|
title: 'Untitled',
|
||||||
content: '',
|
content: '',
|
||||||
tags: tags
|
tags: tags
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
|
<script lang="ts">
|
||||||
|
export let size: number = 24;
|
||||||
|
</script>
|
||||||
|
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="24"
|
width={size}
|
||||||
height="24"
|
height={size}
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="white"
|
stroke="white"
|
||||||
|
|
Before Width: | Height: | Size: 417 B After Width: | Height: | Size: 482 B |
|
@ -13,12 +13,12 @@
|
||||||
|
|
||||||
async function save(closeModal: boolean = true) {
|
async function save(closeModal: boolean = true) {
|
||||||
if (
|
if (
|
||||||
card.projectId != tempCard.projectId ||
|
card.project_id != tempCard.project_id ||
|
||||||
card.title !== tempCard.title ||
|
card.title !== tempCard.title ||
|
||||||
card.content !== tempCard.content
|
card.content !== tempCard.content
|
||||||
) {
|
) {
|
||||||
const response = await api.put(`/v1/cards/${card.id}`, {
|
const response = await api.put(`/v1/cards/${card.id}`, {
|
||||||
project_id: tempCard.projectId,
|
project_id: tempCard.project_id,
|
||||||
title: tempCard.title,
|
title: tempCard.title,
|
||||||
content: tempCard.content
|
content: tempCard.content
|
||||||
});
|
});
|
||||||
|
@ -131,10 +131,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal .buttons button:first-child:hover {
|
.modal .buttons button:first-child:hover {
|
||||||
background-color: #343;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal .buttons button:last-child:hover {
|
|
||||||
background-color: #433;
|
background-color: #433;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,18 +145,6 @@
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal td {
|
|
||||||
margin-right: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal td:first-child {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal td:last-child {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal td button {
|
.modal td button {
|
||||||
width: 30px;
|
width: 30px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
|
|
|
@ -1,79 +1,16 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { TagValue } from '../../../stores/interfaces';
|
import type { Card, MeTag, TagValue } from '../../../stores/interfaces';
|
||||||
import projectTags from '../../../stores/projectTags';
|
import ModalTagTitle from './modal_tag/modal_tag_title.svelte';
|
||||||
import { cards } from '../../../stores/smallStore';
|
import ModalTagValue from './modal_tag/modal_tag_value.svelte';
|
||||||
import api, { processError } from '../../../utils/api';
|
|
||||||
import status from '../../../utils/status';
|
|
||||||
import ModalTagTitle from './modal_tag_title.svelte';
|
|
||||||
|
|
||||||
export let tag: TagValue;
|
export let projectTag: MeTag;
|
||||||
// export let removeTag: (id: number) => void;
|
export let tagValue: TagValue | undefined;
|
||||||
let newValue: string = tag.value;
|
export let card: Card;
|
||||||
let newOption: number = tag.option_id;
|
|
||||||
|
|
||||||
async function saveTag() {
|
|
||||||
if (tag.value === newValue && tag.option_id == newOption) return;
|
|
||||||
// DELETE
|
|
||||||
if ((tag.value !== '' && newValue === '') || (tag.option_id !== -1 && newOption === -1)) {
|
|
||||||
const response = await api.delete(`/v1/cards/${tag.card_id}/tags/${tag.tag_id}`);
|
|
||||||
|
|
||||||
if (response.status !== status.NoContent) {
|
|
||||||
processError(response, 'Failed to delete tag');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// CREATE
|
|
||||||
else if ((tag.value === '' && newValue !== '') || (tag.option_id == -1 && newOption !== -1)) {
|
|
||||||
const response = await api.post(`/v1/cards/${tag.card_id}/tags/${tag.tag_id}`, {
|
|
||||||
value: newValue,
|
|
||||||
option_id: newOption
|
|
||||||
});
|
|
||||||
if (response.status !== status.Created) {
|
|
||||||
processError(response, 'Failed to create tag');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// UPDATE
|
|
||||||
else {
|
|
||||||
const response = await api.put(`/v1/cards/${tag.card_id}/tags/${tag.tag_id}`, {
|
|
||||||
value: newValue,
|
|
||||||
option_id: newOption
|
|
||||||
});
|
|
||||||
if (response.status !== status.NoContent) {
|
|
||||||
processError(response, 'Failed to update tag');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tag.value = newValue;
|
|
||||||
tag.option_id = newOption;
|
|
||||||
cards.reload();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function newTagOption() {
|
|
||||||
const value = prompt('New option value');
|
|
||||||
|
|
||||||
if (value) projectTags.addOption(tag.tag_id, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
const projectTag = $projectTags[tag.tag_id];
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if projectTag}
|
{#if projectTag}
|
||||||
<tr>
|
<tr>
|
||||||
<ModalTagTitle projectTag={$projectTags[tag.tag_id]} />
|
<ModalTagTitle {projectTag} />
|
||||||
<td>
|
<ModalTagValue {projectTag} bind:tagValue {card} />
|
||||||
{#if projectTag.type == 0}
|
|
||||||
<select bind:value={newOption} on:change={saveTag}>
|
|
||||||
<option value={-1}></option>
|
|
||||||
{#each projectTag.options as option}
|
|
||||||
<option value={option.id}>{option.value}</option>
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
<button style="color: white; font-size: 20px;" on:click={newTagOption}>+</button>
|
|
||||||
{:else}
|
|
||||||
<input bind:value={newValue} on:blur={saveTag} />
|
|
||||||
{/if}
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -0,0 +1,95 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { tick } from 'svelte';
|
||||||
|
import AddIcon from '../../../icons/addIcon.svelte';
|
||||||
|
import Menu from '../../../tuils/menu.svelte';
|
||||||
|
import ModalTagTypes from './modal_tag_types.svelte';
|
||||||
|
import projectTags from '../../../../stores/projectTags';
|
||||||
|
import { toastAlert } from '../../../../utils/toasts';
|
||||||
|
|
||||||
|
export let projectId: number;
|
||||||
|
|
||||||
|
let isOpen = false;
|
||||||
|
|
||||||
|
let titleInput: HTMLInputElement;
|
||||||
|
|
||||||
|
let title: string = '';
|
||||||
|
let typeId: number = 0;
|
||||||
|
|
||||||
|
async function openMenu() {
|
||||||
|
isOpen = !isOpen;
|
||||||
|
await tick();
|
||||||
|
if (isOpen && titleInput) titleInput.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createTag() {
|
||||||
|
if (title == '') return;
|
||||||
|
if (!projectId) {
|
||||||
|
toastAlert('Failed to create tag', `ProjectId is ${projectId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await projectTags.add(projectId, title, typeId);
|
||||||
|
isOpen = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
class="add-button"
|
||||||
|
on:click={openMenu}
|
||||||
|
on:keydown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
openMenu();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AddIcon />
|
||||||
|
</button>
|
||||||
|
<Menu
|
||||||
|
{isOpen}
|
||||||
|
onLeave={() => {
|
||||||
|
isOpen = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="menu-items">
|
||||||
|
<input bind:this={titleInput} bind:value={title} />
|
||||||
|
<ModalTagTypes type={typeId} onChoice={(id) => (typeId = id)} />
|
||||||
|
<button on:click={createTag}>Create</button>
|
||||||
|
</div>
|
||||||
|
</Menu>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
.add-button {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 3px 5px;
|
||||||
|
border-radius: 5px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-items {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
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%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,9 +1,10 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { tick } from 'svelte';
|
import { tick } from 'svelte';
|
||||||
import type { MeTag } from '../../../stores/interfaces';
|
import type { MeTag } from '../../../../stores/interfaces';
|
||||||
import Menu from '../../tuils/menu.svelte';
|
import Menu from '../../../tuils/menu.svelte';
|
||||||
import projectTags from '../../../stores/projectTags';
|
import projectTags from '../../../../stores/projectTags';
|
||||||
import { toastAlert } from '../../../utils/toasts';
|
import { toastAlert } from '../../../../utils/toasts';
|
||||||
|
import ModalTagTypes from './modal_tag_types.svelte';
|
||||||
|
|
||||||
export let projectTag: MeTag;
|
export let projectTag: MeTag;
|
||||||
|
|
||||||
|
@ -65,12 +66,24 @@
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<button>Type: </button>
|
<ModalTagTypes
|
||||||
|
type={projectTag.type}
|
||||||
|
onChoice={async (id) => {
|
||||||
|
projectTag.type = id;
|
||||||
|
|
||||||
|
await projectTags.update(projectTag);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
{#if askConfirm}
|
{#if askConfirm}
|
||||||
<div class="askconfirm">
|
<div class="askconfirm">
|
||||||
<span>Confirm?</span>
|
<span>Confirm?</span>
|
||||||
<div>
|
<div>
|
||||||
<button on:click={() => {}}>✓</button>
|
<button
|
||||||
|
on:click={() => {
|
||||||
|
projectTags.delete(projectTag.id);
|
||||||
|
isMenuOpen = false;
|
||||||
|
}}>✓</button
|
||||||
|
>
|
||||||
<button on:click={() => (askConfirm = false)}>✗</button>
|
<button on:click={() => (askConfirm = false)}>✗</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -82,10 +95,15 @@
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<style lang="less">
|
<style lang="less">
|
||||||
|
td {
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
padding: 3px 5px;
|
padding: 3px 5px;
|
||||||
margin: 2px;
|
margin: 2px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
height: 28px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #fff2;
|
background-color: #fff2;
|
|
@ -0,0 +1,78 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import projectTags from '../../../../stores/projectTags';
|
||||||
|
import { getTagTypeFromId, tagTypes } from '../../../../utils/tagTypes';
|
||||||
|
import Menu from '../../../tuils/menu.svelte';
|
||||||
|
|
||||||
|
export let type: number;
|
||||||
|
export let isTagMenuOpen: boolean = false;
|
||||||
|
export let onChoice: (id: number) => void;
|
||||||
|
|
||||||
|
async function onLocalChoice(id: number) {
|
||||||
|
onChoice(id);
|
||||||
|
|
||||||
|
isTagMenuOpen = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button on:click={() => (isTagMenuOpen = !isTagMenuOpen)}>
|
||||||
|
Type: {getTagTypeFromId(type)?.name}
|
||||||
|
</button>
|
||||||
|
<div class="menu">
|
||||||
|
<Menu
|
||||||
|
isOpen={isTagMenuOpen}
|
||||||
|
onLeave={() => {
|
||||||
|
isTagMenuOpen = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#each Object.values(tagTypes) as tagType}
|
||||||
|
<div
|
||||||
|
class="menu-item"
|
||||||
|
role="button"
|
||||||
|
on:click={() => onLocalChoice(tagType.id)}
|
||||||
|
tabindex="0"
|
||||||
|
on:keypress={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
onLocalChoice(tagType.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>{tagType.name}</span>
|
||||||
|
{#if type === tagType.id}
|
||||||
|
<span class="mark"> ✓ </span>
|
||||||
|
{:else}
|
||||||
|
<span class="mark"> </span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
button {
|
||||||
|
padding: 3px 5px;
|
||||||
|
text-align: left;
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
position: relative;
|
||||||
|
bottom: 30px;
|
||||||
|
left: 105px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 5px 20px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mark {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,19 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { Card, MeTag, TagValue } from '../../../../stores/interfaces';
|
||||||
|
import { getTagTypeFromId } from '../../../../utils/tagTypes';
|
||||||
|
import SelectTags from './select_tags.svelte';
|
||||||
|
|
||||||
|
export let projectTag: MeTag;
|
||||||
|
export let tagValue: TagValue | undefined;
|
||||||
|
export let card: Card;
|
||||||
|
|
||||||
|
let tagType = getTagTypeFromId(projectTag.type);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
{#if tagType?.hasOptions}
|
||||||
|
<SelectTags multiple={false} {projectTag} {card} bind:tagValue />
|
||||||
|
{:else if !tagType?.hasOptions}
|
||||||
|
<input />
|
||||||
|
{/if}
|
||||||
|
</td>
|
|
@ -0,0 +1,232 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { Card, MeTag, TagOption, TagValue } from '../../../../stores/interfaces';
|
||||||
|
import projectTags from '../../../../stores/projectTags';
|
||||||
|
import { cards } from '../../../../stores/smallStore';
|
||||||
|
import api, { processError } from '../../../../utils/api';
|
||||||
|
import status from '../../../../utils/status';
|
||||||
|
import TrashIcon from '../../../icons/trashIcon.svelte';
|
||||||
|
import Menu from '../../../tuils/menu.svelte';
|
||||||
|
|
||||||
|
export let multiple: boolean = false;
|
||||||
|
export let card: Card;
|
||||||
|
export let projectTag: MeTag;
|
||||||
|
export let tagValue: TagValue | undefined;
|
||||||
|
|
||||||
|
let lastTagValue = { ...tagValue };
|
||||||
|
let newOption: string = '';
|
||||||
|
|
||||||
|
$: tagOption = projectTag.options.find((o) => o.id === tagValue?.option_id);
|
||||||
|
|
||||||
|
let isOpen = false;
|
||||||
|
|
||||||
|
async function selectOption(option_id: number) {
|
||||||
|
if (lastTagValue.option_id === option_id) {
|
||||||
|
isOpen = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (tagValue) {
|
||||||
|
const response = await api.put(`/v1/cards/${card.id}/tags/${projectTag.id}`, {
|
||||||
|
option_id,
|
||||||
|
value: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status !== status.NoContent) {
|
||||||
|
processError(response, 'Failed to update tag');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
card.tags = card.tags.map((t) => {
|
||||||
|
if (t.tag_id === projectTag.id) {
|
||||||
|
t.option_id = option_id;
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
});
|
||||||
|
|
||||||
|
tagValue = { ...tagValue, option_id };
|
||||||
|
} else {
|
||||||
|
const response = await api.post(`/v1/cards/${card.id}/tags/${projectTag.id}`, {
|
||||||
|
option_id,
|
||||||
|
value: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status !== status.Created) {
|
||||||
|
processError(response, 'Failed to create tag');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tagValue = {
|
||||||
|
card_id: card.id,
|
||||||
|
tag_id: projectTag.id,
|
||||||
|
option_id,
|
||||||
|
value: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
card.tags.push(tagValue);
|
||||||
|
}
|
||||||
|
lastTagValue = { ...tagValue };
|
||||||
|
|
||||||
|
isOpen = false;
|
||||||
|
|
||||||
|
cards.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteOption(_: number | undefined) {
|
||||||
|
const response = await api.delete(`/v1/cards/${card.id}/tags/${projectTag.id}`);
|
||||||
|
|
||||||
|
if (response.status !== status.NoContent) {
|
||||||
|
processError(response, 'Failed to delete tag');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tagValue = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createOption() {
|
||||||
|
if (!newOption) return;
|
||||||
|
projectTags.addOption(projectTag.id, newOption);
|
||||||
|
newOption = '';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="select"
|
||||||
|
on:click={() => (isOpen = !isOpen)}
|
||||||
|
tabindex="0"
|
||||||
|
role="button"
|
||||||
|
on:keydown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
isOpen = !isOpen;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="tags">
|
||||||
|
{#if tagValue}
|
||||||
|
<span class="tag">
|
||||||
|
{tagOption?.value}
|
||||||
|
<button class="real" on:click={() => deleteOption(tagValue?.option_id)}>✗</button>
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Menu
|
||||||
|
{isOpen}
|
||||||
|
onLeave={() => {
|
||||||
|
isOpen = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#each projectTag.options as option}
|
||||||
|
<div
|
||||||
|
class="option"
|
||||||
|
on:click={() => selectOption(option.id)}
|
||||||
|
tabindex="0"
|
||||||
|
role="button"
|
||||||
|
on:keydown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
selectOption(option.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span class="value">{option.value}</span>
|
||||||
|
<button
|
||||||
|
on:click|stopPropagation={() => {
|
||||||
|
projectTags.deleteOption(projectTag.id, option.id);
|
||||||
|
}}><TrashIcon size={16} /></button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
<div class="new">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="New option"
|
||||||
|
on:keydown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
createOption();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
bind:value={newOption}
|
||||||
|
/>
|
||||||
|
<button on:click={createOption}>✓</button>
|
||||||
|
</div>
|
||||||
|
</Menu>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
.select {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 150px;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 3px 20px 3px 10px;
|
||||||
|
height: 30px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select:hover {
|
||||||
|
background-color: #fff2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
background-color: #444;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 1px 2px;
|
||||||
|
cursor: default;
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 2px 4px;
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.option {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
min-width: 150px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #fff2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
margin: 5px 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 5px 8px;
|
||||||
|
background-color: #fff2;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: 15px;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: transparent;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #fff2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.new {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 50%;
|
||||||
|
margin: 5px 10px;
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 1px solid #666;
|
||||||
|
background-color: transparent;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin-left: 15px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,58 +1,21 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Card } from '../../../stores/interfaces';
|
import type { Card } from '../../../stores/interfaces';
|
||||||
import api, { processError } from '../../../utils/api';
|
import projectTags from '../../../stores/projectTags';
|
||||||
import status from '../../../utils/status';
|
|
||||||
import AddIcon from '../../icons/addIcon.svelte';
|
|
||||||
import ModalTag from './modal_tag.svelte';
|
import ModalTag from './modal_tag.svelte';
|
||||||
|
import ModalNewTag from './modal_tag/modal_new_tag.svelte';
|
||||||
|
|
||||||
export let card: Card;
|
export let card: Card;
|
||||||
|
|
||||||
let newTagName = '';
|
|
||||||
|
|
||||||
async function addTag() {
|
|
||||||
if (newTagName === '') return;
|
|
||||||
|
|
||||||
const response = await api.post(`/v1/tags`, {
|
|
||||||
project_id: card.projectId,
|
|
||||||
title: newTagName,
|
|
||||||
type: 0
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.status !== status.Created) {
|
|
||||||
processError(response, 'Failed to create tag');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const id = response.data.id;
|
|
||||||
|
|
||||||
card.tags = [...card.tags, { card_id: card.id, tag_id: id, option_id: -1, value: '' }];
|
|
||||||
newTagName = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function addTagHandler(event: KeyboardEvent) {
|
|
||||||
if (event.key === 'Enter') {
|
|
||||||
addTag();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeTag(id: number) {
|
|
||||||
card.tags = card.tags.filter((tag) => tag.tag_id !== id);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
{#if card.tags}
|
{#if card.tags}
|
||||||
{#each card.tags as tag}
|
{#each Object.values($projectTags) as projectTag}
|
||||||
<ModalTag bind:tag />
|
<ModalTag
|
||||||
|
tagValue={card.tags.find((t) => t.tag_id === projectTag.id)}
|
||||||
|
bind:projectTag
|
||||||
|
{card}
|
||||||
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
<tr class="tag">
|
<ModalNewTag projectId={card.project_id} />
|
||||||
<td class="tag-title"
|
|
||||||
><input placeholder="Add a new tag" on:keydown={addTagHandler} bind:value={newTagName} /></td
|
|
||||||
>
|
|
||||||
<td
|
|
||||||
><button on:click={addTag}>
|
|
||||||
<AddIcon />
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -41,7 +41,9 @@
|
||||||
tag_id: view.primary_tag_id,
|
tag_id: view.primary_tag_id,
|
||||||
value: `No ${$projectTags[view.primary_tag_id].title}`
|
value: `No ${$projectTags[view.primary_tag_id].title}`
|
||||||
}}
|
}}
|
||||||
columnCards={$cards.filter((c) => c.tags.find((t) => t.tag_id)?.option_id == -1 || false)}
|
columnCards={$cards.filter(
|
||||||
|
(c) => !c.tags.map((t) => t.tag_id).includes(view?.primary_tag_id || -2)
|
||||||
|
)}
|
||||||
projectId={project.id}
|
projectId={project.id}
|
||||||
editable={false}
|
editable={false}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -13,7 +13,6 @@
|
||||||
let viewEditValue: string;
|
let viewEditValue: string;
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
console.log('aaa');
|
|
||||||
await views.init(project.id);
|
await views.init(project.id);
|
||||||
|
|
||||||
if ($views.length > 0) currentView.set($views[0]);
|
if ($views.length > 0) currentView.set($views[0]);
|
||||||
|
|
|
@ -5,7 +5,7 @@ export interface Project {
|
||||||
|
|
||||||
export interface Card {
|
export interface Card {
|
||||||
id: number;
|
id: number;
|
||||||
projectId: number;
|
project_id: number;
|
||||||
title: string;
|
title: string;
|
||||||
content: string;
|
content: string;
|
||||||
tags: TagValue[];
|
tags: TagValue[];
|
||||||
|
|
|
@ -2,6 +2,7 @@ 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';
|
||||||
|
import { cards } from './smallStore';
|
||||||
|
|
||||||
const { subscribe, set, update } = writable({} as { [key: number]: MeTag });
|
const { subscribe, set, update } = writable({} as { [key: number]: MeTag });
|
||||||
|
|
||||||
|
@ -49,6 +50,25 @@ export default {
|
||||||
return tags;
|
return tags;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
deleteOption: async (tag_id: number, option_id: number) => {
|
||||||
|
const response = await api.delete(`/v1/tags/${tag_id}/options/${option_id}`);
|
||||||
|
|
||||||
|
if (response.status !== status.NoContent) {
|
||||||
|
processError(response, 'Failed to delete tag option');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
update((tags) => {
|
||||||
|
tags[tag_id].options = tags[tag_id].options.filter((option) => option.id !== option_id);
|
||||||
|
return tags;
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const card of get(cards)) {
|
||||||
|
//TODO: same in db
|
||||||
|
card.tags.filter((tag) => tag.tag_id !== tag_id || tag.option_id !== option_id);
|
||||||
|
}
|
||||||
|
cards.reload();
|
||||||
|
},
|
||||||
delete: async (tag_id: number) => {
|
delete: async (tag_id: number) => {
|
||||||
const response = await api.delete(`/v1/tags/${tag_id}`);
|
const response = await api.delete(`/v1/tags/${tag_id}`);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
export const tagTypes: { [name: string]: { id: number; name: string; hasOptions: boolean } } = {
|
||||||
|
SELECT: {
|
||||||
|
id: 0,
|
||||||
|
name: 'Select',
|
||||||
|
hasOptions: true
|
||||||
|
},
|
||||||
|
MULTISELECT: {
|
||||||
|
id: 1,
|
||||||
|
name: 'Multiselect',
|
||||||
|
hasOptions: true
|
||||||
|
},
|
||||||
|
TEXT: {
|
||||||
|
id: 2,
|
||||||
|
name: 'Text',
|
||||||
|
hasOptions: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTagTypeFromId = (id: number) => {
|
||||||
|
for (let type in tagTypes) {
|
||||||
|
if (tagTypes[type].id === id) {
|
||||||
|
return tagTypes[type];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
Loading…
Reference in New Issue