Drag & Drop

This commit is contained in:
Brieuc Dubois 2024-01-01 17:31:17 +01:00
parent 16996de532
commit cc9ddc21a0
6 changed files with 113 additions and 20 deletions

View File

@ -26,10 +26,27 @@ export async function newCardApi(projectId: number): Promise<Card> {
} }
export async function deleteCardApi(cardID: number): Promise<void> { export async function deleteCardApi(cardID: number): Promise<void> {
const response = await api.delete(`/v2/cards/${cardID}`); const response = await api.delete(`/v1/cards/${cardID}`);
if (response.status !== status.NoContent) { if (response.status !== status.NoContent) {
processError(response, 'Failed to delete card'); processError(response, 'Failed to delete card');
return Promise.reject(); return Promise.reject();
} }
} }
export async function updateCardTagApi(
cardID: number,
tagID: number,
option_id: number,
value: string
): Promise<void> {
const response = await api.put(`/v1/cards/${cardID}/tags/${tagID}`, {
option_id: option_id,
value: value
});
if (response.status !== status.NoContent) {
processError(response, 'Failed to update card tag');
return Promise.reject();
}
}

View File

@ -1,16 +1,18 @@
<script lang="ts"> <script lang="ts">
import type { Card } from '../stores/interfaces'; import type { Card } from '../stores/interfaces';
import projectTags from '../stores/projectTags'; import projectTags from '../stores/projectTags';
import { currentModalCard } from '../stores/smallStore'; import { currentDraggedCard, currentModalCard } from '../stores/smallStore';
import ModalCard from './modal_card.svelte'; import ModalCard from './modal_card.svelte';
export let card: Card; export let card: Card;
</script> </script>
<!-- on:dragend={() => currentDraggedCard.set(null)} -->
<div <div
class="card" class="card"
tabindex="0" tabindex="0"
draggable={true} draggable={true}
on:dragstart={() => currentDraggedCard.set(card)}
on:click={() => ($currentModalCard = card.id)} on:click={() => ($currentModalCard = card.id)}
role="button" role="button"
on:keydown={(e) => { on:keydown={(e) => {

View File

@ -1,15 +1,76 @@
<script lang="ts"> <script lang="ts">
import { updateCardTagApi } from '../api/cards';
import type { Card, TagOption } from '../stores/interfaces'; import type { Card, TagOption } from '../stores/interfaces';
import { cards, currentDraggedCard } from '../stores/smallStore';
import api, { processError } from '../utils/api';
import status from '../utils/status';
import CardC from './card.svelte'; import CardC from './card.svelte';
export let title: string; export let option: TagOption;
export let cards: Card[] = []; export let columnCards: Card[] = [];
async function onDrop(e: DragEvent) {
e.preventDefault();
if ($currentDraggedCard && $currentDraggedCard.tags) {
for (let tag of $currentDraggedCard.tags) {
if (tag.tag_id == option.tag_id) {
try {
if (tag.option_id == option.id) return;
// DELETE
if (tag.option_id !== -1 && option.id === -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.option_id == -1 && option.id !== -1) {
const response = await api.post(`/v1/cards/${tag.card_id}/tags/${tag.tag_id}`, {
value: tag.value,
option_id: option.id
});
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: tag.value,
option_id: option.id
});
if (response.status !== status.NoContent) {
processError(response, 'Failed to update tag');
return;
}
}
tag.option_id = option.id;
cards.reload();
} catch (e) {}
break;
}
}
currentDraggedCard.set(null);
}
}
</script> </script>
<div class="column"> <div
<h3>{title}</h3> class="column"
role="listbox"
tabindex="-1"
on:drop={onDrop}
on:dragover={(e) => {
e.preventDefault();
}}
>
<h3>{option.value}</h3>
<ul> <ul>
{#each cards as card} {#each columnCards as card}
<CardC {card} /> <CardC {card} />
{/each} {/each}
</ul> </ul>
@ -21,7 +82,8 @@
} }
.column { .column {
width: 200px;
margin: 0 10px; margin: 0 10px;
width: 200px;
height: 100%;
} }
</style> </style>

View File

@ -1,11 +1,10 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { type Project, type Card, parseCards, type View } from '../stores/interfaces'; import { type Project, type Card, type TagOption, type View } from '../stores/interfaces';
import projectTags from '../stores/projectTags'; import projectTags from '../stores/projectTags';
import { deleteCardApi, newCardApi } from '../api/cards'; import { getProjectAPI } from '../api/projects';
import { getProjectAPI, getProjectCardsAPI } from '../api/projects';
import Column from './column.svelte'; import Column from './column.svelte';
import { cards, currentModalCard, currentView } from '../stores/smallStore'; import { cards, currentView } from '../stores/smallStore';
export let projectId: number; export let projectId: number;
@ -69,7 +68,7 @@
</svelte:head> </svelte:head>
{#if project} {#if project}
<div id="project"> <section>
<header> <header>
<h2>{project.title}</h2> <h2>{project.title}</h2>
<button on:click={newCard}>New card</button> <button on:click={newCard}>New card</button>
@ -78,22 +77,32 @@
<div class="grid"> <div class="grid">
{#each $projectTags[view.primary_tag_id].options as option} {#each $projectTags[view.primary_tag_id].options as option}
<Column <Column
title={option.value} {option}
cards={$cards.filter((c) => c.tags.map((t) => t.option_id).includes(option.id))} columnCards={$cards.filter((c) => c.tags.map((t) => t.option_id).includes(option.id))}
/> />
{/each} {/each}
<Column <Column
title={`No ${$projectTags[view.primary_tag_id]?.title || 'tag'}`} option={{
cards={$cards.filter((c) => c.tags.find((t) => t.tag_id)?.option_id == -1 || false)} id: -1,
tag_id: view.primary_tag_id,
value: `No ${$projectTags[view.primary_tag_id].title}`
}}
columnCards={$cards.filter((c) => c.tags.find((t) => t.tag_id)?.option_id == -1 || false)}
/> />
</div> </div>
{/if} {/if}
</div> </section>
{/if} {/if}
<style> <style>
section {
display: flex;
flex-direction: column;
}
.grid { .grid {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex: 1;
} }
</style> </style>

View File

@ -1,5 +1,3 @@
import { toastAlert } from '../utils/toasts';
export interface Project { export interface Project {
id: number; id: number;
title: string; title: string;

View File

@ -7,6 +7,8 @@ export const currentView = writable(null as View | null);
export const currentModalCard = writable(-1); export const currentModalCard = writable(-1);
export const currentDraggedCard = writable(null as Card | null);
export const cards = (() => { export const cards = (() => {
const { subscribe, set, update } = writable([] as Card[]); const { subscribe, set, update } = writable([] as Card[]);
@ -28,6 +30,9 @@ export const cards = (() => {
update((cards) => cards.filter((c) => c.id !== card.id)); update((cards) => cards.filter((c) => c.id !== card.id));
currentModalCard.set(-1); currentModalCard.set(-1);
}); });
},
reload: () => {
update((cards) => cards);
} }
}; };
})(); })();