sortable pinned apps

This commit is contained in:
2024-10-20 17:07:53 +03:00
parent ba53e83b13
commit affcd33dff
16 changed files with 519 additions and 180 deletions

View File

@@ -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 (
<iframe ref={iframeRef} style={{
height: `calc(${rootHeight} - 60px - 45px - 20px)`,
border: 'none',
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>
);

View File

@@ -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 (
<Frame id={`browser-iframe-${app?.tabId}` } ref={frameRef} head={
<>

View File

@@ -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" && <Spacer height="30px" />}
{mode === "home" && (
<AppsHome myName={myName} downloadedQapps={downloadedQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} />
<AppsHome availableQapps={availableQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} />
)}
{mode === "library" && (
<AppsLibrary
downloadedQapps={downloadedQapps}
availableQapps={availableQapps}
setMode={setMode}
myName={myName}
@@ -283,7 +280,7 @@ export const Apps = ({ mode, setMode, show , myName}) => {
{isNewTabWindow && mode === "viewer" && (
<>
<Spacer height="30px" />
<AppsHome downloadedQapps={downloadedQapps} setMode={setMode} />
<AppsHome availableQapps={availableQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} />
</>
)}
{mode !== "viewer" && <Spacer height="180px" />}

View File

@@ -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 (
<AppsContainer>
<ButtonBase
@@ -27,149 +28,9 @@ export const AppsHome = ({ downloadedQapps, setMode, myApp, myWebsite, myName })
<AppCircleLabel>Add</AppCircleLabel>
</AppCircleContainer>
</ButtonBase>
{myApp &&(
<ButtonBase
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>
);
})}
<SortablePinnedApps availableQapps={availableQapps} myWebsite={myWebsite} myApp={myApp} />
</AppsContainer>
);
};

View File

@@ -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);

View File

@@ -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 (
<AppsNavBarParent>
<AppsNavBarLeft>
@@ -95,13 +123,119 @@ export const AppsNavBar = () => {
width: '40px'
}} src={NavAdd} />
</ButtonBase>
<ButtonBase>
<ButtonBase onClick={(e)=> {
handleClick(e)
}}>
<img style={{
height: '34px',
width: '34px'
}} src={NavMoreMenu} />
</ButtonBase>
</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>
);
};

View 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>
);
};

View File

@@ -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}
};