From 0d33358eadee27b633069614461fa6c291555cbb Mon Sep 17 00:00:00 2001 From: BFlat Date: Wed, 7 May 2025 16:18:45 +0700 Subject: [PATCH] upload flow update (#587) - changed and moved the main upload asset button to the left sidebar above the categories for better visibility - added individual upload buttons to all the category pages except skybox - they open the same upload modal but sepcific to its category only. - flow update: once an upload succeeds the upload modal closes itself and automatically reopens the asset modal to the mine tab and previously uploaded category (so users don't have to have a hard time finding or navigating to it) --- .../reusable/UploadModal3D/UploadModal3D.tsx | 126 +++++++------- .../src/components/signaled/TopBar/TopBar.tsx | 2 +- .../app/src/pages/PageEnigma/PageEditor.tsx | 4 +- .../PageEnigma/comps/AssetMenu/AssetModal.tsx | 157 ++++++++++++++---- .../comps/Controls3D/Controls3D.tsx | 8 +- .../login-modal/src/lib/login-modal.tsx | 2 +- .../tab-selector/src/lib/tab-selector.tsx | 1 + 7 files changed, 202 insertions(+), 98 deletions(-) diff --git a/frontend/apps/editor3d/app/src/components/reusable/UploadModal3D/UploadModal3D.tsx b/frontend/apps/editor3d/app/src/components/reusable/UploadModal3D/UploadModal3D.tsx index f3658dc39b..c52e57fa14 100644 --- a/frontend/apps/editor3d/app/src/components/reusable/UploadModal3D/UploadModal3D.tsx +++ b/frontend/apps/editor3d/app/src/components/reusable/UploadModal3D/UploadModal3D.tsx @@ -1,12 +1,7 @@ import { useEffect, useState } from "react"; - -import { - TransitionDialogue, - LoadingDots, - Label, - CloseButton, -} from "~/components"; - +import { LoadingDots } from "~/components"; +import { Modal } from "@storyteller/ui-modal"; +import { Label } from "@storyteller/ui-label"; import { UploadAssetError } from "../UploadModal/UploadAssetError"; import { UploadSuccess } from "../UploadModal/UploadSuccess"; import { UploadFiles3D } from "./UploadFiles3D"; @@ -19,18 +14,21 @@ import { CHARACTER_MIXAMO_FILE_TYPE, IMAGEPLANE_FILE_TYPE, } from "~/enums"; -import { assetModalVisible } from "~/pages/PageEnigma/signals"; +import { IconDefinition } from "@fortawesome/fontawesome-svg-core"; interface Props { onClose: () => void; - onSuccess: () => void; + onSuccess: (category: FilterEngineCategories) => void; isOpen: boolean; title: string; + titleIcon: IconDefinition; options?: { fileSubtypes?: { [key: string]: string }[]; hasLength?: boolean; hasThumbnailUpload?: boolean; }; + preselectedCategory?: FilterEngineCategories; + isSelectVisible: boolean; } // Map categories to their allowed file types @@ -43,16 +41,6 @@ const categoryFileTypes: Partial> = { [FilterEngineCategories.LOCATION]: Object.values(OBJECT_FILE_TYPE), [FilterEngineCategories.CREATURE]: Object.values(OBJECT_FILE_TYPE), }; - -// Map engine categories to asset modal tabs -const categoryToAssetTab: Partial> = { - [FilterEngineCategories.OBJECT]: "objects", - [FilterEngineCategories.CHARACTER]: "character", - [FilterEngineCategories.IMAGE_PLANE]: "image-planes", - [FilterEngineCategories.LOCATION]: "sets", - [FilterEngineCategories.CREATURE]: "creatures", -}; - // Excluded category options for the upload modal const categoryOptions = Object.entries(FilterEngineCategories) .filter( @@ -82,12 +70,19 @@ export function UploadModal3D({ onClose, onSuccess, title, + titleIcon, options, -}: Props) { + preselectedCategory, + isSelectVisible, +}: Props & { preselectedCategory?: FilterEngineCategories }) { const [uploaderState, setUploaderState] = useState(initialUploaderState); const [selectedCategory, setSelectedCategory] = - useState(FilterEngineCategories.OBJECT); + useState( + preselectedCategory !== undefined + ? preselectedCategory + : FilterEngineCategories.OBJECT, + ); const updateUploaderState = (newState: UploaderState) => { setUploaderState(newState); @@ -103,30 +98,51 @@ export function UploadModal3D({ } }, [isOpen]); + useEffect(() => { + if (preselectedCategory !== undefined) { + setSelectedCategory(preselectedCategory); + } + }, [preselectedCategory]); + + useEffect(() => { + console.log("Preselected category:", preselectedCategory); + console.log("Initial selected category:", selectedCategory); + }, [preselectedCategory, selectedCategory]); + + useEffect(() => { + if (uploaderState.status === UploaderStates.success) { + onSuccess(selectedCategory); + onClose(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [uploaderState.status]); + const UploaderModalContent = () => { switch (uploaderState.status) { case UploaderStates.ready: return (
-
- - { + if ( + typeof value === "string" && + Object.values(FilterEngineCategories).includes( + value as FilterEngineCategories, + ) + ) { + setSelectedCategory(value as FilterEngineCategories); + } + }} + placeholder="Select a category" + /> +
+ )} Uploading...
); - case UploaderStates.success: + case UploaderStates.success: { return ( { - onSuccess(); onClose(); - // Store the selected category and tab in sessionStorage - const assetTab = categoryToAssetTab[selectedCategory]; - if (assetTab) { - sessionStorage.setItem( - "lastUploadedCategory", - selectedCategory, - ); - sessionStorage.setItem("lastUploadedTab", assetTab); - // Reopen the asset modal - assetModalVisible.value = true; - } else { - // Clear any existing stored values if we don't have a valid tab - sessionStorage.removeItem("lastUploadedCategory"); - sessionStorage.removeItem("lastUploadedTab"); - } + onSuccess(selectedCategory); }} /> ); + } case UploaderStates.assetError: return ( - - + ); } diff --git a/frontend/apps/editor3d/app/src/components/signaled/TopBar/TopBar.tsx b/frontend/apps/editor3d/app/src/components/signaled/TopBar/TopBar.tsx index 1939db801a..835ad885a6 100644 --- a/frontend/apps/editor3d/app/src/components/signaled/TopBar/TopBar.tsx +++ b/frontend/apps/editor3d/app/src/components/signaled/TopBar/TopBar.tsx @@ -29,8 +29,8 @@ interface Props { } const appTabs: TabItem[] = [ - { id: "2D", label: "2D" }, { id: "3D", label: "3D" }, + { id: "2D", label: "2D" }, ]; export const TopBar = ({ pageName, appTabIdSignal, setAppTabId }: Props) => { diff --git a/frontend/apps/editor3d/app/src/pages/PageEnigma/PageEditor.tsx b/frontend/apps/editor3d/app/src/pages/PageEnigma/PageEditor.tsx index b0959b6c31..53a8fad8eb 100644 --- a/frontend/apps/editor3d/app/src/pages/PageEnigma/PageEditor.tsx +++ b/frontend/apps/editor3d/app/src/pages/PageEnigma/PageEditor.tsx @@ -171,12 +171,12 @@ export const PageEditor = () => { }} /> */} - {}} videoSrc2D="/resources/videos/artcraft-canvas-demo.mp4" videoSrc3D="/resources/videos/artcraft-3d-demo.mp4" openAiLogo="/resources/images/openai-logo.png" - /> + /> */} ); }; diff --git a/frontend/apps/editor3d/app/src/pages/PageEnigma/comps/AssetMenu/AssetModal.tsx b/frontend/apps/editor3d/app/src/pages/PageEnigma/comps/AssetMenu/AssetModal.tsx index 5d58f2a139..e92d9df4a9 100644 --- a/frontend/apps/editor3d/app/src/pages/PageEnigma/comps/AssetMenu/AssetModal.tsx +++ b/frontend/apps/editor3d/app/src/pages/PageEnigma/comps/AssetMenu/AssetModal.tsx @@ -1,7 +1,6 @@ import { Modal } from "@storyteller/ui-modal"; import { UploadModal3D } from "~/components/reusable/UploadModal3D"; import { - faPlus, faSearch, faChevronLeft, faLayerGroup, @@ -12,13 +11,13 @@ import { faMountainCity, faDog, faImage, + faUpFromLine, } from "@fortawesome/pro-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Tooltip } from "@storyteller/ui-tooltip"; import { Button } from "@storyteller/ui-button"; import { CloseButton } from "@storyteller/ui-close-button"; import { Input } from "@storyteller/ui-input"; -import { TabSelector } from "~/components/reusable/TabSelector"; +import { TabSelector } from "@storyteller/ui-tab-selector"; import React, { useState, useEffect, @@ -45,6 +44,7 @@ import { isAnyStatusFetching } from "../SidePanelTabs/utilities"; type AssetTab = { id: string; label: string; + labelSingle?: string; icon: typeof faLayerGroup; engineCategory?: FilterEngineCategories; items: MediaItem[]; @@ -81,6 +81,21 @@ const AllTabSection = ({ ); +const categoryToTabIdMap: Record = { + [FilterEngineCategories.CHARACTER]: "characters", + [FilterEngineCategories.CREATURE]: "creatures", + [FilterEngineCategories.IMAGE_PLANE]: "image-planes", + [FilterEngineCategories.LOCATION]: "sets", + [FilterEngineCategories.OBJECT]: "objects", + [FilterEngineCategories.SKYBOX]: "skybox", + [FilterEngineCategories.ANIMATION]: "all", + [FilterEngineCategories.AUDIO]: "all", + [FilterEngineCategories.EXPRESSION]: "all", + [FilterEngineCategories.SCENE]: "all", + [FilterEngineCategories.SET_DRESSING]: "all", + [FilterEngineCategories.VIDEO_PLANE]: "all", +}; + export const AssetModal = () => { useSignals(); const [activeLibraryTab, setActiveLibraryTab] = useState("library"); @@ -88,6 +103,9 @@ export const AssetModal = () => { const [searchTerm, setSearchTerm] = useState(""); const [isUploadModalOpen, setIsUploadModalOpen] = useState(false); const searchInputRef = useRef(null); + const [selectedCategory, setSelectedCategory] = + useState(null); + const [isSelectVisible, setIsSelectVisible] = useState(true); const handleReopenChange = (e: React.ChangeEvent) => { const newValue = e.target.checked; @@ -101,6 +119,10 @@ export const AssetModal = () => { assetModalVisible.value = false; }; + const handleOpen = () => { + assetModalVisible.value = true; + }; + useEffect(() => { if (assetModalVisible.value) { // Check for stored category and tab from upload @@ -116,7 +138,10 @@ export const AssetModal = () => { const timer = setTimeout(() => { searchInputRef.current?.focus(); }, 100); - return () => clearTimeout(timer); + return () => { + clearTimeout(timer); + // Ensure no side effects remain + }; } return undefined; // eslint-disable-next-line react-hooks/exhaustive-deps @@ -232,7 +257,8 @@ export const AssetModal = () => { { id: "all", label: "All", icon: faLayerGroup, items: [] }, { id: "character", - label: "Character", + label: "Characters", + labelSingle: "Character", icon: faUser, engineCategory: FilterEngineCategories.CHARACTER, items: @@ -243,6 +269,7 @@ export const AssetModal = () => { { id: "objects", label: "Objects", + labelSingle: "Object", icon: faCube, engineCategory: FilterEngineCategories.OBJECT, items: @@ -253,6 +280,7 @@ export const AssetModal = () => { { id: "sets", label: "Sets", + labelSingle: "Set", icon: faMountainCity, engineCategory: FilterEngineCategories.LOCATION, items: @@ -263,6 +291,7 @@ export const AssetModal = () => { { id: "creatures", label: "Creatures", + labelSingle: "Creature", icon: faDog, engineCategory: FilterEngineCategories.CREATURE, items: @@ -273,12 +302,14 @@ export const AssetModal = () => { { id: "skybox", label: "Skybox", + labelSingle: "Skybox", icon: faSun, items: activeLibraryTab === "library" ? demoSkyboxItems.value : [], }, { id: "image-planes", label: "Image Planes", + labelSingle: "Image Plane", icon: faImage, engineCategory: FilterEngineCategories.IMAGE_PLANE, items: @@ -397,11 +428,20 @@ export const AssetModal = () => { }; const handleAddAsset = () => { - assetModalVisible.value = false; + handleClose(); setIsUploadModalOpen(true); + setSelectedCategory(null); + setIsSelectVisible(true); }; - const handleUploadSuccess = () => { + const handleAddSpecificAsset = (category: FilterEngineCategories) => { + handleClose(); + setIsUploadModalOpen(true); + setSelectedCategory(category); + setIsSelectVisible(false); + }; + + const handleUploadSuccess = (category: FilterEngineCategories) => { if (reopenAfterDragSignal.value) { // Small delay to allow the modal to close and reopen setTimeout(() => { @@ -410,10 +450,32 @@ export const AssetModal = () => { } else { handleClose(); } + + // Use the provided category to find the AssetTab ID + const lastUploadedTabId = categoryToTabIdMap[category] || "all"; + + // Close the upload modal + setTimeout(() => { + sessionStorage.setItem("lastUploadedTab", lastUploadedTabId); + console.log("AssetModal: lastUploadedTabId set to:", lastUploadedTabId); + setIsUploadModalOpen(false); + handleOpen(); + }, 300); + + fetchUserCharacters(); + fetchUserObjects(); + fetchUserSets(); + fetchUserCreatures(); + fetchUserImagePlanes(); }; const clearSearch = () => setSearchTerm(""); + const getArticle = (word: string | undefined) => { + if (!word) return "a"; + return /^[aeiou]/i.test(word) ? "an" : "a"; + }; + return ( <> {

3D Assets

- + {/* - + */}
-
+
+ {assetTabs.map((tab) => ( - {currentTab.label} +
+
+ + {currentTab.label} +
+ {activeAssetTab !== "skybox" && + activeAssetTab !== "all" && ( + + )}
)} {renderContent()} @@ -551,10 +645,15 @@ export const AssetModal = () => {
setIsUploadModalOpen(false)} - onSuccess={handleUploadSuccess} + onClose={() => { + setIsUploadModalOpen(false); + }} + onSuccess={(category) => handleUploadSuccess(category)} isOpen={isUploadModalOpen} - title="Upload 3D Asset" + title={`Upload ${selectedCategory ? (selectedCategory.toLowerCase().replace(/_/g, " ") === "location" ? "sets" : selectedCategory.toLowerCase().replace(/_/g, " ")) : "3D Asset"}`} + titleIcon={!isSelectVisible ? currentTab.icon : faUpFromLine} + preselectedCategory={selectedCategory || FilterEngineCategories.OBJECT} + isSelectVisible={isSelectVisible} /> ); diff --git a/frontend/apps/editor3d/app/src/pages/PageEnigma/comps/Controls3D/Controls3D.tsx b/frontend/apps/editor3d/app/src/pages/PageEnigma/comps/Controls3D/Controls3D.tsx index 8f1627e715..4515d10b7b 100644 --- a/frontend/apps/editor3d/app/src/pages/PageEnigma/comps/Controls3D/Controls3D.tsx +++ b/frontend/apps/editor3d/app/src/pages/PageEnigma/comps/Controls3D/Controls3D.tsx @@ -23,7 +23,8 @@ export const Controls3D = () => { useEffect(() => { // Check if scene is empty and onboarding helper is not visible const checkSceneEmpty = () => { - const isSceneEmpty = outlinerState.items.value.length === 0; + const isSceneEmpty = + outlinerState.items.value.length === 0 && !assetModalVisible.value; setShowEmptySceneTooltip(isSceneEmpty); }; @@ -37,7 +38,8 @@ export const Controls3D = () => { return () => { unsubscribe(); }; - }, []); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [assetModalVisible.value]); const handleModeChange = (value: string) => { selectedMode.value = value; @@ -119,7 +121,7 @@ export const Controls3D = () => { -
+
= ({ className={({ selected }) => twMerge( "relative z-20 mx-0.5 min-w-max rounded-md border-2 border-transparent px-4 py-0.5 text-center text-sm font-semibold transition-all duration-200 ease-in-out", + "focus-visible:outline-none focus-visible:ring-0", selected ? "text-white" : "text-gray-300 hover:text-white", disabled ? "cursor-not-allowed opacity-60" : "" )