Realtime updates for projects & cards
This commit is contained in:
parent
e01b93f48b
commit
c271af9d93
|
@ -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,
|
"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 {
|
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.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 {
|
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.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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -91,11 +91,16 @@ func CreateProject(c *fiber.Ctx) error {
|
||||||
|
|
||||||
p.ID = id;
|
p.ID = id;
|
||||||
|
|
||||||
|
source := c.Get("X-Request-Source");
|
||||||
|
if source == "" {
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
publish(fiber.Map{
|
publish(fiber.Map{
|
||||||
"object": "project",
|
"object": "project",
|
||||||
"action": "create",
|
"action": "create",
|
||||||
"id": id,
|
"data": p,
|
||||||
"value": p,
|
"X-Request-Source": source,
|
||||||
})
|
})
|
||||||
|
|
||||||
return nil;
|
return nil;
|
||||||
|
@ -130,11 +135,17 @@ func UpdateProject(c *fiber.Ctx) error {
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
source := c.Get("X-Request-Source");
|
||||||
|
if source == "" {
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
publish(fiber.Map{
|
publish(fiber.Map{
|
||||||
"object": "project",
|
"object": "project",
|
||||||
"action": "update",
|
"action": "update",
|
||||||
"id": id,
|
"id": id,
|
||||||
"value": p,
|
"changes": p,
|
||||||
|
"X-Request-Source": source,
|
||||||
})
|
})
|
||||||
|
|
||||||
return nil;
|
return nil;
|
||||||
|
@ -164,10 +175,16 @@ func DeleteProject(c *fiber.Ctx) error {
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
source := c.Get("X-Request-Source");
|
||||||
|
if source == "" {
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
publish(fiber.Map{
|
publish(fiber.Map{
|
||||||
"object": "project",
|
"object": "project",
|
||||||
"action": "delete",
|
"action": "delete",
|
||||||
"id": id,
|
"id": id,
|
||||||
|
"X-Request-Source": source,
|
||||||
})
|
})
|
||||||
|
|
||||||
return nil;
|
return nil;
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||||
"@tauri-apps/cli": "^1.5.9",
|
"@tauri-apps/cli": "^1.5.9",
|
||||||
"@types/eslint": "8.56.0",
|
"@types/eslint": "8.56.0",
|
||||||
|
"@types/node": "^20.11.5",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||||
"@typescript-eslint/parser": "^6.0.0",
|
"@typescript-eslint/parser": "^6.0.0",
|
||||||
"@zerodevx/svelte-toast": "^0.9.5",
|
"@zerodevx/svelte-toast": "^0.9.5",
|
||||||
|
@ -1125,6 +1126,15 @@
|
||||||
"resolved": "https://registry.npmjs.org/@types/marked/-/marked-5.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/marked/-/marked-5.0.2.tgz",
|
||||||
"integrity": "sha512-OucS4KMHhFzhz27KxmWg7J+kIYqyqoW5kdIEI319hqARQQUTqhao3M/F+uFnDXD0Rg72iDDZxZNxq5gvctmLlg=="
|
"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": {
|
"node_modules/@types/pug": {
|
||||||
"version": "2.0.10",
|
"version": "2.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz",
|
||||||
|
@ -3807,6 +3817,12 @@
|
||||||
"node": ">=14.17"
|
"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": {
|
"node_modules/uri-js": {
|
||||||
"version": "4.4.1",
|
"version": "4.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||||
"@tauri-apps/cli": "^1.5.9",
|
"@tauri-apps/cli": "^1.5.9",
|
||||||
"@types/eslint": "8.56.0",
|
"@types/eslint": "8.56.0",
|
||||||
|
"@types/node": "^20.11.5",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||||
"@typescript-eslint/parser": "^6.0.0",
|
"@typescript-eslint/parser": "^6.0.0",
|
||||||
"@zerodevx/svelte-toast": "^0.9.5",
|
"@zerodevx/svelte-toast": "^0.9.5",
|
||||||
|
|
|
@ -3,7 +3,6 @@ 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 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 status from '$lib/utils/status';
|
import status from '$lib/utils/status';
|
||||||
|
|
||||||
async function getAll(): Promise<Project[]> {
|
async function getAll(): Promise<Project[]> {
|
||||||
|
|
|
@ -152,6 +152,14 @@ export default class Card {
|
||||||
return true;
|
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): Card | null;
|
||||||
static parse(json: any, project: Project | null | undefined): Card | null;
|
static parse(json: any, project: Project | null | undefined): Card | null;
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,14 @@
|
||||||
import projectsApi from '$lib/api/projectsApi';
|
import projectsApi from '$lib/api/projectsApi';
|
||||||
import { get, writable } from 'svelte/store';
|
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 {
|
export default class Project {
|
||||||
private _id: number;
|
private _id: number;
|
||||||
|
@ -35,7 +42,7 @@ export default class Project {
|
||||||
|
|
||||||
if (!id) return null;
|
if (!id) return null;
|
||||||
|
|
||||||
const project = new Project(id, 'untitled');
|
const project = new Project(id, 'New project');
|
||||||
|
|
||||||
projects.update((projects) => [...projects, project]);
|
projects.update((projects) => [...projects, project]);
|
||||||
|
|
||||||
|
@ -58,6 +65,10 @@ export default class Project {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateFromDict(dict: any) {
|
||||||
|
if (dict.title) this._title = dict.title;
|
||||||
|
}
|
||||||
|
|
||||||
static parse(json: any): Project | null {
|
static parse(json: any): Project | null {
|
||||||
if (!json) return null;
|
if (!json) return null;
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,9 @@ import { toastAlert } from './toasts';
|
||||||
let backendUrl = 'http://localhost:3000';
|
let backendUrl = 'http://localhost:3000';
|
||||||
let backendWsUrl = 'ws://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() {
|
export function getBackendWsUrl() {
|
||||||
return backendWsUrl;
|
return backendWsUrl;
|
||||||
}
|
}
|
||||||
|
@ -48,9 +51,10 @@ const cachedInstance = setupCache(axiosInstance, {
|
||||||
modifiedSince: true
|
modifiedSince: true
|
||||||
});
|
});
|
||||||
|
|
||||||
axiosInstance.interceptors.request.use(
|
cachedInstance.interceptors.request.use(
|
||||||
(config) => {
|
(config) => {
|
||||||
pendingRequests++;
|
pendingRequests++;
|
||||||
|
config.headers['X-Request-Source'] = randomID;
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
|
@ -59,7 +63,7 @@ axiosInstance.interceptors.request.use(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
axiosInstance.interceptors.response.use(
|
cachedInstance.interceptors.response.use(
|
||||||
(response) => {
|
(response) => {
|
||||||
pendingRequests--;
|
pendingRequests--;
|
||||||
return response;
|
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 = '') {
|
export function processError(response: AxiosResponse<any, any>, message: string = '') {
|
||||||
let title = `${response.status} ${response.statusText}`;
|
let title = `${response.status} ${response.statusText}`;
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
|
import Card, { cards } from '$lib/types/Card';
|
||||||
import Project, { projects } from '$lib/types/Project';
|
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 { toastAlert, toastWarning } from '$lib/utils/toasts';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
|
|
||||||
|
@ -61,6 +64,11 @@ export default class WebSocketManager {
|
||||||
|
|
||||||
this._socket.onmessage = async (event) => {
|
this._socket.onmessage = async (event) => {
|
||||||
const data = JSON.parse(event.data);
|
const data = JSON.parse(event.data);
|
||||||
|
const source = data['X-Request-Source'];
|
||||||
|
// console.log(source);
|
||||||
|
if (!source || source === randomID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
while (hasPendingRequests()) {
|
while (hasPendingRequests()) {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
}
|
}
|
||||||
|
@ -74,6 +82,15 @@ function applyMessage(data: any) {
|
||||||
case 'project':
|
case 'project':
|
||||||
applyProject(data);
|
applyProject(data);
|
||||||
break;
|
break;
|
||||||
|
case 'card':
|
||||||
|
applyCard(data);
|
||||||
|
break;
|
||||||
|
case 'view':
|
||||||
|
applyView(data);
|
||||||
|
break;
|
||||||
|
case 'projectTag':
|
||||||
|
applyProjectTag(data);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
console.log('Unknown object:', data);
|
console.log('Unknown object:', data);
|
||||||
}
|
}
|
||||||
|
@ -82,12 +99,56 @@ function applyMessage(data: any) {
|
||||||
function applyProject(data: any) {
|
function applyProject(data: any) {
|
||||||
switch (data.action) {
|
switch (data.action) {
|
||||||
case 'create':
|
case 'create':
|
||||||
if (get(projects).find((p) => p.id === data.id)) break;
|
Project.parse(data.data);
|
||||||
case 'update':
|
case 'update':
|
||||||
Project.parse(data.value);
|
get(projects)
|
||||||
|
.find((p) => p.id === data.id)
|
||||||
|
?.updateFromDict(data.changes);
|
||||||
|
projects.reload();
|
||||||
break;
|
break;
|
||||||
case 'delete':
|
case 'delete':
|
||||||
projects.set(get(projects).filter((p) => p.id !== data.id));
|
projects.set(get(projects).filter((p) => p.id !== data.id));
|
||||||
break;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -7,11 +7,13 @@
|
||||||
import type Project from '$lib/types/Project';
|
import type Project from '$lib/types/Project';
|
||||||
import { views } from '$lib/types/View';
|
import { views } from '$lib/types/View';
|
||||||
import { checkTauriUrl } from '$lib/utils/api';
|
import { checkTauriUrl } from '$lib/utils/api';
|
||||||
|
import WebSocketManager from '$lib/utils/webSocketManager';
|
||||||
import { SvelteToast } from '@zerodevx/svelte-toast';
|
import { SvelteToast } from '@zerodevx/svelte-toast';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
|
|
||||||
let project: Project;
|
let project: Project;
|
||||||
|
const wsManager = new WebSocketManager();
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await checkTauriUrl(window);
|
await checkTauriUrl(window);
|
||||||
|
@ -29,6 +31,7 @@
|
||||||
if (get(views).length > 0) {
|
if (get(views).length > 0) {
|
||||||
currentView.set(get(views)[0]);
|
currentView.set(get(views)[0]);
|
||||||
}
|
}
|
||||||
|
wsManager.connect();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue