mirror of
https://github.com/Qortal/Qortal-Hub.git
synced 2025-04-23 19:37:52 +00:00
sortable pinned apps
This commit is contained in:
parent
ba53e83b13
commit
affcd33dff
76
package-lock.json
generated
76
package-lock.json
generated
@ -9,6 +9,8 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chatscope/chat-ui-kit-react": "^2.0.3",
|
"@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/react": "^11.11.4",
|
||||||
"@emotion/styled": "^11.11.0",
|
"@emotion/styled": "^11.11.0",
|
||||||
"@mui/icons-material": "^5.16.4",
|
"@mui/icons-material": "^5.16.4",
|
||||||
@ -53,6 +55,7 @@
|
|||||||
"react-redux": "^9.1.2",
|
"react-redux": "^9.1.2",
|
||||||
"react-virtualized": "^9.22.5",
|
"react-virtualized": "^9.22.5",
|
||||||
"react-virtuoso": "^4.10.4",
|
"react-virtuoso": "^4.10.4",
|
||||||
|
"recoil": "^0.7.7",
|
||||||
"short-unique-id": "^5.2.0",
|
"short-unique-id": "^5.2.0",
|
||||||
"slate": "^0.103.0",
|
"slate": "^0.103.0",
|
||||||
"slate-react": "^0.109.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",
|
"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=="
|
"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": {
|
"node_modules/@emotion/babel-plugin": {
|
||||||
"version": "11.11.0",
|
"version": "11.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz",
|
||||||
@ -5007,6 +5059,11 @@
|
|||||||
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
|
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/has-flag": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
|
||||||
@ -9416,6 +9473,25 @@
|
|||||||
"react-dom": ">=16 || >=17 || >= 18"
|
"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": {
|
"node_modules/redent": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
|
||||||
|
@ -13,6 +13,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chatscope/chat-ui-kit-react": "^2.0.3",
|
"@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/react": "^11.11.4",
|
||||||
"@emotion/styled": "^11.11.0",
|
"@emotion/styled": "^11.11.0",
|
||||||
"@mui/icons-material": "^5.16.4",
|
"@mui/icons-material": "^5.16.4",
|
||||||
@ -57,6 +59,7 @@
|
|||||||
"react-redux": "^9.1.2",
|
"react-redux": "^9.1.2",
|
||||||
"react-virtualized": "^9.22.5",
|
"react-virtualized": "^9.22.5",
|
||||||
"react-virtuoso": "^4.10.4",
|
"react-virtuoso": "^4.10.4",
|
||||||
|
"recoil": "^0.7.7",
|
||||||
"short-unique-id": "^5.2.0",
|
"short-unique-id": "^5.2.0",
|
||||||
"slate": "^0.103.0",
|
"slate": "^0.103.0",
|
||||||
"slate-react": "^0.109.0",
|
"slate-react": "^0.109.0",
|
||||||
|
@ -98,6 +98,7 @@ import { DrawerComponent } from "./components/Drawer/Drawer";
|
|||||||
import { AddressQRCode } from "./components/AddressQRCode";
|
import { AddressQRCode } from "./components/AddressQRCode";
|
||||||
import { Settings } from "./components/Group/Settings";
|
import { Settings } from "./components/Group/Settings";
|
||||||
import { MainAvatar } from "./components/MainAvatar";
|
import { MainAvatar } from "./components/MainAvatar";
|
||||||
|
import { useRetrieveDataLocalStorage } from "./useRetrieveDataLocalStorage";
|
||||||
|
|
||||||
type extStates =
|
type extStates =
|
||||||
| "not-authenticated"
|
| "not-authenticated"
|
||||||
@ -322,6 +323,7 @@ function App() {
|
|||||||
const [rootHeight, setRootHeight] = useState("100%");
|
const [rootHeight, setRootHeight] = useState("100%");
|
||||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||||
const qortalRequestCheckbox1Ref = useRef(null);
|
const qortalRequestCheckbox1Ref = useRef(null);
|
||||||
|
useRetrieveDataLocalStorage()
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isMobile) return;
|
if (!isMobile) return;
|
||||||
// Function to set the height of the app to the viewport height
|
// Function to set the height of the app to the viewport height
|
||||||
|
7
src/atoms/global.ts
Normal file
7
src/atoms/global.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { atom } from 'recoil';
|
||||||
|
|
||||||
|
|
||||||
|
export const sortablePinnedAppsAtom = atom({
|
||||||
|
key: 'sortablePinnedAppsFromAtom',
|
||||||
|
default: [],
|
||||||
|
});
|
@ -20,7 +20,7 @@ import { MyContext, getBaseApiReact } from "../../App";
|
|||||||
import LogoSelected from "../../assets/svgs/LogoSelected.svg";
|
import LogoSelected from "../../assets/svgs/LogoSelected.svg";
|
||||||
|
|
||||||
import { Spacer } from "../../common/Spacer";
|
import { Spacer } from "../../common/Spacer";
|
||||||
import { executeEvent } from "../../utils/events";
|
import { executeEvent, subscribeToEvent, unsubscribeFromEvent } from "../../utils/events";
|
||||||
import { useFrame } from "react-frame-component";
|
import { useFrame } from "react-frame-component";
|
||||||
import { useQortalMessageListener } from "./useQortalMessageListener";
|
import { useQortalMessageListener } from "./useQortalMessageListener";
|
||||||
|
|
||||||
@ -31,20 +31,40 @@ export const AppViewer = ({ app }) => {
|
|||||||
const { rootHeight } = useContext(MyContext);
|
const { rootHeight } = useContext(MyContext);
|
||||||
const iframeRef = useRef(null);
|
const iframeRef = useRef(null);
|
||||||
const { document, window } = useFrame();
|
const { document, window } = useFrame();
|
||||||
useQortalMessageListener(window)
|
const {path} = useQortalMessageListener(window)
|
||||||
|
const [url, setUrl] = useState('')
|
||||||
|
|
||||||
const url = useMemo(()=> {
|
useEffect(()=> {
|
||||||
return `${getBaseApiReact()}/render/${app?.service}/${app?.name}${app?.path != null ? app?.path : ''}?theme=dark&identifier=${(app?.identifier != null && app?.identifier != 'null') ? app?.identifier : ''}`
|
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])
|
}, [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 (
|
return (
|
||||||
<iframe ref={iframeRef} style={{
|
<iframe ref={iframeRef} style={{
|
||||||
height: `calc(${rootHeight} - 60px - 45px - 20px)`,
|
height: `calc(${rootHeight} - 60px - 45px - 20px)`,
|
||||||
border: 'none',
|
border: 'none',
|
||||||
width: '100%'
|
width: '100%'
|
||||||
}} id="browser-iframe" src={url} sandbox="allow-scripts allow-same-origin allow-forms allow-downloads allow-modals" allow="fullscreen">
|
}} id="browser-iframe" src={defaultUrl} sandbox="allow-scripts allow-same-origin allow-forms allow-downloads allow-modals" allow="fullscreen">
|
||||||
|
|
||||||
</iframe>
|
</iframe>
|
||||||
);
|
);
|
||||||
|
@ -2,12 +2,24 @@ import React, { useContext, useEffect, useRef } from 'react'
|
|||||||
import { AppViewer } from './AppViewer'
|
import { AppViewer } from './AppViewer'
|
||||||
import Frame from 'react-frame-component';
|
import Frame from 'react-frame-component';
|
||||||
import { MyContext } from '../../App';
|
import { MyContext } from '../../App';
|
||||||
|
import { subscribeToEvent, unsubscribeFromEvent } from '../../utils/events';
|
||||||
|
|
||||||
const AppViewerContainer = ({app, isSelected, hide}) => {
|
const AppViewerContainer = ({app, isSelected, hide}) => {
|
||||||
const { rootHeight } = useContext(MyContext);
|
const { rootHeight } = useContext(MyContext);
|
||||||
const frameRef = useRef(null);
|
const frameRef = useRef(null);
|
||||||
|
|
||||||
|
const refreshAppFunc = (e) => {
|
||||||
|
console.log('getting refresh', e)
|
||||||
|
};
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// subscribeToEvent("refreshAPp", refreshAppFunc);
|
||||||
|
|
||||||
|
// return () => {
|
||||||
|
// unsubscribeFromEvent("refreshApp", refreshAppFunc);
|
||||||
|
// };
|
||||||
|
// }, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Frame id={`browser-iframe-${app?.tabId}` } ref={frameRef} head={
|
<Frame id={`browser-iframe-${app?.tabId}` } ref={frameRef} head={
|
||||||
<>
|
<>
|
||||||
|
@ -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 { AppsHome } from "./AppsHome";
|
||||||
import { Spacer } from "../../common/Spacer";
|
import { Spacer } from "../../common/Spacer";
|
||||||
import { MyContext, getBaseApiReact } from "../../App";
|
import { MyContext, getBaseApiReact } from "../../App";
|
||||||
@ -15,19 +15,27 @@ import { AppViewer } from "./AppViewer";
|
|||||||
import AppViewerContainer from "./AppViewerContainer";
|
import AppViewerContainer from "./AppViewerContainer";
|
||||||
import ShortUniqueId from "short-unique-id";
|
import ShortUniqueId from "short-unique-id";
|
||||||
import { AppPublish } from "./AppPublish";
|
import { AppPublish } from "./AppPublish";
|
||||||
|
import { useRecoilState } from "recoil";
|
||||||
|
|
||||||
const uid = new ShortUniqueId({ length: 8 });
|
const uid = new ShortUniqueId({ length: 8 });
|
||||||
|
|
||||||
export const Apps = ({ mode, setMode, show , myName}) => {
|
export const Apps = ({ mode, setMode, show , myName}) => {
|
||||||
const [availableQapps, setAvailableQapps] = useState([]);
|
const [availableQapps, setAvailableQapps] = useState([]);
|
||||||
const [downloadedQapps, setDownloadedQapps] = useState([]);
|
|
||||||
const [selectedAppInfo, setSelectedAppInfo] = useState(null);
|
const [selectedAppInfo, setSelectedAppInfo] = useState(null);
|
||||||
const [tabs, setTabs] = useState([]);
|
const [tabs, setTabs] = useState([]);
|
||||||
const [selectedTab, setSelectedTab] = useState(null);
|
const [selectedTab, setSelectedTab] = useState(null);
|
||||||
const [isNewTabWindow, setIsNewTabWindow] = useState(false);
|
const [isNewTabWindow, setIsNewTabWindow] = useState(false);
|
||||||
const [categories, setCategories] = useState([])
|
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(() => {
|
useEffect(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -62,12 +70,12 @@ export const Apps = ({ mode, setMode, show , myName}) => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const getQapps = React.useCallback(async (myName) => {
|
const getQapps = React.useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
let apps = [];
|
let apps = [];
|
||||||
let websites = [];
|
let websites = [];
|
||||||
// dispatch(setIsLoadingGlobal(true))
|
// 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, {
|
const response = await fetch(url, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
@ -77,7 +85,8 @@ export const Apps = ({ mode, setMode, show , myName}) => {
|
|||||||
});
|
});
|
||||||
if (!response?.ok) return;
|
if (!response?.ok) return;
|
||||||
const responseData = await response.json();
|
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, {
|
const responseWebsites = await fetch(urlWebsites, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
@ -87,31 +96,20 @@ export const Apps = ({ mode, setMode, show , myName}) => {
|
|||||||
});
|
});
|
||||||
if (!responseWebsites?.ok) return;
|
if (!responseWebsites?.ok) return;
|
||||||
const responseDataWebsites = await responseWebsites.json();
|
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;
|
apps = responseData;
|
||||||
websites = responseDataWebsites;
|
websites = responseDataWebsites;
|
||||||
const combine = [...apps, ...websites];
|
const combine = [...apps, ...websites];
|
||||||
setAvailableQapps(combine);
|
setAvailableQapps(combine);
|
||||||
setDownloadedQapps(
|
|
||||||
combine.filter((qapp) => qapp?.status?.status === "READY")
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
} finally {
|
} finally {
|
||||||
// dispatch(setIsLoadingGlobal(false))
|
// dispatch(setIsLoadingGlobal(false))
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getQapps(myName);
|
getQapps();
|
||||||
getCategories()
|
getCategories()
|
||||||
}, [getQapps, getCategories, myName]);
|
}, [getQapps, getCategories]);
|
||||||
|
|
||||||
const selectedAppInfoFunc = (e) => {
|
const selectedAppInfoFunc = (e) => {
|
||||||
const data = e.detail?.data;
|
const data = e.detail?.data;
|
||||||
@ -256,11 +254,10 @@ export const Apps = ({ mode, setMode, show , myName}) => {
|
|||||||
>
|
>
|
||||||
{mode !== "viewer" && <Spacer height="30px" />}
|
{mode !== "viewer" && <Spacer height="30px" />}
|
||||||
{mode === "home" && (
|
{mode === "home" && (
|
||||||
<AppsHome myName={myName} downloadedQapps={downloadedQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} />
|
<AppsHome availableQapps={availableQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} />
|
||||||
)}
|
)}
|
||||||
{mode === "library" && (
|
{mode === "library" && (
|
||||||
<AppsLibrary
|
<AppsLibrary
|
||||||
downloadedQapps={downloadedQapps}
|
|
||||||
availableQapps={availableQapps}
|
availableQapps={availableQapps}
|
||||||
setMode={setMode}
|
setMode={setMode}
|
||||||
myName={myName}
|
myName={myName}
|
||||||
@ -283,7 +280,7 @@ export const Apps = ({ mode, setMode, show , myName}) => {
|
|||||||
{isNewTabWindow && mode === "viewer" && (
|
{isNewTabWindow && mode === "viewer" && (
|
||||||
<>
|
<>
|
||||||
<Spacer height="30px" />
|
<Spacer height="30px" />
|
||||||
<AppsHome downloadedQapps={downloadedQapps} setMode={setMode} />
|
<AppsHome availableQapps={availableQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{mode !== "viewer" && <Spacer height="180px" />}
|
{mode !== "viewer" && <Spacer height="180px" />}
|
||||||
|
@ -11,8 +11,9 @@ import { Add } from "@mui/icons-material";
|
|||||||
import { getBaseApiReact } from "../../App";
|
import { getBaseApiReact } from "../../App";
|
||||||
import LogoSelected from "../../assets/svgs/LogoSelected.svg";
|
import LogoSelected from "../../assets/svgs/LogoSelected.svg";
|
||||||
import { executeEvent } from "../../utils/events";
|
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 (
|
return (
|
||||||
<AppsContainer>
|
<AppsContainer>
|
||||||
<ButtonBase
|
<ButtonBase
|
||||||
@ -27,149 +28,9 @@ export const AppsHome = ({ downloadedQapps, setMode, myApp, myWebsite, myName })
|
|||||||
<AppCircleLabel>Add</AppCircleLabel>
|
<AppCircleLabel>Add</AppCircleLabel>
|
||||||
</AppCircleContainer>
|
</AppCircleContainer>
|
||||||
</ButtonBase>
|
</ButtonBase>
|
||||||
{myApp &&(
|
|
||||||
<ButtonBase
|
<SortablePinnedApps availableQapps={availableQapps} myWebsite={myWebsite} myApp={myApp} />
|
||||||
sx={{
|
|
||||||
height: "80px",
|
|
||||||
width: "60px",
|
|
||||||
}}
|
|
||||||
onClick={()=> {
|
|
||||||
executeEvent("addTab", {
|
|
||||||
data: myApp
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<AppCircleContainer>
|
|
||||||
<AppCircle
|
|
||||||
sx={{
|
|
||||||
border: "none",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Avatar
|
|
||||||
sx={{
|
|
||||||
height: "31px",
|
|
||||||
width: "31px",
|
|
||||||
'& img': {
|
|
||||||
objectFit: 'fill',
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
alt={myApp?.name}
|
|
||||||
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
|
|
||||||
myApp?.name
|
|
||||||
}/qortal_avatar?async=true`}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
style={{
|
|
||||||
width: "31px",
|
|
||||||
height: "auto",
|
|
||||||
}}
|
|
||||||
src={LogoSelected}
|
|
||||||
alt="center-icon"
|
|
||||||
/>
|
|
||||||
</Avatar>
|
|
||||||
</AppCircle>
|
|
||||||
<AppCircleLabel>
|
|
||||||
{myApp?.name}
|
|
||||||
</AppCircleLabel>
|
|
||||||
</AppCircleContainer>
|
|
||||||
</ButtonBase>
|
|
||||||
)}
|
|
||||||
{myWebsite &&(
|
|
||||||
<ButtonBase
|
|
||||||
sx={{
|
|
||||||
height: "80px",
|
|
||||||
width: "60px",
|
|
||||||
}}
|
|
||||||
onClick={()=> {
|
|
||||||
executeEvent("addTab", {
|
|
||||||
data: myWebsite
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<AppCircleContainer>
|
|
||||||
<AppCircle
|
|
||||||
sx={{
|
|
||||||
border: "none",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Avatar
|
|
||||||
sx={{
|
|
||||||
height: "31px",
|
|
||||||
width: "31px",
|
|
||||||
'& img': {
|
|
||||||
objectFit: 'fill',
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
alt={myWebsite?.name}
|
|
||||||
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
|
|
||||||
myWebsite?.name
|
|
||||||
}/qortal_avatar?async=true`}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
style={{
|
|
||||||
width: "31px",
|
|
||||||
height: "auto",
|
|
||||||
}}
|
|
||||||
src={LogoSelected}
|
|
||||||
alt="center-icon"
|
|
||||||
/>
|
|
||||||
</Avatar>
|
|
||||||
</AppCircle>
|
|
||||||
<AppCircleLabel>
|
|
||||||
{myWebsite?.name}
|
|
||||||
</AppCircleLabel>
|
|
||||||
</AppCircleContainer>
|
|
||||||
</ButtonBase>
|
|
||||||
)}
|
|
||||||
{downloadedQapps?.filter((item)=> item?.name !== myName).map((app) => {
|
|
||||||
return (
|
|
||||||
<ButtonBase
|
|
||||||
sx={{
|
|
||||||
height: "80px",
|
|
||||||
width: "60px",
|
|
||||||
}}
|
|
||||||
onClick={()=> {
|
|
||||||
executeEvent("addTab", {
|
|
||||||
data: app
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<AppCircleContainer>
|
|
||||||
<AppCircle
|
|
||||||
sx={{
|
|
||||||
border: "none",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Avatar
|
|
||||||
sx={{
|
|
||||||
height: "31px",
|
|
||||||
width: "31px",
|
|
||||||
'& img': {
|
|
||||||
objectFit: 'fill',
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
alt={app?.name}
|
|
||||||
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
|
|
||||||
app?.name
|
|
||||||
}/qortal_avatar?async=true`}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
style={{
|
|
||||||
width: "31px",
|
|
||||||
height: "auto",
|
|
||||||
}}
|
|
||||||
src={LogoSelected}
|
|
||||||
alt="center-icon"
|
|
||||||
/>
|
|
||||||
</Avatar>
|
|
||||||
</AppCircle>
|
|
||||||
<AppCircleLabel>
|
|
||||||
{app?.name}
|
|
||||||
</AppCircleLabel>
|
|
||||||
</AppCircleContainer>
|
|
||||||
</ButtonBase>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</AppsContainer>
|
</AppsContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -74,7 +74,7 @@ const ScrollerStyled = styled('div')({
|
|||||||
"-ms-overflow-style": "none",
|
"-ms-overflow-style": "none",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const AppsLibrary = ({ downloadedQapps, availableQapps, setMode, myName, hasPublishApp }) => {
|
export const AppsLibrary = ({ availableQapps, setMode, myName, hasPublishApp }) => {
|
||||||
const [searchValue, setSearchValue] = useState("");
|
const [searchValue, setSearchValue] = useState("");
|
||||||
const virtuosoRef = useRef();
|
const virtuosoRef = useRef();
|
||||||
const { rootHeight } = useContext(MyContext);
|
const { rootHeight } = useContext(MyContext);
|
||||||
|
@ -7,15 +7,41 @@ import {
|
|||||||
import NavBack from "../../assets/svgs/NavBack.svg";
|
import NavBack from "../../assets/svgs/NavBack.svg";
|
||||||
import NavAdd from "../../assets/svgs/NavAdd.svg";
|
import NavAdd from "../../assets/svgs/NavAdd.svg";
|
||||||
import NavMoreMenu from "../../assets/svgs/NavMoreMenu.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 { executeEvent, subscribeToEvent, unsubscribeFromEvent } from "../../utils/events";
|
||||||
import TabComponent from "./TabComponent";
|
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 = () => {
|
export const AppsNavBar = () => {
|
||||||
const [tabs, setTabs] = useState([])
|
const [tabs, setTabs] = useState([])
|
||||||
const [selectedTab, setSelectedTab] = useState([])
|
const [selectedTab, setSelectedTab] = useState([])
|
||||||
const [isNewTabWindow, setIsNewTabWindow] = useState(false)
|
const [isNewTabWindow, setIsNewTabWindow] = useState(false)
|
||||||
const tabsRef = useRef(null);
|
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(() => {
|
useEffect(() => {
|
||||||
// Scroll to the last tab whenever the tabs array changes (e.g., when a new tab is added)
|
// 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);
|
unsubscribeFromEvent("setTabsToNav", setTabsToNav);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const isSelectedAppPinned = !!sortablePinnedApps?.find((item)=> item?.name === selectedTab?.name && item?.service === selectedTab?.service)
|
||||||
return (
|
return (
|
||||||
<AppsNavBarParent>
|
<AppsNavBarParent>
|
||||||
<AppsNavBarLeft>
|
<AppsNavBarLeft>
|
||||||
@ -95,13 +123,119 @@ export const AppsNavBar = () => {
|
|||||||
width: '40px'
|
width: '40px'
|
||||||
}} src={NavAdd} />
|
}} src={NavAdd} />
|
||||||
</ButtonBase>
|
</ButtonBase>
|
||||||
<ButtonBase>
|
<ButtonBase onClick={(e)=> {
|
||||||
|
handleClick(e)
|
||||||
|
}}>
|
||||||
<img style={{
|
<img style={{
|
||||||
height: '34px',
|
height: '34px',
|
||||||
width: '34px'
|
width: '34px'
|
||||||
}} src={NavMoreMenu} />
|
}} src={NavMoreMenu} />
|
||||||
</ButtonBase>
|
</ButtonBase>
|
||||||
</AppsNavBarRight>
|
</AppsNavBarRight>
|
||||||
|
<Menu
|
||||||
|
id="navbar-more-mobile"
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
open={open}
|
||||||
|
onClose={handleClose}
|
||||||
|
MenuListProps={{
|
||||||
|
"aria-labelledby": "basic-button",
|
||||||
|
}}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: 'bottom',
|
||||||
|
horizontal: 'center',
|
||||||
|
|
||||||
|
}}
|
||||||
|
transformOrigin={{
|
||||||
|
vertical: 'top',
|
||||||
|
horizontal: 'center',
|
||||||
|
}}
|
||||||
|
slotProps={{
|
||||||
|
paper: {
|
||||||
|
sx: {
|
||||||
|
backgroundColor: 'var(--bg-primary)',
|
||||||
|
color: '#fff',
|
||||||
|
width: '148px',
|
||||||
|
borderRadius: '5px'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
marginTop: '10px'
|
||||||
|
}}
|
||||||
|
|
||||||
|
>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
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();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ListItemIcon sx={{
|
||||||
|
|
||||||
|
minWidth: '24px !important',
|
||||||
|
marginRight: '5px'
|
||||||
|
}}>
|
||||||
|
<PushPinIcon height={20} sx={{
|
||||||
|
color: isSelectedAppPinned ? 'red' : "rgba(250, 250, 250, 0.5)",
|
||||||
|
|
||||||
|
}} />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText sx={{
|
||||||
|
"& .MuiTypography-root": {
|
||||||
|
fontSize: "12px",
|
||||||
|
fontWeight: 600,
|
||||||
|
color: isSelectedAppPinned ? 'red' : "rgba(250, 250, 250, 0.5)"
|
||||||
|
},
|
||||||
|
}} primary={`${isSelectedAppPinned ? 'Unpin app' : 'Pin app'}`} />
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
executeEvent('refreshApp', {
|
||||||
|
tabId: selectedTab?.tabId
|
||||||
|
})
|
||||||
|
handleClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ListItemIcon sx={{
|
||||||
|
|
||||||
|
minWidth: '24px !important',
|
||||||
|
marginRight: '5px'
|
||||||
|
}}>
|
||||||
|
<RefreshIcon height={20} sx={{
|
||||||
|
color:"rgba(250, 250, 250, 0.5)"
|
||||||
|
}} />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText sx={{
|
||||||
|
"& .MuiTypography-root": {
|
||||||
|
fontSize: "12px",
|
||||||
|
fontWeight: 600,
|
||||||
|
color:"rgba(250, 250, 250, 0.5)"
|
||||||
|
},
|
||||||
|
}} primary="Refresh" />
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
</AppsNavBarParent>
|
</AppsNavBarParent>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
166
src/components/Apps/SortablePinnedApps.tsx
Normal file
166
src/components/Apps/SortablePinnedApps.tsx
Normal file
@ -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 (
|
||||||
|
<ButtonBase
|
||||||
|
ref={setNodeRef} {...attributes} {...listeners}
|
||||||
|
sx={{
|
||||||
|
height: "80px",
|
||||||
|
width: "60px",
|
||||||
|
transform: CSS.Transform.toString(transform),
|
||||||
|
transition,
|
||||||
|
}}
|
||||||
|
onClick={()=> {
|
||||||
|
executeEvent("addTab", {
|
||||||
|
data: app
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AppCircleContainer>
|
||||||
|
<AppCircle
|
||||||
|
sx={{
|
||||||
|
border: "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
sx={{
|
||||||
|
height: "31px",
|
||||||
|
width: "31px",
|
||||||
|
'& img': {
|
||||||
|
objectFit: 'fill',
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
alt={app?.metadata?.title || app?.name}
|
||||||
|
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
|
||||||
|
app?.name
|
||||||
|
}/qortal_avatar?async=true`}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
style={{
|
||||||
|
width: "31px",
|
||||||
|
height: "auto",
|
||||||
|
}}
|
||||||
|
// src={LogoSelected}
|
||||||
|
alt="center-icon"
|
||||||
|
/>
|
||||||
|
</Avatar>
|
||||||
|
</AppCircle>
|
||||||
|
<AppCircleLabel>
|
||||||
|
{app?.metadata?.title || app?.name}
|
||||||
|
</AppCircleLabel>
|
||||||
|
</AppCircleContainer>
|
||||||
|
</ButtonBase>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
||||||
|
<SortableContext items={transformPinnedApps.map((app) => `${app?.service}-${app?.name}`)}>
|
||||||
|
{transformPinnedApps.map((app) => (
|
||||||
|
<SortableItem key={`${app?.service}-${app?.name}`} id={`${app?.service}-${app?.name}`} name={app?.name} app={app} />
|
||||||
|
))}
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
class Semaphore {
|
class Semaphore {
|
||||||
constructor(count) {
|
constructor(count) {
|
||||||
@ -313,6 +313,7 @@ const UIQortalRequests = [
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useQortalMessageListener = (frameWindow) => {
|
export const useQortalMessageListener = (frameWindow) => {
|
||||||
|
const [path, setPath] = useState('')
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log("Listener added react");
|
console.log("Listener added react");
|
||||||
|
|
||||||
@ -376,7 +377,11 @@ export const useQortalMessageListener = (frameWindow) => {
|
|||||||
error: 'Failed to prepare data for publishing',
|
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
|
// 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 true; // Keep the message channel open for async response
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return {path}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ import { ArrowDownIcon } from "../../assets/Icons/ArrowDownIcon";
|
|||||||
import { MessagingIcon } from "../../assets/Icons/MessagingIcon";
|
import { MessagingIcon } from "../../assets/Icons/MessagingIcon";
|
||||||
import { MessagingIcon2 } from "../../assets/Icons/MessagingIcon2";
|
import { MessagingIcon2 } from "../../assets/Icons/MessagingIcon2";
|
||||||
import { HubsIcon } from "../../assets/Icons/HubsIcon";
|
import { HubsIcon } from "../../assets/Icons/HubsIcon";
|
||||||
|
import { Save } from "../Save/Save";
|
||||||
|
|
||||||
const Header = ({
|
const Header = ({
|
||||||
logoutFunc,
|
logoutFunc,
|
||||||
@ -135,6 +136,7 @@ const Header = ({
|
|||||||
|
|
||||||
/>
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
<Save />
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={logoutFunc}
|
onClick={logoutFunc}
|
||||||
edge="end"
|
edge="end"
|
||||||
|
16
src/components/Save/Save.tsx
Normal file
16
src/components/Save/Save.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import React, { useMemo, useState } from 'react'
|
||||||
|
import { useRecoilState } from 'recoil';
|
||||||
|
import isEqual from 'lodash/isEqual'; // Import deep comparison utility
|
||||||
|
import { sortablePinnedAppsAtom } from '../../atoms/global';
|
||||||
|
export const Save = () => {
|
||||||
|
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 (
|
||||||
|
<div>{hasChanged && 'Save'}</div>
|
||||||
|
)
|
||||||
|
}
|
@ -6,7 +6,7 @@ import './index.css'
|
|||||||
import { ThemeProvider, createTheme } from '@mui/material/styles';
|
import { ThemeProvider, createTheme } from '@mui/material/styles';
|
||||||
import { CssBaseline } from '@mui/material';
|
import { CssBaseline } from '@mui/material';
|
||||||
import { MessageQueueProvider } from './MessageQueueContext.tsx';
|
import { MessageQueueProvider } from './MessageQueueContext.tsx';
|
||||||
|
import { RecoilRoot } from 'recoil';
|
||||||
const theme = createTheme({
|
const theme = createTheme({
|
||||||
palette: {
|
palette: {
|
||||||
primary: {
|
primary: {
|
||||||
@ -50,7 +50,9 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
|||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<MessageQueueProvider>
|
<MessageQueueProvider>
|
||||||
|
<RecoilRoot>
|
||||||
<App />
|
<App />
|
||||||
|
</RecoilRoot>
|
||||||
</MessageQueueProvider>
|
</MessageQueueProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
|
34
src/useRetrieveDataLocalStorage.tsx
Normal file
34
src/useRetrieveDataLocalStorage.tsx
Normal file
@ -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])
|
||||||
|
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user