diff --git a/frontend/package.json b/frontend/package.json index efa18e553a8..ef7f69d7238 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -29,6 +29,7 @@ "onnxruntime-web": "^1.18.0", "pluralize": "^8.0.0", "react": "^18.2.0", + "react-beautiful-dnd": "13.1.1", "react-device-detect": "^2.2.2", "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", diff --git a/frontend/src/components/Sidebar/ActiveWorkspaces/ThreadContainer/ThreadItem/index.jsx b/frontend/src/components/Sidebar/ActiveWorkspaces/ThreadContainer/ThreadItem/index.jsx index 4c12b05b51c..9ecd1527530 100644 --- a/frontend/src/components/Sidebar/ActiveWorkspaces/ThreadContainer/ThreadItem/index.jsx +++ b/frontend/src/components/Sidebar/ActiveWorkspaces/ThreadContainer/ThreadItem/index.jsx @@ -43,7 +43,7 @@ export default function ThreadItem({ isActive ? "border-l-2 border-b-2 border-white light:border-theme-sidebar-border z-[2]" : "border-l border-b border-[#6F6F71] light:border-theme-sidebar-border z-[1]" - } h-[50%] absolute top-0 left-2 rounded-bl-lg`} + } h-[50%] absolute top-0 left-3 rounded-bl-lg`} > {/* Downstroke border for next item */} {hasNext && ( @@ -53,7 +53,7 @@ export default function ThreadItem({ idx <= activeIdx && !isActive ? "border-l-2 border-white light:border-theme-sidebar-border z-[2]" : "border-l border-[#6F6F71] light:border-theme-sidebar-border z-[1]" - } h-[100%] absolute top-0 left-2`} + } h-[100%] absolute top-0 left-3`} > )} diff --git a/frontend/src/components/Sidebar/ActiveWorkspaces/index.jsx b/frontend/src/components/Sidebar/ActiveWorkspaces/index.jsx index 5e9c9fbd211..26c844c033b 100644 --- a/frontend/src/components/Sidebar/ActiveWorkspaces/index.jsx +++ b/frontend/src/components/Sidebar/ActiveWorkspaces/index.jsx @@ -7,10 +7,17 @@ import ManageWorkspace, { } from "../../Modals/ManageWorkspace"; import paths from "@/utils/paths"; import { useParams } from "react-router-dom"; -import { GearSix, SquaresFour, UploadSimple } from "@phosphor-icons/react"; +import { + GearSix, + SquaresFour, + UploadSimple, + DotsSixVertical, +} from "@phosphor-icons/react"; import useUser from "@/hooks/useUser"; import ThreadContainer from "./ThreadContainer"; import { Link, useMatch } from "react-router-dom"; +import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd"; +import showToast from "@/utils/toast"; export default function ActiveWorkspaces() { const { slug } = useParams(); @@ -25,7 +32,7 @@ export default function ActiveWorkspaces() { async function getWorkspaces() { const workspaces = await Workspace.all(); setLoading(false); - setWorkspaces(workspaces); + setWorkspaces(Workspace.orderWorkspaces(workspaces)); } getWorkspaces(); }, []); @@ -44,108 +51,167 @@ export default function ActiveWorkspaces() { ); } + /** + * Reorders workspaces in the UI via localstorage on client side. + * @param {number} startIndex - the index of the workspace to move + * @param {number} endIndex - the index to move the workspace to + */ + function reorderWorkspaces(startIndex, endIndex) { + const reorderedWorkspaces = Array.from(workspaces); + const [removed] = reorderedWorkspaces.splice(startIndex, 1); + reorderedWorkspaces.splice(endIndex, 0, removed); + setWorkspaces(reorderedWorkspaces); + const success = Workspace.storeWorkspaceOrder( + reorderedWorkspaces.map((w) => w.id) + ); + if (!success) { + showToast("Failed to reorder workspaces", "error"); + Workspace.all().then((workspaces) => setWorkspaces(workspaces)); + } + } + + const onDragEnd = (result) => { + if (!result.destination) return; + reorderWorkspaces(result.source.index, result.destination.index); + }; + return ( -
- {workspaces.map((workspace) => { - const isActive = workspace.slug === slug; - return ( + + + {(provided) => (
-
- - )} -
- -
- {isActive && ( - + + ); + })} + {provided.placeholder} + {showing && ( + )}
- ); - })} - {showing && ( - - )} - + )} + + ); } diff --git a/frontend/src/models/workspace.js b/frontend/src/models/workspace.js index c27c087804c..c9f72084c1e 100644 --- a/frontend/src/models/workspace.js +++ b/frontend/src/models/workspace.js @@ -1,11 +1,13 @@ import { API_BASE } from "@/utils/constants"; -import { baseHeaders } from "@/utils/request"; +import { baseHeaders, safeJsonParse } from "@/utils/request"; import { fetchEventSource } from "@microsoft/fetch-event-source"; import WorkspaceThread from "@/models/workspaceThread"; import { v4 } from "uuid"; import { ABORT_STREAM_EVENT } from "@/utils/chat"; const Workspace = { + workspaceOrderStorageKey: "anythingllm-workspace-order", + new: async function (data = {}) { const { workspace, message } = await fetch(`${API_BASE}/workspace/new`, { method: "POST", @@ -469,6 +471,43 @@ const Workspace = { ); return response.ok; }, + + /** + * Reorders workspaces in the UI via localstorage on client side. + * @param {string[]} workspaceIds - array of workspace ids to reorder + * @returns {boolean} + */ + storeWorkspaceOrder: function (workspaceIds = []) { + try { + localStorage.setItem( + this.workspaceOrderStorageKey, + JSON.stringify(workspaceIds) + ); + return true; + } catch (error) { + console.error("Error reordering workspaces:", error); + return false; + } + }, + + /** + * Orders workspaces based on the order preference stored in localstorage + * @param {Array} workspaces - array of workspace JSON objects + * @returns {Array} - ordered workspaces + */ + orderWorkspaces: function (workspaces = []) { + const workspaceOrderPreference = + safeJsonParse(localStorage.getItem(this.workspaceOrderStorageKey)) || []; + if (workspaceOrderPreference.length === 0) return workspaces; + const orderedWorkspaces = Array.from(workspaces); + orderedWorkspaces.sort( + (a, b) => + workspaceOrderPreference.indexOf(a.id) - + workspaceOrderPreference.indexOf(b.id) + ); + return orderedWorkspaces; + }, + threads: WorkspaceThread, }; diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 0e26588a87d..42190665ee9 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -183,6 +183,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/runtime@^7.15.4", "@babel/runtime@^7.9.2": + version "7.26.10" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.10.tgz#a07b4d8fa27af131a633d7b3524db803eb4764c2" + integrity sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.9", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7": version "7.24.8" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.8.tgz#5d958c3827b13cc6d05e038c07fb2e5e3420d82e" @@ -702,6 +709,14 @@ resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.11.tgz#56588b17ae8f50c53983a524fc3cc47437969d64" integrity sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA== +"@types/hoist-non-react-statics@^3.3.0": + version "3.3.6" + resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.6.tgz#6bba74383cdab98e8db4e20ce5b4a6b98caed010" + integrity sha512-lPByRJUer/iN/xa4qpyL0qmL11DqNW81iU/IG1S3uvRUq4oKagz8VCxZjiWkumgt66YT3vOdDgZ0o32sGKtCEw== + dependencies: + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + "@types/node@>=13.7.0": version "22.1.0" resolved "https://registry.yarnpkg.com/@types/node/-/node-22.1.0.tgz#6d6adc648b5e03f0e83c78dc788c2b037d0ad94b" @@ -721,6 +736,16 @@ dependencies: "@types/react" "*" +"@types/react-redux@^7.1.20": + version "7.1.34" + resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.34.tgz#83613e1957c481521e6776beeac4fd506d11bd0e" + integrity sha512-GdFaVjEbYv4Fthm2ZLvj1VSCedV7TqE5y1kNwnjSdBOTXuRSgowux6J8TAct15T3CKBr63UMk+2CO7ilRhyrAQ== + dependencies: + "@types/hoist-non-react-statics" "^3.3.0" + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + redux "^4.0.0" + "@types/react-router-dom@^5.3.3": version "5.3.3" resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.3.3.tgz#e9d6b4a66fcdbd651a5f106c2656a30088cc1e83" @@ -1163,6 +1188,13 @@ cross-spawn@^7.0.1: shebang-command "^2.0.0" which "^2.0.1" +css-box-model@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1" + integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw== + dependencies: + tiny-invariant "^1.0.6" + css-line-break@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/css-line-break/-/css-line-break-2.1.0.tgz#bfef660dfa6f5397ea54116bb3cb4873edbc4fa0" @@ -2049,6 +2081,13 @@ highlight.js@^11.9.0: resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.10.0.tgz#6e3600dc4b33d6dc23d5bd94fbf72405f5892b92" integrity sha512-SYVnVFswQER+zu1laSya563s+F8VDGt7o35d4utbamowvUNLLMovFqwCLSocpZTz3MgaSRA1IbqRWZv97dtErQ== +hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" + integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== + dependencies: + react-is "^16.7.0" + html-parse-stringify@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz#dfc1017347ce9f77c8141a507f233040c59c55d2" @@ -2520,6 +2559,11 @@ mdurl@^1.0.1: resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" integrity sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g== +memoize-one@^5.1.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" + integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== + merge2@^1.3.0: version "1.4.1" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" @@ -2870,7 +2914,7 @@ prettier@^3.0.3: resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.3.tgz#30c54fe0be0d8d12e6ae61dbb10109ea00d53105" integrity sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew== -prop-types@^15.6.2, prop-types@^15.8.1: +prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -2907,6 +2951,24 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +raf-schd@^4.0.2: + version "4.0.3" + resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a" + integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ== + +react-beautiful-dnd@13.1.1: + version "13.1.1" + resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz#b0f3087a5840920abf8bb2325f1ffa46d8c4d0a2" + integrity sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ== + dependencies: + "@babel/runtime" "^7.9.2" + css-box-model "^1.2.0" + memoize-one "^5.1.1" + raf-schd "^4.0.2" + react-redux "^7.2.0" + redux "^4.0.4" + use-memo-one "^1.1.1" + react-day-picker@^8.10.1: version "8.10.1" resolved "https://registry.yarnpkg.com/react-day-picker/-/react-day-picker-8.10.1.tgz#4762ec298865919b93ec09ba69621580835b8e80" @@ -2944,16 +3006,33 @@ react-i18next@^14.1.1: "@babel/runtime" "^7.23.9" html-parse-stringify "^3.0.1" -react-is@^16.10.2, react-is@^16.13.1: +react-is@^16.10.2, react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== +react-is@^17.0.2: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" + integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== + react-loading-skeleton@^3.1.0: version "3.4.0" resolved "https://registry.yarnpkg.com/react-loading-skeleton/-/react-loading-skeleton-3.4.0.tgz#c71a3a17259d08e4064974aa0b07f150a09dfd57" integrity sha512-1oJEBc9+wn7BbkQQk7YodlYEIjgeR+GrRjD+QXkVjwZN7LGIcAFHrx4NhT7UHGBxNY1+zax3c+Fo6XQM4R7CgA== +react-redux@^7.2.0: + version "7.2.9" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.9.tgz#09488fbb9416a4efe3735b7235055442b042481d" + integrity sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ== + dependencies: + "@babel/runtime" "^7.15.4" + "@types/react-redux" "^7.1.20" + hoist-non-react-statics "^3.3.2" + loose-envify "^1.4.0" + prop-types "^15.7.2" + react-is "^17.0.2" + react-refresh@^0.14.2: version "0.14.2" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.2.tgz#3833da01ce32da470f1f936b9d477da5c7028bf9" @@ -3072,6 +3151,13 @@ recharts@^2.12.5, recharts@^2.12.7: tiny-invariant "^1.3.1" victory-vendor "^36.6.8" +redux@^4.0.0, redux@^4.0.4: + version "4.2.1" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.1.tgz#c08f4306826c49b5e9dc901dee0452ea8fce6197" + integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w== + dependencies: + "@babel/runtime" "^7.9.2" + reflect.getprototypeof@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz#3ab04c32a8390b770712b7a8633972702d278859" @@ -3599,7 +3685,7 @@ thenify-all@^1.0.0: dependencies: any-promise "^1.0.0" -tiny-invariant@^1.3.1: +tiny-invariant@^1.0.6, tiny-invariant@^1.3.1: version "1.3.3" resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127" integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg== @@ -3727,6 +3813,11 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +use-memo-one@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.3.tgz#2fd2e43a2169eabc7496960ace8c79efef975e99" + integrity sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ== + util-deprecate@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"