Projects page

This commit is contained in:
Brieuc Dubois 2024-01-06 23:21:56 +01:00
parent 47de3a4bbc
commit 720fb35326
9 changed files with 107 additions and 342 deletions

View File

@ -1,10 +1,34 @@
import type Card from '$lib/types/Card';
import type Project from '$lib/types/Project';
import Project from '$lib/types/Project';
import ProjectTag from '$lib/types/ProjectTag';
import api, { processError } from '$lib/utils/api';
import { parseCards } from '$lib/utils/parser';
import status from '$lib/utils/status';
async function getAll(): Promise<Project[]> {
const response = await api.get('/v1/projects');
if (response.status !== status.OK) {
processError(response, 'Failed to fetch projects');
return [];
}
return Project.parseAll(response.data);
}
async function create(title: string): Promise<number | null> {
const response = await api.post('/v1/projects', {
title
});
if (response.status !== status.Created) {
processError(response, 'Failed to create project');
return null;
}
return response.data.id;
}
async function get(projectId: number): Promise<number | null> {
const response = await api.get(`/v1/projects/${projectId}`);
@ -16,6 +40,30 @@ async function get(projectId: number): Promise<number | null> {
return response.data;
}
async function update(projectId: number, title: string): Promise<boolean> {
const response = await api.put(`/v1/projects/${projectId}`, {
title
});
if (response.status !== status.NoContent) {
processError(response, 'Failed to update project');
return false;
}
return true;
}
async function delete_(projectId: number): Promise<boolean> {
const response = await api.delete(`/v1/projects/${projectId}`);
if (response.status !== status.NoContent) {
processError(response, 'Failed to delete project');
return false;
}
return true;
}
async function getCards(projectId: number): Promise<Card[]> {
const response = await api.get(`/v1/projects/${projectId}/cards`);
@ -39,3 +87,13 @@ async function getTags(project: Project): Promise<ProjectTag[]> {
return projectTags;
}
export default {
create,
get,
update,
delete: delete_,
getAll,
getCards,
getTags
};

View File

@ -4,11 +4,10 @@
import type Card from '$lib/types/Card';
import type TagValue from '$lib/types/TagValue';
import { get } from 'svelte/store';
import { createCardTagApi, deleteCardTagApi, updateCardTagApi } from '../../api/cards';
import { updateTagAPI as updateTagOptionAPI } from '../../api/tags';
import projectTags from '../../stores/projectTags';
import CardComponent from '../card/Card.svelte';
import AddIcon from '../icons/AddIcon.svelte';
import { import } from { createCardTagApi, deleteCardTagApi, updateCardTagApi };
export let projectId: number;
export let optionId: number | null = null;

View File

@ -1,10 +1,7 @@
<script lang="ts">
import type Project from '$lib/types/Project';
import api, { processError } from '$lib/utils/api';
import status from '$lib/utils/status';
export let project: Project;
export let deleteProject: (project: Project) => void;
let edit = false;
let newTitle = project.title;
@ -19,14 +16,7 @@
return;
}
const response = await api.put(`/v1/projects/${project.id}`, { title: newTitle });
if (response.status !== status.NoContent) {
processError(response, 'Failed to update project');
return;
}
project.title = newTitle;
if (!(await project.setTitle(newTitle))) return;
edit = false;
}
@ -83,12 +73,12 @@
src="/img/delete-icon.svg"
alt="Delete"
class="button"
on:click={() => deleteProject(project)}
on:click={() => project.delete()}
role="button"
tabindex="0"
on:keydown={(e) => {
if (e.key === 'Enter') {
deleteProject(project);
project.delete();
}
}}
/>

View File

@ -1,6 +1,4 @@
<script lang="ts">
import { updateCardTagApi } from '$lib/api/cards';
import TrashIcon from '$lib/components/icons/TrashIcon.svelte';
import Menu from '$lib/components/menu/Menu.svelte';
import cards from '$lib/stores/cards';
import project_tags from '$lib/stores/projectTags';
@ -9,6 +7,7 @@
import type TagValue from '$lib/types/TagValue';
import api, { processError } from '$lib/utils/api';
import status from '$lib/utils/status';
import { updatimport } from { updateCardTagApi };
export const multiple: boolean = false;
export let card: Card;

