Sidebar fixed

This commit is contained in:
Brieuc Dubois 2024-01-07 01:47:02 +01:00
parent 720fb35326
commit 5e272a9bcc
8 changed files with 174 additions and 83 deletions

View File

@ -1,6 +1,7 @@
import type Card from '$lib/types/Card'; import type Card from '$lib/types/Card';
import Project from '$lib/types/Project'; import Project from '$lib/types/Project';
import ProjectTag from '$lib/types/ProjectTag'; import ProjectTag from '$lib/types/ProjectTag';
import View from '$lib/types/View';
import api, { processError } from '$lib/utils/api'; import api, { processError } from '$lib/utils/api';
import { parseCards } from '$lib/utils/parser'; import { parseCards } from '$lib/utils/parser';
import status from '$lib/utils/status'; import status from '$lib/utils/status';
@ -29,7 +30,7 @@ async function create(title: string): Promise<number | null> {
return response.data.id; return response.data.id;
} }
async function get(projectId: number): Promise<number | null> { async function get(projectId: number): Promise<Project | null> {
const response = await api.get(`/v1/projects/${projectId}`); const response = await api.get(`/v1/projects/${projectId}`);
if (response.status !== status.OK) { if (response.status !== status.OK) {
@ -37,7 +38,7 @@ async function get(projectId: number): Promise<number | null> {
return null; return null;
} }
return response.data; return Project.parse(response.data);
} }
async function update(projectId: number, title: string): Promise<boolean> { async function update(projectId: number, title: string): Promise<boolean> {
@ -83,9 +84,20 @@ async function getTags(project: Project): Promise<ProjectTag[]> {
return []; return [];
} }
const projectTags: ProjectTag[] = ProjectTag.parseAll(response.data, project); return ProjectTag.parseAll(response.data, project);
}
return projectTags; async function getViews(project: Project): Promise<View[]> {
const response = await api.get(`/v1/projects/${project.id}/views`);
if (response.status !== status.OK) {
processError(response, 'Failed to fetch views');
return [];
}
const views: View[] = View.parseAll(response.data, project);
return views;
} }
export default { export default {
@ -95,5 +107,6 @@ export default {
delete: delete_, delete: delete_,
getAll, getAll,
getCards, getCards,
getTags getTags,
getViews
}; };

View File

@ -4,7 +4,7 @@ import api, { processError } from '$lib/utils/api';
import status from '$lib/utils/status'; import status from '$lib/utils/status';
async function create(project: Project): Promise<number | null> { async function create(project: Project): Promise<number | null> {
const response = await api.post('/views', { const response = await api.post('/v1/views', {
project: project.id project: project.id
}); });
@ -16,17 +16,25 @@ async function create(project: Project): Promise<number | null> {
return response.data.id; return response.data.id;
} }
async function update(view: View): Promise<boolean> { async function update(
const response = await api.put(`/views/${view.id}`, { id: number,
project: view.project.id, projectId: number,
primary_tag_id: view.primaryTag?.id, primaryTagId: number | null,
secondary_tag_id: view.secondaryTag?.id, secondaryTagId: number | null,
title: view.title, title: string,
sort_tag_id: view.sortTag?.id, sortTagId: number | null,
sort_direction: view.sortDirection sortDirection: number | null
): Promise<boolean> {
const response = await api.put(`/v1/views/${id}`, {
project_id: projectId,
primary_tag_id: primaryTagId,
secondary_tag_id: secondaryTagId,
title: title,
sort_tag_id: sortTagId,
sort_direction: sortDirection
}); });
if (response.status !== status.OK) { if (response.status !== status.NoContent) {
processError(response, 'Failed to update view'); processError(response, 'Failed to update view');
return false; return false;
} }
@ -35,7 +43,7 @@ async function update(view: View): Promise<boolean> {
} }
async function delete_(viewId: number): Promise<boolean> { async function delete_(viewId: number): Promise<boolean> {
const response = await api.delete(`/views/${viewId}`); const response = await api.delete(`/v1/views/${viewId}`);
if (response.status !== status.OK) { if (response.status !== status.OK) {
processError(response, 'Failed to delete view'); processError(response, 'Failed to delete view');

View File

@ -1,52 +1,44 @@
<script lang="ts"> <script lang="ts">
import currentView from '$lib/stores/currentView'; import currentView from '$lib/stores/currentView';
import views from '$lib/stores/views'; import Project from '$lib/types/Project';
import type Project from '$lib/types/Project'; import View, { views } from '$lib/types/View';
import type View from '$lib/types/View';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
import projectTags from '../../stores/projectTags';
import EditIcon from '../icons/EditIcon.svelte'; import EditIcon from '../icons/EditIcon.svelte';
import MenuOpener from '../icons/MenuOpener.svelte'; import MenuOpener from '../icons/MenuOpener.svelte';
import ViewIcon from '../icons/ViewIcon.svelte'; import ViewIcon from '../icons/ViewIcon.svelte';
export let project: Project; export let project: Project;
let viewEditId: number; let viewEdit: View | null;
let viewEditValue: string; let newTitle: string;
let isVisible = false; let isVisible = false;
onMount(async () => { onMount(() => {
await views.init(project.id); if (views && $views.length > 0) currentView.set($views[0]);
if ($views && $views.length > 0) currentView.set($views[0]);
}); });
async function createView() { async function createView() {
if (!$views) return; if (!$views) return;
const primaryTagId = const newView = await View.create(project);
$currentView?.primary_tag_id || Object.values($projectTags).find((t) => true)?.id || null;
const newView = await views.add(project.id, 'New view', primaryTagId);
if (!newView) return; if (!newView) return;
currentView.set(newView); currentView.set(newView);
viewEditId = newView.id; viewEdit = newView;
viewEditValue = newView.title;
document.getElementById(`viewTitle-${newView.id}`)?.focus(); document.getElementById(`viewTitle-${newView.id}`)?.focus();
} }
async function saveView(view: View) { async function saveView() {
if (!view || !$views.includes(view)) return; if (!viewEdit) return;
if (viewEditId === view.id && viewEditValue !== view.title) { if (!newTitle) return;
if (!(await views.edit(view))) return; if (newTitle != viewEdit.title) {
await viewEdit.setTitle(newTitle);
} }
viewEditId = -1; viewEdit = null;
viewEditValue = '';
} }
</script> </script>
@ -60,7 +52,7 @@
<h2>{project.title}</h2> <h2>{project.title}</h2>
{#if views} {#if views}
<ul> <ul>
{#each get(views) as view} {#each $views as view}
<!-- svelte-ignore a11y-no-noninteractive-element-to-interactive-role --> <!-- svelte-ignore a11y-no-noninteractive-element-to-interactive-role -->
<li <li
on:click={() => currentView.set(view)} on:click={() => currentView.set(view)}
@ -74,26 +66,29 @@
class:active={$currentView === view} class:active={$currentView === view}
> >
<ViewIcon /> <ViewIcon />
<input {#if viewEdit && viewEdit === view}
type="text" <input
readonly={viewEditId !== view.id} type="text"
bind:value={view.title} bind:value={newTitle}
class:inEdit={viewEditId === view.id} on:blur={saveView}
on:blur={() => saveView(view)} id="viewTitle-{view.id}"
id="viewTitle-{view.id}" class="inEdit"
on:keydown={(e) => { on:keydown={(e) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
e.currentTarget.blur(); e.currentTarget.blur();
} }
}} }}
/> />
{:else}
<span class="title">{view.title}</span>
{/if}
<button <button
on:click={() => { on:click={() => {
if (viewEditId === view.id) { if (viewEdit && viewEdit.id === view.id) {
saveView(view); saveView();
} else { } else {
viewEditId = view.id; viewEdit = view;
viewEditValue = view.title; newTitle = view.title;
document.getElementById(`viewTitle-${view.id}`)?.focus(); document.getElementById(`viewTitle-${view.id}`)?.focus();
} }
}} }}
@ -213,9 +208,16 @@
li { li {
cursor: pointer; cursor: pointer;
padding: 0 10px; padding: 0 10px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 5px;
} }
span.title,
input { input {
display: inline-block;
cursor: pointer; cursor: pointer;
padding: 10px; padding: 10px;
border-radius: 5px; border-radius: 5px;
@ -239,8 +241,6 @@
border: none; border: none;
color: inherit; color: inherit;
font-size: 17px; font-size: 17px;
padding: 5px 0;
float: right;
cursor: pointer; cursor: pointer;
} }

View File

@ -90,7 +90,13 @@ export default class Card {
card._tags = CardTag.parseAll(json.tags, card); card._tags = CardTag.parseAll(json.tags, card);
cards.update((cards) => [...cards, card]); cards.update((cards) => {
if (!cards.find((c) => c.id === card.id)) {
return [...cards, card];
}
return cards.map((c) => (c.id === card.id ? card : c));
});
return card; return card;
} }

View File

@ -63,7 +63,11 @@ export default class Project {
const project = new Project(json.id, json.title); const project = new Project(json.id, json.title);
projects.update((projects) => [...projects, project]); projects.update((projects) => {
if (!projects.find((p) => p.id === project.id)) return [...projects, project];
return projects.map((p) => (p.id === project.id ? project : p));
});
return project; return project;
} }

View File

@ -4,7 +4,7 @@ import { get, writable } from 'svelte/store';
import TagOption from './TagOption'; import TagOption from './TagOption';
import Project from './Project'; import Project from './Project';
const projectTags = writable([] as ProjectTag[]); export const projectTags = writable([] as ProjectTag[]);
export default class ProjectTag { export default class ProjectTag {
private _id: number; private _id: number;
@ -47,11 +47,8 @@ export default class ProjectTag {
return this._options; return this._options;
} }
static getAll(): ProjectTag[] { static fromId(id: number | null | undefined): ProjectTag | null {
return get(projectTags); if (!id) return null;
}
static fromId(id: number): ProjectTag | null {
for (const projectTag of get(projectTags)) { for (const projectTag of get(projectTags)) {
if (projectTag.id === id) { if (projectTag.id === id) {
return projectTag; return projectTag;
@ -76,7 +73,13 @@ export default class ProjectTag {
projectTag._options = options; projectTag._options = options;
projectTags.update((projectTags) => [...projectTags, projectTag]); projectTags.update((projectTags) => {
if (!projectTags.find((projectTag) => projectTag.id === json.id)) {
return [...projectTags, projectTag];
}
return projectTags.map((pt) => (pt.id === json.id ? projectTag : pt));
});
return projectTag; return projectTag;
} }

View File

@ -2,8 +2,9 @@ import { writable } from 'svelte/store';
import Project from './Project'; import Project from './Project';
import ProjectTag from './ProjectTag'; import ProjectTag from './ProjectTag';
import viewsApi from '$lib/api/viewsApi'; import viewsApi from '$lib/api/viewsApi';
import { toastAlert } from '$lib/utils/toasts';
const views = writable([] as View[]); export const views = writable([] as View[]);
export default class View { export default class View {
private _id: number; private _id: number;
@ -80,23 +81,43 @@ export default class View {
return true; return true;
} }
async setTitle(title: string): Promise<boolean> {
if (
!(await viewsApi.update(
this.id,
this.project.id,
this.primaryTag?.id || null,
this.secondaryTag?.id || null,
title,
this.sortTag?.id || null,
this.sortDirection || null
))
)
return false;
this._title = title;
return true;
}
static parse(json: any): View | null; static parse(json: any): View | null;
static parse(json: any, project: Project | null | undefined): View | null; static parse(json: any, project: Project | null | undefined): View | null;
static parse(json: any, project?: Project | null | undefined): View | null { static parse(json: any, project?: Project | null | undefined): View | null {
if (!json) return null; if (!json) {
toastAlert('Failed to parse view');
return null;
}
if (!project) project = Project.fromId(json.project_id); if (!project) project = Project.fromId(json.project_id);
if (!project) return null; if (!project) {
toastAlert('Failed to find project');
return null;
}
const primaryTag = ProjectTag.fromId(json.primary_tag_id); const primaryTag = ProjectTag.fromId(json.primary_tag_id);
if (!primaryTag) return null;
const secondaryTag = ProjectTag.fromId(json.secondary_tag_id); const secondaryTag = ProjectTag.fromId(json.secondary_tag_id);
if (!secondaryTag) return null;
const sortTag = ProjectTag.fromId(json.sort_tag_id); const sortTag = ProjectTag.fromId(json.sort_tag_id);
if (!sortTag) return null;
const view = new View( const view = new View(
json.id, json.id,
@ -108,8 +129,40 @@ export default class View {
json.sort_direction json.sort_direction
); );
views.update((views) => [...views, view]); views.update((views) => {
if (!views.find((view) => view.id === json.id)) {
return [...views, view];
}
return views.map((v) => (v.id === json.id ? view : v));
});
return view; return view;
} }
static parseAll(json: any): View[];
static parseAll(json: any, project: Project | null | undefined): View[];
static parseAll(json: any, project?: Project | null | undefined): View[] {
if (!json) {
toastAlert('Failed to parse views');
return [];
}
if (!project) project = Project.fromId(json.project_id);
if (!project) {
toastAlert('Failed to find project');
return [];
}
const views: View[] = [];
for (const viewJson of json) {
const view = View.parse(viewJson, project);
if (view) views.push(view);
}
return views;
}
} }

View File

@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/stores'; import { page } from '$app/stores';
import { getProjectAPI } from '$lib/api/projects'; import projectsApi from '$lib/api/projectsApi';
import ProjectComponent from '$lib/components/project/Project.svelte';
import Sidebar from '$lib/components/project/Sidebar.svelte'; import Sidebar from '$lib/components/project/Sidebar.svelte';
import type Project from '$lib/types/Project'; import type Project from '$lib/types/Project';
import { SvelteToast } from '@zerodevx/svelte-toast'; import { SvelteToast } from '@zerodevx/svelte-toast';
@ -11,17 +10,22 @@
let project: Project; let project: Project;
onMount(() => { onMount(async () => {
getProjectAPI(projectId).then((p) => { const res = await projectsApi.get(projectId);
project = p;
}); if (!res) return;
project = res;
await projectsApi.getTags(project);
await projectsApi.getViews(project);
}); });
</script> </script>
{#if project} {#if project}
<div> <div>
<Sidebar {project} /> <Sidebar {project} />
<ProjectComponent {project} /> <!-- <ProjectComponent {project} /> -->
</div> </div>
<SvelteToast /> <SvelteToast />
{/if} {/if}