diff --git a/src/DialogueEditor/components/Explorer/components/DialogueGroupContainer/DialogueGroupContainer.test.luau b/src/DialogueEditor/components/Explorer/components/DialogueGroupContainer/DialogueGroupContainer.test.luau index e9762e7..c8d1aea 100644 --- a/src/DialogueEditor/components/Explorer/components/DialogueGroupContainer/DialogueGroupContainer.test.luau +++ b/src/DialogueEditor/components/Explorer/components/DialogueGroupContainer/DialogueGroupContainer.test.luau @@ -14,15 +14,18 @@ local it = IJW.it; local screenGui: ScreenGui?; local reactRoot: ReactRoblox.RootType?; +local propagatedErrorMessage; +local dialogueGroupContainer; +local dialogueGroup; return { + describe("DialogueGroupContainer", function() local function MockComponent(properties: any) local dialogueType = properties.type or "Message"; local onRendered = properties.onRendered; - local selectedScript = properties.selectedScript; local onErrored = properties.onErrored or function() end; React.useEffect(function() @@ -42,189 +45,203 @@ return { React.createElement(DialogueGroupContainer, { name = dialogueType; plugin = VirtualService.mocks.globals.plugin; - selectedScript = selectedScript; layoutOrder = 1; }) }); end; - return { + local function verifyReactStatus() - it("refreshes the selected dialogue group if a dialogue script is added to a dialogue folder", function() + if propagatedErrorMessage then - expect(function() + error(propagatedErrorMessage); - -- Render the component and wait for it to finish rendering. - assert(screenGui, "ScreenGui should be initialized before running tests."); - assert(reactRoot, "React root should be initialized before running tests."); + end; - local messageCount = 4; - local dialogueFolder = Instance.new("Folder"); - dialogueFolder.Name = "Messages"; + end; - for i = 1, messageCount do + local function getDialogueItems(screenGui: ScreenGui): {Frame} - local dialogueScript = Instance.new("ModuleScript"); - dialogueScript.Name = tostring(i); - dialogueScript:SetAttribute("DialogueType", "Message"); - dialogueScript:AddTag("DialogueMakerDialogueScript"); - dialogueScript.Parent = dialogueFolder; + dialogueGroupContainer = screenGui:FindFirstChildOfClass("Frame"); + assert(dialogueGroupContainer, "DialogueGroupContainer should be rendered in the ScreenGui."); - end; + dialogueGroup = dialogueGroupContainer:FindFirstChild("DialogueGroup"); + if not dialogueGroup then - local selectedScript = Instance.new("ModuleScript"); - selectedScript:SetAttribute("DialogueType", "Message"); - selectedScript:AddTag("DialogueMakerDialogueScript"); - dialogueFolder.Parent = selectedScript; + return {}; - local propagatedErrorMessage; - local didRender = false; - local element = React.createElement(MockComponent, { - selectedScript = selectedScript; - onErrored = function(errorMessage) + end; - propagatedErrorMessage = errorMessage; + local dialogueItems = {}; + for _, child in dialogueGroup:GetChildren() do - end; - onRendered = function() - - didRender = true; + if child:IsA("Frame") then + + table.insert(dialogueItems, child); + + end; - end; - }); + end; - reactRoot:render(element); - repeat task.wait() until didRender or propagatedErrorMessage; - if propagatedErrorMessage then + return dialogueItems; - error(propagatedErrorMessage); + end; - end; + local function createDialogueScript(name: string) - -- Verify that there are four dialogue items rendered. - local dialogueGroupContainer = screenGui:FindFirstChildOfClass("Frame"); - assert(dialogueGroupContainer, "DialogueGroupContainer should be rendered in the ScreenGui."); + local dialogueScript = Instance.new("ModuleScript"); + dialogueScript.Name = name; + dialogueScript:SetAttribute("DialogueType", "Message"); + dialogueScript:AddTag("DialogueMakerDialogueScript"); + return dialogueScript; - local dialogueGroup = dialogueGroupContainer:FindFirstChild("DialogueGroup"); - assert(dialogueGroup, "DialogueGroup should be rendered in the DialogueGroupContainer."); + end; - local function getDialogueItems() + local function initializeDialogueFolder(messageCount: number) - local dialogueItems = {}; - for _, child in dialogueGroup:GetChildren() do + local dialogueFolder = Instance.new("Folder"); + dialogueFolder.Name = "Messages"; - if child:IsA("Frame") then - - table.insert(dialogueItems, child); - - end; + for i = 1, messageCount do - end; + local dialogueScript = createDialogueScript(tostring(i)); + dialogueScript.Parent = dialogueFolder; - return dialogueItems; + end; - end; + local selectedScript = Instance.new("ModuleScript"); + selectedScript:SetAttribute("DialogueType", "Message"); + selectedScript:AddTag("DialogueMakerDialogueScript"); + dialogueFolder.Parent = selectedScript; - expect(#getDialogueItems()).toBe(4); + return dialogueFolder, selectedScript; - -- Add a new dialogue script to the folder and verify that the component refreshes. - local newDialogueScript = Instance.new("ModuleScript"); - newDialogueScript.Name = tostring(messageCount + 1); - newDialogueScript:SetAttribute("DialogueType", "Message"); - newDialogueScript:AddTag("DialogueMakerDialogueScript"); - newDialogueScript.Parent = dialogueFolder; - dialogueGroup.ChildAdded:Wait(); - expect(#getDialogueItems()).toBe(5); + end; - end).toFinishBeforeSeconds(1); + local function render() - end); + assert(reactRoot, "React root should be initialized before running tests."); - it("refreshes the selected dialogue group if a dialogue script is removed from a dialogue folder", function() + local didRender = false; + local element = React.createElement(MockComponent, { + onErrored = function(errorMessage) - expect(function() + propagatedErrorMessage = errorMessage; - -- Render the component and wait for it to finish rendering. - assert(screenGui, "ScreenGui should be initialized before running tests."); - assert(reactRoot, "React root should be initialized before running tests."); + end; + onRendered = function() + + didRender = true; - local messageCount = 4; - local dialogueFolder = Instance.new("Folder"); - dialogueFolder.Name = "Messages"; + end; + }); - local scriptToDestroy; + reactRoot:render(element); + repeat task.wait() until didRender or propagatedErrorMessage; - for i = 1, messageCount do + end; - local dialogueScript = Instance.new("ModuleScript"); - dialogueScript.Name = tostring(i); - dialogueScript:SetAttribute("DialogueType", "Message"); - dialogueScript:AddTag("DialogueMakerDialogueScript"); - dialogueScript.Parent = dialogueFolder; - scriptToDestroy = dialogueScript; + return { - end; + it("refreshes the selected dialogue group if a dialogue script is added to a dialogue folder", function() - local selectedScript = Instance.new("ModuleScript"); - selectedScript:SetAttribute("DialogueType", "Message"); - selectedScript:AddTag("DialogueMakerDialogueScript"); - dialogueFolder.Parent = selectedScript; + expect(function() - local propagatedErrorMessage; - local didRender = false; - local element = React.createElement(MockComponent, { - selectedScript = selectedScript; - onErrored = function(errorMessage) + -- Render the component and wait for it to finish rendering. + assert(screenGui, "ScreenGui should be initialized before running tests."); + render(); + verifyReactStatus(); - propagatedErrorMessage = errorMessage; + -- Verify that there are no dialogue items rendered. + expect(#getDialogueItems(screenGui)).toBe(0); + assert(dialogueGroupContainer); - end; - onRendered = function() - - didRender = true; + local dialogueFolder, selectedScript = initializeDialogueFolder(4); + VirtualService.mocks.services.Selection:Set({selectedScript}); + dialogueGroupContainer.ChildAdded:Wait(); + verifyReactStatus(); + expect(#getDialogueItems(screenGui)).toBe(#dialogueFolder:GetChildren()); - end; - }); + -- Create a new dialogue script, but verify that it is not rendered yet. + assert(dialogueGroup); - reactRoot:render(element); - repeat task.wait() until didRender or propagatedErrorMessage; - if propagatedErrorMessage then + local newDialogueScript = createDialogueScript(tostring(#dialogueFolder:GetChildren() + 1)); + verifyReactStatus(); + expect(#getDialogueItems(screenGui)).toBe(#dialogueFolder:GetChildren()); - error(propagatedErrorMessage); + -- Add the new dialogue script to the folder and verify that it is rendered. + newDialogueScript.Parent = dialogueFolder; + dialogueGroup.ChildAdded:Wait(); + verifyReactStatus(); + expect(#getDialogueItems(screenGui)).toBe(#dialogueFolder:GetChildren()); + + end).toFinishBeforeSeconds(1); + + end); + + it("refreshes the selected dialogue group if a dialogue script is removed from a dialogue folder", function() - end; + expect(function() - -- Verify that there are four dialogue items rendered. - local dialogueGroupContainer = screenGui:FindFirstChildOfClass("Frame"); - assert(dialogueGroupContainer, "DialogueGroupContainer should be rendered in the ScreenGui."); + -- Render the component and wait for it to finish rendering. + assert(screenGui, "ScreenGui should be initialized before running tests."); + render(); + verifyReactStatus(); - local dialogueGroup = dialogueGroupContainer:FindFirstChild("DialogueGroup"); - assert(dialogueGroup, "DialogueGroup should be rendered in the DialogueGroupContainer."); + -- Verify that there are no dialogue items rendered. + expect(#getDialogueItems(screenGui)).toBe(0); + assert(dialogueGroupContainer); - local function getDialogueItems() + local dialogueFolder, selectedScript = initializeDialogueFolder(4); + VirtualService.mocks.services.Selection:Set({selectedScript}); + dialogueGroupContainer.ChildAdded:Wait(); + verifyReactStatus(); + expect(#getDialogueItems(screenGui)).toBe(#dialogueFolder:GetChildren()); - local dialogueItems = {}; - for _, child in dialogueGroup:GetChildren() do + -- Delete a new dialogue script and verify that it is not rendered. + assert(dialogueGroup); - if child:IsA("Frame") then - - table.insert(dialogueItems, child); - - end; + dialogueFolder:GetChildren()[1]:Destroy(); + dialogueGroup.ChildRemoved:Wait(); + verifyReactStatus(); + expect(#getDialogueItems(screenGui)).toBe(#dialogueFolder:GetChildren()); - end; + end).toFinishBeforeSeconds(1); - return dialogueItems; + end); - end; + it("refreshes the conversation group if a conversation is added", function() - expect(#getDialogueItems()).toBe(4); + expect(function() - -- Remove a dialogue script to the folder and verify that the component refreshes. - scriptToDestroy:Destroy(); - dialogueGroup.ChildRemoved:Wait(); - expect(#getDialogueItems()).toBe(3); + -- Render the component and wait for it to finish rendering. + assert(screenGui, "ScreenGui should be initialized before running tests."); + render(); + verifyReactStatus(); + + -- Select the conversations folder parent and verify that there are no conversations rendered. + local conversationsFolderParent = Instance.new("Folder"); + local conversationsFolder = Instance.new("Folder"); + conversationsFolder.Name = "Conversations"; + conversationsFolder.Parent = conversationsFolderParent; + + VirtualService.mocks.services.Selection:Set({conversationsFolderParent}); + verifyReactStatus(); + expect(#getDialogueItems(screenGui)).toBe(#conversationsFolder:GetChildren()); + + -- Create a new conversation script, but verify that it is not rendered yet. + local conversationScript = Instance.new("ModuleScript"); + conversationScript:AddTag("DialogueMakerConversationScript"); + verifyReactStatus(); + expect(#getDialogueItems(screenGui)).toBe(#conversationsFolder:GetChildren()); + + -- Add the conversation script to the conversations folder and verify that it is rendered. + conversationScript.Parent = conversationsFolder; + verifyReactStatus(); + assert(dialogueGroup); + dialogueGroup.ChildAdded:Wait(); + expect(#getDialogueItems(screenGui)).toBe(#conversationsFolder:GetChildren()); end).toFinishBeforeSeconds(1); @@ -258,6 +275,12 @@ return { end; + VirtualService.mocks.services.Selection:Set({}); + + propagatedErrorMessage = nil; + dialogueGroupContainer = nil; + dialogueGroup = nil; + end; }) }; \ No newline at end of file diff --git a/src/DialogueEditor/components/Explorer/components/DialogueGroupContainer/init.luau b/src/DialogueEditor/components/Explorer/components/DialogueGroupContainer/init.luau index 50dc69e..2e264c1 100644 --- a/src/DialogueEditor/components/Explorer/components/DialogueGroupContainer/init.luau +++ b/src/DialogueEditor/components/Explorer/components/DialogueGroupContainer/init.luau @@ -11,12 +11,12 @@ local useRefreshDialogueMakerScripts = require(root.DialogueEditor.hooks.useRefr local TabSelector = require(root.DialogueEditor.components.TabSelector); local TabSelectorButton = require(root.DialogueEditor.components.TabSelector.TabSelectorButton); local useErrorBoundary = ReactErrorBoundary.useErrorBoundary; +local useDialogueMakerScriptSelection = require(root.DialogueEditor.hooks.useDialogueMakerScriptSelection); type DialogueItemType = DialogueGroup.DialogueItemType; export type DialogueTableBodyProperties = { layoutOrder: number; - selectedScript: ModuleScript?; plugin: Plugin; } @@ -25,7 +25,7 @@ local function DialogueGroupContainer(props: DialogueTableBodyProperties) Selection = if VirtualService.mocks.isEnabled then VirtualService.mocks.services.Selection else Selection; local layoutOrder = props.layoutOrder; - local selectedScript = props.selectedScript; + local selectedScript = useDialogueMakerScriptSelection(); local refreshDialogueMakerScripts = useRefreshDialogueMakerScripts(); local errorBoundary = useErrorBoundary(); @@ -54,8 +54,10 @@ local function DialogueGroupContainer(props: DialogueTableBodyProperties) local getDialogueScripts = React.useCallback(function(folderName: string): {ModuleScript} - local dialogueFolder = if selectedScript then selectedScript:FindFirstChild(folderName) else nil; - if not selectedScript or not dialogueFolder then + local selection = Selection:Get(); + local conversationsParent = if selection[1] then (if selection[1]:IsA("Folder") and selection[1].Name == "Conversations" then selection[1] else selection[1]:FindFirstChild("Conversations")) else nil; + local dialogueFolder = if selectedScript then selectedScript:FindFirstChild(folderName) else conversationsParent; + if not dialogueFolder then return {}; @@ -111,7 +113,7 @@ local function DialogueGroupContainer(props: DialogueTableBodyProperties) local function refreshTable() - local didRefresh, errorMessage = pcall(function() + local didRefresh, errorMessage = pcall(function(): nil cleanupConnections(); refreshDialogueMakerScripts(); @@ -168,51 +170,47 @@ local function DialogueGroupContainer(props: DialogueTableBodyProperties) else local selection = Selection:Get(); - if #selection == 1 then + if #selection ~= 1 then - local conversationsParent = if selection[1]:IsA("Folder") then selection[1] else selection[1]:FindFirstChild("Conversations"); + return; - if not conversationsParent then - - table.insert(contentScriptConnections, selection[1].ChildAdded:Connect(refreshTable)); + end; - else + local conversationsParent = if selection[1]:IsA("Folder") and selection[1].Name == "Conversations" then selection[1] else selection[1]:FindFirstChild("Conversations"); - table.insert(contentScriptConnections, conversationsParent.ChildAdded:Connect(refreshTable)); - table.insert(contentScriptConnections, conversationsParent.ChildRemoved:Connect(refreshTable)); + if conversationsParent then - for _, possibleConversationScript in conversationsParent:GetChildren() do + table.insert(contentScriptConnections, conversationsParent.ChildAdded:Connect(refreshTable)); + table.insert(contentScriptConnections, conversationsParent.ChildRemoved:Connect(refreshTable)); - if possibleConversationScript:IsA("ModuleScript") and possibleConversationScript:HasTag("DialogueMakerConversationScript") then - - table.insert(conversations, possibleConversationScript); - - end; + else - end; - - end; + table.insert(contentScriptConnections, selection[1].ChildAdded:Connect(refreshTable)); end; end; + return; + end); if not didRefresh then - print("Error refreshing dialogue scripts: ", errorMessage); errorBoundary.showBoundary(errorMessage); end; end; + + local selectionChangedConnection = Selection.SelectionChanged:Connect(refreshTable); task.spawn(refreshTable); return function() cleanupConnections(); + selectionChangedConnection:Disconnect(); end; diff --git a/src/DialogueEditor/components/Explorer/init.luau b/src/DialogueEditor/components/Explorer/init.luau index 86e1c74..d9cadb2 100644 --- a/src/DialogueEditor/components/Explorer/init.luau +++ b/src/DialogueEditor/components/Explorer/init.luau @@ -8,11 +8,11 @@ local DialogueGroupContainer = require(script.components.DialogueGroupContainer) local Preview = require(script.components.Preview); local useStudioColors = require(root.DialogueEditor.hooks.useStudioColors); local useDialogueScriptType = require(root.DialogueEditor.hooks.useDialogueScriptType); +local useDialogueMakerScriptSelection = require(root.DialogueEditor.hooks.useDialogueMakerScriptSelection); type DialogueScriptType = useDialogueScriptType.DialogueScriptType; export type DialogueTableProperties = { - selectedScript: ModuleScript?; plugin: Plugin; setSettingsTarget: (target: ModuleScript?) -> (); layoutOrder: number; @@ -21,7 +21,7 @@ export type DialogueTableProperties = { local function Explorer(props: DialogueTableProperties) local plugin = props.plugin; - local selectedScript = props.selectedScript; + local selectedScript = useDialogueMakerScriptSelection(); local setSettingsTarget = props.setSettingsTarget; local layoutOrder = props.layoutOrder; local colors = useStudioColors(); diff --git a/src/DialogueEditor/hooks/useDialogueMakerScriptSelection.luau b/src/DialogueEditor/hooks/useDialogueMakerScriptSelection.luau index 2110bfd..7685ad7 100644 --- a/src/DialogueEditor/hooks/useDialogueMakerScriptSelection.luau +++ b/src/DialogueEditor/hooks/useDialogueMakerScriptSelection.luau @@ -4,65 +4,82 @@ local Selection = game:GetService("Selection"); local root = script.Parent.Parent.Parent; local React = require(root.roblox_packages.react); +local VirtualService = require(root.VirtualService); local function useDialogueMakerScriptSelection() - local selectedScript, setSelectedScript = React.useState(nil :: ModuleScript?); - local conversationScript, setConversationScript = React.useState(nil :: ModuleScript?); + Selection = if VirtualService.mocks.isEnabled then VirtualService.mocks.services.Selection else Selection; - React.useEffect(function() - - local function checkSelection() + local getScripts = React.useCallback(function() - local selection = Selection:Get(); - if #selection == 1 then + local selection = Selection:Get(); + if #selection ~= 1 then - local selectedInstance = selection[1]; - local isSelectionAModuleScript = selectedInstance:IsA("ModuleScript"); + return nil, nil; - if not isSelectionAModuleScript and selectedInstance:IsA("Folder") and selectedInstance.Parent and selectedInstance.Parent:IsA("ModuleScript") then + end; - selectedInstance = selectedInstance.Parent; - isSelectionAModuleScript = true; + local selectedInstance = selection[1]; + local isSelectionAModuleScript = selectedInstance:IsA("ModuleScript"); - end; + if not isSelectionAModuleScript and selectedInstance:IsA("Folder") and selectedInstance.Parent and selectedInstance.Parent:IsA("ModuleScript") then - local conversationScript = if isSelectionAModuleScript and selectedInstance:HasTag("DialogueMakerConversationScript") then selectedInstance else nil; - if not conversationScript then + selectedInstance = selectedInstance.Parent; + isSelectionAModuleScript = true; - local parent = selectedInstance.Parent; - while parent do + end; - if not parent:IsA("ModuleScript") then - - break; + local conversationScript = if isSelectionAModuleScript and selectedInstance:HasTag("DialogueMakerConversationScript") then selectedInstance else nil; + if not conversationScript then - end; - - if parent:HasTag("DialogueMakerConversationScript") then - - conversationScript = parent; - break; + local parent = selectedInstance.Parent; + while parent do - end; - - parent = parent.Parent; + if not parent:IsA("ModuleScript") then - end; + break; end; + + if parent:HasTag("DialogueMakerConversationScript") then + + conversationScript = parent; + break; - local selectedScript = if conversationScript == selectedInstance or (isSelectionAModuleScript and selectedInstance:HasTag("DialogueMakerDialogueScript")) then selectedInstance else nil; + end; + + parent = parent.Parent; + + end; - setConversationScript(conversationScript); - setSelectedScript(selectedScript); + end; - else + local selectedScript = if conversationScript == selectedInstance or (isSelectionAModuleScript and selectedInstance:HasTag("DialogueMakerDialogueScript")) then selectedInstance else nil; - setConversationScript(nil); - setSelectedScript(nil); + return selectedScript, conversationScript; - end + end, {}); + local initialSelectedScript, initialConversationScript = React.useMemo(getScripts, {}); + local selectedScript, setSelectedScript = React.useState(initialSelectedScript); + local conversationScript, setConversationScript = React.useState(initialConversationScript); + + React.useEffect(function() + + local function checkSelection() + + local newSelectedScript, newConversationScript = getScripts(); + + if newSelectedScript ~= selectedScript then + + setSelectedScript(newSelectedScript); + + end; + + if newConversationScript ~= conversationScript then + + setConversationScript(newConversationScript); + + end; end; @@ -75,7 +92,7 @@ local function useDialogueMakerScriptSelection() end; - end, {}); + end, {selectedScript, conversationScript}); return selectedScript, conversationScript; diff --git a/src/DialogueEditor/init.luau b/src/DialogueEditor/init.luau index 9fe6d71..bbf061b 100644 --- a/src/DialogueEditor/init.luau +++ b/src/DialogueEditor/init.luau @@ -54,7 +54,6 @@ local function DialogueEditor(props: DialogueEditorProperties) }); Explorer = if not settingsTarget then React.createElement(Explorer, { - selectedScript = selectedScript; plugin = props.plugin; setSettingsTarget = setSettingsTarget; layoutOrder = 2;