View File

@ -1,53 +0,0 @@
import { deleteCardApi, newCardApi, updateCardApi } from '$lib/api/cards';
import { getProjectCardsAPI } from '$lib/api/projects';
import type Card from '$lib/types/Card';
import type TagValue from '$lib/types/TagValue';
import { writable } from 'svelte/store';
import { parseCards } from '../utils/parser';
import currentModalCard from './currentModalCard';
const { subscribe, set, update } = writable([] as Card[]);
async function init(projectId: number) {
getProjectCardsAPI(projectId).then((c) => {
set(parseCards(c));
});
}
async function add(projectId: number, tags: TagValue[]) {
await newCardApi(projectId, tags).then((card) => {
update((cards) => [...cards, card]);
currentModalCard.set(card.id);
});
}
async function remove(card: Card) {
await deleteCardApi(card.id).then(() => {
update((cards) => cards.filter((c) => c.id !== card.id));
currentModalCard.set(null);
});
}
async function edit(card: Card): Promise<boolean> {
if (await updateCardApi(card)) {
update((cards) => cards.map((c) => (c.id === card.id ? card : c)));
return true;
}
return false;
}
/**
* @deprecated The method should not be used. It is only used to force a reload of the cards.
*/
function reload() {
update((cards) => cards);
}
export default {
subscribe,
init,
add,
remove,
edit,
reload
};

View File

@ -1,126 +0,0 @@
import type MeTag from '$lib/types/MeTag';
import type TagOption from '$lib/types/TagOption';
import api, { processError } from '$lib/utils/api';
import status from '$lib/utils/status';
import { get, writable } from 'svelte/store';
import cards from './cards';
const { subscribe, set, update } = writable({} as { [key: number]: MeTag });
export default {
subscribe,
init: async (projectID: number): Promise<boolean> => {
const response = await api.get(`/v1/projects/${projectID}/tags`);
if (response.status !== 200) {
processError(response);
return false;
}
const metags: MeTag[] = response.data;
const tags: { [key: number]: MeTag } = {};
if (metags) {
metags.forEach((tag: MeTag) => {
if (tag.options === null) tag.options = [];
tags[tag.id] = tag;
});
}
set(tags);
return true;
},
addOption: async (tag_id: number, value: string) => {
const response = await api.post(`/v1/tags/${tag_id}/options`, {
value
});
if (response.status !== status.Created) {
processError(response, 'Failed to create tag option');
return;
}
const option: TagOption = {
id: response.data.id,
tag_id,
value
};
update((tags) => {
tags[tag_id].options.push(option);
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) => {
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;
});
}
};

View File

@ -1,88 +0,0 @@
import type View from '$lib/types/View';
import api, { processError } from '$lib/utils/api';
import status from '$lib/utils/status';
import { writable } from 'svelte/store';
const { subscribe, set, update } = writable([] as View[]);
async function init(projectId: number): Promise<boolean> {
const response = await api.get(`/v1/projects/${projectId}/views`);
if (response.status !== status.OK) {
processError(response, 'Failed to get views');
return false;
}
if (response.data) {
set(response.data);
}
return true;
}
async function add(projectId: number, title: string, primaryTagId: number | null): Promise<View> {
const response = await api.post(`/v1/views`, {
title,
project_id: projectId,
primary_tag_id: primaryTagId
});
if (response.status !== status.Created) {
processError(response, 'Failed to add view');
return Promise.reject();
}
const view: View = {
id: response.data.id,
title: title,
project_id: projectId,
primary_tag_id: primaryTagId,
secondary_tag_id: null,
sort_tag_id: null,
sort_direction: null
};
update((views) => [...views, view]);
return view;
}
async function remove(view: View): Promise<boolean> {
const response = await api.delete(`/v1/views/${view.id}`);
if (response.status !== status.NoContent) {
processError(response, 'Failed to delete view');
return false;
}
update((views) => views.filter((v) => v.id !== view.id));
return true;
}
async function edit(view: View): Promise<boolean> {
if (view.title === '') {
if (confirm('Are you sure you want to delete this view?')) {
return remove(view);
} else {
return false;
}
}
const response = await api.put(`/v1/views/${view.id}`, view);
if (response.status !== status.NoContent) {
processError(response, 'Failed to update view');
return false;
}
update((views) => views.map((v) => (v.id === view.id ? view : v)));
return true;
}
export default {
subscribe,
init,
add,
remove,
edit
};

