From affcd33dff64b28695cd171b227e68c0dcda324e Mon Sep 17 00:00:00 2001 From: PhilReact Date: Sun, 20 Oct 2024 17:07:53 +0300 Subject: [PATCH] sortable pinned apps --- package-lock.json | 76 ++++++++ package.json | 3 + src/App.tsx | 2 + src/atoms/global.ts | 7 + src/components/Apps/AppViewer.tsx | 30 +++- src/components/Apps/AppViewerContainer.tsx | 14 +- src/components/Apps/Apps.tsx | 45 +++-- src/components/Apps/AppsHome.tsx | 149 +--------------- src/components/Apps/AppsLibrary.tsx | 2 +- src/components/Apps/AppsNavBar.tsx | 138 ++++++++++++++- src/components/Apps/SortablePinnedApps.tsx | 166 ++++++++++++++++++ .../Apps/useQortalMessageListener.tsx | 11 +- src/components/Mobile/MobileHeader.tsx | 2 + src/components/Save/Save.tsx | 16 ++ src/main.tsx | 4 +- src/useRetrieveDataLocalStorage.tsx | 34 ++++ 16 files changed, 519 insertions(+), 180 deletions(-) create mode 100644 src/atoms/global.ts create mode 100644 src/components/Apps/SortablePinnedApps.tsx create mode 100644 src/components/Save/Save.tsx create mode 100644 src/useRetrieveDataLocalStorage.tsx diff --git a/package-lock.json b/package-lock.json index 1e8c9c9..03cad1f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,8 @@ "version": "0.0.0", "dependencies": { "@chatscope/chat-ui-kit-react": "^2.0.3", + "@dnd-kit/core": "^6.1.0", + "@dnd-kit/sortable": "^8.0.0", "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.0", "@mui/icons-material": "^5.16.4", @@ -53,6 +55,7 @@ "react-redux": "^9.1.2", "react-virtualized": "^9.22.5", "react-virtuoso": "^4.10.4", + "recoil": "^0.7.7", "short-unique-id": "^5.2.0", "slate": "^0.103.0", "slate-react": "^0.109.0", @@ -483,6 +486,55 @@ "resolved": "https://registry.npmjs.org/@chatscope/chat-ui-kit-styles/-/chat-ui-kit-styles-1.4.0.tgz", "integrity": "sha512-016mBJD3DESw7Nh+lkKcPd22xG92ghA0VpIXIbjQtmXhC7Ve6wRazTy8z1Ahut+Tbv179+JxrftuMngsj/yV8Q==" }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.0.tgz", + "integrity": "sha512-ea7IkhKvlJUv9iSHJOnxinBcoOI3ppGnnL+VDJ75O45Nss6HtZd8IdN8touXPDtASfeI2T2LImb8VOZcL47wjQ==", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.1.0.tgz", + "integrity": "sha512-J3cQBClB4TVxwGo3KEjssGEXNJqGVWx17aRTZ1ob0FliR5IjYgTxl5YJbKTzA6IzrtelotH19v6y7uoIRUZPSg==", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.0", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-8.0.0.tgz", + "integrity": "sha512-U3jk5ebVXe1Lr7c2wU7SBZjcWdQP+j7peHJfCspnA81enlu88Mgd7CC8Q+pub9ubP7eKVETzJW+IBAhsqbSu/g==", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.1.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@emotion/babel-plugin": { "version": "11.11.0", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", @@ -5007,6 +5059,11 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "node_modules/hamt_plus": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/hamt_plus/-/hamt_plus-1.0.2.tgz", + "integrity": "sha512-t2JXKaehnMb9paaYA7J0BX8QQAY8lwfQ9Gjf4pg/mk4krt+cmwmU652HOoWonf+7+EQV97ARPMhhVgU1ra2GhA==" + }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -9416,6 +9473,25 @@ "react-dom": ">=16 || >=17 || >= 18" } }, + "node_modules/recoil": { + "version": "0.7.7", + "resolved": "https://registry.npmjs.org/recoil/-/recoil-0.7.7.tgz", + "integrity": "sha512-8Og5KPQW9LwC577Vc7Ug2P0vQshkv1y3zG3tSSkWMqkWSwHmE+by06L8JtnGocjW6gcCvfwB3YtrJG6/tWivNQ==", + "dependencies": { + "hamt_plus": "1.0.2" + }, + "peerDependencies": { + "react": ">=16.13.1" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", diff --git a/package.json b/package.json index 07e3164..185fe71 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,8 @@ }, "dependencies": { "@chatscope/chat-ui-kit-react": "^2.0.3", + "@dnd-kit/core": "^6.1.0", + "@dnd-kit/sortable": "^8.0.0", "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.0", "@mui/icons-material": "^5.16.4", @@ -57,6 +59,7 @@ "react-redux": "^9.1.2", "react-virtualized": "^9.22.5", "react-virtuoso": "^4.10.4", + "recoil": "^0.7.7", "short-unique-id": "^5.2.0", "slate": "^0.103.0", "slate-react": "^0.109.0", diff --git a/src/App.tsx b/src/App.tsx index 53d88e8..48aaf6a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -98,6 +98,7 @@ import { DrawerComponent } from "./components/Drawer/Drawer"; import { AddressQRCode } from "./components/AddressQRCode"; import { Settings } from "./components/Group/Settings"; import { MainAvatar } from "./components/MainAvatar"; +import { useRetrieveDataLocalStorage } from "./useRetrieveDataLocalStorage"; type extStates = | "not-authenticated" @@ -322,6 +323,7 @@ function App() { const [rootHeight, setRootHeight] = useState("100%"); const [isSettingsOpen, setIsSettingsOpen] = useState(false); const qortalRequestCheckbox1Ref = useRef(null); + useRetrieveDataLocalStorage() useEffect(() => { if (!isMobile) return; // Function to set the height of the app to the viewport height diff --git a/src/atoms/global.ts b/src/atoms/global.ts new file mode 100644 index 0000000..54d4186 --- /dev/null +++ b/src/atoms/global.ts @@ -0,0 +1,7 @@ +import { atom } from 'recoil'; + + +export const sortablePinnedAppsAtom = atom({ + key: 'sortablePinnedAppsFromAtom', + default: [], +}); \ No newline at end of file diff --git a/src/components/Apps/AppViewer.tsx b/src/components/Apps/AppViewer.tsx index 3f9486d..ba371ae 100644 --- a/src/components/Apps/AppViewer.tsx +++ b/src/components/Apps/AppViewer.tsx @@ -20,7 +20,7 @@ import { MyContext, getBaseApiReact } from "../../App"; import LogoSelected from "../../assets/svgs/LogoSelected.svg"; import { Spacer } from "../../common/Spacer"; -import { executeEvent } from "../../utils/events"; +import { executeEvent, subscribeToEvent, unsubscribeFromEvent } from "../../utils/events"; import { useFrame } from "react-frame-component"; import { useQortalMessageListener } from "./useQortalMessageListener"; @@ -31,20 +31,40 @@ export const AppViewer = ({ app }) => { const { rootHeight } = useContext(MyContext); const iframeRef = useRef(null); const { document, window } = useFrame(); - useQortalMessageListener(window) + const {path} = useQortalMessageListener(window) + const [url, setUrl] = useState('') - const url = useMemo(()=> { - return `${getBaseApiReact()}/render/${app?.service}/${app?.name}${app?.path != null ? app?.path : ''}?theme=dark&identifier=${(app?.identifier != null && app?.identifier != 'null') ? app?.identifier : ''}` + useEffect(()=> { + setUrl(`${getBaseApiReact()}/render/${app?.service}/${app?.name}${app?.path != null ? app?.path : ''}?theme=dark&identifier=${(app?.identifier != null && app?.identifier != 'null') ? app?.identifier : ''}`) }, [app?.service, app?.name, app?.identifier, app?.path]) + const defaultUrl = useMemo(()=> { + return url + }, [url]) + const refreshAppFunc = (e) => { + const {tabId} = e.detail + if(tabId === app?.tabId){ + const constructUrl = `${getBaseApiReact()}/render/${app?.service}/${app?.name}${path != null ? path : ''}?theme=dark&identifier=${app?.identifier != null ? app?.identifier : ''}&time=${new Date().getMilliseconds()}` + setUrl(constructUrl) + } + }; + + useEffect(() => { + subscribeToEvent("refreshApp", refreshAppFunc); + + return () => { + unsubscribeFromEvent("refreshApp", refreshAppFunc); + }; + }, [app, path]); + return ( ); diff --git a/src/components/Apps/AppViewerContainer.tsx b/src/components/Apps/AppViewerContainer.tsx index 4152ab2..98303ad 100644 --- a/src/components/Apps/AppViewerContainer.tsx +++ b/src/components/Apps/AppViewerContainer.tsx @@ -2,12 +2,24 @@ import React, { useContext, useEffect, useRef } from 'react' import { AppViewer } from './AppViewer' import Frame from 'react-frame-component'; import { MyContext } from '../../App'; +import { subscribeToEvent, unsubscribeFromEvent } from '../../utils/events'; const AppViewerContainer = ({app, isSelected, hide}) => { const { rootHeight } = useContext(MyContext); const frameRef = useRef(null); - + const refreshAppFunc = (e) => { + console.log('getting refresh', e) + }; + + // useEffect(() => { + // subscribeToEvent("refreshAPp", refreshAppFunc); + + // return () => { + // unsubscribeFromEvent("refreshApp", refreshAppFunc); + // }; + // }, []); + return ( diff --git a/src/components/Apps/Apps.tsx b/src/components/Apps/Apps.tsx index 1c532c2..0a6ea90 100644 --- a/src/components/Apps/Apps.tsx +++ b/src/components/Apps/Apps.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useEffect, useRef, useState } from "react"; +import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; import { AppsHome } from "./AppsHome"; import { Spacer } from "../../common/Spacer"; import { MyContext, getBaseApiReact } from "../../App"; @@ -15,19 +15,27 @@ import { AppViewer } from "./AppViewer"; import AppViewerContainer from "./AppViewerContainer"; import ShortUniqueId from "short-unique-id"; import { AppPublish } from "./AppPublish"; +import { useRecoilState } from "recoil"; const uid = new ShortUniqueId({ length: 8 }); export const Apps = ({ mode, setMode, show , myName}) => { const [availableQapps, setAvailableQapps] = useState([]); - const [downloadedQapps, setDownloadedQapps] = useState([]); const [selectedAppInfo, setSelectedAppInfo] = useState(null); const [tabs, setTabs] = useState([]); const [selectedTab, setSelectedTab] = useState(null); const [isNewTabWindow, setIsNewTabWindow] = useState(false); const [categories, setCategories] = useState([]) - const [myApp, setMyApp] = useState(null) - const [myWebsite, setMyWebsite] = useState(null) + + + const myApp = useMemo(()=> { + + return availableQapps.find((app)=> app.name === myName && app.service === 'APP') + }, [myName, availableQapps]) + const myWebsite = useMemo(()=> { + + return availableQapps.find((app)=> app.name === myName && app.service === 'WEBSITE') + }, [myName, availableQapps]) useEffect(() => { setTimeout(() => { @@ -62,12 +70,12 @@ export const Apps = ({ mode, setMode, show , myName}) => { } }, []); - const getQapps = React.useCallback(async (myName) => { + const getQapps = React.useCallback(async () => { try { let apps = []; let websites = []; // dispatch(setIsLoadingGlobal(true)) - const url = `${getBaseApiReact()}/arbitrary/resources/search?service=APP&mode=ALL&includestatus=true&limit=0&includemetadata=true`; + const url = `${getBaseApiReact()}/arbitrary/resources/search?service=APP&mode=ALL&limit=0&includestatus=true&includemetadata=true`; const response = await fetch(url, { method: "GET", @@ -77,7 +85,8 @@ export const Apps = ({ mode, setMode, show , myName}) => { }); if (!response?.ok) return; const responseData = await response.json(); - const urlWebsites = `${getBaseApiReact()}/arbitrary/resources/search?service=WEBSITE&mode=ALL&includestatus=true&limit=0&includemetadata=true`; + console.log('responseData', responseData) + const urlWebsites = `${getBaseApiReact()}/arbitrary/resources/search?service=WEBSITE&mode=ALL&limit=0&includestatus=true&includemetadata=true`; const responseWebsites = await fetch(urlWebsites, { method: "GET", @@ -87,31 +96,20 @@ export const Apps = ({ mode, setMode, show , myName}) => { }); if (!responseWebsites?.ok) return; const responseDataWebsites = await responseWebsites.json(); - const findMyWebsite = responseDataWebsites.find((web)=> web.name === myName) - if(findMyWebsite){ - setMyWebsite(findMyWebsite) - } - const findMyApp = responseData.find((web)=> web.name === myName) - console.log('findMyApp', findMyApp) - if(findMyApp){ - setMyWebsite(findMyApp) - } + apps = responseData; websites = responseDataWebsites; const combine = [...apps, ...websites]; setAvailableQapps(combine); - setDownloadedQapps( - combine.filter((qapp) => qapp?.status?.status === "READY") - ); } catch (error) { } finally { // dispatch(setIsLoadingGlobal(false)) } }, []); useEffect(() => { - getQapps(myName); + getQapps(); getCategories() - }, [getQapps, getCategories, myName]); + }, [getQapps, getCategories]); const selectedAppInfoFunc = (e) => { const data = e.detail?.data; @@ -256,11 +254,10 @@ export const Apps = ({ mode, setMode, show , myName}) => { > {mode !== "viewer" && } {mode === "home" && ( - + )} {mode === "library" && ( { {isNewTabWindow && mode === "viewer" && ( <> - + )} {mode !== "viewer" && } diff --git a/src/components/Apps/AppsHome.tsx b/src/components/Apps/AppsHome.tsx index d9142a1..2927694 100644 --- a/src/components/Apps/AppsHome.tsx +++ b/src/components/Apps/AppsHome.tsx @@ -11,8 +11,9 @@ import { Add } from "@mui/icons-material"; import { getBaseApiReact } from "../../App"; import LogoSelected from "../../assets/svgs/LogoSelected.svg"; import { executeEvent } from "../../utils/events"; +import { SortablePinnedApps } from "./SortablePinnedApps"; -export const AppsHome = ({ downloadedQapps, setMode, myApp, myWebsite, myName }) => { +export const AppsHome = ({ setMode, myApp, myWebsite, availableQapps }) => { return ( Add - {myApp &&( - { - executeEvent("addTab", { - data: myApp - }) - }} - > - - - - center-icon - - - - {myApp?.name} - - - - )} - {myWebsite &&( - { - executeEvent("addTab", { - data: myWebsite - }) - }} - > - - - - center-icon - - - - {myWebsite?.name} - - - - )} - {downloadedQapps?.filter((item)=> item?.name !== myName).map((app) => { - return ( - { - executeEvent("addTab", { - data: app - }) - }} - > - - - - center-icon - - - - {app?.name} - - - - ); - })} + + + ); }; diff --git a/src/components/Apps/AppsLibrary.tsx b/src/components/Apps/AppsLibrary.tsx index d220a4e..78f083a 100644 --- a/src/components/Apps/AppsLibrary.tsx +++ b/src/components/Apps/AppsLibrary.tsx @@ -74,7 +74,7 @@ const ScrollerStyled = styled('div')({ "-ms-overflow-style": "none", }); -export const AppsLibrary = ({ downloadedQapps, availableQapps, setMode, myName, hasPublishApp }) => { +export const AppsLibrary = ({ availableQapps, setMode, myName, hasPublishApp }) => { const [searchValue, setSearchValue] = useState(""); const virtuosoRef = useRef(); const { rootHeight } = useContext(MyContext); diff --git a/src/components/Apps/AppsNavBar.tsx b/src/components/Apps/AppsNavBar.tsx index 09c4e22..0a703cf 100644 --- a/src/components/Apps/AppsNavBar.tsx +++ b/src/components/Apps/AppsNavBar.tsx @@ -7,15 +7,41 @@ import { import NavBack from "../../assets/svgs/NavBack.svg"; import NavAdd from "../../assets/svgs/NavAdd.svg"; import NavMoreMenu from "../../assets/svgs/NavMoreMenu.svg"; -import { ButtonBase, Tab, Tabs } from "@mui/material"; +import { ButtonBase, ListItemIcon, ListItemText, Menu, MenuItem, Tab, Tabs } from "@mui/material"; import { executeEvent, subscribeToEvent, unsubscribeFromEvent } from "../../utils/events"; import TabComponent from "./TabComponent"; +import PushPinIcon from '@mui/icons-material/PushPin'; +import RefreshIcon from '@mui/icons-material/Refresh'; +import { useRecoilState } from "recoil"; +import { sortablePinnedAppsAtom } from "../../atoms/global"; + +export function saveToLocalStorage(key, value) { + try { + const serializedValue = JSON.stringify(value); + localStorage.setItem(key, serializedValue); + console.log(`Data saved to localStorage with key: ${key}`); + } catch (error) { + console.error('Error saving to localStorage:', error); + } +} + export const AppsNavBar = () => { const [tabs, setTabs] = useState([]) const [selectedTab, setSelectedTab] = useState([]) const [isNewTabWindow, setIsNewTabWindow] = useState(false) const tabsRef = useRef(null); + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + const [sortablePinnedApps, setSortablePinnedApps] = useRecoilState(sortablePinnedAppsAtom); + + const handleClick = (event) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; useEffect(() => { // Scroll to the last tab whenever the tabs array changes (e.g., when a new tab is added) @@ -44,6 +70,8 @@ export const AppsNavBar = () => { unsubscribeFromEvent("setTabsToNav", setTabsToNav); }; }, []); + + const isSelectedAppPinned = !!sortablePinnedApps?.find((item)=> item?.name === selectedTab?.name && item?.service === selectedTab?.service) return ( @@ -95,13 +123,119 @@ export const AppsNavBar = () => { width: '40px' }} src={NavAdd} /> - + { + handleClick(e) + }}> + + { + if (!selectedTab) return; + + setSortablePinnedApps((prev) => { + let updatedApps; + + if (isSelectedAppPinned) { + // Remove the selected app if it is pinned + updatedApps = prev.filter( + (item) => !(item?.name === selectedTab?.name && item?.service === selectedTab?.service) + ); + } else { + // Add the selected app if it is not pinned + updatedApps = [...prev, { + name: selectedTab?.name, + service: selectedTab?.service, + }]; + } + + saveToLocalStorage('sortablePinnedApps', updatedApps) + return updatedApps; + }); + + handleClose(); + }} + > + + + + + + { + executeEvent('refreshApp', { + tabId: selectedTab?.tabId + }) + handleClose(); + }} + > + + + + + + ); }; diff --git a/src/components/Apps/SortablePinnedApps.tsx b/src/components/Apps/SortablePinnedApps.tsx new file mode 100644 index 0000000..374cb1d --- /dev/null +++ b/src/components/Apps/SortablePinnedApps.tsx @@ -0,0 +1,166 @@ +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import { DndContext, closestCenter } from '@dnd-kit/core'; +import { arrayMove, SortableContext, sortableKeyboardCoordinates, useSortable } from '@dnd-kit/sortable'; +import { KeyboardSensor, PointerSensor, TouchSensor, useSensor, useSensors } from '@dnd-kit/core'; +import { CSS } from '@dnd-kit/utilities'; +import { Avatar, ButtonBase } from '@mui/material'; +import { AppCircle, AppCircleContainer, AppCircleLabel } from './Apps-styles'; +import { getBaseApiReact } from '../../App'; +import { executeEvent } from '../../utils/events'; +import { sortablePinnedAppsAtom } from '../../atoms/global'; +import { useRecoilState } from 'recoil'; +import { saveToLocalStorage } from './AppsNavBar'; + +const SortableItem = ({ id, name, app }) => { + const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id }); + console.log('namednd', name) + const style = { + transform: CSS.Transform.toString(transform), + transition, + padding: '10px', + border: '1px solid #ccc', + marginBottom: '5px', + borderRadius: '4px', + backgroundColor: '#f9f9f9', + cursor: 'grab', + color: 'black' + }; + + return ( + { + executeEvent("addTab", { + data: app + }) + }} + > + + + + center-icon + + + + {app?.metadata?.title || app?.name} + + + + ); +}; + +export const SortablePinnedApps = ({ myWebsite, myApp, availableQapps = [] }) => { + const [pinnedApps, setPinnedApps] = useRecoilState(sortablePinnedAppsAtom); + + const transformPinnedApps = useMemo(()=> { + console.log({myWebsite, myApp, availableQapps, pinnedApps}) + let pinned = [...pinnedApps] + const findMyWebsite = pinned?.find((item)=> item?.service === myWebsite?.service && item?.name === myWebsite?.name) + const findMyApp = pinned?.find((item)=> item?.service === myApp?.service && item?.name === myApp?.name) + + if(myWebsite && !findMyWebsite){ + pinned.unshift(myWebsite) + } + if(myApp && !findMyApp){ + pinned.unshift(myApp) + } + pinned = pinned.map((pin)=> { + const findIndex = availableQapps?.findIndex((item)=> item?.service === pin?.service && item?.name === pin?.name) + if(findIndex !== -1) return availableQapps[findIndex] + + return pin + }) + return pinned + }, [myApp, myWebsite, pinnedApps, availableQapps]) + console.log('transformPinnedApps', transformPinnedApps) + // const hasSetPinned = useRef(false) + // useEffect(() => { + // if (!apps || apps.length === 0) return; + + // setPinnedApps((prevPinnedApps) => { + // // Create a map of the previous pinned apps for easy lookup + // const pinnedAppsMap = new Map(prevPinnedApps.map(app => [`${app?.service}-${app?.name}`, app])); + + // // Update the pinnedApps list based on new apps + // const updatedPinnedApps = apps.map(app => { + // const id = `${app?.service}-${app?.name}`; + // // Keep the existing app from pinnedApps if it exists + // return pinnedAppsMap.get(id) || app; + // }); + + // return updatedPinnedApps; + // }); + // }, [apps]); + + console.log('dnd',{pinnedApps}) + const sensors = useSensors( + useSensor(PointerSensor, { + activationConstraint: { + distance: 10, // Set a distance to avoid triggering drag on small movements + }, + }), + useSensor(TouchSensor, { + activationConstraint: { + distance: 10, // Also apply to touch + }, + }), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }) + ); + + const handleDragEnd = (event) => { + const { active, over } = event; + + if (!over) return; // Make sure the drop target exists + + if (active.id !== over.id) { + const oldIndex = transformPinnedApps.findIndex((item) => `${item?.service}-${item?.name}` === active.id); + const newIndex = transformPinnedApps.findIndex((item) => `${item?.service}-${item?.name}` === over.id); + + const newOrder = arrayMove(transformPinnedApps, oldIndex, newIndex); + setPinnedApps(newOrder); + saveToLocalStorage('sortablePinnedApps', newOrder) + + } + }; + return ( + + `${app?.service}-${app?.name}`)}> + {transformPinnedApps.map((app) => ( + + ))} + + + ); +}; + diff --git a/src/components/Apps/useQortalMessageListener.tsx b/src/components/Apps/useQortalMessageListener.tsx index 270a540..c9ace3f 100644 --- a/src/components/Apps/useQortalMessageListener.tsx +++ b/src/components/Apps/useQortalMessageListener.tsx @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; class Semaphore { constructor(count) { @@ -313,6 +313,7 @@ const UIQortalRequests = [ } export const useQortalMessageListener = (frameWindow) => { + const [path, setPath] = useState('') useEffect(() => { console.log("Listener added react"); @@ -376,7 +377,11 @@ export const useQortalMessageListener = (frameWindow) => { error: 'Failed to prepare data for publishing', }); } - } + } else if(event?.data?.action === 'LINK_TO_QDN_RESOURCE' || + event?.data?.action === 'QDN_RESOURCE_DISPLAYED'){ + const pathUrl = event?.data?.path != null ? (event?.data?.path.startsWith('/') ? '' : '/') + event?.data?.path : null + setPath(pathUrl) + } }; // Add the listener for messages coming from the frameWindow @@ -401,5 +406,7 @@ export const useQortalMessageListener = (frameWindow) => { return true; // Keep the message channel open for async response } }); + + return {path} }; diff --git a/src/components/Mobile/MobileHeader.tsx b/src/components/Mobile/MobileHeader.tsx index 38e6f73..00918cb 100644 --- a/src/components/Mobile/MobileHeader.tsx +++ b/src/components/Mobile/MobileHeader.tsx @@ -19,6 +19,7 @@ import { ArrowDownIcon } from "../../assets/Icons/ArrowDownIcon"; import { MessagingIcon } from "../../assets/Icons/MessagingIcon"; import { MessagingIcon2 } from "../../assets/Icons/MessagingIcon2"; import { HubsIcon } from "../../assets/Icons/HubsIcon"; +import { Save } from "../Save/Save"; const Header = ({ logoutFunc, @@ -135,6 +136,7 @@ const Header = ({ /> + { + const [pinnedApps, setPinnedApps] = useRecoilState(sortablePinnedAppsAtom); + const [oldPinnedApps, setOldPinnedApps] = useState(pinnedApps) + console.log('oldpin', {oldPinnedApps, pinnedApps}) + + const hasChanged = useMemo(()=> { + return !isEqual(pinnedApps, oldPinnedApps) + }, [oldPinnedApps, pinnedApps]) + return ( +
{hasChanged && 'Save'}
+ ) +} diff --git a/src/main.tsx b/src/main.tsx index 8a477a3..2bdf4b3 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -6,7 +6,7 @@ import './index.css' import { ThemeProvider, createTheme } from '@mui/material/styles'; import { CssBaseline } from '@mui/material'; import { MessageQueueProvider } from './MessageQueueContext.tsx'; - +import { RecoilRoot } from 'recoil'; const theme = createTheme({ palette: { primary: { @@ -50,7 +50,9 @@ ReactDOM.createRoot(document.getElementById('root')!).render( + + , diff --git a/src/useRetrieveDataLocalStorage.tsx b/src/useRetrieveDataLocalStorage.tsx new file mode 100644 index 0000000..93831e0 --- /dev/null +++ b/src/useRetrieveDataLocalStorage.tsx @@ -0,0 +1,34 @@ +import React, { useCallback, useEffect } from 'react' +import { useSetRecoilState } from 'recoil'; +import { sortablePinnedAppsAtom } from './atoms/global'; + +function fetchFromLocalStorage(key) { + try { + const serializedValue = localStorage.getItem(key); + if (serializedValue === null) { + console.log(`No data found for key: ${key}`); + return null; + } + return JSON.parse(serializedValue); + } catch (error) { + console.error('Error fetching from localStorage:', error); + return null; + } +} + +export const useRetrieveDataLocalStorage = () => { + const setSortablePinnedApps = useSetRecoilState(sortablePinnedAppsAtom); + + + const getSortablePinnedApps = useCallback(()=> { + const pinnedAppsLocal = fetchFromLocalStorage('sortablePinnedApps') + if(pinnedAppsLocal){ + setSortablePinnedApps(pinnedAppsLocal) + } + }, []) + useEffect(()=> { + + getSortablePinnedApps() + }, [getSortablePinnedApps]) + +}