From 929a13920591faeb03cd2142320e3647cd906237 Mon Sep 17 00:00:00 2001 From: Mukul Jain Date: Mon, 17 May 2021 23:49:06 +0530 Subject: [PATCH] =?UTF-8?q?feat(external-apps):=20list=20external=20apps?= =?UTF-8?q?=20in=20route=20and=20load=20them=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apps/appManager/store.ts | 46 +++++++++++++++++++++ src/apps/externalAppShell/index.tsx | 60 ++++++++++++++++++++++++++++ src/apps/externalAppShell/loader.tsx | 43 ++++++++++++++++++++ src/apps/folder/store/index.ts | 36 +++++++++++++++++ src/apps/index.tsx | 21 ++++++++++ src/apps/photo/index.tsx | 2 +- src/atoms/styled/appImage/index.tsx | 7 +++- src/base/index.tsx | 8 ++-- src/base/store/index.ts | 16 ++++---- src/store/reducer.ts | 9 ++++- src/utils/hooks/useScript/index.ts | 16 ++++++-- 11 files changed, 247 insertions(+), 17 deletions(-) create mode 100644 src/apps/appManager/store.ts create mode 100644 src/apps/externalAppShell/index.tsx create mode 100644 src/apps/externalAppShell/loader.tsx diff --git a/src/apps/appManager/store.ts b/src/apps/appManager/store.ts new file mode 100644 index 0000000..eb9179a --- /dev/null +++ b/src/apps/appManager/store.ts @@ -0,0 +1,46 @@ +import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"; +import api from "utils/api"; +import { IApp } from "./interface"; + +interface IAppManagerState { + apps: IApp[]; + loading: boolean; + error?: any; +} + +const initialState: IAppManagerState = { apps: [], loading: true }; + +const getAppsAsync = createAsyncThunk( + "appManager/getAppsAsync", + (_, thunkAPI) => { + return api + .get("/manager/apps/") + .then(({ data }) => data) + .catch((e) => { + thunkAPI.rejectWithValue(e); + }); + } +); + +const authSlice = createSlice({ + name: "appManager", + initialState, + reducers: { + getApps: () => {}, + }, + extraReducers: { + // Add reducers for additional action types here, and handle loading state as needed + [getAppsAsync.fulfilled.type]: (state, action) => { + state.apps.push(action.payload); + state.loading = false; + }, + [getAppsAsync.rejected.type]: (state, action) => { + state.loading = false; + state.error = action.error; + }, + }, +}); + +export const {} = authSlice.actions; +export { getAppsAsync }; +export default authSlice.reducer; diff --git a/src/apps/externalAppShell/index.tsx b/src/apps/externalAppShell/index.tsx new file mode 100644 index 0000000..38c560b --- /dev/null +++ b/src/apps/externalAppShell/index.tsx @@ -0,0 +1,60 @@ +import * as React from "react"; +import AppShell from "apps/shell"; +import styled from "styled-components"; + +import useAnimationEndRender from "utils/hooks/useAnimationEndRender"; +import useScript from "utils/hooks/useScript"; +import If from "atoms/If"; +import Loader from "./loader"; + +const Wrapper = styled.div` + height: 100%; + width: 100%; + overflow: auto; + background: ${({ theme }) => theme.colors.plain}; +`; + +interface IProps { + instanceId: string; + appName: string; + onMouseDown: (event: React.MouseEvent, dragId: string) => void; + dragId: string; + data: Record; +} + +const ExternalAppShell = ({ + appName, + instanceId, + onMouseDown, + dragId, + data, +}: IProps) => { + const { render } = useAnimationEndRender({ instanceId }); + const status = useScript( + `http://localhost:8000/api/manager/assests/${data.appId}/main.js`, + true + ); + console.log(data.options); + return ( + + 0}> + + + + {/* keep it always ready */} + + + + ); +}; + +export default React.memo(ExternalAppShell); diff --git a/src/apps/externalAppShell/loader.tsx b/src/apps/externalAppShell/loader.tsx new file mode 100644 index 0000000..6483526 --- /dev/null +++ b/src/apps/externalAppShell/loader.tsx @@ -0,0 +1,43 @@ +import * as React from "react"; +import styled from "styled-components"; +import { ProgressIndicator } from "@fluentui/react"; + +import { Stack, Text, StackItem } from "atoms/styled"; +import { StatusTypes } from "utils/hooks/useScript"; + +interface IProps { + status: StatusTypes; +} + +const Wrapper = styled(Stack)` + background: ${({ theme }) => theme.colors.plain}; + ${StackItem} { + width: 320px; + } +`; + +const ScriptStatus = ({ status }: IProps) => { + return ( + + + + + + {status === "error" && ( + Something went wrong while loading the app. + )} + + + ); +}; + +export default ScriptStatus; diff --git a/src/apps/folder/store/index.ts b/src/apps/folder/store/index.ts index 1456a39..06e0a31 100644 --- a/src/apps/folder/store/index.ts +++ b/src/apps/folder/store/index.ts @@ -3,6 +3,8 @@ import { deleteChildren, refreshRoutes } from "./helper"; import { IFolder, IFolderRoutes, IFile } from "../interfaces"; import { folderPool, folderMap } from "./routes"; import { sortBy, get } from "lodash-es"; +import { getAppsAsync } from "apps/appManager/store"; +import { IApp } from "apps/appManager/interface"; export interface IBaseState { folderPool: Record; @@ -169,6 +171,40 @@ const folderSlice = createSlice({ refreshRoutes(state); }, }, + extraReducers: { + [getAppsAsync.fulfilled.type]: ( + state, + { payload }: PayloadAction + ) => { + //data: { + // id: "img2", + // name: "lady.jpg", + // icon: "image", + // path: "lady.jpg", + // }, + // appName: "photo", + // order: 1, + const applicationsFolder = get( + state.root, + state.routeToFolder["/applications"] + ).files; + const startingOrder = Object.keys(applicationsFolder).length; + payload.forEach((customApp, index) => { + applicationsFolder[customApp.appId] = { + data: { + appId: customApp.appId, + icon: customApp.icon, + name: customApp.name, + appType: "EXTERNAL", + options: customApp.options, + }, + appName: customApp.appId, + order: startingOrder + index, + }; + }); + refreshRoutes(state); + }, + }, }); export const { diff --git a/src/apps/index.tsx b/src/apps/index.tsx index 5995f42..46f7389 100644 --- a/src/apps/index.tsx +++ b/src/apps/index.tsx @@ -5,6 +5,7 @@ import Photo from "apps/photo"; import AppManager from "apps/appManager"; import { IApp, IMetaData } from "base/interfaces"; import AnimatedFileWrapper from "atoms/animatedFileWrapper"; +import ExternalAppShell from "apps/externalAppShell"; interface IProps { app: IApp; @@ -81,6 +82,26 @@ const App = (props: IProps) => { } } + if (app.data.appType === "EXTERNAL") { + return ( + + + + ); + } + return null; }; diff --git a/src/apps/photo/index.tsx b/src/apps/photo/index.tsx index a52c721..0c0bdf0 100644 --- a/src/apps/photo/index.tsx +++ b/src/apps/photo/index.tsx @@ -13,7 +13,7 @@ interface IProps { path: string; instanceId: string; appName: string; - onMouseDown: (event: React.MouseEvent) => void; + onMouseDown: (event: React.MouseEvent, dragId: string) => void; dragId: string; } diff --git a/src/atoms/styled/appImage/index.tsx b/src/atoms/styled/appImage/index.tsx index e99ee00..a4dfffe 100644 --- a/src/atoms/styled/appImage/index.tsx +++ b/src/atoms/styled/appImage/index.tsx @@ -36,13 +36,18 @@ type IProps = ReactHTMLElement< const Image = ({ name, size, ref, height, width, ...rest }: IProps) => { const iconName = availableIcons[name] || "generic.svg"; + let href = ""; + + if (name && name.includes("http")) { + href = name; + } return ( ); }; diff --git a/src/base/index.tsx b/src/base/index.tsx index f222ea8..5f922ec 100644 --- a/src/base/index.tsx +++ b/src/base/index.tsx @@ -12,6 +12,7 @@ import { toggleStartMenu as toggleStartMenuAction } from "base/store"; import { toggleQuickActions } from "apps/actionCenter/store"; import { initRoutes } from "apps/folder/store"; import ActionCenter from "apps/actionCenter"; +import { getAppsAsync } from "apps/appManager/store"; interface IProps {} @@ -24,7 +25,7 @@ const Base = ({}: IProps) => { const wrapperRef = React.useRef(null); let user = useSelector((state) => state.auth.user); - const openedApps = useSelector((state) => state.base.apps); + const runningApp = useSelector((state) => state.base.runningApp); const routes = useSelector((state) => state.folder.routes, shallowEqual); const { store, handleMouseDown } = useDraggable({ wrapperRef }); @@ -52,6 +53,7 @@ const Base = ({}: IProps) => { wrapper: wrapperRef.current!, }; dispatch(initRoutes(user?.name || "Guest user")); + dispatch(getAppsAsync()); }, []); const handleMouseDownEvent = React.useCallback( @@ -66,7 +68,7 @@ const Base = ({}: IProps) => { - {Object.values(openedApps).map((app) => { + {Object.values(runningApp).map((app) => { return ( <> {app.instances.map((instance, index) => { @@ -101,7 +103,7 @@ const Base = ({}: IProps) => { diff --git a/src/base/store/index.ts b/src/base/store/index.ts index c347c28..e652267 100644 --- a/src/base/store/index.ts +++ b/src/base/store/index.ts @@ -2,7 +2,7 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { IApp, IAppGroup } from "base/interfaces"; interface IBaseState { - apps: Record; + runningApp: Record; currentWeight: number; menu: { @@ -11,7 +11,7 @@ interface IBaseState { } const initialState: IBaseState = { - apps: {}, + runningApp: {}, currentWeight: 0, menu: { show: false, @@ -28,7 +28,7 @@ const baseSlice = createSlice({ payload: { appName, instanceId }, }: PayloadAction<{ appName: string; instanceId: string }> ) => { - const runningApp = state.apps[appName]; + const runningApp = state.runningApp[appName]; if ( runningApp.instances[runningApp.instances.length - 1].id === instanceId ) { @@ -56,7 +56,7 @@ const baseSlice = createSlice({ openApp(state, action: PayloadAction) { const newApp = action.payload; const instanceId = `${newApp.appName}-${newApp.id}`; - const runningApp = state.apps[newApp.appName]; + const runningApp = state.runningApp[newApp.appName]; if (runningApp) { runningApp.weight = state.currentWeight; @@ -71,7 +71,7 @@ const baseSlice = createSlice({ } runningApp.instances.push({ ...newApp, id: instanceId }); } else { - state.apps[newApp.appName] = { + state.runningApp[newApp.appName] = { initialWeight: state.currentWeight, weight: state.currentWeight, id: newApp.id, @@ -79,7 +79,7 @@ const baseSlice = createSlice({ instances: [{ ...newApp, id: instanceId }], }; } - // relying on this to never cross the css z-index limit - 10000 + // relying on this, to never cross the css z-index limit - 10000 // TODO: if does, have to reset every app weight that's all state.currentWeight++; }, @@ -87,13 +87,13 @@ const baseSlice = createSlice({ state, action: PayloadAction<{ appName: string; instanceId: string }> ) => { - const app = state.apps[action.payload.appName]; + const app = state.runningApp[action.payload.appName]; if (!app) { return; } if (app.instances.length === 1) { - delete state.apps[action.payload.appName]; + delete state.runningApp[action.payload.appName]; } else { app.instances = app.instances.filter( (instance) => instance.id !== action.payload.instanceId diff --git a/src/store/reducer.ts b/src/store/reducer.ts index 8536ace..5b0b4ad 100644 --- a/src/store/reducer.ts +++ b/src/store/reducer.ts @@ -3,8 +3,15 @@ import auth from "auth/store"; import base from "base/store"; import folder from "apps/folder/store"; import actionCenter from "apps/actionCenter/store"; +import appManager from "apps/appManager/store"; -const rootReducer = combineReducers({ auth, base, folder, actionCenter }); +const rootReducer = combineReducers({ + auth, + base, + folder, + actionCenter, + appManager, +}); export type RootState = ReturnType; diff --git a/src/utils/hooks/useScript/index.ts b/src/utils/hooks/useScript/index.ts index cea6571..5a8d7da 100644 --- a/src/utils/hooks/useScript/index.ts +++ b/src/utils/hooks/useScript/index.ts @@ -1,8 +1,12 @@ import * as React from "react"; -function useScript(src: string) { +export type StatusTypes = "loading" | "idle" | "error" | "ready"; + +function useScript(src: string, remove?: boolean) { // Keep track of script status ("idle", "loading", "ready", "error") - const [status, setStatus] = React.useState(src ? "loading" : "idle"); + const [status, setStatus] = React.useState( + src ? "loading" : "idle" + ); React.useEffect( () => { // Allow falsy src value if waiting on other data needed for @@ -36,7 +40,7 @@ function useScript(src: string) { script.addEventListener("error", setAttributeFromEvent); } else { // Grab existing script status from attribute and set to state. - setStatus(script!.getAttribute("data-status")!); + setStatus(script!.getAttribute("data-status")! as StatusTypes); } // Script event handler to update status in state // Note: Even if the script already exists we still need to add @@ -53,6 +57,12 @@ function useScript(src: string) { script.removeEventListener("load", setStateFromEvent); script.removeEventListener("error", setStateFromEvent); } + if (remove) { + let script = document.querySelector( + `script[src="${src}"]` + ) as HTMLScriptElement; + script.parentNode?.removeChild(script); + } }; }, [src] // Only re-run effect if script src changes