From 8898fa0b9f6ccff392ac04ef884696e4c787b2e9 Mon Sep 17 00:00:00 2001 From: Kaushik Narayan R Date: Sun, 5 Jan 2025 02:23:23 -0700 Subject: [PATCH] a lil abstraction, styling --- package-lock.json | 134 +++++++++++++------- src/App.tsx | 5 +- src/api/auth.ts | 6 +- src/api/axiosInstance.ts | 10 +- src/api/operations.ts | 21 +++- src/api/paths.ts | 1 + src/components/APIWrapper/index.tsx | 55 ++++++++ src/components/Button/Button.module.css | 2 +- src/pages/Graph/Graph.module.css | 15 ++- src/pages/Graph/index.tsx | 159 +++++++++++++----------- 10 files changed, 273 insertions(+), 135 deletions(-) create mode 100644 src/components/APIWrapper/index.tsx diff --git a/package-lock.json b/package-lock.json index 07f8281..ea11984 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4041,9 +4041,9 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.2.tgz", - "integrity": "sha512-vluaspfvWEtE4vcSDlKRNer52DvOGrB2xv6diXy6UKyKW0lqZiWHGNApSyxOv+8DE5Z27IzVvE7hNkxg7EXIcg==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.3.tgz", + "integrity": "sha512-JEhMNwUJt7bw728CydvYzntD0XJeTmDnvwLlbfbAhE7Tbslm/ax6bdIiUwTgeVlZTsJQPwZwKpAkyDtIjsvx3g==", "dev": true, "dependencies": { "@types/node": "*", @@ -4137,9 +4137,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "22.10.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", - "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", + "version": "22.10.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz", + "integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==", "dev": true, "dependencies": { "undici-types": "~6.20.0" @@ -7473,9 +7473,9 @@ } }, "node_modules/es-abstract": { - "version": "1.23.8", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.8.tgz", - "integrity": "sha512-lfab8IzDn6EpI1ibZakcgS6WsfEBiB+43cuJo+wgylx1xKXf+Sp+YR3vFuQwC/u3sxYwV8Cxe3B0DpVUu/WiJQ==", + "version": "1.23.9", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", + "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", "dev": true, "dependencies": { "array-buffer-byte-length": "^1.0.2", @@ -7489,10 +7489,11 @@ "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", - "es-set-tostringtag": "^2.0.3", + "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.2.6", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.0", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", @@ -7513,11 +7514,12 @@ "object-inspect": "^1.13.3", "object-keys": "^1.1.1", "object.assign": "^4.1.7", - "own-keys": "^1.0.0", + "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.3", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", @@ -7605,14 +7607,15 @@ } }, "node_modules/es-set-tostringtag": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", - "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "dev": true, "dependencies": { - "get-intrinsic": "^1.2.4", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", - "hasown": "^2.0.1" + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -9039,21 +9042,21 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.6.tgz", - "integrity": "sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", + "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", "dev": true, "dependencies": { "call-bind-apply-helpers": "^1.0.1", - "dunder-proto": "^1.0.0", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", + "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", - "math-intrinsics": "^1.0.0" + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -9077,6 +9080,19 @@ "node": ">=8.0.0" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -9850,12 +9866,15 @@ "dev": true }, "node_modules/is-async-function": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", - "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.0.tgz", + "integrity": "sha512-GExz9MtyhlZyXYLxzlJRj5WUCE661zhDa1Yna52CN57AJsymh+DvXXjyveSioqSRdxvUrdKdvqB1b5cVKsNpWQ==", "dev": true, "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -10025,12 +10044,15 @@ } }, "node_modules/is-generator-function": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", - "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", "dev": true, "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -10432,16 +10454,16 @@ } }, "node_modules/iterator.prototype": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.4.tgz", - "integrity": "sha512-x4WH0BWmrMmg4oHHl+duwubhrvczGlyuGAZu3nvrf0UXOfPu8IhZObFEr7DE/iv01YgVZrsOiRcqw2srkKEDIA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", "dev": true, "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", "has-symbols": "^1.1.0", - "reflect.getprototypeof": "^1.0.8", "set-function-name": "^2.0.2" }, "engines": { @@ -14788,18 +14810,18 @@ } }, "node_modules/reflect.getprototypeof": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.9.tgz", - "integrity": "sha512-r0Ay04Snci87djAsI4U+WNRcSw5S4pOH7qFjd/veA5gC7TbqESR3tcj28ia95L/fYUDw11JKP7uqUKUAfVvV5Q==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", "dev": true, "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", - "dunder-proto": "^1.0.1", - "es-abstract": "^1.23.6", + "es-abstract": "^1.23.9", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "gopd": "^1.2.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" }, "engines": { @@ -14848,14 +14870,16 @@ "dev": true }, "node_modules/regexp.prototype.flags": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz", - "integrity": "sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", "dev": true, "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", "set-function-name": "^2.0.2" }, "engines": { @@ -15277,9 +15301,9 @@ "dev": true }, "node_modules/sass": { - "version": "1.83.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.83.0.tgz", - "integrity": "sha512-qsSxlayzoOjdvXMVLkzF84DJFc2HZEL/rFyGIKbbilYtAvlCxyuzUeff9LawTn4btVnLKg75Z8MMr1lxU1lfGw==", + "version": "1.83.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.83.1.tgz", + "integrity": "sha512-EVJbDaEs4Rr3F0glJzFSOvtg2/oy2V/YrGFPqPY24UqcLDWcI9ZY5sN+qyO3c/QCZwzgfirvhXvINiJCE/OLcA==", "dev": true, "dependencies": { "chokidar": "^4.0.0", @@ -15656,6 +15680,20 @@ "node": ">= 0.4" } }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", diff --git a/src/App.tsx b/src/App.tsx index a64f409..d09b38c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -27,7 +27,7 @@ import { ReactFlowProvider } from "@xyflow/react"; // Contexts export const WidthContext = createContext(0); export const AuthContext = createContext(false); -export const RefreshAuthContext = createContext(null); +export const RefreshAuthContext = createContext(async () => false); function App() { // States @@ -123,9 +123,8 @@ function App() { diff --git a/src/api/auth.ts b/src/api/auth.ts index 3e00f01..dbd664a 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -1,9 +1,9 @@ import { AxiosResponse } from "axios"; -import { apiRespBase, axiosInstance } from "./axiosInstance"; +import { apiRespBaseType, axiosInstance } from "./axiosInstance"; import { authHealthCheckURL, authRefreshURL } from "./paths"; export const apiAuthCheck = async (): Promise< - AxiosResponse + AxiosResponse > => { try { const response = await axiosInstance.get(authHealthCheckURL); @@ -14,7 +14,7 @@ export const apiAuthCheck = async (): Promise< }; export const apiAuthRefresh = async (): Promise< - AxiosResponse + AxiosResponse > => { try { const response = await axiosInstance.get(authRefreshURL); diff --git a/src/api/axiosInstance.ts b/src/api/axiosInstance.ts index c43ec80..0c8d70f 100644 --- a/src/api/axiosInstance.ts +++ b/src/api/axiosInstance.ts @@ -6,11 +6,11 @@ export const axiosInstance = axios.create({ withCredentials: true, timeout: 20000, headers: { - "Content-Type": "application/json" + "Content-Type": "application/json", }, }); -export interface apiRespBase { - message?: string, - errors?: any[], -}; +export interface apiRespBaseType { + message?: string; + errors?: any[]; +} diff --git a/src/api/operations.ts b/src/api/operations.ts index 98905d1..fa18b0b 100644 --- a/src/api/operations.ts +++ b/src/api/operations.ts @@ -1,8 +1,8 @@ import { AxiosResponse } from "axios"; -import { apiRespBase, axiosInstance } from "./axiosInstance"; -import { opFetchGraphURL } from "./paths"; +import { apiRespBaseType, axiosInstance } from "./axiosInstance"; +import { opFetchGraphURL, opUpdateUserDataURL } from "./paths"; -interface fetchGraphDataType extends apiRespBase { +interface fetchGraphDataType extends apiRespBaseType { playlists?: { playlistID: string; playlistName: string; @@ -13,6 +13,10 @@ interface fetchGraphDataType extends apiRespBase { }[]; } +interface updateUserDataType extends apiRespBaseType { + removedLinks: boolean; +} + export const apiFetchGraph = async (): Promise< AxiosResponse > => { @@ -23,3 +27,14 @@ export const apiFetchGraph = async (): Promise< return error.response; } }; + +export const apiUpdateUserData = async (): Promise< + AxiosResponse +> => { + try { + const response = await axiosInstance.put(opUpdateUserDataURL); + return response; + } catch (error: any) { + return error.response; + } +}; diff --git a/src/api/paths.ts b/src/api/paths.ts index 1be76a7..8f6d834 100644 --- a/src/api/paths.ts +++ b/src/api/paths.ts @@ -7,3 +7,4 @@ export const authHealthCheckURL = "auth-health"; export const authRefreshURL = "api/auth/refresh"; export const opFetchGraphURL = "api/operations/fetch"; +export const opUpdateUserDataURL = "api/operations/update"; diff --git a/src/components/APIWrapper/index.tsx b/src/components/APIWrapper/index.tsx new file mode 100644 index 0000000..f70e69a --- /dev/null +++ b/src/components/APIWrapper/index.tsx @@ -0,0 +1,55 @@ +import { AxiosRequestConfig, AxiosResponse } from "axios"; + +import { apiRespBaseType } from "../../api/axiosInstance"; +import { + showErrorToastNotification, + showWarnToastNotification, +} from "../ToastNotification"; + +const maxRetries = 3; + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +// TODO: refreshAuth fn needs to be prop drilled (well, it's not really 'drilling', but still it's a single level) +// because hooks (namely, useContext) can't be used outside functional components +// so find a better way to pass refreshAuth + +type APIWrapperProps = { + apiFn( + data?: any, + config?: AxiosRequestConfig + ): Promise>; + refreshAuth: () => Promise; + data?: any; + config?: AxiosRequestConfig; +}; + +const APIWrapper = async ({ + apiFn, + refreshAuth, + data, + config, +}: APIWrapperProps) => { + for (let i = 1; i <= maxRetries + 1; i++) { + const apiResp = await apiFn(data, config); + + if (apiResp === undefined) { + showErrorToastNotification("Please try again after sometime"); + } else if (apiResp.status === 200) { + return apiResp; + } else if (apiResp.status === 401) { + showWarnToastNotification("Session expired, refreshing..."); + if (!(await refreshAuth())) { + showErrorToastNotification("Session invalid."); + return; + } + } else { + showErrorToastNotification(apiResp.data.message); + } + await sleep(i * i * 1000); + } + showErrorToastNotification("Please try again after sometime"); + return; +}; + +export default APIWrapper; diff --git a/src/components/Button/Button.module.css b/src/components/Button/Button.module.css index cb09206..d6957c0 100644 --- a/src/components/Button/Button.module.css +++ b/src/components/Button/Button.module.css @@ -8,6 +8,6 @@ cursor: pointer; text-decoration: none; color: var(--text); - box-shadow: 8px 8px var(--bg); + box-shadow: 4px 4px var(--bg); background-color: var(--bgLinkInactive); } diff --git a/src/pages/Graph/Graph.module.css b/src/pages/Graph/Graph.module.css index c522a6b..7d40323 100644 --- a/src/pages/Graph/Graph.module.css +++ b/src/pages/Graph/Graph.module.css @@ -11,8 +11,21 @@ flex-direction: column; align-items: center; justify-content: flex-start; - gap: var(--mb-3); + gap: var(--mb-1); height: 100vh; width: 10vw; padding: var(--mb-3); } + +.operations_wrapper .icons { + display: flex; + flex-direction: row; +} + +.divider { + display: block; + height: 1px; + width: 100%; + margin: var(--mb-2) auto; + border-top: 1px solid white; +} diff --git a/src/pages/Graph/index.tsx b/src/pages/Graph/index.tsx index 40e9511..556f455 100644 --- a/src/pages/Graph/index.tsx +++ b/src/pages/Graph/index.tsx @@ -22,7 +22,7 @@ import { type OnDelete, type OnBeforeDelete, } from "@xyflow/react"; -import Dagre, { type GraphLabel } from "@dagrejs/dagre"; +import Dagre from "@dagrejs/dagre"; import "@xyflow/react/dist/style.css"; import styles from "./Graph.module.css"; @@ -30,16 +30,19 @@ import styles from "./Graph.module.css"; import { IoIosGitNetwork } from "react-icons/io"; import { WiCloudRefresh } from "react-icons/wi"; import { MdOutlineLock, MdOutlineLockOpen } from "react-icons/md"; +import { AiFillSpotify } from "react-icons/ai"; import { showErrorToastNotification, showInfoToastNotification, + showWarnToastNotification, } from "../../components/ToastNotification"; -import { apiFetchGraph } from "../../api/operations"; +import { apiFetchGraph, apiUpdateUserData } from "../../api/operations"; import { RefreshAuthContext } from "../../App"; import Button from "../../components/Button"; +import APIWrapper from "../../components/APIWrapper"; const initialNodes: Node[] = []; const initialEdges: Edge[] = []; @@ -67,11 +70,13 @@ const nodeOffsets = { }, }; -interface Interactive { +type Interactive = { ndDrag: boolean; ndConn: boolean; elsSel: boolean; -} +}; + +type rankdirType = "TB" | "BT" | "LR" | "RL"; const initialInteractive: Interactive = { ndDrag: true, @@ -147,12 +152,12 @@ const Graph = () => { ); type getLayoutedElementsOpts = { - direction: GraphLabel["rankdir"]; + direction: rankdirType; }; const getLayoutedElements = ( nodes: Node[], edges: Edge[], - options: getLayoutedElementsOpts = { direction: "TB" } + options: getLayoutedElementsOpts ) => { const g = new Dagre.graphlib.Graph(); g.setDefaultEdgeLabel(() => ({})); @@ -224,7 +229,7 @@ const Graph = () => { return finalLayout; }; - const arrangeLayout = (direction: GraphLabel["rankdir"]) => { + const arrangeLayout = (direction: rankdirType) => { const layouted = getLayoutedElements(playlistNodes, linkEdges, { direction, }); @@ -237,66 +242,59 @@ const Graph = () => { }; const fetchGraph = useCallback(async () => { - const resp = await apiFetchGraph(); - if (resp === undefined) { - showErrorToastNotification("Please try again after sometime"); - return; - } - if (resp.status === 200) { - console.debug( - `graph fetched with ${resp.data.playlists?.length} nodes and ${resp.data.links?.length} edges` - ); - // place playlist nodes - setPlaylistNodes( - resp.data.playlists?.map((pl, idx) => { - return { - id: `${pl.playlistID}`, - position: { - x: - nodeOffsets.unconnected.origin.x + - Math.floor(idx / 15) * nodeOffsets.unconnected.scaling.x, - y: - nodeOffsets.unconnected.origin.y + - Math.floor(idx % 15) * nodeOffsets.unconnected.scaling.y, + const resp = await APIWrapper({ apiFn: apiFetchGraph, refreshAuth }); + console.debug( + `graph fetched with ${resp?.data.playlists?.length} nodes and ${resp?.data.links?.length} edges` + ); + // place playlist nodes + setPlaylistNodes( + resp?.data.playlists?.map((pl, idx) => { + return { + id: `${pl.playlistID}`, + position: { + x: + nodeOffsets.unconnected.origin.x + + Math.floor(idx / 15) * nodeOffsets.unconnected.scaling.x, + y: + nodeOffsets.unconnected.origin.y + + Math.floor(idx % 15) * nodeOffsets.unconnected.scaling.y, + }, + data: { + label: pl.playlistName, + metadata: { + pl, }, - data: { - label: pl.playlistName, - metadata: { - pl, - }, - }, - }; - }) ?? [] - ); - // connect links - setLinkEdges( - resp.data.links?.map((link, idx) => { - return { - id: `${link.from}->${link.to}`, - source: link.from, - target: link.to, - }; - }) ?? [] - ); - showInfoToastNotification("Graph updated."); - return; - } - if (resp.status >= 500) { - showErrorToastNotification(resp.data.message); - return; - } - if (resp.status === 401) { - await refreshAuth(); - } - showErrorToastNotification(resp.data.message); - return; + }, + }; + }) ?? [] + ); + // connect links + setLinkEdges( + resp?.data.links?.map((link, idx) => { + return { + id: `${link.from}->${link.to}`, + source: link.from, + target: link.to, + }; + }) ?? [] + ); + showInfoToastNotification("Graph updated."); }, [refreshAuth]); - const onArrange = () => { - arrangeLayout("TB"); + const updateUserData = async () => { + const resp = await APIWrapper({ + apiFn: apiUpdateUserData, + refreshAuth, + }); + showInfoToastNotification("Spotify synced."); + if (resp?.data.removedLinks) + showWarnToastNotification( + "Some links with deleted playlists were removed." + ); + await refreshGraph(); }; - const onRefresh = async () => { + const refreshGraph = async () => { await fetchGraph(); arrangeLayout("TB"); }; @@ -304,19 +302,30 @@ const Graph = () => { useEffect(() => { fetchGraph(); // TODO: how to invoke async and sync fns in order correctly inside useEffect? - // onRefresh(); + // refreshGraph(); }, [fetchGraph]); - const toggleInteractive = () => { + const disableInteractive = () => { setInteractive({ - ndDrag: !interactive.ndDrag, - ndConn: !interactive.ndConn, - elsSel: !interactive.elsSel, + ndDrag: false, + ndConn: false, + elsSel: false, + }); + }; + const enableInteractive = () => { + setInteractive({ + ndDrag: true, + ndConn: true, + elsSel: true, }); }; const isInteractive = () => { - return interactive.ndDrag && interactive.ndConn && interactive.elsSel; + return interactive.ndDrag || interactive.ndConn || interactive.elsSel; + }; + + const toggleInteractive = () => { + isInteractive() ? disableInteractive() : enableInteractive(); }; return ( @@ -349,11 +358,11 @@ const Graph = () => {
- - @@ -365,6 +374,14 @@ const Graph = () => { )} {isInteractive() ? "Lock" : "Unlock"} +
+
);