Replace -1 by nullable values

This commit is contained in:
Brieuc Dubois 2024-01-04 02:08:48 +01:00
parent e2e87ce9ec
commit 98e54f94a6
17 changed files with 145 additions and 165 deletions

View File

@ -6,6 +6,8 @@ import (
var db *sql.DB
const DB_VERSION = 1
func InitDB(driver string, connStr string) error {
var err error
db, err = sql.Open(driver, connStr)
@ -14,16 +16,13 @@ func InitDB(driver string, connStr string) error {
}
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS schema_version (
version INTEGER PRIMARY KEY,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS projects (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT
);
CREATE TABLE IF NOT EXISTS lists (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id INTEGER,
title TEXT,
color TEXT,
FOREIGN KEY(project_id) REFERENCES projects(id)
);
CREATE TABLE IF NOT EXISTS cards (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@ -61,13 +60,47 @@ func InitDB(driver string, connStr string) error {
primary_tag_id INTEGER,
secondary_tag_id INTEGER,
title TEXT,
sort_tag_id INTEGER,
sort_direction INTEGER,
FOREIGN KEY(project_id) REFERENCES projects(id),
FOREIGN KEY(primary_tag_id) REFERENCES tags(id),
FOREIGN KEY(secondary_tag_id) REFERENCES tags(id)
);
`)
INSERT INTO schema_version (version)
SELECT ? WHERE NOT EXISTS (SELECT 1 FROM schema_version);
`, DB_VERSION)
if err != nil {
return err
}
return updateDB()
}
func updateDB() error {
var currentVersion int
err := db.QueryRow("SELECT MAX(version) FROM schema_version").Scan(&currentVersion)
if err != nil {
return err
}
if currentVersion < 2 {
_, err := db.Exec(`
ALTER TABLE views
ADD COLUMN sort_tag_id INTEGER;
ALTER TABLE views
ADD COLUMN sort_direction INTEGER;
INSERT INTO schema_version (version)
SELECT ? WHERE NOT EXISTS (SELECT 1 FROM schema_version WHERE version = ?);
`, 2, 2)
if err != nil {
return err
}
}
return nil
}

View File

@ -3,7 +3,7 @@ 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)
res, err := db.Exec("INSERT INTO views (project_id, primary_tag_id, secondary_tag_id, title, sort_tag_id, sort_direction) VALUES (?, ?, ?, ?, ?, ?)", v.ProjectID, v.PrimaryTagID, v.SecondaryTagID, v.Title, v.SortTagID, v.SortDirection)
if err != nil {
return 0, err
}
@ -26,7 +26,7 @@ func GetProjectViews(projectID int) ([]types.View, error) {
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 {
if err := rows.Scan(&v.ID, &v.ProjectID, &v.PrimaryTagID, &v.SecondaryTagID, &v.Title, &v.SortTagID, &v.SortDirection); err != nil {
return nil, err
}
@ -52,13 +52,13 @@ func GetView(id int) (*types.View, error) {
}
var v types.View
rows.Scan(&v.ID, &v.ProjectID, v.PrimaryTagID, v.SecondaryTagID, v.Title)
rows.Scan(&v.ID, &v.ProjectID, v.PrimaryTagID, v.SecondaryTagID, v.Title, v.SortTagID, v.SortDirection)
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)
res, err := db.Exec("UPDATE views SET project_id = ?, primary_tag_id = ?, secondary_tag_id = ?, title = ?, sort_tag_id = ?, sort_direction = ? WHERE id = ?", v.ProjectID, v.PrimaryTagID, v.SecondaryTagID, v.Title, v.SortTagID, v.SortDirection, v.ID)
if err != nil {
return 0, err
}

View File

@ -1,17 +1,17 @@
package types
type CardTag struct {
CardID int `json:"card_id"`
TagID int `json:"tag_id"`
OptionID int `json:"option_id"`
Value string `json:"value"`
CardID int `json:"card_id"`
TagID int `json:"tag_id"`
OptionID *int `json:"option_id"`
Value *string `json:"value"`
}
type FullCardTag struct {
CardID int `json:"card_id"`
TagID int `json:"tag_id"`
TagTitle string `json:"tag_title"`
TagType int `json:"tag_type"`
OptionID int `json:"option_id"`
Value string `json:"value"`
CardID int `json:"card_id"`
TagID int `json:"tag_id"`
TagTitle string `json:"tag_title"`
TagType int `json:"tag_type"`
OptionID *int `json:"option_id"`
Value *string `json:"value"`
}

View File

@ -1,8 +0,0 @@
package types
type List struct {
ID int `json:"id"`
ProjectID int `json:"project_id"`
Title string `json:"title"`
Color string `json:"color"`
}

View File

@ -1,9 +1,11 @@
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"`
ID int `json:"id"`
ProjectID int `json:"project_id"`
Title string `json:"title"`
PrimaryTagID *int `json:"primary_tag_id"`
SecondaryTagID *int `json:"secondary_tag_id"`
SortTagID *int `json:"sort_tag_id"`
SortDirection *string `json:"sort_direction"`
}

View File

@ -15,7 +15,6 @@
"devDependencies": {
"@playwright/test": "^1.28.1",
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/adapter-static": "^3.0.1",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@types/eslint": "8.56.0",
@ -836,15 +835,6 @@
"@sveltejs/kit": "^2.0.0"
}
},
"node_modules/@sveltejs/adapter-static": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.1.tgz",
"integrity": "sha512-6lMvf7xYEJ+oGeR5L8DFJJrowkefTK6ZgA4JiMqoClMkKq0s6yvsd3FZfCFvX1fQ0tpCD7fkuRVHsnUVgsHyNg==",
"dev": true,
"peerDependencies": {
"@sveltejs/kit": "^2.0.0"
}
},
"node_modules/@sveltejs/kit": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.0.6.tgz",

View File

@ -60,8 +60,8 @@ export async function deleteCardApi(cardID: number): Promise<void> {
export async function createCardTagApi(
cardId: number,
tagId: number,
optionId: number,
value: string
optionId: number | null,
value: string | null
): Promise<boolean> {
const response = await api.post(`/v1/cards/${cardId}/tags/${tagId}`, {
option_id: optionId,
@ -76,15 +76,24 @@ export async function createCardTagApi(
return true;
}
export async function deleteCardTagApi(cardID: number, tagID: number): Promise<void> {
const response = await api.delete(`/v1/cards/${cardID}/tags/${tagID}`);
if (response.status !== status.NoContent) {
processError(response, 'Failed to delete tag');
return Promise.reject();
}
}
export async function updateCardTagApi(
cardID: number,
tagID: number,
option_id: number,
value: string
option_id: number | null,
value: string | null
): Promise<void> {
const response = await api.put(`/v1/cards/${cardID}/tags/${tagID}`, {
option_id: option_id,
value: value
option_id: option_id == -1 ? null : option_id,
value: value == '' ? null : value
});
if (response.status !== status.NoContent) {

View File

@ -24,7 +24,7 @@
{#if card.tags}
<div class="tags">
{#each card.tags as tag}
{#if tag.option_id && tag.option_id !== -1}
{#if tag.option_id}
{#if $projectTags[tag.tag_id]}
<span class="tag" style="border: 1px solid #333"
>{$projectTags[tag.tag_id]?.options.find((o) => o.id == tag.option_id)?.value}</span

View File

@ -24,7 +24,7 @@
card.content = newContent;
}
}
if (closeModal) currentModalCard.set(-1);
if (closeModal) currentModalCard.set(null);
}
</script>
@ -39,7 +39,7 @@
<button on:click={() => cards.remove(card)}>
<TrashIcon />
</button>
<button on:click={() => currentModalCard.set(-1)}>
<button on:click={() => currentModalCard.set(null)}>
<CloseIcon />
</button>
</div>

View File

@ -1,4 +1,5 @@
<script lang="ts">
import { updateCardTagApi } from '../../../../api/cards';
import type { Card, MeTag, TagOption, TagValue } from '../../../../stores/interfaces';
import projectTags from '../../../../stores/projectTags';
import { cards } from '../../../../stores/smallStore';
@ -7,7 +8,7 @@
import TrashIcon from '../../../icons/trashIcon.svelte';
import Menu from '../../../tuils/menu.svelte';
export let multiple: boolean = false;
export const multiple: boolean = false;
export let card: Card;
export let projectTag: MeTag;
export let tagValue: TagValue | undefined;
@ -19,21 +20,14 @@
let isOpen = false;
async function selectOption(option_id: number) {
async function selectOption(option_id: number | null) {
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;
}
if (tagValue) {
await updateCardTagApi(card.id, projectTag.id, option_id, tagValue.value);
card.tags = card.tags.map((t) => {
if (t.tag_id === projectTag.id) {
@ -70,7 +64,7 @@
cards.reload();
}
async function deleteOption(_: number | undefined) {
async function deleteOption() {
const response = await api.delete(`/v1/cards/${card.id}/tags/${projectTag.id}`);
if (response.status !== status.NoContent) {
@ -107,7 +101,7 @@
{#if tagValue}
<span class="tag">
{tagOption?.value}
<button class="real" on:click={() => deleteOption(tagValue?.option_id)}>✗</button>
<button class="real" on:click={() => deleteOption()}>✗</button>
</span>
{/if}
</div>

View File

@ -1,79 +1,51 @@
<script lang="ts">
import type { TagOption, Card, MeTag, TagValue } from '../../stores/interfaces';
import type { Card, TagValue } from '../../stores/interfaces';
import { cards, currentDraggedCard } from '../../stores/smallStore';
import api, { processError } from '../../utils/api';
import status from '../../utils/status';
import CardC from './card/card.svelte';
import AddIcon from '../icons/addIcon.svelte';
import projectTags from '../../stores/projectTags';
import { updateTagAPI as updateTagOptionAPI } from '../../api/tags';
import { get } from 'svelte/store';
import { createCardTagApi, deleteCardTagApi, updateCardTagApi } from '../../api/cards';
export let projectId: number;
export let editable: boolean = true;
export let option: TagOption;
export let optionId: number | null = null;
export let primary_tag_id: number | null = null;
export let title: string;
export let columnCards: Card[] = [];
let lastOptionValue = option.value;
let lastTitle = title;
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 (!$currentDraggedCard || !$currentDraggedCard.tags) return;
for (let tag of $currentDraggedCard.tags) {
if (tag.tag_id !== primary_tag_id) continue;
if (tag.option_id == optionId) return;
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;
}
}
try {
if (tag.option_id && optionId) await deleteCardTagApi(tag.card_id, tag.tag_id);
else if (tag.option_id && optionId)
await createCardTagApi(tag.card_id, tag.tag_id, tag.option_id, tag.value);
else await updateCardTagApi(tag.card_id, tag.tag_id, optionId, tag.value);
tag.option_id = option.id;
cards.reload();
} catch (e) {}
break;
}
}
currentDraggedCard.set(null);
tag.option_id = optionId;
cards.reload();
} catch (e) {}
break;
}
currentDraggedCard.set(null);
}
async function addCard() {
const tags: TagValue[] = [];
for (let tag of Object.values(get(projectTags))) {
if (tag.id === option.tag_id) {
if (tag.id === primary_tag_id) {
tags.push({
card_id: -1,
tag_id: tag.id,
option_id: option.id,
value: ''
option_id: optionId,
value: null
});
}
}
@ -93,15 +65,20 @@
>
<header>
<input
bind:value={option.value}
bind:value={title}
type="text"
on:blur={async () => {
if (lastOptionValue === option.value) return;
await updateTagOptionAPI(option);
lastOptionValue = option.value;
if (lastTitle === title) return;
if (!optionId || !primary_tag_id) return;
await updateTagOptionAPI({
id: optionId,
tag_id: primary_tag_id,
value: title
});
lastTitle = title;
cards.reload();
}}
disabled={!editable}
disabled={optionId === null}
/>
<span>
<span>{columnCards.length}</span>

View File

@ -4,7 +4,7 @@
export let isOpen = false;
export let choices: { id: number; value: string }[] = [];
export let onChoice = (id: number) => {};
export let currentChoice: number;
export let currentChoice: number | null;
</script>
<Menu {isOpen}>

View File

@ -1,5 +1,4 @@
<script lang="ts">
import { get } from 'svelte/store';
import type { Project, TagValue, View } from '../../stores/interfaces';
import { cards, currentView, views } from '../../stores/smallStore';
import projectTags from '../../stores/projectTags';
@ -9,19 +8,6 @@
export let view: View;
let groupMenuOpen = false;
function getEmptyTags(): TagValue[] {
const tags: TagValue[] = [];
for (let tag of Object.values(get(projectTags))) {
tags.push({
card_id: -1,
tag_id: tag.id,
option_id: -1,
value: ''
});
}
return tags;
}
async function setGroup(id: number): Promise<boolean> {
if ($currentView == null) return false;
@ -44,7 +30,7 @@
<div>
<button
on:click={() => (groupMenuOpen = !groupMenuOpen)}
class:defined={$currentView?.primary_tag_id !== -1}>Group</button
class:defined={$currentView?.primary_tag_id}>Group</button
>
<GroupMenu
isOpen={groupMenuOpen}
@ -59,7 +45,7 @@
<button class:disabled={true}>Sub-group</button>
<button class:disabled={true}>Filter</button>
<button class:disabled={true}>Sort</button>
<button id="newButton" on:click={async () => cards.add(project.id, getEmptyTags())}>New</button>
<button id="newButton" on:click={async () => cards.add(project.id, [])}>New</button>
</nav>
</header>

View File

@ -29,10 +29,12 @@
<Header {project} {view} />
{#if cards}
<div class="grid">
{#if view.primary_tag_id !== -1}
{#if view.primary_tag_id}
{#each $projectTags[view.primary_tag_id].options as option}
<Column
{option}
optionId={option.id}
primary_tag_id={view.primary_tag_id}
title={option.value}
columnCards={$cards.filter((c) =>
c.tags.map((t) => t.option_id).includes(option.id)
)}
@ -41,21 +43,16 @@
{/each}
{/if}
<Column
option={{
id: -1,
tag_id: view.primary_tag_id,
value:
view.primary_tag_id !== -1
? `No ${$projectTags[view.primary_tag_id].title}`
: 'No groups'
}}
columnCards={view.primary_tag_id !== -1
primary_tag_id={view.primary_tag_id}
title={view.primary_tag_id
? `No ${$projectTags[view.primary_tag_id].title}`
: 'No groups'}
columnCards={view.primary_tag_id
? $cards.filter(
(c) => !c.tags.map((t) => t.tag_id).includes(view?.primary_tag_id || -2)
)
: $cards}
projectId={project.id}
editable={false}
/>
</div>
{/if}

View File

@ -26,7 +26,7 @@
if (!$views) return;
const primaryTagId =
$currentView?.primary_tag_id || Object.values($projectTags).find((t) => true)?.id || -1;
$currentView?.primary_tag_id || Object.values($projectTags).find((t) => true)?.id || null;
const newView = await views.add(project.id, 'New view', primaryTagId);
@ -233,10 +233,6 @@
}
}
span {
padding-left: 10px;
}
button {
background-color: transparent;
border: none;

View File

@ -14,8 +14,8 @@ export interface Card {
export interface TagValue {
card_id: number;
tag_id: number;
option_id: number;
value: string;
option_id: number | null;
value: string | null;
}
export interface MeTag {
@ -35,9 +35,11 @@ export interface TagOption {
export interface View {
id: number;
project_id: number;
primary_tag_id: number;
secondary_tag_id: number;
primary_tag_id: number | null;
secondary_tag_id: number | null;
title: string;
sort_tag_id: number | null;
sort_direction: number | null;
}
export function parseCard(c: any) {

View File

@ -26,7 +26,7 @@ export const currentView = (() => {
};
})();
export const currentModalCard = writable(-1);
export const currentModalCard = writable(null as number | null);
export const currentDraggedCard = writable(null as Card | null);
@ -49,7 +49,7 @@ export const cards = (() => {
remove: async (card: Card) => {
await deleteCardApi(card.id).then(() => {
update((cards) => cards.filter((c) => c.id !== card.id));
currentModalCard.set(-1);
currentModalCard.set(null);
});
},
edit: async (card: Card): Promise<boolean> => {
@ -82,7 +82,7 @@ export const views = (() => {
return true;
};
const add = async (projectId: number, title: string, primaryTagId: number): Promise<View> => {
const add = async (projectId: number, title: string, primaryTagId: number | null): Promise<View> => {
const response = await api.post(`/v1/views`, {
title,
project_id: projectId,
@ -99,7 +99,9 @@ export const views = (() => {
title: title,
project_id: projectId,
primary_tag_id: primaryTagId,
secondary_tag_id: 0
secondary_tag_id: null,
sort_tag_id: null,
sort_direction: null
};
update((views) => [...views, view]);