Realtime updates for projects & cards

This commit is contained in:
Brieuc Dubois 2024-01-18 04:10:28 +01:00
parent e01b93f48b
commit c271af9d93
10 changed files with 186 additions and 15 deletions

View File

@ -34,9 +34,27 @@ func CreateCard(c *fiber.Ctx) error {
})
}
return c.Status(fiber.StatusCreated).JSON(fiber.Map{
if err := c.Status(fiber.StatusCreated).JSON(fiber.Map{
"id": id,
}); err != nil {
return err
}
card.ID = id;
source := c.Get("X-Request-Source");
if source == "" {
return nil;
}
publish(fiber.Map{
"object": "card",
"action": "create",
"data": card,
"X-Request-Source": source,
})
return nil;
}
func GetCard(c *fiber.Ctx) error {
@ -77,7 +95,23 @@ func DeleteCard(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusNotFound)
}
return c.SendStatus(fiber.StatusNoContent)
if err := c.SendStatus(fiber.StatusNoContent); err != nil {
return err
}
source := c.Get("X-Request-Source");
if source == "" {
return nil;
}
publish(fiber.Map{
"object": "card",
"action": "delete",
"id": id,
"X-Request-Source": source,
});
return nil;
}
func UpdateCard(c *fiber.Ctx) error {
@ -103,5 +137,22 @@ func UpdateCard(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusNotFound)
}
return c.SendStatus(fiber.StatusNoContent)
if err := c.SendStatus(fiber.StatusNoContent); err != nil {
return err
}
source := c.Get("X-Request-Source");
if source == "" {
return nil;
}
publish(fiber.Map{
"object": "card",
"action": "update",
"id": id,
"changes": card,
"X-Request-Source": source,
})
return nil;
}

View File

@ -91,11 +91,16 @@ func CreateProject(c *fiber.Ctx) error {
p.ID = id;
source := c.Get("X-Request-Source");
if source == "" {
return nil;
}
publish(fiber.Map{
"object": "project",
"action": "create",
"id": id,
"value": p,
"data": p,
"X-Request-Source": source,
})
return nil;
@ -130,11 +135,17 @@ func UpdateProject(c *fiber.Ctx) error {
return err;
}
source := c.Get("X-Request-Source");
if source == "" {
return nil;
}
publish(fiber.Map{
"object": "project",
"action": "update",
"id": id,
"value": p,
"changes": p,
"X-Request-Source": source,
})
return nil;
@ -164,10 +175,16 @@ func DeleteProject(c *fiber.Ctx) error {
return err;
}
source := c.Get("X-Request-Source");
if source == "" {
return nil;
}
publish(fiber.Map{
"object": "project",
"action": "delete",
"id": id,
"X-Request-Source": source,
})
return nil;

View File

@ -21,6 +21,7 @@
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@tauri-apps/cli": "^1.5.9",
"@types/eslint": "8.56.0",
"@types/node": "^20.11.5",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"@zerodevx/svelte-toast": "^0.9.5",
@ -1125,6 +1126,15 @@
"resolved": "https://registry.npmjs.org/@types/marked/-/marked-5.0.2.tgz",
"integrity": "sha512-OucS4KMHhFzhz27KxmWg7J+kIYqyqoW5kdIEI319hqARQQUTqhao3M/F+uFnDXD0Rg72iDDZxZNxq5gvctmLlg=="
},
"node_modules/@types/node": {
"version": "20.11.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.5.tgz",
"integrity": "sha512-g557vgQjUUfN76MZAN/dt1z3dzcUsimuysco0KeluHgrPdJXkP/XdAURgyO2W9fZWHRtRBiVKzKn8vyOAwlG+w==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/@types/pug": {
"version": "2.0.10",
"resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz",
@ -3807,6 +3817,12 @@
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
},
"node_modules/uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",

View File

@ -21,6 +21,7 @@
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@tauri-apps/cli": "^1.5.9",
"@types/eslint": "8.56.0",
"@types/node": "^20.11.5",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"@zerodevx/svelte-toast": "^0.9.5",

View File

@ -3,7 +3,6 @@ import Project from '$lib/types/Project';
import ProjectTag from '$lib/types/ProjectTag';
import View from '$lib/types/View';
import api, { processError } from '$lib/utils/api';
import { parseCards } from '$lib/utils/parser';
import status from '$lib/utils/status';
async function getAll(): Promise<Project[]> {

View File

@ -152,6 +152,14 @@ export default class Card {
return true;
}
updateFromDict(dict: any) {
if (dict.project_id && dict.project_id !== this._project.id) {
this._project = Project.fromId(dict.project_id) as Project;
}
if (dict.title) this._title = dict.title;
if (dict.content) this._content = dict.content;
}
static parse(json: any): Card | null;
static parse(json: any, project: Project | null | undefined): Card | null;

View File

@ -1,7 +1,14 @@
import projectsApi from '$lib/api/projectsApi';
import { get, writable } from 'svelte/store';
export const projects = writable([] as Project[]);
const { subscribe, set, update } = writable([] as Project[]);
export const projects = {
subscribe,
set,
update,
reload: () => update((projects) => projects)
};
export default class Project {
private _id: number;
@ -35,7 +42,7 @@ export default class Project {
if (!id) return null;
const project = new Project(id, 'untitled');
const project = new Project(id, 'New project');
projects.update((projects) => [...projects, project]);
@ -58,6 +65,10 @@ export default class Project {
return true;
}
updateFromDict(dict: any) {
if (dict.title) this._title = dict.title;
}
static parse(json: any): Project | null {
if (!json) return null;

View File

@ -7,6 +7,9 @@ import { toastAlert } from './toasts';
let backendUrl = 'http://localhost:3000';
let backendWsUrl = 'ws://localhost:3000';
export let randomID =
Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
export function getBackendWsUrl() {
return backendWsUrl;
}
@ -48,9 +51,10 @@ const cachedInstance = setupCache(axiosInstance, {
modifiedSince: true
});
axiosInstance.interceptors.request.use(
cachedInstance.interceptors.request.use(
(config) => {
pendingRequests++;
config.headers['X-Request-Source'] = randomID;
return config;
},
(error) => {
@ -59,7 +63,7 @@ axiosInstance.interceptors.request.use(
}
);
axiosInstance.interceptors.response.use(
cachedInstance.interceptors.response.use(
(response) => {
pendingRequests--;
return response;
@ -70,7 +74,7 @@ axiosInstance.interceptors.response.use(
}
);
export default axiosInstance;
export default cachedInstance;
export function processError(response: AxiosResponse<any, any>, message: string = '') {
let title = `${response.status} ${response.statusText}`;

View File

@ -1,5 +1,8 @@
import Card, { cards } from '$lib/types/Card';
import Project, { projects } from '$lib/types/Project';
import { getBackendWsUrl, hasPendingRequests } from '$lib/utils/api';
import ProjectTag, { projectTags } from '$lib/types/ProjectTag';
import View, { views } from '$lib/types/View';
import { getBackendWsUrl, hasPendingRequests, randomID } from '$lib/utils/api';
import { toastAlert, toastWarning } from '$lib/utils/toasts';
import { get } from 'svelte/store';
@ -61,6 +64,11 @@ export default class WebSocketManager {
this._socket.onmessage = async (event) => {
const data = JSON.parse(event.data);
const source = data['X-Request-Source'];
// console.log(source);
if (!source || source === randomID) {
return;
}
while (hasPendingRequests()) {
await new Promise((resolve) => setTimeout(resolve, 100));
}
@ -74,6 +82,15 @@ function applyMessage(data: any) {
case 'project':
applyProject(data);
break;
case 'card':
applyCard(data);
break;
case 'view':
applyView(data);
break;
case 'projectTag':
applyProjectTag(data);
break;
default:
console.log('Unknown object:', data);
}
@ -82,12 +99,56 @@ function applyMessage(data: any) {
function applyProject(data: any) {
switch (data.action) {
case 'create':
if (get(projects).find((p) => p.id === data.id)) break;
Project.parse(data.data);
case 'update':
Project.parse(data.value);
get(projects)
.find((p) => p.id === data.id)
?.updateFromDict(data.changes);
projects.reload();
break;
case 'delete':
projects.set(get(projects).filter((p) => p.id !== data.id));
break;
}
}
function applyCard(data: any) {
switch (data.action) {
case 'create':
Card.parse(data.data);
break;
case 'update':
get(cards)
.find((c) => c.id === data.id)
?.updateFromDict(data.changes);
cards.reload();
break;
case 'delete':
cards.set(get(cards).filter((c) => c.id !== data.id));
break;
}
}
function applyView(data: any) {
switch (data.action) {
case 'create':
case 'update':
View.parse(data.value);
break;
case 'delete':
views.set(get(views).filter((v) => v.id !== data.id));
break;
}
}
function applyProjectTag(data: any) {
switch (data.action) {
case 'create':
case 'update':
ProjectTag.parse(data.value);
break;
case 'delete':
projectTags.set(get(projectTags).filter((t) => t.id !== data.id));
break;
}
}

View File

@ -7,11 +7,13 @@
import type Project from '$lib/types/Project';
import { views } from '$lib/types/View';
import { checkTauriUrl } from '$lib/utils/api';
import WebSocketManager from '$lib/utils/webSocketManager';
import { SvelteToast } from '@zerodevx/svelte-toast';
import { onMount } from 'svelte';
import { get } from 'svelte/store';
let project: Project;
const wsManager = new WebSocketManager();
onMount(async () => {
await checkTauriUrl(window);
@ -29,6 +31,7 @@
if (get(views).length > 0) {
currentView.set(get(views)[0]);
}
wsManager.connect();
});
</script>