View
This commit is contained in:
parent
b796195270
commit
4ca04b8cc8
|
@ -55,6 +55,16 @@ func InitDB(driver string, connStr string) error {
|
|||
value TEXT,
|
||||
FOREIGN KEY(tag_id) REFERENCES tags(id)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS views (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
project_id INTEGER,
|
||||
primary_tag_id INTEGER,
|
||||
secondary_tag_id INTEGER,
|
||||
title TEXT,
|
||||
FOREIGN KEY(project_id) REFERENCES projects(id),
|
||||
FOREIGN KEY(primary_tag_id) REFERENCES tags(id),
|
||||
FOREIGN KEY(secondary_tag_id) REFERENCES tags(id)
|
||||
);
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
package db
|
||||
|
||||
import "git.bhasher.com/bhasher/focus/backend/types"
|
||||
|
||||
func CreateView(v types.View) (int, error) {
|
||||
res, err := db.Exec("INSERT INTO views (project_id, primary_tag_id, secondary_tag_id, title) VALUES (?, ?, ?, ?)", v.ProjectID, v.PrimaryTagID, v.SecondaryTagID, v.Title)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
id, err := res.LastInsertId()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return int(id), nil
|
||||
}
|
||||
|
||||
func GetProjectViews(projectID int) ([]types.View, error) {
|
||||
rows, err := db.Query("SELECT * FROM views WHERE project_id = ?", projectID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var views []types.View
|
||||
for rows.Next() {
|
||||
var v types.View
|
||||
if err := rows.Scan(&v.ID, &v.ProjectID, &v.PrimaryTagID, &v.SecondaryTagID, &v.Title); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
views = append(views, v)
|
||||
}
|
||||
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return views, nil
|
||||
}
|
||||
|
||||
func GetView(id int) (*types.View, error) {
|
||||
rows, err := db.Query("SELECT * FROM views WHERE id = ?", id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
if !rows.Next() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var v types.View
|
||||
rows.Scan(&v.ID, &v.ProjectID, v.PrimaryTagID, v.SecondaryTagID, v.Title)
|
||||
|
||||
return &v, nil
|
||||
}
|
||||
|
||||
func UpdateView(v types.View) (int64, error) {
|
||||
res, err := db.Exec("UPDATE views SET project_id = ?, primary_tag_id = ?, secondary_tag_id = ?, title = ? WHERE id = ?", v.ProjectID, v.PrimaryTagID, v.SecondaryTagID, v.Title, v.ID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return res.RowsAffected()
|
||||
}
|
||||
|
||||
func DeleteView(id int) (int64, error) {
|
||||
res, err := db.Exec("DELETE FROM views WHERE id = ?", id)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return res.RowsAffected()
|
||||
}
|
|
@ -23,10 +23,7 @@ func cardsRouter(router fiber.Router) error {
|
|||
func CreateCard(c *fiber.Ctx) error {
|
||||
card := types.Card{}
|
||||
if err := c.BodyParser(&card); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": "Cannot parse request",
|
||||
"trace": fmt.Sprint(err),
|
||||
})
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Cannot parse request"})
|
||||
}
|
||||
|
||||
id, err := db.CreateCard(card)
|
||||
|
@ -45,10 +42,7 @@ func CreateCard(c *fiber.Ctx) error {
|
|||
func GetCard(c *fiber.Ctx) error {
|
||||
id, err := strconv.Atoi(c.Params("id"))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": "Invalid card ID",
|
||||
"trace": fmt.Sprint(err),
|
||||
})
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid card ID"})
|
||||
}
|
||||
|
||||
card, err := db.GetCard(id)
|
||||
|
@ -68,10 +62,7 @@ func GetCard(c *fiber.Ctx) error {
|
|||
func DeleteCard(c *fiber.Ctx) error {
|
||||
id, err := strconv.Atoi(c.Params("id"))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": "Invalid card ID",
|
||||
"trace": fmt.Sprint(err),
|
||||
})
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid card ID"})
|
||||
}
|
||||
|
||||
count, err := db.DeleteCard(id)
|
||||
|
|
|
@ -10,6 +10,7 @@ func v1Router(router fiber.Router) error {
|
|||
projectsRouter(router.Group("/projects"))
|
||||
cardsRouter(router.Group("/cards"))
|
||||
tagsRouter(router.Group("/tags"))
|
||||
viewsRouter(router.Group("/views"))
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ func projectsRouter(router fiber.Router) error {
|
|||
router.Delete("/:id", DeleteProject)
|
||||
router.Get(":id/cards", GetProjectCards)
|
||||
router.Get(":id/tags", GetProjectTags)
|
||||
router.Get(":id/views", GetProjectViews)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -175,3 +176,32 @@ func GetProjectTags(c *fiber.Ctx) error {
|
|||
|
||||
return c.Status(fiber.StatusOK).JSON(tags)
|
||||
}
|
||||
|
||||
func GetProjectViews(c *fiber.Ctx) error {
|
||||
projectID, err := strconv.Atoi(c.Params("id"))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid project ID"})
|
||||
}
|
||||
|
||||
exists, err := db.ExistProject(projectID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"error": "Error finding project",
|
||||
"trace": fmt.Sprint(err),
|
||||
})
|
||||
}
|
||||
|
||||
if !exists {
|
||||
return c.SendStatus(fiber.StatusNotFound)
|
||||
}
|
||||
|
||||
views, err := db.GetProjectViews(projectID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"error": "Cannot retrieve views",
|
||||
"trace": fmt.Sprint(err),
|
||||
})
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(views)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"git.bhasher.com/bhasher/focus/backend/db"
|
||||
"git.bhasher.com/bhasher/focus/backend/types"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func viewsRouter(router fiber.Router) error {
|
||||
router.Post("/", CreateView)
|
||||
router.Get("/:id", GetView)
|
||||
router.Put("/:id", UpdateView)
|
||||
router.Delete("/:id", DeleteView)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func CreateView(c *fiber.Ctx) error {
|
||||
view := types.View{}
|
||||
if err := c.BodyParser(&view); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": "Cannot parse request",
|
||||
"trace": fmt.Sprint(err),
|
||||
})
|
||||
}
|
||||
|
||||
id, err := db.CreateView(view)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"error": "Cannot create view",
|
||||
"trace": fmt.Sprint(err),
|
||||
})
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusCreated).JSON(fiber.Map{
|
||||
"id": id,
|
||||
})
|
||||
}
|
||||
|
||||
func GetView(c *fiber.Ctx) error {
|
||||
id, err := strconv.Atoi(c.Params("id"))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid view ID"})
|
||||
}
|
||||
|
||||
view, err := db.GetView(id)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"error": "Cannot retrieve view",
|
||||
"trace": fmt.Sprint(err),
|
||||
})
|
||||
}
|
||||
if view == nil {
|
||||
return c.SendStatus(fiber.StatusNotFound)
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(view)
|
||||
}
|
||||
|
||||
func UpdateView(c *fiber.Ctx) error {
|
||||
id, err := strconv.Atoi(c.Params("id"))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid view ID"})
|
||||
}
|
||||
|
||||
view := types.View{ID: id}
|
||||
if err := c.BodyParser(&view); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Cannot parse request"})
|
||||
}
|
||||
|
||||
count, err := db.UpdateView(view)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"error": "Cannot update view",
|
||||
"trace": fmt.Sprint(err),
|
||||
})
|
||||
}
|
||||
if count == 0 {
|
||||
return c.SendStatus(fiber.StatusNotFound)
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusNoContent)
|
||||
}
|
||||
|
||||
func DeleteView(c *fiber.Ctx) error {
|
||||
id, err := strconv.Atoi(c.Params("id"))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid view ID"})
|
||||
}
|
||||
|
||||
count, err := db.DeleteView(id)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"error": "Cannot delete view",
|
||||
"trace": fmt.Sprint(err),
|
||||
})
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
return c.SendStatus(fiber.StatusNotFound)
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusNoContent)
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package types
|
||||
|
||||
type View struct {
|
||||
ID int `json:"id"`
|
||||
ProjectID int `json:"project_id"`
|
||||
PrimaryTagID int `json:"primary_tag_id"`
|
||||
SecondaryTagID int `json:"secondary_tag_id"`
|
||||
Title string `json:"title"`
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { type Card, type MeTag } from '../stores/interfaces';
|
||||
import type { Card } from '../stores/interfaces';
|
||||
import projectTags from '../stores/projectTags';
|
||||
import ModalCard from './modal_card.svelte';
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts">
|
||||
import ModalTags from './modal_tags.svelte';
|
||||
import type { Card, MeTag } from '../stores/interfaces';
|
||||
import type { Card } from '../stores/interfaces';
|
||||
import api, { processError } from '../utils/api';
|
||||
import status from '../utils/status';
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import type { TagValue, MeTag } from '../stores/interfaces';
|
||||
import type { TagValue } from '../stores/interfaces';
|
||||
import projectTags from '../stores/projectTags';
|
||||
import api, { processError } from '../utils/api';
|
||||
import status from '../utils/status';
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import ModalTag from './modal_tag.svelte';
|
||||
import status from '../utils/status';
|
||||
import type { Card, MeTag } from '../stores/interfaces';
|
||||
import type { Card } from '../stores/interfaces';
|
||||
import api, { processError } from '../utils/api';
|
||||
|
||||
export let card: Card;
|
||||
|
|
|
@ -1,16 +1,12 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import CardC from './card.svelte';
|
||||
import {
|
||||
type Project,
|
||||
type Card,
|
||||
parseCards,
|
||||
type MeTag,
|
||||
parseMeTags
|
||||
} from '../stores/interfaces';
|
||||
import { type Project, type Card, parseCards, type View } from '../stores/interfaces';
|
||||
import status from '../utils/status';
|
||||
import api, { processError } from '../utils/api';
|
||||
import projectTags from '../stores/projectTags';
|
||||
import currentView from '../stores/currentView';
|
||||
import Card from './card.svelte';
|
||||
|
||||
export let projectId: number;
|
||||
|
||||
|
@ -80,6 +76,31 @@
|
|||
|
||||
cards = cards.filter((card) => card.id !== cardID);
|
||||
}
|
||||
|
||||
let view: View | null = null;
|
||||
let columns: { id: number; title: string; cards: Card[] }[] = [];
|
||||
|
||||
currentView.subscribe((v) => {
|
||||
view = v;
|
||||
if (!v) return;
|
||||
let primary_tag_id = v.primary_tag_id;
|
||||
columns = $projectTags[primary_tag_id].options.map((o) => {
|
||||
return {
|
||||
id: o.id,
|
||||
title: o.value,
|
||||
cards: cards.filter((c) => c.tags.map((t) => t.option_id).includes(o.id))
|
||||
};
|
||||
});
|
||||
columns.push({
|
||||
id: -1,
|
||||
title: 'No tag',
|
||||
cards: cards.filter((c) => {
|
||||
const tag = c.tags.find((t) => t.tag_id === primary_tag_id);
|
||||
|
||||
return tag?.option_id == -1;
|
||||
})
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
@ -94,16 +115,51 @@
|
|||
<h2>{project.title}</h2>
|
||||
<button on:click={newCard}>New card</button>
|
||||
</header>
|
||||
<ul>
|
||||
{#if cards}
|
||||
{#each cards as card}
|
||||
<CardC
|
||||
{card}
|
||||
showModal={modalID === card.id}
|
||||
onDelete={async () => await deleteCard(card.id)}
|
||||
/>
|
||||
{#if view}
|
||||
<div class="grid">
|
||||
{#each columns as column}
|
||||
<div class="column">
|
||||
<h3>{column.title}</h3>
|
||||
<ul>
|
||||
{#each column.cards as card}
|
||||
<CardC
|
||||
{card}
|
||||
showModal={modalID === card.id}
|
||||
onDelete={async () => await deleteCard(card.id)}
|
||||
/>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</ul>
|
||||
</div>
|
||||
{:else}
|
||||
<ul>
|
||||
{#if cards}
|
||||
{#each cards as card}
|
||||
<CardC
|
||||
{card}
|
||||
showModal={modalID === card.id}
|
||||
onDelete={async () => await deleteCard(card.id)}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
#project .grid {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#project .column {
|
||||
width: 200px;
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
#project .column h3 {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,73 +1,22 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import api, { processError } from '../utils/api';
|
||||
import type { Project } from '../stores/interfaces';
|
||||
import type { View } from '../stores/interfaces';
|
||||
import currentView from '../stores/currentView';
|
||||
|
||||
let newProject = false;
|
||||
let editProject: number | undefined = undefined;
|
||||
let projects: Project[];
|
||||
export let projectID: number;
|
||||
let views: View[];
|
||||
|
||||
onMount(async () => {
|
||||
const response = await api.get(`/v1/projects`);
|
||||
const response = await api.get(`/v1/projects/${projectID}/views`);
|
||||
|
||||
if (response.status !== 200) {
|
||||
processError(response, 'Failed to fetch projects');
|
||||
processError(response, 'Failed to fetch views');
|
||||
return;
|
||||
}
|
||||
|
||||
projects = response.data;
|
||||
views = response.data;
|
||||
});
|
||||
|
||||
async function createProject(project: Project) {
|
||||
const response = await api.post(`/v1/projects`, project);
|
||||
|
||||
if (response.status !== 201) {
|
||||
processError(response, 'Failed to create project');
|
||||
return;
|
||||
}
|
||||
|
||||
project.id = response.data.id;
|
||||
|
||||
projects = [...projects, project];
|
||||
}
|
||||
|
||||
async function updateProject(project: Project) {
|
||||
const response = await api.put(`/v1/projects/${project.id}`, project);
|
||||
|
||||
if (response.status !== 204) {
|
||||
processError(response, 'Failed to update project');
|
||||
return;
|
||||
}
|
||||
|
||||
projects = projects.map((p) => {
|
||||
if (p.id === project.id) {
|
||||
return project;
|
||||
}
|
||||
return p;
|
||||
});
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent, id: number | undefined = undefined) {
|
||||
if (event.key === 'Enter' && event.target) {
|
||||
if (id !== undefined) {
|
||||
updateProject({
|
||||
id: id,
|
||||
title: (event.target as HTMLInputElement).value
|
||||
});
|
||||
editProject = undefined;
|
||||
} else {
|
||||
createNewProject((event.target as HTMLInputElement).value);
|
||||
newProject = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createNewProject(value: string) {
|
||||
createProject({
|
||||
title: value,
|
||||
id: undefined
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
@ -84,38 +33,18 @@
|
|||
</div>
|
||||
<div class="boards">
|
||||
<h2>Projects</h2>
|
||||
{#if projects}
|
||||
{#if views}
|
||||
<ul>
|
||||
{#each projects as project}
|
||||
{#each views as view}
|
||||
<li>
|
||||
{#if editProject === project.id && editProject !== undefined}
|
||||
<input
|
||||
type="text"
|
||||
on:keydown={(e) => handleKeydown(e, project.id)}
|
||||
value={project.title}
|
||||
class="edit-input"
|
||||
/>
|
||||
{:else}
|
||||
<a href="/{project.id}">{project.title}</a>
|
||||
<img src="img/edit-icon.svg" alt="" on:click={() => (editProject = project.id)} />
|
||||
{/if}
|
||||
<span
|
||||
on:click={() => {
|
||||
currentView.set(view);
|
||||
}}>{view.title}</span
|
||||
>
|
||||
</li>
|
||||
{/each}
|
||||
{#if newProject}
|
||||
<li>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Enter project title"
|
||||
on:keydown={handleKeydown}
|
||||
style="padding: 8px; border: 1px solid #ccc; border-radius: 4px; width: 100%;"
|
||||
/>
|
||||
</li>
|
||||
{/if}
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="bottom-links">
|
||||
<span on:click={() => (newProject = true)}>New project</span>
|
||||
<span>Settings</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -3,13 +3,16 @@
|
|||
import Sidebar from '../../components/sidebar.svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { SvelteToast } from '@zerodevx/svelte-toast';
|
||||
import type { View } from '../../stores/interfaces';
|
||||
|
||||
let projectId: number = +$page.params.project;
|
||||
let projectID: number = +$page.params.project;
|
||||
|
||||
let currentView: View;
|
||||
</script>
|
||||
|
||||
<div id="projectPage">
|
||||
<Sidebar />
|
||||
<Project {projectId} />
|
||||
<Sidebar {projectID} />
|
||||
<Project projectId={projectID} />
|
||||
</div>
|
||||
<SvelteToast />
|
||||
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
import { writable } from "svelte/store";
|
||||
import type { View } from "./interfaces";
|
||||
|
||||
export default writable(null as View | null);
|
|
@ -34,6 +34,14 @@ export interface TagOption {
|
|||
value: string;
|
||||
}
|
||||
|
||||
export interface View {
|
||||
id: number;
|
||||
project_id: number;
|
||||
primary_tag_id: number;
|
||||
secondary_tag_id: number;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export function parseCard (c: any) {
|
||||
let card: Card = c;
|
||||
if (card.tags == null) card.tags = [];
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
border-radius: 6px;
|
||||
border: 1px solid #555;
|
||||
margin: 10px;
|
||||
width: 200px;
|
||||
/* width: 200px; */
|
||||
font-family: "Open Sans", sans-serif;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue