diff --git a/packages/services/src/api.service.ts b/packages/services/src/api.service.ts index 81adb43f3..619a0d4ec 100644 --- a/packages/services/src/api.service.ts +++ b/packages/services/src/api.service.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import axios, { AxiosInstance, AxiosRequestConfig } from "axios"; +import { IndexedDBService } from "./indexedDB.service"; /** * Abstract base class for making HTTP requests using axios diff --git a/packages/services/src/indexedDB.service.ts b/packages/services/src/indexedDB.service.ts new file mode 100644 index 000000000..1b568f2ae --- /dev/null +++ b/packages/services/src/indexedDB.service.ts @@ -0,0 +1,62 @@ +export abstract class IndexedDBService { + private dbName: string; + private version: number; + private db: IDBDatabase | null = null; + + constructor(dbName: string, version: number) { + this.dbName = dbName; + this.version = version; + } + + async init(): Promise { + return new Promise((resolve, reject) => { + const request = indexedDB.open(this.dbName, this.version); + + request.onerror = () => reject(request.error); + request.onsuccess = () => { + this.db = request.result; + resolve(); + }; + + request.onupgradeneeded = (event) => { + const db = (event.target as IDBOpenDBRequest).result; + if (!db.objectStoreNames.contains("workspaces")) { + db.createObjectStore("workspaces", { keyPath: "id" }); + } + }; + }); + } + + async save(workspaces: any[]): Promise { + if (!this.db) throw new Error("Database not initialized"); + + const transaction = this.db.transaction("workspaces", "readwrite"); + const store = transaction.objectStore("workspaces"); + + return new Promise((resolve, reject) => { + // Clear existing data + store.clear(); + + // Add new workspaces + workspaces.forEach((workspace) => { + store.add(workspace); + }); + + transaction.oncomplete = () => resolve(); + transaction.onerror = () => reject(transaction.error); + }); + } + + async query(): Promise { + if (!this.db) throw new Error("Database not initialized"); + + const transaction = this.db.transaction("workspaces", "readonly"); + const store = transaction.objectStore("workspaces"); + + return new Promise((resolve, reject) => { + const request = store.getAll(); + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); + } +} diff --git a/packages/shared-state/.eslintignore b/packages/shared-state/.eslintignore new file mode 100644 index 000000000..6019047c3 --- /dev/null +++ b/packages/shared-state/.eslintignore @@ -0,0 +1,3 @@ +build/* +dist/* +out/* \ No newline at end of file diff --git a/packages/shared-state/.eslintrc.js b/packages/shared-state/.eslintrc.js new file mode 100644 index 000000000..558b8f76e --- /dev/null +++ b/packages/shared-state/.eslintrc.js @@ -0,0 +1,9 @@ +/** @type {import("eslint").Linter.Config} */ +module.exports = { + root: true, + extends: ["@plane/eslint-config/library.js"], + parser: "@typescript-eslint/parser", + parserOptions: { + project: true, + }, +}; diff --git a/packages/shared-state/.prettierrc b/packages/shared-state/.prettierrc new file mode 100644 index 000000000..87d988f1b --- /dev/null +++ b/packages/shared-state/.prettierrc @@ -0,0 +1,5 @@ +{ + "printWidth": 120, + "tabWidth": 2, + "trailingComma": "es5" +} diff --git a/packages/shared-state/package.json b/packages/shared-state/package.json new file mode 100644 index 000000000..10e802c67 --- /dev/null +++ b/packages/shared-state/package.json @@ -0,0 +1,20 @@ +{ + "name": "@plane/shared-state", + "version": "0.24.1", + "description": "Shared state shared across multiple apps internally", + "private": true, + "main": "./src/index.ts", + "types": "./src/index.ts", + "scripts": { + "lint": "eslint src --ext .ts,.tsx", + "lint:errors": "eslint src --ext .ts,.tsx --quiet" + }, + "dependencies": { + "zod": "^3.22.2" + }, + "devDependencies": { + "@plane/eslint-config": "*", + "@types/node": "^22.5.4", + "typescript": "^5.3.3" + } +} diff --git a/packages/shared-state/src/index.ts b/packages/shared-state/src/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/shared-state/src/store/user.store.ts b/packages/shared-state/src/store/user.store.ts new file mode 100644 index 000000000..f7f30f5ba --- /dev/null +++ b/packages/shared-state/src/store/user.store.ts @@ -0,0 +1,136 @@ +import { makeObservable, observable } from "mobx"; +import { IWorkspaceStore } from "./workspace.store"; + +export interface IUserStore { + user: any; + workspaces: Map; + isLoading: boolean; + error: any; +} + +export class UserStore implements IUserStore { + user: any = null; + workspaces: Map = new Map(); + isLoading: boolean = false; + error: any = null; + + constructor() { + makeObservable(this, { + user: observable.ref, + workspaces: observable, + isLoading: observable.ref, + error: observable.ref, + }); + } +} + +// userStore.ts + +// class UserStore { +// user: User | null = null; +// workspaces: Workspace[] = []; +// isLoading = false; +// error: string | null = null; +// private indexedDBService: IndexedDBService; + +// constructor() { +// makeAutoObservable(this); +// this.indexedDBService = new IndexedDBService(); +// this.init(); +// } + +// private async init() { +// try { +// await this.indexedDBService.init(); +// await this.loadWorkspacesFromIndexedDB(); +// } catch (error) { +// runInAction(() => { +// this.error = "Failed to initialize store"; +// console.error("Store initialization error:", error); +// }); +// } +// } + +// setUser(user: User | null) { +// this.user = user; +// } + +// async loadWorkspacesFromIndexedDB() { +// try { +// const workspaces = await this.indexedDBService.getWorkspaces(); +// runInAction(() => { +// this.workspaces = workspaces; +// }); +// } catch (error) { +// runInAction(() => { +// this.error = "Failed to load workspaces from IndexedDB"; +// console.error("Load workspaces error:", error); +// }); +// } +// } + +// async fetchAndSyncWorkspaces() { +// this.isLoading = true; +// this.error = null; + +// try { +// // Simulate API call to fetch workspaces +// const response = await fetch("/api/workspaces"); +// const workspaces = await response.json(); + +// // Save to IndexedDB +// await this.indexedDBService.saveWorkspaces(workspaces); + +// // Update MobX store +// runInAction(() => { +// this.workspaces = workspaces; +// this.isLoading = false; +// }); +// } catch (error) { +// runInAction(() => { +// this.error = "Failed to fetch workspaces"; +// this.isLoading = false; +// console.error("Fetch workspaces error:", error); +// }); +// } +// } + +// // Additional methods for workspace management +// async addWorkspace(workspace: Omit) { +// this.isLoading = true; +// this.error = null; + +// try { +// // Simulate API call to create workspace +// const response = await fetch("/api/workspaces", { +// method: "POST", +// body: JSON.stringify(workspace), +// }); +// const newWorkspace = await response.json(); + +// // Update local storage and state +// const updatedWorkspaces = [...this.workspaces, newWorkspace]; +// await this.indexedDBService.saveWorkspaces(updatedWorkspaces); + +// runInAction(() => { +// this.workspaces.push(newWorkspace); +// this.isLoading = false; +// }); +// } catch (error) { +// runInAction(() => { +// this.error = "Failed to add workspace"; +// this.isLoading = false; +// console.error("Add workspace error:", error); +// }); +// } +// } + +// logout() { +// this.user = null; +// this.workspaces = []; +// // Optionally clear IndexedDB data +// this.indexedDBService.init().then(() => { +// this.indexedDBService.saveWorkspaces([]); +// }); +// } +// } diff --git a/packages/shared-state/src/store/workspace.store.ts b/packages/shared-state/src/store/workspace.store.ts new file mode 100644 index 000000000..18af2840a --- /dev/null +++ b/packages/shared-state/src/store/workspace.store.ts @@ -0,0 +1,28 @@ +import { makeObservable, observable } from "mobx"; + +export interface IWorkspaceStore { + id: string; + name: string; + createdAt: string; + updatedAt: string; +} + +export class WorkspaceStore implements IWorkspaceStore { + id: string; + name: string; + createdAt: string; + updatedAt: string; + + constructor(data: IWorkspaceStore) { + makeObservable(this, { + id: observable.ref, + name: observable.ref, + createdAt: observable.ref, + updatedAt: observable.ref, + }); + this.id = data.id; + this.name = data.name; + this.createdAt = data.createdAt; + this.updatedAt = data.updatedAt; + } +} diff --git a/packages/shared-state/tsconfig.json b/packages/shared-state/tsconfig.json new file mode 100644 index 000000000..0c2f64d1a --- /dev/null +++ b/packages/shared-state/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@plane/typescript-config/react-library.json", + "compilerOptions": { + "jsx": "react", + "lib": ["esnext", "dom"], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["./src"], + "exclude": ["dist", "build", "node_modules"] +} diff --git a/yarn.lock b/yarn.lock index 3a69fe733..8ed0dd8fc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11337,7 +11337,16 @@ streamx@^2.15.0, streamx@^2.21.0: optionalDependencies: bare-events "^2.2.0" -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -11430,7 +11439,14 @@ string_decoder@^1.1.1, string_decoder@^1.3.0: dependencies: safe-buffer "~5.2.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -12687,7 +12703,16 @@ word-wrap@^1.2.5: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -12824,6 +12849,11 @@ zeed-dom@^0.15.1: css-what "^6.1.0" entities "^5.0.0" +zod@^3.22.2: + version "3.24.1" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.1.tgz#27445c912738c8ad1e9de1bea0359fa44d9d35ee" + integrity sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A== + zxcvbn@^4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/zxcvbn/-/zxcvbn-4.4.2.tgz#28ec17cf09743edcab056ddd8b1b06262cc73c30"