From 1178168a1007dca999e432d9c3a9e72aa29cf301 Mon Sep 17 00:00:00 2001 From: Aditya Mitra <55396651+aditya-mitra@users.noreply.github.com> Date: Mon, 12 Aug 2024 17:40:06 +0530 Subject: [PATCH 01/29] fix tooltip css applied to all popups (#10938) --- packages/ui/src/primitives/tailwind/Tooltip/index.tsx | 4 ++++ packages/ui/src/primitives/tailwind/Tooltip/tooltip.css | 6 ------ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/ui/src/primitives/tailwind/Tooltip/index.tsx b/packages/ui/src/primitives/tailwind/Tooltip/index.tsx index 83266c9dca..fae7fa566d 100644 --- a/packages/ui/src/primitives/tailwind/Tooltip/index.tsx +++ b/packages/ui/src/primitives/tailwind/Tooltip/index.tsx @@ -44,6 +44,10 @@ const Tooltip = ({ title, titleClassName, content, children, className, ...rest keepTooltipInside repositionOnResize arrow={false} + contentStyle={{ + animation: 'expandFromCenter 0.3s cubic-bezier(0.38, 0.1, 0.36, 0.9) forwards', + transformOrigin: 'center' + }} {...rest} >
diff --git a/packages/ui/src/primitives/tailwind/Tooltip/tooltip.css b/packages/ui/src/primitives/tailwind/Tooltip/tooltip.css index 552f7d5e98..be3c3454e5 100644 --- a/packages/ui/src/primitives/tailwind/Tooltip/tooltip.css +++ b/packages/ui/src/primitives/tailwind/Tooltip/tooltip.css @@ -12,9 +12,3 @@ opacity: 1; } } - -.popup-content { - animation: expandFromCenter 0.3s cubic-bezier(0.38, 0.1, 0.36, 0.9) forwards; - -webkit-animation: expandFromCenter 0.3s cubic-bezier(0.38, 0.1, 0.36, 0.9) forwards; - transform-origin: center; -} From a2df55f740cf4b53b0242b032d6b238358d12fa7 Mon Sep 17 00:00:00 2001 From: dtlehrer <19999194+dtlehrer@users.noreply.github.com> Date: Mon, 12 Aug 2024 10:12:12 -0500 Subject: [PATCH 02/29] prevent scene from disappearing when double-clicking on a HierarchyTreeNode (#10932) --- .../editor/panels/Hierarchy/container/index.tsx | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/ui/src/components/editor/panels/Hierarchy/container/index.tsx b/packages/ui/src/components/editor/panels/Hierarchy/container/index.tsx index ae728106b9..09252525c2 100644 --- a/packages/ui/src/components/editor/panels/Hierarchy/container/index.tsx +++ b/packages/ui/src/components/editor/panels/Hierarchy/container/index.tsx @@ -23,7 +23,12 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ -import { getComponent, getMutableComponent, useOptionalComponent } from '@etherealengine/ecs/src/ComponentFunctions' +import { + getComponent, + getMutableComponent, + getOptionalComponent, + useOptionalComponent +} from '@etherealengine/ecs/src/ComponentFunctions' import { AllFileTypes } from '@etherealengine/engine/src/assets/constants/fileTypes' import { getMutableState, getState, none, useHookstate, useMutableState } from '@etherealengine/hyperflux' import { NameComponent } from '@etherealengine/spatial/src/common/NameComponent' @@ -276,9 +281,11 @@ function HierarchyPanelContents(props: { sceneURL: string; rootEntity: Entity; i } setPrevClickedNode(entity) } else if (e.detail === 2) { - const editorCameraState = getMutableComponent(Engine.instance.cameraEntity, CameraOrbitComponent) - editorCameraState.focusedEntities.set([entity]) - editorCameraState.refocus.set(true) + if (entity && getOptionalComponent(entity, CameraOrbitComponent)) { + const editorCameraState = getMutableComponent(Engine.instance.cameraEntity, CameraOrbitComponent) + editorCameraState.focusedEntities.set([entity]) + editorCameraState.refocus.set(true) + } } }, [prevClickedNode, entityHierarchy] From b45db2accba362e4125185b58c3ff545f3020fae Mon Sep 17 00:00:00 2001 From: dtlehrer <19999194+dtlehrer@users.noreply.github.com> Date: Mon, 12 Aug 2024 13:32:28 -0500 Subject: [PATCH 03/29] [IR-3324] studio: drag-n-drop folders (#10737) * enable movement of folders via drag n drop * update handleBrowserItemClick & include folders in multi-select * pass tests? * revert * update moveFolderRecursively * Update file-browser.class.ts * fix s3.storage.isDirectory * change s3.storage.isDirectory command prefix * fix moveObject key - to work with directories and files * add test cases for isDirectory, moveObject, and getIncrementalName * add test case for moveFolderRecursively * Update FileUtil.test.ts * Update FileUtil.test.ts * Update storageprovider.test.ts * Update storageprovider.test.ts * Update storageprovider.test.ts * pass ui check * Update index.tsx * double-click always navigates to folder * select folders on single click and navigate into them on double click * don't allow folders to be dropped into self * make local.storage.ts moveObject function work with directories --------- Co-authored-by: Rahul Ghosh Co-authored-by: Daniel Belmes <3631206+DanielBelmes@users.noreply.github.com> --- .../server-core/src/media/FileUtil.test.ts | 34 ++++++++ packages/server-core/src/media/FileUtil.ts | 9 ++- .../media/file-browser/file-browser.class.ts | 37 ++++++++- .../media/storageprovider/local.storage.ts | 11 ++- .../src/media/storageprovider/s3.storage.ts | 11 ++- .../storageprovider/storageprovider.test.ts | 77 ++++++++++++++++--- .../editor/panels/Files/browserGrid/index.tsx | 8 +- .../editor/panels/Files/container/index.tsx | 26 +++---- 8 files changed, 171 insertions(+), 42 deletions(-) diff --git a/packages/server-core/src/media/FileUtil.test.ts b/packages/server-core/src/media/FileUtil.test.ts index 2effa93ac9..f77b319aff 100644 --- a/packages/server-core/src/media/FileUtil.test.ts +++ b/packages/server-core/src/media/FileUtil.test.ts @@ -157,6 +157,40 @@ describe('FileUtil functions', () => { fs.rmdirSync(path.join(STORAGE_PATH, dirName)) fs.rmdirSync(path.join(STORAGE_PATH, dirName_1)) }) + + it('should handle singular and plural directory names correctly', async () => { + const singularDirName = 'testdir' + const pluralDirName = 'testdirs' + + // ensure directories don't exist before starting + if (fs.existsSync(path.join(STORAGE_PATH, singularDirName))) { + fs.rmdirSync(path.join(STORAGE_PATH, singularDirName)) + } + if (fs.existsSync(path.join(STORAGE_PATH, pluralDirName))) { + fs.rmdirSync(path.join(STORAGE_PATH, pluralDirName)) + } + + // create 'testdirs' directory + fs.mkdirSync(path.join(STORAGE_PATH, pluralDirName)) + + // try to create 'testdir' directory + let name = await getIncrementalName(singularDirName, TEST_DIR, store, true) + assert.equal(name, singularDirName, "Should return 'testdir' as it doesn't exist") + + // create 'testdir' directory + fs.mkdirSync(path.join(STORAGE_PATH, singularDirName)) + + // try to create another 'testdir' directory + name = await getIncrementalName(singularDirName, TEST_DIR, store, true) + assert.equal(name, `${singularDirName}(1)`, "Should return 'testdir(1)' as 'testdir' already exists") + + // try to create 'testdirs' directory + name = await getIncrementalName(pluralDirName, TEST_DIR, store, true) + assert.equal(name, `${pluralDirName}(1)`, "Should return 'testdirs(1)' as 'testdirs' already exists") + + fs.rmdirSync(path.join(STORAGE_PATH, singularDirName)) + fs.rmdirSync(path.join(STORAGE_PATH, pluralDirName)) + }) }) after(() => { diff --git a/packages/server-core/src/media/FileUtil.ts b/packages/server-core/src/media/FileUtil.ts index 535cf90366..e82cc7032a 100644 --- a/packages/server-core/src/media/FileUtil.ts +++ b/packages/server-core/src/media/FileUtil.ts @@ -50,22 +50,23 @@ export const getIncrementalName = async function ( let filename = name if (!(await store.doesExist(filename, directoryPath))) return filename + if (isDirectory && !(await store.isDirectory(filename, directoryPath))) return filename let count = 1 if (isDirectory) { - do { + while (await store.isDirectory(filename, directoryPath)) { filename = `${name}(${count})` count++ - } while (await store.doesExist(filename, directoryPath)) + } } else { const extension = path.extname(name) const baseName = path.basename(name, extension) - do { + while (await store.doesExist(filename, directoryPath)) { filename = `${baseName}(${count})${extension}` count++ - } while (await store.doesExist(filename, directoryPath)) + } } return filename diff --git a/packages/server-core/src/media/file-browser/file-browser.class.ts b/packages/server-core/src/media/file-browser/file-browser.class.ts index bcf3ca6902..f196369c3b 100755 --- a/packages/server-core/src/media/file-browser/file-browser.class.ts +++ b/packages/server-core/src/media/file-browser/file-browser.class.ts @@ -51,7 +51,7 @@ import config from '../../appconfig' import { getContentType } from '../../util/fileUtils' import { getIncrementalName } from '../FileUtil' import { getStorageProvider } from '../storageprovider/storageprovider' -import { StorageObjectInterface } from '../storageprovider/storageprovider.interface' +import { StorageObjectInterface, StorageProviderInterface } from '../storageprovider/storageprovider.interface' import { uploadStaticResource } from './file-helper' export const projectsRootFolder = path.join(appRootPath.path, 'packages/projects') @@ -221,7 +221,16 @@ export class FileBrowserService const isDirectory = await storageProvider.isDirectory(oldName, oldDirectory) const fileName = await getIncrementalName(newName, newDirectory, storageProvider, isDirectory) - await storageProvider.moveObject(oldName, fileName, oldDirectory, newDirectory, data.isCopy) + + if (isDirectory) { + await this.moveFolderRecursively( + storageProvider, + path.join(oldDirectory, oldName), + path.join(newDirectory, fileName) + ) + } else { + await storageProvider.moveObject(oldName, fileName, oldDirectory, newDirectory, data.isCopy) + } const staticResources = (await this.app.service(staticResourcePath).find({ query: { @@ -265,6 +274,30 @@ export class FileBrowserService return results } + private async moveFolderRecursively(storageProvider: StorageProviderInterface, oldPath: string, newPath: string) { + const items = await storageProvider.listFolderContent(oldPath + '/') + + for (const item of items) { + const oldItemPath = path.join(oldPath, item.name) + const newItemPath = path.join(newPath, item.name) + + if (item.type === 'directory') { + await this.moveFolderRecursively(storageProvider, oldItemPath, newItemPath) + } else { + await storageProvider.moveObject(item.name, item.name, oldPath, newPath, false) + } + } + + // move the folder itself + await storageProvider.moveObject( + path.basename(oldPath), + path.basename(newPath), + path.dirname(oldPath), + path.dirname(newPath), + false + ) + } + /** * Upload file */ diff --git a/packages/server-core/src/media/storageprovider/local.storage.ts b/packages/server-core/src/media/storageprovider/local.storage.ts index cd335fd873..7e1859b462 100755 --- a/packages/server-core/src/media/storageprovider/local.storage.ts +++ b/packages/server-core/src/media/storageprovider/local.storage.ts @@ -39,7 +39,6 @@ import config from '../../appconfig' import logger from '../../ServerLogger' import { ServerMode, ServerState } from '../../ServerState' import { getContentType } from '../../util/fileUtils' -import { copyRecursiveSync } from '../FileUtil' import { BlobStore, PutObjectParams, @@ -423,7 +422,15 @@ export class LocalStorage implements StorageProviderInterface { if (!fs.existsSync(path.dirname(newFilePath))) fs.mkdirSync(path.dirname(newFilePath), { recursive: true }) try { - isCopy ? copyRecursiveSync(oldFilePath, newFilePath) : fs.renameSync(oldFilePath, newFilePath) + if (isCopy) { + if (fs.lstatSync(oldFilePath).isDirectory()) { + fs.mkdirSync(newFilePath) + } else { + fs.copyFileSync(oldFilePath, newFilePath) + } + } else { + fs.renameSync(oldFilePath, newFilePath) + } } catch (err) { return false } diff --git a/packages/server-core/src/media/storageprovider/s3.storage.ts b/packages/server-core/src/media/storageprovider/s3.storage.ts index deecc2dfd8..11e34b9024 100755 --- a/packages/server-core/src/media/storageprovider/s3.storage.ts +++ b/packages/server-core/src/media/storageprovider/s3.storage.ts @@ -260,12 +260,12 @@ export class S3Provider implements StorageProviderInterface { // https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-folders.htmlhow to const command = new ListObjectsV2Command({ Bucket: this.bucket, - Prefix: path.join(directoryPath, fileName), + Prefix: path.join(directoryPath, fileName, '/'), MaxKeys: 1 }) try { const response = await this.provider.send(command) - return response?.Contents?.[0]?.Key?.endsWith('/') || false + return (response.Contents && response.Contents.length > 0) || false } catch { return false } @@ -740,13 +740,16 @@ export class S3Provider implements StorageProviderInterface { * @param isCopy If true it will create a copy of object. */ async moveObject(oldName: string, newName: string, oldPath: string, newPath: string, isCopy = false) { + const isDirectory = await this.isDirectory(oldName, oldPath) const oldFilePath = path.join(oldPath, oldName) const newFilePath = path.join(newPath, newName) - const listResponse = await this.listObjects(oldFilePath, true) + const listResponse = await this.listObjects(oldFilePath + (isDirectory ? '/' : ''), false) const result = await Promise.all([ ...listResponse.Contents.map(async (file) => { - const key = path.join(newFilePath, file.Key.replace(oldFilePath, '')) + const relativePath = file.Key.replace(oldFilePath, '') + const key = newFilePath + relativePath + const input = { Bucket: this.bucket, CopySource: `/${this.bucket}/${file.Key}`, diff --git a/packages/server-core/tests/storageprovider/storageprovider.test.ts b/packages/server-core/tests/storageprovider/storageprovider.test.ts index bee09620d7..12100a0a54 100644 --- a/packages/server-core/tests/storageprovider/storageprovider.test.ts +++ b/packages/server-core/tests/storageprovider/storageprovider.test.ts @@ -58,9 +58,20 @@ describe('storageprovider', () => { storageProviders.forEach((providerType) => { describe(`tests for ${providerType.name}`, () => { let provider + let testRootPath + + const createTestDirectories = async () => { + testRootPath = path.join(process.cwd(), 'packages', 'server', 'upload', testFolderName) + await fs.ensureDir(testRootPath) + await fs.ensureDir(path.join(testRootPath, 'temp')) + await fs.ensureDir(path.join(testRootPath, 'temp2')) + await fs.ensureDir(path.join(testRootPath, 'testDirectory')) + } + before(async function () { createEngine() provider = new providerType() + await createTestDirectories() await providerBeforeTest(provider, testFolderName, folderKeyTemp, folderKeyTemp2) }) @@ -165,19 +176,27 @@ describe('storageprovider', () => { }) it(`should put over 1000 objects in ${providerType.name}`, async function () { - const promises: any[] = [] - for (let i = 0; i < 1010; i++) { - const fileKey = path.join(testFolderName, `${i}-${testFileName}`) - const data = Buffer.from([]) - promises.push( - provider.putObject({ - Body: data, - Key: fileKey, - ContentType: getContentType(fileKey) - }) - ) + this.timeout(30000) // increase timeout to 30 seconds + + const batchSize = 100 + const totalObjects = 1010 + + for (let i = 0; i < totalObjects; i += batchSize) { + const promises: any[] = [] + for (let j = i; j < Math.min(i + batchSize, totalObjects); j++) { + const fileKey = path.join(testFolderName, `${j}-${testFileName}`) + const data = Buffer.from([]) + promises.push( + provider.putObject({ + Body: data, + Key: fileKey, + ContentType: getContentType(fileKey) + }) + ) + } + await Promise.all(promises) + await new Promise((resolve) => setTimeout(resolve, 100)) // Add a small delay between batches } - await Promise.all(promises) }) it(`should list over 1000 objects in ${providerType.name}`, async function () { @@ -185,9 +204,43 @@ describe('storageprovider', () => { assert(res.length > 1000) }) + it(`isDirectory: should correctly identify directories in ${providerType.name}`, async function () { + const dirName = 'testDirectory' + const dirPath = path.join(testRootPath, dirName) + const fileName = `testFile-${uuidv4()}.txt` + const filePath = path.join(dirPath, fileName) + + // create a directory + await provider.putObject( + { + Key: dirPath, + Body: Buffer.from(''), + ContentType: 'application/x-directory' + }, + { isDirectory: true } + ) + + // create a file inside the directory + await provider.putObject({ + Body: Buffer.from('test content'), + Key: filePath, + ContentType: 'text/plain' + }) + + // test isDirectory + assert(await provider.isDirectory(dirName, testRootPath), 'Should identify directory') + assert(!(await provider.isDirectory(fileName, filePath)), 'Should not identify file as directory') + assert( + !(await provider.isDirectory('nonexistent', testFolderName)), + 'Should not identify non-existent path as directory' + ) + }) + after(async function () { await destroyEngine() await providerAfterTest(provider, testFolderName) + // clean up the test directory + await fs.remove(testRootPath) }) }) }) diff --git a/packages/ui/src/components/editor/panels/Files/browserGrid/index.tsx b/packages/ui/src/components/editor/panels/Files/browserGrid/index.tsx index 70c60946f2..a0bfc3f0be 100644 --- a/packages/ui/src/components/editor/panels/Files/browserGrid/index.tsx +++ b/packages/ui/src/components/editor/panels/Files/browserGrid/index.tsx @@ -212,6 +212,7 @@ type FileBrowserItemType = { staticResourceModifiedDates: Record isSelected: boolean refreshDirectory: () => Promise + selectedFileKeys: string[] } function fileConsistsOfContentType(file: FileDataType, contentType: string): boolean { @@ -240,7 +241,8 @@ export function FileBrowserItem({ isListView, staticResourceModifiedDates, isSelected, - refreshDirectory + refreshDirectory, + selectedFileKeys }: FileBrowserItemType) { const { t } = useTranslation() const [anchorEvent, setAnchorEvent] = React.useState>(undefined) @@ -322,7 +324,9 @@ export function FileBrowserItem({ accept: [...SupportedFileTypes], drop: (dropItem) => handleDropItemsOnPanel(dropItem, item), canDrop: (dropItem: Record) => - item.isFolder && ('key' in dropItem || canDropItemOverFolder(item.key)), + item.isFolder && + ('key' in dropItem || canDropItemOverFolder(item.key)) && + !selectedFileKeys.includes(item.key), collect: (monitor) => ({ isOver: monitor.canDrop() && monitor.isOver() }) diff --git a/packages/ui/src/components/editor/panels/Files/container/index.tsx b/packages/ui/src/components/editor/panels/Files/container/index.tsx index 3008bb79bc..3d34986aca 100644 --- a/packages/ui/src/components/editor/panels/Files/container/index.tsx +++ b/packages/ui/src/components/editor/panels/Files/container/index.tsx @@ -297,7 +297,10 @@ const FileBrowserContentPanel: React.FC = (props) } const onSelect = (event, params: FileDataType) => { - if (params.type !== 'folder') { + if (params.isFolder && event.detail === 2) { + const newPath = `${selectedDirectory.value}${params.name}/` + changeDirectoryByPath(newPath) + } else { props.onSelectionChanged({ resourceUrl: params.url, name: params.name, @@ -306,11 +309,6 @@ const FileBrowserContentPanel: React.FC = (props) }) ClickPlacementState.setSelectedAsset(params.url) - } else { - if (event.detail === 2) { - const newPath = `${selectedDirectory.value}${params.name}/` - changeDirectoryByPath(newPath) - } } } @@ -324,7 +322,6 @@ const FileBrowserContentPanel: React.FC = (props) selectedFileKeys?: string[] ) => { if (isLoading) return - const destinationPath = dropOn?.isFolder ? `${dropOn.key}/` : selectedDirectory.value if (selectedFileKeys && selectedFileKeys.length > 0) { @@ -332,18 +329,14 @@ const FileBrowserContentPanel: React.FC = (props) selectedFileKeys.map(async (fileKey) => { const file = files.find((f) => f.key === fileKey) if (file) { - if (file.isFolder) { - await fileService.create(`${destinationPath}${file.name}`) - } else { - const newName = `${file.name}${file.type ? '.' + file.type : ''}` - await moveContent(file.fullName, newName, file.path, destinationPath, false) - } + const newName = file.isFolder ? file.name : `${file.name}${file.type ? '.' + file.type : ''}` + await moveContent(file.fullName, newName, file.path, destinationPath, false) } }) ) } else if (isFileDataType(data)) { if (dropOn?.isFolder) { - const newName = `${data.name}${data.type ? '.' + data.type : ''}` + const newName = data.isFolder ? data.name : `${data.name}${data.type ? '.' + data.type : ''}` await moveContent(data.fullName, newName, data.path, destinationPath, false) } } else { @@ -571,8 +564,8 @@ const FileBrowserContentPanel: React.FC = (props) item={file} disableDnD={props.disableDnD} projectName={projectName} - onClick={(event, currentFile) => { - handleFileBrowserItemClick(event, currentFile) + onClick={(event) => { + handleFileBrowserItemClick(event, file) onSelect(event, file) }} onContextMenu={(event, currentFile) => { @@ -637,6 +630,7 @@ const FileBrowserContentPanel: React.FC = (props) staticResourceModifiedDates={staticResourceModifiedDates.value} isSelected={fileProperties.value.some(({ key }) => key === file.key)} refreshDirectory={refreshDirectory} + selectedFileKeys={fileProperties.value.map((file) => file.key)} /> ))} From 85de0d82c0e083d53e001fa94f6dcaa71c937d71 Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Mon, 12 Aug 2024 14:04:34 -0700 Subject: [PATCH 04/29] Component dependencies (#10916) * WIP component dependencies for loading * naming * naming * Race condition with model.scene not being reactive * Missed check * Logic update * Scene loaded hooks * use scene loaded hooks instead of progress * Remove component dependencies * remove * UUID typing * Don't need to do this in a use effect anymore * remove console log --- .../src/networking/AvatarSpawnSystem.tsx | 3 +- .../src/systems/LoadingUISystem.tsx | 4 +- .../hierarchy/HierarchyTreeWalker.ts | 6 +- .../src/systems/ClickPlacementSystem.tsx | 7 +- packages/engine/src/gltf/GLTFComponent.tsx | 156 +++++++++++++++--- .../components/ParticleSystemComponent.ts | 6 +- .../components/VisualScriptComponent.tsx | 3 +- .../common/functions/OnBeforeCompilePlugin.ts | 1 - .../panels/Viewport/container/index.tsx | 6 +- 9 files changed, 154 insertions(+), 38 deletions(-) diff --git a/packages/client-core/src/networking/AvatarSpawnSystem.tsx b/packages/client-core/src/networking/AvatarSpawnSystem.tsx index 11a04785da..da6513dacd 100644 --- a/packages/client-core/src/networking/AvatarSpawnSystem.tsx +++ b/packages/client-core/src/networking/AvatarSpawnSystem.tsx @@ -34,7 +34,6 @@ import { getComponent, getOptionalComponent, PresentationSystemGroup, - useComponent, useQuery, UUIDComponent } from '@etherealengine/ecs' @@ -56,7 +55,7 @@ import { AuthState } from '../user/services/AuthService' export const AvatarSpawnReactor = (props: { sceneEntity: Entity }) => { if (!isClient) return null const { sceneEntity } = props - const gltfLoaded = useComponent(sceneEntity, GLTFComponent).progress.value === 100 + const gltfLoaded = GLTFComponent.useSceneLoaded(sceneEntity) const searchParams = useMutableState(SearchParamState) const spawnAvatar = useHookstate(false) diff --git a/packages/client-core/src/systems/LoadingUISystem.tsx b/packages/client-core/src/systems/LoadingUISystem.tsx index d306f00d61..e44b4442f9 100755 --- a/packages/client-core/src/systems/LoadingUISystem.tsx +++ b/packages/client-core/src/systems/LoadingUISystem.tsx @@ -140,9 +140,9 @@ export const LoadingUISystemState = defineState({ const LoadingReactor = (props: { sceneEntity: Entity }) => { const { sceneEntity } = props - const gltfComponent = useComponent(props.sceneEntity, GLTFComponent) + const gltfComponent = useComponent(sceneEntity, GLTFComponent) const loadingProgress = gltfComponent.progress.value - const sceneLoaded = loadingProgress === 100 + const sceneLoaded = GLTFComponent.useSceneLoaded(sceneEntity) const locationState = useMutableState(LocationState) const state = useMutableState(LoadingUISystemState) diff --git a/packages/editor/src/components/hierarchy/HierarchyTreeWalker.ts b/packages/editor/src/components/hierarchy/HierarchyTreeWalker.ts index 29b0d85a9d..307507df45 100644 --- a/packages/editor/src/components/hierarchy/HierarchyTreeWalker.ts +++ b/packages/editor/src/components/hierarchy/HierarchyTreeWalker.ts @@ -23,8 +23,8 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ -import { ComponentType, getComponent, hasComponent } from '@etherealengine/ecs/src/ComponentFunctions' -import { Entity } from '@etherealengine/ecs/src/Entity' +import { getComponent, hasComponent } from '@etherealengine/ecs/src/ComponentFunctions' +import { Entity, EntityUUID } from '@etherealengine/ecs/src/Entity' import { entityExists } from '@etherealengine/ecs/src/EntityFunctions' import { SourceComponent } from '@etherealengine/engine/src/scene/components/SourceComponent' import { getState } from '@etherealengine/hyperflux' @@ -68,7 +68,7 @@ function buildHierarchyTree( sceneID: string, showModelChildren: boolean ) { - const uuid = node.extensions && (node.extensions[UUIDComponent.jsonID] as ComponentType) + const uuid = node.extensions && (node.extensions[UUIDComponent.jsonID] as EntityUUID) const entity = UUIDComponent.getEntityByUUID(uuid!) if (!entity || !entityExists(entity)) return diff --git a/packages/editor/src/systems/ClickPlacementSystem.tsx b/packages/editor/src/systems/ClickPlacementSystem.tsx index 475a25b9a5..db23567ede 100644 --- a/packages/editor/src/systems/ClickPlacementSystem.tsx +++ b/packages/editor/src/systems/ClickPlacementSystem.tsx @@ -36,7 +36,6 @@ import { getOptionalComponent, removeComponent, setComponent, - useComponent, useOptionalComponent } from '@etherealengine/ecs' import { GLTFComponent } from '@etherealengine/engine/src/gltf/GLTFComponent' @@ -115,7 +114,7 @@ const ClickPlacementReactor = (props: { parentEntity: Entity }) => { const { parentEntity } = props const clickState = useState(getMutableState(ClickPlacementState)) const editorState = useState(getMutableState(EditorHelperState)) - const gltfComponent = useComponent(parentEntity, GLTFComponent) + const sceneLoaded = GLTFComponent.useSceneLoaded(parentEntity) const errors = useEntityErrors(clickState.placementEntity.value, ModelComponent) // const renderers = defineQuery([RendererComponent]) @@ -132,7 +131,7 @@ const ClickPlacementReactor = (props: { parentEntity: Entity }) => { // }, [editorState.placementMode]) useEffect(() => { - if (gltfComponent.progress.value < 100) return + if (!sceneLoaded) return if (editorState.placementMode.value === PlacementMode.CLICK) { SelectionState.updateSelection([]) if (clickState.placementEntity.value) return @@ -146,7 +145,7 @@ const ClickPlacementReactor = (props: { parentEntity: Entity }) => { clickState.placementEntity.set(UndefinedEntity) SelectionState.updateSelection(selectedEntities) } - }, [editorState.placementMode, gltfComponent.progress]) + }, [editorState.placementMode, sceneLoaded]) useEffect(() => { if (!clickState.placementEntity.value) return diff --git a/packages/engine/src/gltf/GLTFComponent.tsx b/packages/engine/src/gltf/GLTFComponent.tsx index c2803003e4..af991ac3ac 100644 --- a/packages/engine/src/gltf/GLTFComponent.tsx +++ b/packages/engine/src/gltf/GLTFComponent.tsx @@ -28,6 +28,8 @@ import React, { useEffect } from 'react' import { parseStorageProviderURLs } from '@etherealengine/common/src/utils/parseSceneJSON' import { + Component, + ComponentJSONIDMap, defineComponent, Entity, EntityUUID, @@ -44,13 +46,36 @@ import { dispatchAction, getState, useHookstate } from '@etherealengine/hyperflu import { FileLoader } from '../assets/loaders/base/FileLoader' import { BINARY_EXTENSION_HEADER_MAGIC, EXTENSIONS, GLTFBinaryExtension } from '../assets/loaders/gltf/GLTFExtensions' -import { ModelComponent } from '../scene/components/ModelComponent' import { SourceComponent } from '../scene/components/SourceComponent' import { SceneJsonType } from '../scene/types/SceneTypes' import { migrateSceneJSONToGLTF } from './convertJsonToGLTF' import { GLTFDocumentState, GLTFSnapshotAction } from './GLTFDocumentState' import { ResourcePendingComponent } from './ResourcePendingComponent' +const loadDependencies = { + ['EE_model']: ['scene'] +} as Record + +type ComponentDependencies = Record + +const buildComponentDependencies = (json: GLTF.IGLTF) => { + const dependencies = {} as ComponentDependencies + if (!json.nodes) return dependencies + for (const node of json.nodes) { + if (!node.extensions || !node.extensions[UUIDComponent.jsonID]) continue + const uuid = node.extensions[UUIDComponent.jsonID] as EntityUUID + const extensions = Object.keys(node.extensions) + for (const extension of extensions) { + if (loadDependencies[extension]) { + if (!dependencies[uuid]) dependencies[uuid] = [] + dependencies[uuid].push(ComponentJSONIDMap.get(extension)!) + } + } + } + + return dependencies +} + export const GLTFComponent = defineComponent({ name: 'GLTFComponent', @@ -59,7 +84,8 @@ export const GLTFComponent = defineComponent({ src: '', // internals extensions: {}, - progress: 0 + progress: 0, + dependencies: undefined as ComponentDependencies | undefined } }, @@ -67,41 +93,61 @@ export const GLTFComponent = defineComponent({ if (typeof json?.src === 'string') component.src.set(json.src) }, + useDependenciesLoaded(entity: Entity) { + const dependencies = useComponent(entity, GLTFComponent).dependencies + return !!(dependencies.value && !dependencies.keys?.length) + }, + + useSceneLoaded(entity: Entity) { + const gltfComponent = useComponent(entity, GLTFComponent) + const dependencies = gltfComponent.dependencies + const progress = gltfComponent.progress.value + return !!(dependencies.value && !dependencies.keys?.length) && progress === 100 + }, + + isSceneLoaded(entity: Entity) { + const gltfComponent = getComponent(entity, GLTFComponent) + const dependencies = gltfComponent.dependencies + const progress = gltfComponent.progress + return !!(dependencies && !Object.keys(dependencies).length) && progress === 100 + }, + reactor: () => { const entity = useEntityContext() const gltfComponent = useComponent(entity, GLTFComponent) + const dependencies = gltfComponent.dependencies useGLTFDocument(gltfComponent.src.value, entity) const documentID = useComponent(entity, SourceComponent).value - return + return ( + <> + + {dependencies.value && dependencies.keys?.length ? ( + + ) : null} + + ) } }) const ResourceReactor = (props: { documentID: string; entity: Entity }) => { + const dependenciesLoaded = GLTFComponent.useDependenciesLoaded(props.entity) const resourceQuery = useQuery([SourceComponent, ResourcePendingComponent]) const sourceEntities = useHookstate(SourceComponent.entitiesBySourceState[props.documentID]) useEffect(() => { if (getComponent(props.entity, GLTFComponent).progress === 100) return if (!getState(GLTFDocumentState)[props.documentID]) return - const document = getState(GLTFDocumentState)[props.documentID] - const modelNodes = document.nodes?.filter((node) => !!node.extensions?.[ModelComponent.jsonID]) - if (modelNodes) { - for (const node of modelNodes) { - //check if an entity exists for this node, and has a model component - const uuid = node.extensions![UUIDComponent.jsonID] as EntityUUID - if (!UUIDComponent.entitiesByUUIDState[uuid]) return - const entity = UUIDComponent.entitiesByUUIDState[uuid].value - const model = getOptionalComponent(entity, ModelComponent) - //ensure that model contents have been loaded into the scene - if (!model?.scene) return - } - } + const entities = resourceQuery.filter((e) => getComponent(e, SourceComponent) === props.documentID) if (!entities.length) { - getMutableComponent(props.entity, GLTFComponent).progress.set(100) + if (dependenciesLoaded) getMutableComponent(props.entity, GLTFComponent).progress.set(100) return } @@ -121,14 +167,83 @@ const ResourceReactor = (props: { documentID: string; entity: Entity }) => { const progress = resources.reduce((acc, resource) => acc + resource.progress, 0) const total = resources.reduce((acc, resource) => acc + resource.total, 0) + if (!total) return - const percentage = total === 0 ? 100 : (progress / total) * 100 + const percentage = Math.floor(Math.min((progress / total) * 100, dependenciesLoaded ? 100 : 99)) getMutableComponent(props.entity, GLTFComponent).progress.set(percentage) - }, [resourceQuery, sourceEntities]) + }, [resourceQuery, sourceEntities, dependenciesLoaded]) return null } +const ComponentReactor = (props: { gltfComponentEntity: Entity; entity: Entity; component: Component }) => { + const { gltfComponentEntity, entity, component } = props + const dependencies = loadDependencies[component.jsonID!] + const comp = useComponent(entity, component) + + useEffect(() => { + const compValue = comp.value + for (const key of dependencies) { + if (!compValue[key]) return + } + + // console.log(`All dependencies loaded for entity: ${entity} on component: ${component.jsonID}`) + + const gltfComponent = getMutableComponent(gltfComponentEntity, GLTFComponent) + const uuid = getComponent(entity, UUIDComponent) + gltfComponent.dependencies.set((prev) => { + const dependencyArr = prev![uuid] as Component[] + const index = dependencyArr.findIndex((compItem) => compItem.jsonID === component.jsonID) + dependencyArr.splice(index, 1) + if (!dependencyArr.length) { + delete prev![uuid] + } + return prev + }) + }, [...dependencies.map((key) => comp[key])]) + + return null +} + +const DependencyEntryReactor = (props: { gltfComponentEntity: Entity; uuid: string; components: Component[] }) => { + const { gltfComponentEntity, uuid, components } = props + const entity = UUIDComponent.useEntityByUUID(uuid as EntityUUID) as Entity | undefined + return entity ? ( + <> + {components.map((component) => { + return ( + + ) + })} + + ) : null +} + +const DependencyReactor = (props: { gltfComponentEntity: Entity; dependencies: ComponentDependencies }) => { + const { gltfComponentEntity, dependencies } = props + const entries = Object.entries(dependencies) + + return ( + <> + {entries.map(([uuid, components]) => { + return ( + + ) + })} + + ) +} + const onError = (error: ErrorEvent) => { // console.error(error) } @@ -187,6 +302,9 @@ const useGLTFDocument = (url: string, entity: Entity) => { json = migrateSceneJSONToGLTF(json) } + const dependencies = buildComponentDependencies(json) + state.dependencies.set(dependencies) + dispatchAction( GLTFSnapshotAction.createSnapshot({ source: getComponent(entity, SourceComponent), diff --git a/packages/engine/src/scene/components/ParticleSystemComponent.ts b/packages/engine/src/scene/components/ParticleSystemComponent.ts index 374bffde30..d267efa9a1 100644 --- a/packages/engine/src/scene/components/ParticleSystemComponent.ts +++ b/packages/engine/src/scene/components/ParticleSystemComponent.ts @@ -868,7 +868,7 @@ export const ParticleSystemComponent = defineComponent({ const metadata = useHookstate({ textures: {}, geometries: {}, materials: {} } as ParticleSystemMetadata) const sceneID = useOptionalComponent(entity, SourceComponent)?.value const rootEntity = useHookstate(getMutableState(GLTFSourceState))[sceneID ?? ''].value - const rootGLTF = useOptionalComponent(rootEntity, GLTFComponent) + const sceneLoaded = GLTFComponent.useSceneLoaded(rootEntity) const refreshed = useHookstate(false) const [geoDependency] = useGLTF(componentState.value.systemParameters.instancingGeometry!, entity, (url) => { @@ -890,7 +890,7 @@ export const ParticleSystemComponent = defineComponent({ }) //@todo: this is a hack to make trail rendering mode work correctly. We need to find out why an additional snapshot is needed useEffect(() => { - if (rootGLTF?.value?.progress !== 100) return + if (!sceneLoaded) return if (refreshed.value) return //if (componentState.systemParameters.renderMode.value === RenderMode.Trail) { @@ -898,7 +898,7 @@ export const ParticleSystemComponent = defineComponent({ dispatchAction(GLTFSnapshotAction.createSnapshot(snapshot)) //} refreshed.set(true) - }, [rootGLTF?.value?.progress]) + }, [sceneLoaded]) useEffect(() => { //add dud material diff --git a/packages/engine/src/visualscript/components/VisualScriptComponent.tsx b/packages/engine/src/visualscript/components/VisualScriptComponent.tsx index e82d5c73e3..4189e15dc6 100644 --- a/packages/engine/src/visualscript/components/VisualScriptComponent.tsx +++ b/packages/engine/src/visualscript/components/VisualScriptComponent.tsx @@ -111,8 +111,7 @@ export const VisualScriptComponent = defineComponent({ }) const LoadReactor = (props: { entity: Entity; gltfAncestor: Entity }) => { - const gltfComponent = useComponent(props.gltfAncestor, GLTFComponent) - const loaded = gltfComponent.progress.value === 100 + const loaded = GLTFComponent.useSceneLoaded(props.gltfAncestor) useEffect(() => { setComponent(props.entity, VisualScriptComponent, { run: true }) diff --git a/packages/spatial/src/common/functions/OnBeforeCompilePlugin.ts b/packages/spatial/src/common/functions/OnBeforeCompilePlugin.ts index 20e8dd1758..179f8d4aa5 100644 --- a/packages/spatial/src/common/functions/OnBeforeCompilePlugin.ts +++ b/packages/spatial/src/common/functions/OnBeforeCompilePlugin.ts @@ -58,7 +58,6 @@ export type PluginType = PluginObjectType | typeof Material.prototype.onBeforeCo /**@deprecated Use setPlugin instead */ export function addOBCPlugin(material: Material, plugin: PluginType): void { material.onBeforeCompile = plugin as any - console.log(material.onBeforeCompile) material.needsUpdate = true } diff --git a/packages/ui/src/components/editor/panels/Viewport/container/index.tsx b/packages/ui/src/components/editor/panels/Viewport/container/index.tsx index e744cfb1b6..580bd920c3 100644 --- a/packages/ui/src/components/editor/panels/Viewport/container/index.tsx +++ b/packages/ui/src/components/editor/panels/Viewport/container/index.tsx @@ -123,6 +123,7 @@ const ViewportDnD = ({ children }: { children: React.ReactNode }) => { const SceneLoadingProgress = ({ rootEntity }) => { const { t } = useTranslation() const progress = useComponent(rootEntity, GLTFComponent).progress.value + const loaded = GLTFComponent.useSceneLoaded(rootEntity) const resourcePendingQuery = useQuery([ResourcePendingComponent]) const root = getComponent(rootEntity, SourceComponent) const sceneModified = useHookstate(getMutableState(GLTFModifiedState)[root]).value @@ -142,12 +143,13 @@ const SceneLoadingProgress = ({ rootEntity }) => { } }, [sceneModified]) - if (progress === 100) return null + if (loaded) return null return ( ) @@ -183,8 +185,8 @@ const ViewPortPanelContainer = () => { {sceneName.value ? : null} {sceneName.value ? ( <> - {rootEntity.value && }
+ {rootEntity.value && } ) : (
From 5755786b9cc5633739373b970eb11a9a21a19593 Mon Sep 17 00:00:00 2001 From: Appaji <52322531+CITIZENDOT@users.noreply.github.com> Date: Tue, 13 Aug 2024 12:12:56 +0530 Subject: [PATCH 05/29] IR-3053: Add project-history service (#10736) * Add project-history service * Add license * Add hooks to project service * Pass actionIdentifier & fix resolver & hooks order Import ProjectHistoryService in services.ts * replace `isSuperAdmin` hook * Fix errors * Implement base UI * Update table structure And Add history hooks to remaining services * add license * remove userId in query * Add User Interface for Project-History service - Update AvatarImage's label parsing logic * Parse URLs * Add `updatedBy` column in `static-resource` table * Add stored procedure and trigger for static-resource * Fix typo * Fix typo * remove hook * Add new line at the end * `dropForeign` column in migration * revert prop types in Text * move schema and path to common files * Move schema and path to common package * Add `static-resource` & `location` triggers and stored procedures Added triggers for all the operations (CREATE, UPDATE, DELETE) Also added `updatedBy` columns to the both tables * revert tsconfig * Add `project` triggers and stored procedures * Add `project-permission` triggers + stored procedures + updatedBy column * fix CI/CD error * fix CI/CD error * fix errors * move strings from multitenancy.json * remove individual action types * typo * allow multipleStatements in knexfile * improve ui * Fixed enum * fix tooltip value for permission actions set actionDetail type to json * change `CONCAT` to `JSON_OBJECT` to construct json strings * remove unnecessary checks * Revert "Fixed enum" This reverts commit 7c0f76afc104bfca496172581596a5d446f8f9ab. * add newline * check user permissions before find query * revert tsconfig * Changed permissions * permit only owner scope (and project:read) * join avatarimage labels without comma * move delete log operations to API write * Fix scene & resource classification --------- Co-authored-by: Liam Broza Co-authored-by: hanzlamateen --- packages/client-core/i18n/en/admin.json | 8 +- .../components/project/ProjectHistory.tsx | 302 ++++++++++++++++++ .../project/ProjectHistoryModal.tsx | 45 +++ .../admin/components/project/ProjectTable.tsx | 12 + .../src/social/services/LocationService.ts | 1 + packages/common/src/schema.type.module.ts | 4 + .../schemas/media/static-resource.schema.ts | 3 + .../projects/project-history.schema.ts | 110 +++++++ .../projects/project-permission.schema.ts | 3 + .../src/schemas/projects/project.schema.ts | 6 +- .../src/schemas/social/location.schema.ts | 4 + packages/server-core/knexfile.ts | 10 +- .../migrations/20240731173405_updatedBy.ts | 59 ++++ .../static-resource/static-resource.hooks.ts | 33 +- .../static-resource.resolvers.ts | 10 +- packages/server-core/src/mysql.ts | 3 +- .../20240730102300_project-history.ts | 86 +++++ ...20240731180000_static_resource_triggers.ts | 51 +++ .../20240806175210_location_triggers.ts | 51 +++ .../20240806191009_project_triggers.ts | 48 +++ ...40806192128_project-permission_triggers.ts | 51 +++ .../migrations/location_triggers.sql | 108 +++++++ .../project-permission_triggers.sql | 120 +++++++ .../migrations/project-triggers.sql | 46 +++ .../migrations/static_resource_triggers.sql | 142 ++++++++ .../project-history/project-history.class.ts | 43 +++ .../project-history/project-history.docs.ts | 44 +++ .../project-history/project-history.hooks.ts | 163 ++++++++++ .../project-history.resolvers.ts | 81 +++++ .../project-history/project-history.ts | 59 ++++ .../migrations/20240806192034_updatedBy.ts | 58 ++++ .../project-permission.hooks.ts | 27 +- .../project-permission.resolvers.ts | 6 + .../migrations/20240806170758_updatedBy.ts | 58 ++++ .../src/projects/project/project.resolvers.ts | 6 + packages/server-core/src/projects/services.ts | 4 +- .../src/social/location/location.hooks.ts | 24 +- .../src/social/location/location.resolvers.ts | 6 + .../migrations/20240806175038_updatedBy.ts | 58 ++++ .../editor/panels/Files/container/index.tsx | 1 + .../primitives/tailwind/AvatarImage/index.tsx | 18 +- .../ui/src/primitives/tailwind/Text/index.tsx | 6 +- 42 files changed, 1961 insertions(+), 17 deletions(-) create mode 100644 packages/client-core/src/admin/components/project/ProjectHistory.tsx create mode 100644 packages/client-core/src/admin/components/project/ProjectHistoryModal.tsx create mode 100644 packages/common/src/schemas/projects/project-history.schema.ts create mode 100644 packages/server-core/src/media/static-resource/migrations/20240731173405_updatedBy.ts create mode 100644 packages/server-core/src/projects/project-history/migrations/20240730102300_project-history.ts create mode 100644 packages/server-core/src/projects/project-history/migrations/20240731180000_static_resource_triggers.ts create mode 100644 packages/server-core/src/projects/project-history/migrations/20240806175210_location_triggers.ts create mode 100644 packages/server-core/src/projects/project-history/migrations/20240806191009_project_triggers.ts create mode 100644 packages/server-core/src/projects/project-history/migrations/20240806192128_project-permission_triggers.ts create mode 100644 packages/server-core/src/projects/project-history/migrations/location_triggers.sql create mode 100644 packages/server-core/src/projects/project-history/migrations/project-permission_triggers.sql create mode 100644 packages/server-core/src/projects/project-history/migrations/project-triggers.sql create mode 100644 packages/server-core/src/projects/project-history/migrations/static_resource_triggers.sql create mode 100644 packages/server-core/src/projects/project-history/project-history.class.ts create mode 100644 packages/server-core/src/projects/project-history/project-history.docs.ts create mode 100644 packages/server-core/src/projects/project-history/project-history.hooks.ts create mode 100644 packages/server-core/src/projects/project-history/project-history.resolvers.ts create mode 100644 packages/server-core/src/projects/project-history/project-history.ts create mode 100644 packages/server-core/src/projects/project-permission/migrations/20240806192034_updatedBy.ts create mode 100644 packages/server-core/src/projects/project/migrations/20240806170758_updatedBy.ts create mode 100644 packages/server-core/src/social/location/migrations/20240806175038_updatedBy.ts diff --git a/packages/client-core/i18n/en/admin.json b/packages/client-core/i18n/en/admin.json index 98fef2a4d5..d40aa031d7 100755 --- a/packages/client-core/i18n/en/admin.json +++ b/packages/client-core/i18n/en/admin.json @@ -86,7 +86,9 @@ "lastUpdatedBy": "Last updated by user id: {{userId}} on {{updatedAt}}", "fillRequiredFields": "Please fill all required field", "fixErrorFields": "Please fix all errors", - "logOut": "Log Out" + "logOut": "Log Out", + "newestFirst": "Newest First", + "oldestFirst": "Oldest First" }, "analytics": { "loading": "Loading analytics...", @@ -214,8 +216,10 @@ "repo": "Repo", "access": "Access", "invalidateCache": "Invalidate Cache", - "update": "Update" + "update": "Update", + "history": "History" }, + "projectHistory": "Project History", "addProject": "Add Project", "updateProject": "Update Project", "downloadProject": "Download Project", diff --git a/packages/client-core/src/admin/components/project/ProjectHistory.tsx b/packages/client-core/src/admin/components/project/ProjectHistory.tsx new file mode 100644 index 0000000000..0dc57f958f --- /dev/null +++ b/packages/client-core/src/admin/components/project/ProjectHistory.tsx @@ -0,0 +1,302 @@ +/* +CPAL-1.0 License + +The contents of this file are subject to the Common Public Attribution License +Version 1.0. (the "License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at +https://github.com/EtherealEngine/etherealengine/blob/dev/LICENSE. +The License is based on the Mozilla Public License Version 1.1, but Sections 14 +and 15 have been added to cover use of software over a computer network and +provide for limited attribution for the Original Developer. In addition, +Exhibit A has been modified to be consistent with Exhibit B. + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + +The Original Code is Ethereal Engine. + +The Original Developer is the Initial Developer. The Initial Developer of the +Original Code is the Ethereal Engine team. + +All portions of the code written by the Ethereal Engine team are Copyright © 2021-2023 +Ethereal Engine. All Rights Reserved. +*/ + +import { projectHistoryPath } from '@etherealengine/common/src/schema.type.module' +import { ProjectHistoryType } from '@etherealengine/common/src/schemas/projects/project-history.schema' +import { useFind } from '@etherealengine/spatial/src/common/functions/FeathersHooks' + +import { toDisplayDateTime } from '@etherealengine/common/src/utils/datetime-sql' +import AvatarImage from '@etherealengine/ui/src/primitives/tailwind/AvatarImage' +import Button from '@etherealengine/ui/src/primitives/tailwind/Button' +import { TablePagination } from '@etherealengine/ui/src/primitives/tailwind/Table' +import Text from '@etherealengine/ui/src/primitives/tailwind/Text' +import Tooltip from '@etherealengine/ui/src/primitives/tailwind/Tooltip' +import React from 'react' +import { useTranslation } from 'react-i18next' +import { FaSortAmountDown, FaSortAmountUpAlt } from 'react-icons/fa' + +const PROJECT_HISTORY_PAGE_LIMIT = 10 + +const getRelativeURLFromProject = (projectName: string, url: string) => { + const prefix = `projects/${projectName}/` + if (url.startsWith(prefix)) { + return url.replace(prefix, '') + } + return url +} + +const getResourceURL = (projectName: string, url: string, resourceType: 'resource' | 'scene') => { + const relativeURL = getRelativeURLFromProject(projectName, url) + const resourceURL = + resourceType === 'resource' + ? `/projects/${projectName}/${relativeURL}` + : `/studio?project=${projectName}&scenePath=${url}` + return { + relativeURL, + resourceURL + } +} + +function HistoryLog({ projectHistory, projectName }: { projectHistory: ProjectHistoryType; projectName: string }) { + const { t } = useTranslation() + + const RenderAction = () => { + if (projectHistory.action === 'LOCATION_PUBLISHED' || projectHistory.action === 'LOCATION_UNPUBLISHED') { + const actionDetail = JSON.parse(projectHistory.actionDetail) as { + locationName: string + sceneURL: string + sceneId: string + } + + const verb = projectHistory.action === 'LOCATION_PUBLISHED' ? 'published' : 'unpublished' + + const { relativeURL, resourceURL } = getResourceURL(projectName, actionDetail.sceneURL, 'scene') + + return ( + <> + {verb} the location + + {verb === 'published' ? ( + + + {actionDetail.locationName} + + + ) : ( + + {actionDetail.locationName} + + )} + + from the scene + + + {relativeURL}. + + + ) + } else if (projectHistory.action === 'LOCATION_MODIFIED') { + const actionDetail = JSON.parse(projectHistory.actionDetail) as { + locationName: string + } + + return ( + <> + modified the location + + + + {actionDetail.locationName} + + + + ) + } else if (projectHistory.action === 'PERMISSION_CREATED' || projectHistory.action === 'PERMISSION_REMOVED') { + const actionDetail = JSON.parse(projectHistory.actionDetail) as { + userName: string + userId: string + permissionType: string + } + + const verb = projectHistory.action === 'PERMISSION_CREATED' ? 'added' : 'removed' + + return ( + <> + {verb} the + {actionDetail.permissionType} + + access to + + + {actionDetail.userName} + + + ) + } else if (projectHistory.action === 'PERMISSION_MODIFIED') { + const actionDetail = JSON.parse(projectHistory.actionDetail) as { + userName: string + userId: string + oldPermissionType: string + newPermissionType: string + } + + return ( + <> + updated the permission of the user + + {actionDetail.userName} + + from + {actionDetail.oldPermissionType} + to + {actionDetail.newPermissionType} + + ) + } else if (projectHistory.action === 'PROJECT_CREATED') { + return created the project + } else if ( + projectHistory.action === 'RESOURCE_CREATED' || + projectHistory.action === 'RESOURCE_REMOVED' || + projectHistory.action === 'SCENE_CREATED' || + projectHistory.action === 'SCENE_REMOVED' + ) { + const verb = + projectHistory.action === 'RESOURCE_CREATED' || projectHistory.action === 'SCENE_CREATED' + ? 'created' + : 'removed' + const object = + projectHistory.action === 'RESOURCE_CREATED' || projectHistory.action === 'RESOURCE_REMOVED' + ? 'resource' + : 'scene' + + const actionDetail = JSON.parse(projectHistory.actionDetail) as { + url: string + } + + const { relativeURL, resourceURL } = getResourceURL(projectName, actionDetail.url, object) + + return ( + <> + + {verb} the {object} + + + {relativeURL} + + + ) + } else if (projectHistory.action === 'RESOURCE_RENAMED' || projectHistory.action === 'SCENE_RENAMED') { + const object = projectHistory.action === 'RESOURCE_RENAMED' ? 'resource' : 'scene' + const actionDetail = JSON.parse(projectHistory.actionDetail) as { + oldURL: string + newURL: string + } + + const { relativeURL: oldRelativeURL } = getResourceURL(projectName, actionDetail.oldURL, object) + const { relativeURL: newRelativeURL, resourceURL: newResourceURL } = getResourceURL( + projectName, + actionDetail.newURL, + object + ) + + return ( + <> + renamed a {object} from + + {oldRelativeURL} + to + + {getRelativeURLFromProject(projectName, newRelativeURL)} + + + ) + } else if (projectHistory.action === 'RESOURCE_MODIFIED' || projectHistory.action === 'SCENE_MODIFIED') { + const object = projectHistory.action === 'RESOURCE_MODIFIED' ? 'resource' : 'scene' + const actionDetail = JSON.parse(projectHistory.actionDetail) as { + url: string + } + + const { relativeURL, resourceURL } = getResourceURL(projectName, actionDetail.url, object) + + return ( + <> + modified the {object} + + {relativeURL} + + + ) + } + + return null + } + + return ( +
+
+ + + {projectHistory.userName} + + +
+ + {toDisplayDateTime(projectHistory.createdAt)} +
+ ) +} + +export const ProjectHistory = ({ projectId, projectName }: { projectId: string; projectName: string }) => { + const { t } = useTranslation() + const projectHistoryQuery = useFind(projectHistoryPath, { + query: { + projectId: projectId, + $sort: { + createdAt: -1 + }, + $limit: PROJECT_HISTORY_PAGE_LIMIT + } + }) + + const sortOrder = projectHistoryQuery.sort.createdAt + + const toggleSortOrder = () => { + projectHistoryQuery.setSort({ + createdAt: sortOrder === -1 ? 1 : -1 + }) + } + + return ( +
+ + + {projectHistoryQuery.data && + projectHistoryQuery.data.map((projectHistory, index) => ( + + ))} + + projectHistoryQuery.setPage(newPage)} + /> +
+ ) +} diff --git a/packages/client-core/src/admin/components/project/ProjectHistoryModal.tsx b/packages/client-core/src/admin/components/project/ProjectHistoryModal.tsx new file mode 100644 index 0000000000..033242d548 --- /dev/null +++ b/packages/client-core/src/admin/components/project/ProjectHistoryModal.tsx @@ -0,0 +1,45 @@ +/* +CPAL-1.0 License + +The contents of this file are subject to the Common Public Attribution License +Version 1.0. (the "License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at +https://github.com/EtherealEngine/etherealengine/blob/dev/LICENSE. +The License is based on the Mozilla Public License Version 1.1, but Sections 14 +and 15 have been added to cover use of software over a computer network and +provide for limited attribution for the Original Developer. In addition, +Exhibit A has been modified to be consistent with Exhibit B. + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + +The Original Code is Ethereal Engine. + +The Original Developer is the Initial Developer. The Initial Developer of the +Original Code is the Ethereal Engine team. + +All portions of the code written by the Ethereal Engine team are Copyright © 2021-2023 +Ethereal Engine. All Rights Reserved. +*/ + +import Modal from '@etherealengine/ui/src/primitives/tailwind/Modal' +import React from 'react' +import { useTranslation } from 'react-i18next' +import { PopoverState } from '../../../common/services/PopoverState' +import { ProjectHistory } from './ProjectHistory' + +export const ProjectHistoryModal = ({ projectId, projectName }: { projectId: string; projectName: string }) => { + const { t } = useTranslation() + return ( + { + PopoverState.hidePopupover() + }} + > + + + ) +} diff --git a/packages/client-core/src/admin/components/project/ProjectTable.tsx b/packages/client-core/src/admin/components/project/ProjectTable.tsx index a664aeff37..f27d9e058b 100644 --- a/packages/client-core/src/admin/components/project/ProjectTable.tsx +++ b/packages/client-core/src/admin/components/project/ProjectTable.tsx @@ -28,6 +28,7 @@ import { useTranslation } from 'react-i18next' import { GrGithub } from 'react-icons/gr' import { HiOutlineArrowPath, + HiOutlineClock, HiOutlineCommandLine, HiOutlineExclamationCircle, HiOutlineFolder, @@ -55,6 +56,7 @@ import { ProjectRowType, projectsColumns } from '../../common/constants/project' import { ProjectUpdateState } from '../../services/ProjectUpdateService' import AddEditProjectModal from './AddEditProjectModal' import ManageUserPermissionModal from './ManageUserPermissionModal' +import { ProjectHistoryModal } from './ProjectHistoryModal' const logger = multiLogger.child({ component: 'client-core:ProjectTable' }) @@ -186,6 +188,16 @@ export default function ProjectTable(props: { search: string }) { > {t('admin:components.common.view')} + + +
) From a3ed347b97089b58f81692e23a1d213a2d4a03a6 Mon Sep 17 00:00:00 2001 From: Jeremy Sachs Date: Tue, 13 Aug 2024 03:53:31 -0700 Subject: [PATCH 10/29] Left aligning the text in the "Add Component" and "Add Entity" menus (#10946) --- .../editor/panels/Properties/elementList/index.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/ui/src/components/editor/panels/Properties/elementList/index.tsx b/packages/ui/src/components/editor/panels/Properties/elementList/index.tsx index 03ae756def..07df1cbde3 100644 --- a/packages/ui/src/components/editor/panels/Properties/elementList/index.tsx +++ b/packages/ui/src/components/editor/panels/Properties/elementList/index.tsx @@ -78,10 +78,10 @@ const ComponentListItem = ({ item, onSelect }: { item: Component; onSelect: () = startIcon={} >
- + {startCase(jsonName.replace('-', ' ').toLowerCase())} - + {t(`editor:layout.assetGrid.component-detail.${jsonName}`, '')}
@@ -107,8 +107,8 @@ const PrefabListItem = ({ item, onSelect }: { item: PrefabShelfItem; onSelect: ( startIcon={} >
- {item.name} - + {item.name} + {item.detail}
From 81d431741f591686276010e702d1a6d8823bf026 Mon Sep 17 00:00:00 2001 From: Jeremy Sachs Date: Tue, 13 Aug 2024 03:54:32 -0700 Subject: [PATCH 11/29] Updating the description of hemisphere lights to better explain what they do and why. (#10945) --- packages/client-core/i18n/en/editor.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/client-core/i18n/en/editor.json b/packages/client-core/i18n/en/editor.json index 9544c2e0c7..3fbb540085 100755 --- a/packages/client-core/i18n/en/editor.json +++ b/packages/client-core/i18n/en/editor.json @@ -568,7 +568,7 @@ }, "hemisphere": { "name": "Hemisphere Light", - "description": "A light which illuminates the scene from directly overhead.", + "description": "A light which illuminates the scene with a sky color from above and a ground color from below.", "lbl-skyColor": "Sky Color", "lbl-groundColor": "Ground Color", "lbl-intensity": "Intensity" @@ -1177,7 +1177,7 @@ "point-light": "A light which emits in all directions from a single point.", "spot-light": "Creates a light that shines in a specific direction.", "directional-light": "Creates a light that emits evenly in a single direction.", - "hemisphere-light": "A light which illuminates the scene from directly overhead.", + "hemisphere-light": "A light which illuminates the scene with a sky color from above and a ground color from below.", "particle-system": "Creates a particle emitter.", "system": "Inserts code into the scene by creating a new Entity Component System based on the provided .ts file", "visual-script": "Customizes state and behavior of entities through a visual node connections.", From 47ca0b896b5659915584d30472968e0798522062 Mon Sep 17 00:00:00 2001 From: MbfloydIR <144718558+MbfloydIR@users.noreply.github.com> Date: Tue, 13 Aug 2024 03:57:30 -0700 Subject: [PATCH 12/29] added tailwind class to break the line in the middle of words to prevent overflowing the alocated space (#10944) --- .../src/components/editor/panels/Properties/material/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/components/editor/panels/Properties/material/index.tsx b/packages/ui/src/components/editor/panels/Properties/material/index.tsx index d9900fab51..1df3f0a34b 100644 --- a/packages/ui/src/components/editor/panels/Properties/material/index.tsx +++ b/packages/ui/src/components/editor/panels/Properties/material/index.tsx @@ -220,7 +220,7 @@ export function MaterialEditor(props: { materialUUID: EntityUUID }) {
-
{getOptionalComponent(entity, SourceComponent) ?? 'None'}
+
{getOptionalComponent(entity, SourceComponent) ?? 'None'}

From 3e648cd8371286512c0ae84f20cbb78258be0564 Mon Sep 17 00:00:00 2001 From: dtlehrer <19999194+dtlehrer@users.noreply.github.com> Date: Tue, 13 Aug 2024 06:43:16 -0500 Subject: [PATCH 13/29] [IR-3547] studio: show unsaved changes dialog when switching between scenes (#10921) * show unsaved changes dialog when switching between scenes * replace beforeunload event * rename function to `confirmSceneSaveIfModified` * use web standard alert for saving changes --------- Co-authored-by: aditya-mitra <55396651+aditya-mitra@users.noreply.github.com> --- packages/client-core/i18n/en/editor.json | 5 ++- .../editor/src/components/EditorContainer.tsx | 10 +++++ .../components/dialogs/SaveSceneDialog.tsx | 1 + .../editor/src/components/toolbar/Toolbar.tsx | 39 +++++-------------- .../editor/panels/Scenes/container/index.tsx | 5 ++- .../panels/Viewport/container/index.tsx | 25 ++---------- 6 files changed, 31 insertions(+), 54 deletions(-) diff --git a/packages/client-core/i18n/en/editor.json b/packages/client-core/i18n/en/editor.json index 3fbb540085..a48ec56ec9 100755 --- a/packages/client-core/i18n/en/editor.json +++ b/packages/client-core/i18n/en/editor.json @@ -1360,7 +1360,10 @@ "lbl-thumbnail": "Generate thumbnail & envmap", "lbl-confirm": "Save Scene", "info-confirm": "Are you sure you want to save the scene?", - "info-question": "Do you want to save the current scene?" + "info-question": "Do you want to save the current scene?", + "unsavedChanges": { + "title": "Unsaved Changes" + } }, "saveNewScene": { "title": "Save As", diff --git a/packages/editor/src/components/EditorContainer.tsx b/packages/editor/src/components/EditorContainer.tsx index 6f374f3b35..e62b06f908 100644 --- a/packages/editor/src/components/EditorContainer.tsx +++ b/packages/editor/src/components/EditorContainer.tsx @@ -200,6 +200,16 @@ const EditorContainer = () => { } }, [errorState]) + useEffect(() => { + const handleBeforeUnload = async (event: BeforeUnloadEvent) => { + if (EditorState.isModified()) { + event.preventDefault() + } + } + window.addEventListener('beforeunload', handleBeforeUnload) + return () => window.removeEventListener('beforeunload', handleBeforeUnload) + }, []) + return (
return ( { PopoverState.hidePopupover() diff --git a/packages/editor/src/components/toolbar/Toolbar.tsx b/packages/editor/src/components/toolbar/Toolbar.tsx index 316da9c6c9..4ecf87012e 100644 --- a/packages/editor/src/components/toolbar/Toolbar.tsx +++ b/packages/editor/src/components/toolbar/Toolbar.tsx @@ -60,25 +60,21 @@ const onImportAsset = async () => { } } -const onClickNewScene = async () => { +export const confirmSceneSaveIfModified = async () => { const isModified = EditorState.isModified() if (isModified) { - const confirm = await new Promise((resolve) => { + return new Promise((resolve) => { PopoverState.showPopupover( - { - resolve(true) - }} - onCancel={() => { - resolve(false) - }} - /> + resolve(true)} onCancel={() => resolve(false)} /> ) }) - if (!confirm) return } + return true +} + +const onClickNewScene = async () => { + if (!(await confirmSceneSaveIfModified())) return const newSceneUIAddons = getState(EditorState).uiAddons.newScene if (Object.keys(newSceneUIAddons).length > 0) { @@ -89,24 +85,7 @@ const onClickNewScene = async () => { } const onCloseProject = async () => { - const isModified = EditorState.isModified() - - if (isModified) { - const confirm = await new Promise((resolve) => { - PopoverState.showPopupover( - { - resolve(true) - }} - onCancel={() => { - resolve(false) - }} - /> - ) - }) - if (!confirm) return - } + if (!(await confirmSceneSaveIfModified())) return const editorState = getMutableState(EditorState) getMutableState(GLTFModifiedState).set({}) diff --git a/packages/ui/src/components/editor/panels/Scenes/container/index.tsx b/packages/ui/src/components/editor/panels/Scenes/container/index.tsx index cb0883dfb8..e11d76ee1b 100644 --- a/packages/ui/src/components/editor/panels/Scenes/container/index.tsx +++ b/packages/ui/src/components/editor/panels/Scenes/container/index.tsx @@ -27,6 +27,7 @@ import { SceneItem } from '@etherealengine/client-core/src/admin/components/scen import { PopoverState } from '@etherealengine/client-core/src/common/services/PopoverState' import { StaticResourceType, fileBrowserPath, staticResourcePath } from '@etherealengine/common/src/schema.type.module' import CreateSceneDialog from '@etherealengine/editor/src/components/dialogs/CreateScenePanelDialog' +import { confirmSceneSaveIfModified } from '@etherealengine/editor/src/components/toolbar/Toolbar' import { onNewScene } from '@etherealengine/editor/src/functions/sceneFunctions' import { EditorState } from '@etherealengine/editor/src/services/EditorServices' import { getMutableState, useHookstate, useMutableState } from '@etherealengine/hyperflux' @@ -47,7 +48,9 @@ export default function ScenesPanel() { const scenesLoading = scenesQuery.status === 'pending' - const onClickScene = (scene: StaticResourceType) => { + const onClickScene = async (scene: StaticResourceType) => { + if (!(await confirmSceneSaveIfModified())) return + getMutableState(EditorState).merge({ scenePath: scene.key }) diff --git a/packages/ui/src/components/editor/panels/Viewport/container/index.tsx b/packages/ui/src/components/editor/panels/Viewport/container/index.tsx index 580bd920c3..11531b6605 100644 --- a/packages/ui/src/components/editor/panels/Viewport/container/index.tsx +++ b/packages/ui/src/components/editor/panels/Viewport/container/index.tsx @@ -29,21 +29,19 @@ import { uploadToFeathersService } from '@etherealengine/client-core/src/util/up import { FeatureFlags } from '@etherealengine/common/src/constants/FeatureFlags' import { clientSettingPath, fileBrowserUploadPath } from '@etherealengine/common/src/schema.type.module' import { processFileName } from '@etherealengine/common/src/utils/processFileName' -import { getComponent, useComponent, useQuery } from '@etherealengine/ecs' +import { useComponent, useQuery } from '@etherealengine/ecs' import { ItemTypes, SupportedFileTypes } from '@etherealengine/editor/src/constants/AssetTypes' import { EditorControlFunctions } from '@etherealengine/editor/src/functions/EditorControlFunctions' import { addMediaNode } from '@etherealengine/editor/src/functions/addMediaNode' import { getCursorSpawnPosition } from '@etherealengine/editor/src/functions/screenSpaceFunctions' import { EditorState } from '@etherealengine/editor/src/services/EditorServices' import { GLTFComponent } from '@etherealengine/engine/src/gltf/GLTFComponent' -import { GLTFModifiedState } from '@etherealengine/engine/src/gltf/GLTFDocumentState' import { ResourcePendingComponent } from '@etherealengine/engine/src/gltf/ResourcePendingComponent' -import { SourceComponent } from '@etherealengine/engine/src/scene/components/SourceComponent' import useFeatureFlags from '@etherealengine/engine/src/useFeatureFlags' -import { getMutableState, useHookstate, useMutableState } from '@etherealengine/hyperflux' +import { useMutableState } from '@etherealengine/hyperflux' import { TransformComponent } from '@etherealengine/spatial' import { useFind } from '@etherealengine/spatial/src/common/functions/FeathersHooks' -import React, { useEffect } from 'react' +import React from 'react' import { useDrop } from 'react-dnd' import { useTranslation } from 'react-i18next' import { twMerge } from 'tailwind-merge' @@ -125,23 +123,6 @@ const SceneLoadingProgress = ({ rootEntity }) => { const progress = useComponent(rootEntity, GLTFComponent).progress.value const loaded = GLTFComponent.useSceneLoaded(rootEntity) const resourcePendingQuery = useQuery([ResourcePendingComponent]) - const root = getComponent(rootEntity, SourceComponent) - const sceneModified = useHookstate(getMutableState(GLTFModifiedState)[root]).value - - useEffect(() => { - if (!sceneModified) return - const onBeforeUnload = (e: BeforeUnloadEvent) => { - alert('You have unsaved changes. Please save before leaving.') - e.preventDefault() - e.returnValue = '' - } - - window.addEventListener('beforeunload', onBeforeUnload) - - return () => { - window.removeEventListener('beforeunload', onBeforeUnload) - } - }, [sceneModified]) if (loaded) return null From 38535e7707b614806e454837710c83366c5a769a Mon Sep 17 00:00:00 2001 From: Rahul Ghosh Date: Tue, 13 Aug 2024 18:28:43 +0530 Subject: [PATCH 14/29] Add download progress bar and optimize util files (#10914) * download progress bar * optimize util files * move handleProjectDownload to projectDownload.tsx --------- Co-authored-by: aditya-mitra <55396651+aditya-mitra@users.noreply.github.com> --- packages/client-core/i18n/en/editor.json | 1 + packages/common/src/utils/btyesToSize.ts | 43 +++++ .../src/utils/{getOS.ts => getDeviceStats.ts} | 16 ++ packages/common/src/utils/mapToObject.ts | 4 + packages/common/src/utils/miscUtils.ts | 50 ++++++ packages/editor/src/functions/utils.ts | 82 +-------- .../components/editor/input/Numeric/index.tsx | 3 +- .../src/components/editor/layout/Scrubber.tsx | 3 +- .../editor/panels/Files/container/index.tsx | 164 ++++++++---------- .../panels/Files/download/projectDownload.tsx | 111 ++++++++++++ 10 files changed, 303 insertions(+), 174 deletions(-) create mode 100644 packages/common/src/utils/btyesToSize.ts rename packages/common/src/utils/{getOS.ts => getDeviceStats.ts} (75%) create mode 100644 packages/ui/src/components/editor/panels/Files/download/projectDownload.tsx diff --git a/packages/client-core/i18n/en/editor.json b/packages/client-core/i18n/en/editor.json index a48ec56ec9..cbd6e57567 100755 --- a/packages/client-core/i18n/en/editor.json +++ b/packages/client-core/i18n/en/editor.json @@ -1217,6 +1217,7 @@ "uploadFiles": "Upload Files", "uploadFolder": "Upload Folder", "uploadingFiles": "Uploading Files ({{completed}}/{{total}})", + "downloadingProject": "Downloading Project ({{completed}}/{{total}})", "search-placeholder": "Search", "generatingThumbnails": "Generating Thumbnails ({{count}} remaining)", "file": "File", diff --git a/packages/common/src/utils/btyesToSize.ts b/packages/common/src/utils/btyesToSize.ts new file mode 100644 index 0000000000..edffca23f8 --- /dev/null +++ b/packages/common/src/utils/btyesToSize.ts @@ -0,0 +1,43 @@ +/* +CPAL-1.0 License + +The contents of this file are subject to the Common Public Attribution License +Version 1.0. (the "License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at +https://github.com/EtherealEngine/etherealengine/blob/dev/LICENSE. +The License is based on the Mozilla Public License Version 1.1, but Sections 14 +and 15 have been added to cover use of software over a computer network and +provide for limited attribution for the Original Developer. In addition, +Exhibit A has been modified to be consistent with Exhibit B. + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + +The Original Code is Ethereal Engine. + +The Original Developer is the Initial Developer. The Initial Developer of the +Original Code is the Ethereal Engine team. + +All portions of the code written by the Ethereal Engine team are Copyright © 2021-2023 +Ethereal Engine. All Rights Reserved. +*/ + +/** + * Converts bytes to a human-readable size + * @param bytes The number of bytes + * @param decimals The number of decimal places to include + * @returns The human-readable size + */ + +export function bytesToSize(bytes: number, decimals = 2) { + if (bytes === 0) return '0 Bytes' + + const k = 1024 + const dm = decimals < 0 ? 0 : decimals + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] + + const i = Math.floor(Math.log(bytes) / Math.log(k)) + + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i] +} diff --git a/packages/common/src/utils/getOS.ts b/packages/common/src/utils/getDeviceStats.ts similarity index 75% rename from packages/common/src/utils/getOS.ts rename to packages/common/src/utils/getDeviceStats.ts index 40e44c697e..5fa025b4d2 100644 --- a/packages/common/src/utils/getOS.ts +++ b/packages/common/src/utils/getDeviceStats.ts @@ -35,3 +35,19 @@ export function getOS() { } return 'other' } + +export const isApple = () => { + if ('navigator' in globalThis === false) return false + + const iOS_1to12 = /iPad|iPhone|iPod/.test(navigator.platform) + + const iOS13_iPad = navigator.platform === 'MacIntel' + + const iOS1to12quirk = () => { + const audio = new Audio() // temporary Audio object + audio.volume = 0.5 // has no effect on iOS <= 12 + return audio.volume === 1 + } + + return iOS_1to12 || iOS13_iPad || iOS1to12quirk() +} diff --git a/packages/common/src/utils/mapToObject.ts b/packages/common/src/utils/mapToObject.ts index 9db2ca7dfe..6370aa323f 100644 --- a/packages/common/src/utils/mapToObject.ts +++ b/packages/common/src/utils/mapToObject.ts @@ -52,3 +52,7 @@ export const iterativeMapToObject = (root: Record) => { } return cloneDeep(iterate(root)) } + +export function objectToMap(object: object) { + return new Map(Object.entries(object)) +} diff --git a/packages/common/src/utils/miscUtils.ts b/packages/common/src/utils/miscUtils.ts index 24b6f9ff93..3cdbb44c5c 100644 --- a/packages/common/src/utils/miscUtils.ts +++ b/packages/common/src/utils/miscUtils.ts @@ -38,6 +38,11 @@ export function isNumber(value: string | number): boolean { return value != null && value !== '' && !isNaN(Number(value.toString())) } +export function toPrecision(value, precision) { + const p = 1 / precision + return Math.round(value * p) / p +} + export function combine(first, second, third) { const res: any[] = [] @@ -47,6 +52,23 @@ export function combine(first, second, third) { return res } + +export const unique = (arr: T[], keyFinder: (item: T) => S): T[] => { + const set = new Set() + const newArr = [] as T[] + if (!keyFinder) keyFinder = (item: T) => item as any as S + + for (const item of arr) { + const key = keyFinder(item) + if (set.has(key)) continue + + newArr.push(item) + set.add(key) + } + + return newArr +} + export function combineArrays(arrays: [[]]) { const res = [] @@ -59,6 +81,23 @@ export function combineArrays(arrays: [[]]) { return res } +export function insertArraySeparator(children, separatorFn) { + if (!Array.isArray(children)) { + return children + } + const length = children.length + if (length === 1) { + return children[0] + } + return children.reduce((acc, item, index) => { + acc.push(item) + if (index !== length - 1) { + acc.push(separatorFn(index)) + } + return acc + }, []) +} + export function arraysAreEqual(arr1: any[], arr2: any[]): boolean { if (arr1.length !== arr2.length) return false @@ -154,3 +193,14 @@ export const toCapitalCase = (source: string) => { return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase() }) } + +export function toCamelPad(source: string) { + return source + .replace(/([A-Z]+)([A-Z][a-z])/g, ' $1 $2') + .replace(/([a-z\d])([A-Z])/g, '$1 $2') + .replace(/([a-zA-Z])(\d)/g, '$1 $2') + .replace(/^./, (str) => { + return str.toUpperCase() + }) + .trim() +} diff --git a/packages/editor/src/functions/utils.ts b/packages/editor/src/functions/utils.ts index 38e654987e..f1d2b197f5 100755 --- a/packages/editor/src/functions/utils.ts +++ b/packages/editor/src/functions/utils.ts @@ -22,60 +22,7 @@ Original Code is the Ethereal Engine team. All portions of the code written by the Ethereal Engine team are Copyright © 2021-2023 Ethereal Engine. All Rights Reserved. */ - -export function insertSeparator(children, separatorFn) { - if (!Array.isArray(children)) { - return children - } - const length = children.length - if (length === 1) { - return children[0] - } - return children.reduce((acc, item, index) => { - acc.push(item) - if (index !== length - 1) { - acc.push(separatorFn(index)) - } - return acc - }, []) -} -export function objectToMap(object: object) { - return new Map(Object.entries(object)) -} - -export const unique = (arr: T[], keyFinder: (item: T) => S): T[] => { - const set = new Set() - const newArr = [] as T[] - if (!keyFinder) keyFinder = (item: T) => item as any as S - - for (const item of arr) { - const key = keyFinder(item) - if (set.has(key)) continue - - newArr.push(item) - set.add(key) - } - - return newArr -} - -export const isApple = () => { - if ('navigator' in globalThis === false) return false - - const iOS_1to12 = /iPad|iPhone|iPod/.test(navigator.platform) - - const iOS13_iPad = navigator.platform === 'MacIntel' - - const iOS1to12quirk = () => { - const audio = new Audio() // temporary Audio object - audio.volume = 0.5 // has no effect on iOS <= 12 - return audio.volume === 1 - } - - return iOS_1to12 || iOS13_iPad || iOS1to12quirk() -} - -export const cmdOrCtrlString = isApple() ? 'meta' : 'ctrl' +import { isApple } from '@etherealengine/common/src/utils/getDeviceStats' export function getStepSize(event, smallStep, mediumStep, largeStep) { if (event.altKey) { @@ -86,29 +33,4 @@ export function getStepSize(event, smallStep, mediumStep, largeStep) { return mediumStep } -export function toPrecision(value, precision) { - const p = 1 / precision - return Math.round(value * p) / p -} -// https://stackoverflow.com/a/26188910 -export function camelPad(str) { - return str - .replace(/([A-Z]+)([A-Z][a-z])/g, ' $1 $2') - .replace(/([a-z\d])([A-Z])/g, '$1 $2') - .replace(/([a-zA-Z])(\d)/g, '$1 $2') - .replace(/^./, (str) => { - return str.toUpperCase() - }) - .trim() -} -export function bytesToSize(bytes: number, decimals = 2) { - if (bytes === 0) return '0 Bytes' - - const k = 1024 - const dm = decimals < 0 ? 0 : decimals - const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] - - const i = Math.floor(Math.log(bytes) / Math.log(k)) - - return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i] -} +export const cmdOrCtrlString = isApple() ? 'meta' : 'ctrl' diff --git a/packages/ui/src/components/editor/input/Numeric/index.tsx b/packages/ui/src/components/editor/input/Numeric/index.tsx index ba948c519a..d6feb6c37e 100644 --- a/packages/ui/src/components/editor/input/Numeric/index.tsx +++ b/packages/ui/src/components/editor/input/Numeric/index.tsx @@ -27,7 +27,8 @@ import React from 'react' import { clamp } from '@etherealengine/spatial/src/common/functions/MathLerpFunctions' -import { getStepSize, toPrecision } from '@etherealengine/editor/src/functions/utils' +import { toPrecision } from '@etherealengine/common/src/utils/miscUtils' +import { getStepSize } from '@etherealengine/editor/src/functions/utils' import { useHookstate } from '@etherealengine/hyperflux' import { twMerge } from 'tailwind-merge' import Text from '../../../../primitives/tailwind/Text' diff --git a/packages/ui/src/components/editor/layout/Scrubber.tsx b/packages/ui/src/components/editor/layout/Scrubber.tsx index 2eccf3f469..08198f3f4b 100644 --- a/packages/ui/src/components/editor/layout/Scrubber.tsx +++ b/packages/ui/src/components/editor/layout/Scrubber.tsx @@ -23,7 +23,8 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ -import { getStepSize, toPrecision } from '@etherealengine/editor/src/functions/utils' +import { toPrecision } from '@etherealengine/common/src/utils/miscUtils' +import { getStepSize } from '@etherealengine/editor/src/functions/utils' import { useHookstate } from '@etherealengine/hyperflux' import React, { useRef } from 'react' import { twMerge } from 'tailwind-merge' diff --git a/packages/ui/src/components/editor/panels/Files/container/index.tsx b/packages/ui/src/components/editor/panels/Files/container/index.tsx index feaca8373a..5793b4bc76 100644 --- a/packages/ui/src/components/editor/panels/Files/container/index.tsx +++ b/packages/ui/src/components/editor/panels/Files/container/index.tsx @@ -25,18 +25,17 @@ Ethereal Engine. All Rights Reserved. import { FileThumbnailJobState } from '@etherealengine/client-core/src/common/services/FileThumbnailJobState' import { NotificationService } from '@etherealengine/client-core/src/common/services/NotificationService' import { PopoverState } from '@etherealengine/client-core/src/common/services/PopoverState' -import config from '@etherealengine/common/src/config' import { FileBrowserContentType, StaticResourceType, UserID, - archiverPath, fileBrowserPath, projectPath, staticResourcePath } from '@etherealengine/common/src/schema.type.module' import { CommonKnownContentTypes } from '@etherealengine/common/src/utils/CommonKnownContentTypes' -import { Engine } from '@etherealengine/ecs' +import { bytesToSize } from '@etherealengine/common/src/utils/btyesToSize' +import { unique } from '@etherealengine/common/src/utils/miscUtils' import { AssetSelectionChangePropsType } from '@etherealengine/editor/src/components/assets/AssetsPreviewPanel' import { FilesViewModeSettings, @@ -48,12 +47,7 @@ import ImageCompressionPanel from '@etherealengine/editor/src/components/assets/ import ModelCompressionPanel from '@etherealengine/editor/src/components/assets/ModelCompressionPanel' import { DndWrapper } from '@etherealengine/editor/src/components/dnd/DndWrapper' import { SupportedFileTypes } from '@etherealengine/editor/src/constants/AssetTypes' -import { - downloadBlobAsZip, - handleUploadFiles, - inputFileWithAddToScene -} from '@etherealengine/editor/src/functions/assetFunctions' -import { bytesToSize, unique } from '@etherealengine/editor/src/functions/utils' +import { handleUploadFiles, inputFileWithAddToScene } from '@etherealengine/editor/src/functions/assetFunctions' import { EditorState } from '@etherealengine/editor/src/services/EditorServices' import { ClickPlacementState } from '@etherealengine/editor/src/systems/ClickPlacementSystem' import { AssetLoader } from '@etherealengine/engine/src/assets/classes/AssetLoader' @@ -86,6 +80,7 @@ import InputGroup from '../../../input/Group' import { FileBrowserItem, FileTableWrapper, canDropItemOverFolder } from '../browserGrid' import DeleteFileModal from '../browserGrid/DeleteFileModal' import FilePropertiesModal from '../browserGrid/FilePropertiesModal' +import { ProjectDownloadProgress, handleDownloadProject } from '../download/projectDownload' import { FileUploadProgress } from '../upload/FileUploadProgress' type FileBrowserContentPanelProps = { @@ -230,6 +225,66 @@ function GeneratingThumbnailsProgress() { ) } +export const ViewModeSettings = () => { + const { t } = useTranslation() + + const filesViewMode = useMutableState(FilesViewModeState).viewMode + + const viewModeSettings = useHookstate(getMutableState(FilesViewModeSettings)) + return ( + +
+ {isLoading && ( )} @@ -888,64 +929,3 @@ export default function FilesPanelContainer() { /> ) } - -export const ViewModeSettings = () => { - const { t } = useTranslation() - - const filesViewMode = useMutableState(FilesViewModeState).viewMode - - const viewModeSettings = useHookstate(getMutableState(FilesViewModeSettings)) - return ( - -