View File

@ -1,6 +1,7 @@
import projectsApi from '$lib/api/projectsApi';
import { get, writable } from 'svelte/store';
const projects = writable([] as Project[]);
export const projects = writable([] as Project[]);
export default class Project {
private _id: number;
@ -19,10 +20,6 @@ export default class Project {
return this._title;
}
static getAll(): Project[] {
return get(projects);
}
static fromId(id: number): Project | null {
for (const project of get(projects)) {
if (project.id === id) {
@ -33,10 +30,42 @@ export default class Project {
return null;
}
static async create(): Promise<Project | null> {
const id = await projectsApi.create('untitled');
if (!id) return null;
const project = new Project(id, 'untitled');
projects.update((projects) => [...projects, project]);
return project;
}
async setTitle(title: string): Promise<boolean> {
if (!(await projectsApi.update(this._id, title))) return false;
this._title = title;
return true;
}
async delete(): Promise<boolean> {
if (!(await projectsApi.delete(this._id))) return false;
projects.update((projects) => projects.filter((project) => project.id !== this._id));
return true;
}
static parse(json: any): Project | null {
if (!json) return null;
return new Project(json.id, json.title);
const project = new Project(json.id, json.title);
projects.update((projects) => [...projects, project]);
return project;
}
static parseAll(json: any): Project[] {

View File

@ -1,64 +1,21 @@
<script lang="ts">
import type Project from '$lib/types/Project';
import { SvelteToast } from '@zerodevx/svelte-toast';
import { onMount } from 'svelte';
import SelectProject from '../lib/components/projects/SelectProject.svelte';
import api, { processError } from '../lib/utils/api';
import { toastAlert } from '../lib/utils/toasts';
let projects: Project[];
import Project, { projects } from '$lib/types/Project';
import projectsApi from '$lib/api/projectsApi';
onMount(async () => {
try {
const response = await api.get(`/v1/projects`);
if (response.status !== 200) {
processError(response, 'Failed to fetch projects');
return;
}
projects = response.data || [];
} catch (e: any) {
toastAlert('Failed to fetch projects', e);
// setTimeout(() => {
// window.location.reload();
// }, 11000);
}
await projectsApi.getAll();
});
async function deleteProject(project: Project) {
if (!confirm(`Are you sure you want to delete ${project.title}?`)) {
return;
}
const response = await api.delete(`/v1/projects/${project.id}`);
if (response.status !== 204) {
processError(response, 'Failed to delete project');
return;
}
projects = projects.filter((p) => p.id !== project.id);
}
async function createProject() {
const response = await api.post(`/v1/projects`, {
title: 'New Project'
});
if (response.status !== 201) {
processError(response, 'Failed to create project');
return;
}
projects = [...projects, response.data];
}
</script>
<section>
<h2>Projects</h2>
<ul>
{#if projects}
{#each projects as project}
<SelectProject {project} {deleteProject} />
{#if $projects}
{#each $projects as project}
<SelectProject {project} />
{/each}
{/if}
</ul>
@ -66,10 +23,10 @@
id="add"
tabindex="0"
role="button"
on:click={createProject}
on:click={Project.create}
on:keydown={(e) => {
if (e.key === 'Enter') {
createProject();
Project.create();
}
}}
>