import React, { useEffect } from 'react' import { useTranslation } from 'react-i18next' import ProjectDrawer from '@xrengine/client-core/src/admin/components/Project/ProjectDrawer' import { ProjectService, useProjectState } from '@xrengine/client-core/src/common/services/ProjectService' import { useRouter } from '@xrengine/client-core/src/common/services/RouterService' import { useAuthState } from '@xrengine/client-core/src/user/services/AuthService' import { ProjectInterface } from '@xrengine/common/src/interfaces/ProjectInterface' import multiLogger from '@xrengine/common/src/logger' import { Engine } from '@xrengine/engine/src/ecs/classes/Engine' import { initSystems } from '@xrengine/engine/src/ecs/functions/SystemFunctions' import { dispatchAction, useHookstate } from '@xrengine/hyperflux' import { ArrowRightRounded, Check, Clear, Delete, Download, DownloadDone, FilterList, Group, Link, LinkOff, Search, Settings, Upload } from '@mui/icons-material' import { Accordion, AccordionDetails, AccordionSummary, Box, CircularProgress, IconButton, InputBase, Menu, MenuItem, Button as MuiButton, Paper } from '@mui/material' import { getProjects } from '../../functions/projectFunctions' import { EditorAction } from '../../services/EditorServices' import { Button, MediumButton } from '../inputs/Button' import { CreateProjectDialog } from './CreateProjectDialog' import { DeleteDialog } from './DeleteDialog' import { EditPermissionsDialog } from './EditPermissionsDialog' import styles from './styles.module.scss' const logger = multiLogger.child({ component: 'editor:ProjectsPage' }) function sortAlphabetical(a, b) { if (a > b) return -1 if (b > a) return 1 return 0 } const OfficialProjectData = [ { id: '1570ae00-889a-11ec-886e-b126f7590685', name: 'Development Test Suite', repositoryPath: 'https://github.com/XRFoundation/XREngine-development-test-suite', thumbnail: '/static/etherealengine.png', description: 'Assets and tests for xrengine core development', needsRebuild: true }, { id: '1570ae01-889a-11ec-886e-b126f7590685', name: 'Translations', repositoryPath: 'https://github.com/XRFoundation/XREngine-i18n', thumbnail: '/static/etherealengine.png', description: 'Complete language translations in over 100 languages.', needsRebuild: true }, { id: '1570ae02-889a-11ec-886e-b126f7590685', name: 'Test Bot', repositoryPath: 'https://github.com/XRFoundation/XREngine-Bot', thumbnail: '/static/etherealengine.png', description: 'A test bot using puppeteer', needsRebuild: true }, { id: '1570ae11-889a-11ec-886e-b126f7590685', name: 'Maps', repositoryPath: 'https://github.com/XRFoundation/XREngine-Project-Maps', thumbnail: '/static/etherealengine.png', description: 'Procedurally generated map tiles using geojson data with mapbox and turf.js', needsRebuild: true }, { id: '1570ae12-889a-11ec-886e-b126f7590685', name: 'Inventory', repositoryPath: 'https://github.com/XRFoundation/XREngine-Project-Inventory', thumbnail: '/static/etherealengine.png', description: 'Item inventory, trade & virtual currency. Allow your users to use a database, IPFS, DID or blockchain backed item storage for equippables, wearables and tradable items.', needsRebuild: true }, { id: '1570ae14-889a-11ec-886e-b126f7590685', name: 'Digital Beings', repositoryPath: 'https://github.com/XRFoundation/XREngine-Project-Digital-Beings', thumbnail: '/static/etherealengine.png', description: 'Enhance your virtual worlds with GPT-3 backed AI agents!', needsRebuild: true }, { id: '1570ae15-889a-11ec-886e-b126f7590685', name: 'Harmony Chat', repositoryPath: 'https://github.com/XRFoundation/Harmony-Chat', thumbnail: '/static/etherealengine.png', description: 'An elegant and minimalist messenger client with group text, audio, video and screensharing capabilities.', needsRebuild: true } ] const ProjectUpdateSystemInjection = { uuid: 'core.admin.ProjectUpdateSystem', type: 'PRE_RENDER', systemLoader: () => import('@xrengine/client-core/src/systems/ProjectUpdateSystem') } as const const CommunityProjectData = [] as any const ProjectExpansionList = (props: React.PropsWithChildren<{ id: string; summary: string }>) => { return (

{props.summary}

{props.children}
) } const ProjectsPage = () => { const installedProjects = useHookstate([]) // constant projects initialized with an empty array. const officialProjects = useHookstate([]) const communityProjects = useHookstate([]) const activeProject = useHookstate(null) const loading = useHookstate(false) const error = useHookstate(null) const query = useHookstate('') const filterAnchorEl = useHookstate(null) const projectAnchorEl = useHookstate(null) const filter = useHookstate({ installed: false, official: true, community: true }) const isCreateDialogOpen = useHookstate(false) const isDeleteDialogOpen = useHookstate(false) const updatingProject = useHookstate(false) const uploadingProject = useHookstate(false) const editPermissionsDialogOpen = useHookstate(false) const projectDrawerOpen = useHookstate(false) const changeDestination = useHookstate(false) const authState = useAuthState() const projectState = useProjectState() const authUser = authState.authUser const user = authState.user const githubProvider = user.identityProviders.value?.find((ip) => ip.type === 'github') const { t } = useTranslation() const route = useRouter() const fetchInstalledProjects = async () => { loading.set(true) try { const data = await getProjects() installedProjects.set(data.sort(sortAlphabetical) ?? []) if (activeProject.value) activeProject.set(data.find((item) => item.id === activeProject.value?.id) as ProjectInterface | null) } catch (error) { logger.error(error) error.set(error) } loading.set(false) } const fetchOfficialProjects = async (query?: string) => { loading.set(true) try { const data = await (query ? OfficialProjectData.filter((p) => p.name.includes(query) || p.description.includes(query)) : OfficialProjectData) officialProjects.set((data.sort(sortAlphabetical) as ProjectInterface[]) ?? []) } catch (error) { logger.error(error) error.set(error) } loading.set(false) } const fetchCommunityProjects = async (query?: string) => { loading.set(true) try { const data = await (query ? CommunityProjectData.filter((p) => p.name.includes(query) || p.description.includes(query)) : CommunityProjectData) communityProjects.set(data.sort(sortAlphabetical) ?? []) } catch (error) { logger.error(error) error.set(error) } loading.set(false) } const refreshGithubRepoAccess = () => { ProjectService.refreshGithubRepoAccess() fetchInstalledProjects() } useEffect(() => { initSystems(Engine.instance.currentWorld, [ProjectUpdateSystemInjection]) }, []) useEffect(() => { if (!authUser || !user) return if (authUser.accessToken.value == null || authUser.accessToken.value.length <= 0 || user.id.value == null) return fetchInstalledProjects() fetchOfficialProjects() fetchCommunityProjects() }, [authUser.accessToken]) // TODO: Implement tutorial #7257 const openTutorial = () => { logger.info('Implement Tutorial...') } const onClickExisting = (event, project) => { event.preventDefault() if (!isInstalled(project)) return dispatchAction(EditorAction.sceneChanged({ sceneName: null })) dispatchAction(EditorAction.projectChanged({ projectName: project.name })) route(`/studio/${project.name}`) } const onCreateProject = async (name) => { await ProjectService.createProject(name) await fetchInstalledProjects() } const onCreatePermission = async (userInviteCode: string, projectId: string) => { await ProjectService.createPermission(userInviteCode, projectId) await fetchInstalledProjects() } const onPatchPermission = async (id: string, type: string) => { await ProjectService.patchPermission(id, type) await fetchInstalledProjects() } const onRemovePermission = async (id: string) => { await ProjectService.removePermission(id) await fetchInstalledProjects() } const openDeleteConfirm = () => isDeleteDialogOpen.set(true) const closeDeleteConfirm = () => isDeleteDialogOpen.set(false) const openCreateDialog = () => isCreateDialogOpen.set(true) const closeCreateDialog = () => isCreateDialogOpen.set(false) const openEditPermissionsDialog = () => editPermissionsDialogOpen.set(true) const closeEditPermissionsDialog = () => editPermissionsDialogOpen.set(false) const deleteProject = async () => { closeDeleteConfirm() updatingProject.set(true) if (activeProject.value) { try { const proj = installedProjects.value.find((proj) => proj.id === activeProject.value?.id)! await ProjectService.removeProject(proj.id) await fetchInstalledProjects() } catch (err) { logger.error(err) } } closeProjectContextMenu() updatingProject.set(false) } const pushProject = async (id: string) => { uploadingProject.set(true) try { await ProjectService.pushProject(id) await fetchInstalledProjects() } catch (err) { logger.error(err) } uploadingProject.set(false) } const isInstalled = (project: ProjectInterface | null) => { if (!project) return false for (const installedProject of installedProjects.value) { if (project.repositoryPath === installedProject.repositoryPath) return true } return false } const hasRepo = (project: ProjectInterface | null) => { if (!project) return false return project.repositoryPath && project.repositoryPath.length > 0 } const handleSearch = (e) => { query.set(e.target.value) if (filter.value.installed) { } if (filter.value.official) fetchOfficialProjects(e.target.value) if (filter.value.community) fetchCommunityProjects(e.target.value) } const clearSearch = () => query.set('') const openFilterMenu = (e) => filterAnchorEl.set(e.target) const closeFilterMenu = () => filterAnchorEl.set(null) const toggleFilter = (type: string) => filter.set({ ...filter.value, [type]: !filter.value[type] }) const openProjectContextMenu = (event: MouseEvent, project: ProjectInterface) => { event.preventDefault() event.stopPropagation() activeProject.set(JSON.parse(JSON.stringify(project))) projectAnchorEl.set(event.target) } const closeProjectContextMenu = () => projectAnchorEl.set(null) const renderProjectList = (projects: ProjectInterface[], areInstalledProjects?: boolean) => { if (!projects || projects.length <= 0) return <> return ( ) } const handleOpenProjectDrawer = (change = false) => { projectDrawerOpen.set(true) changeDestination.set(change) } const handleCloseProjectDrawer = () => { changeDestination.set(false) projectDrawerOpen.set(false) } /** * Rendering view for projects page, if user is not login yet then showing login view. * if user is logged in and has no existing projects then the welcome view is shown, providing link to the tutorials. * if user has existing projects then we show the existing projects in grids and a grid to add new project. * */ if (!authUser?.accessToken.value || authUser.accessToken.value.length === 0 || !user?.id.value) return <> return (

{t(`editor.projects.title`)}

toggleFilter('installed')}> {filter.value.installed && } {t(`editor.projects.installed`)} toggleFilter('official')}> {filter.value.official && } {t(`editor.projects.official`)} toggleFilter('community')}> {filter.value.community && } {t(`editor.projects.community`)} {query.value ? ( ) : ( )}
{githubProvider != null && ( refreshGithubRepoAccess()} > {projectState.refreshingGithubRepoAccess.value ? ( {t('admin:components.project.refreshingGithubRepoAccess')} ) : ( t('admin:components.project.refreshGithubRepoAccess') )} )}
{error.value &&
{error.value.message}
} {(!query.value || filter.value.installed) && ( {renderProjectList(installedProjects.value, true)} )} {(!query.value || (query.value && filter.value.official && officialProjects.value.length > 0)) && ( {renderProjectList(officialProjects.value)} )} {(!query.value || (!query.value && filter.value.community && communityProjects.value.length > 0)) && ( {renderProjectList(communityProjects.value)} )}
{installedProjects.value.length < 2 && !loading ? (

{t('editor.projects.welcomeMsg')}

{t('editor.projects.description')}

{t('editor.projects.lbl-startTutorial')}
) : null}
{activeProject.value?.name !== 'default-project' && ( activeProject.set(null) }} classes={{ paper: styles.filterMenu }} > {activeProject.value && isInstalled(activeProject.value) && ( {t(`editor.projects.permissions`)} )} {activeProject.value && isInstalled(activeProject.value) && hasRepo(activeProject.value) && activeProject.value.hasWriteAccess && ( handleOpenProjectDrawer(false)}> {t(`editor.projects.updateFromGithub`)} )} {activeProject.value && isInstalled(activeProject.value) && !hasRepo(activeProject.value) && activeProject.value.hasWriteAccess && ( handleOpenProjectDrawer(true)}> {t(`editor.projects.link`)} )} {activeProject.value && isInstalled(activeProject.value) && hasRepo(activeProject.value) && activeProject.value.hasWriteAccess && ( handleOpenProjectDrawer(true)}> {t(`editor.projects.unlink`)} )} {activeProject.value?.hasWriteAccess && hasRepo(activeProject.value) && ( activeProject?.value?.id && pushProject(activeProject.value.id)} > {uploadingProject.value ? : } {t(`editor.projects.pushToGithub`)} )} {isInstalled(activeProject.value) && activeProject.value?.hasWriteAccess && ( {updatingProject.value ? : } {t(`editor.projects.uninstall`)} )} {!isInstalled(activeProject.value) && ( handleOpenProjectDrawer(false)}> {updatingProject.value ? : } {t(`editor.projects.install`)} )} )} {activeProject.value && activeProject.value.project_permissions && ( )}
) } export default ProjectsPage