diff --git a/.github/workflows/run-luau-tests.yml b/.github/workflows/run-luau-tests.yml new file mode 100644 index 00000000..2227eefa --- /dev/null +++ b/.github/workflows/run-luau-tests.yml @@ -0,0 +1,124 @@ +name: Run Luau tests + +on: + workflow_dispatch: + push: + branches: + - production + paths: + - "src/**" + - "pesde.toml" + - "development.project.json" + - ".github/workflows/run-luau-tests.yml" + pull_request: + types: [opened, reopened, synchronize, ready_for_review] + paths: + - "src/**" + - "pesde.toml" + - "development.project.json" + - ".github/workflows/run-luau-tests.yml" + +concurrency: + group: ${{ github.workflow }} + +jobs: + build: + if: ${{ !github.event.pull_request.draft }} + name: Build testing place + runs-on: ubuntu-24.04 + timeout-minutes: 1 + steps: + - name: Checkout repository + uses: actions/checkout@v4.1.7 + with: + submodules: recursive + + - name: Set up pesde + uses: ernisto/setup-pesde@5f9a3399d5ae0fe78b014f5f13cd913f27755fb4 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Install dependencies + run: pesde install + + - name: Build Rojo project + run: rojo build development.project.json -o build.rbxl + + - name: Upload project + uses: actions/upload-artifact@v4.4.0 + with: + name: build.rbxl + path: build.rbxl + + publish: + name: Deploy testing place to Roblox + runs-on: ubuntu-24.04 + timeout-minutes: 1 + needs: build + steps: + - name: Download project + uses: actions/download-artifact@v4.1.8 + with: + name: build.rbxl + + - name: POST to Roblox API + env: + ROBLOX_API_KEY: ${{ secrets.ROBLOX_DEPLOYMENT_API_KEY }} + ROBLOX_UNIVERSE_ID: ${{ vars.ROBLOX_UNIVERSE_ID }} + ROBLOX_PLACE_ID: ${{ vars.ROBLOX_PLACE_ID }} + run: | + curl \ + --fail-with-body \ + -H "x-api-key: $ROBLOX_API_KEY" \ + -H "Content-Type: application/xml" \ + --data-binary @build.rbxl \ + "https://apis.roblox.com/universes/v1/""$ROBLOX_UNIVERSE_ID""/places/""$ROBLOX_PLACE_ID""/versions?versionType=Published" + + test: + runs-on: ubuntu-24.04 + name: Run test cases + timeout-minutes: 1 + needs: publish + steps: + - name: Checkout repository + uses: actions/checkout@v4.1.7 + + - name: Execute script + uses: grand-hawk/action-roblox-luau-execution@v1.0.1 + with: + roblox_api_key: ${{ secrets.ROBLOX_DEPLOYMENT_API_KEY }} + universe_id: ${{ vars.ROBLOX_UNIVERSE_ID }} + place_id: ${{ vars.ROBLOX_PLACE_ID }} + luau_file: "runTests.luau" + output_file: "testResults.json" + dump_to_summary: true + + - name: Verify all tests passed + uses: actions/github-script@v7 + with: + script: | + try { + + const outputFile = require("./testResults.json"); + const testResults = outputFile[0]; + if (!testResults) { + + throw new Error("No test results found."); + + } + + for (const test of testResults) { + + if (!test.didPass) { + + throw new Error("A test case failed."); + + } + + } + + } catch (error) { + + core.setFailed(error); + + } \ No newline at end of file diff --git a/default.project.json b/default.project.json index 8cd5f222..e49dc228 100644 --- a/default.project.json +++ b/default.project.json @@ -1,5 +1,5 @@ { - "name": "DialoguePluginScript", + "name": "DialoguePlugDialogueMakerPlugininScript", "tree": { "$path": "src", "roblox_packages": { diff --git a/development.project.json b/development.project.json index 0d032c40..75980cd2 100644 --- a/development.project.json +++ b/development.project.json @@ -1,9 +1,9 @@ { - "name": "DialoguePluginScript", + "name": "DialogueMakerPlugin", "tree": { "$className": "DataModel", "ServerStorage": { - "DialoguePluginScript": { + "DialogueMakerPlugin": { "$path": "src", "roblox_packages": { "$path": "roblox_packages" diff --git a/pesde.lock b/pesde.lock index c4d40a08..e7ad2dcf 100644 --- a/pesde.lock +++ b/pesde.lock @@ -18,6 +18,18 @@ frktest = [{ name = "itsfrank/frktest", version = "^0.0.2", index = "https://git luau-lsp = [{ name = "pesde/luau_lsp", version = "=1.39.2", index = "https://github.com/pesde-pkg/index", target = "lune" }, "dev"] stylua = [{ name = "pesde/stylua", version = "=2.0.2", index = "https://github.com/pesde-pkg/index", target = "lune" }, "dev"] +[graph."beastslash/ijw@1.1.2 roblox"] +direct = ["ijw", { name = "beastslash/ijw", version = "^1.1.2", index = "default" }, "dev"] + +[graph."beastslash/ijw@1.1.2 roblox".pkg_ref] +ref_ty = "pesde" +index_url = "https://github.com/pesde-pkg/index" + +[graph."beastslash/ijw@1.1.2 roblox".pkg_ref.dependencies] +luau_lsp = [{ name = "pesde/luau_lsp", version = "^1.47.0", index = "https://github.com/pesde-pkg/index", target = "lune" }, "dev"] +rojo = [{ name = "pesde/rojo", version = "^7.4.4", index = "https://github.com/pesde-pkg/index", target = "lune" }, "dev"] +scripts = [{ name = "pesde/scripts_rojo", version = "^0.1.0", index = "https://github.com/pesde-pkg/index", target = "lune" }, "dev"] + [graph."corecii/greentea@0.4.11 lune".pkg_ref] ref_ty = "pesde" index_url = "https://github.com/daimond113/pesde-index" diff --git a/pesde.toml b/pesde.toml index 46f29730..7880fd33 100644 --- a/pesde.toml +++ b/pesde.toml @@ -8,18 +8,18 @@ includes = [ "pesde.toml", "README.md", "LICENSE", - "src/**/*.lua", + "src/**/*.luau", ] workspace_members = ["src/DialogueMakerKit"] private = true [target] environment = "roblox" -lib = "src/init.lua" +lib = "src/init.luau" build_files = ["src"] [engines] -pesde = "^0.7.0-rc.3" +pesde = "^0.7.0-rc.6" lune = "^0.8.9" [indices] @@ -36,6 +36,7 @@ sourcemap_generator = ".pesde/scripts/sourcemap_generator.luau" scripts = { name = "pesde/scripts_rojo", version = "^0.1.0", target = "lune" } rojo = { name = "pesde/rojo", version = "^7.4.4", target = "lune" } luau_lsp = { name = "pesde/luau_lsp", version = "^1.47.0", target = "lune" } +ijw = { name = "beastslash/ijw", version = "^1.1.2" } [dependencies] react = { wally = "jsdotlua/react", version = "^17.2.1" } diff --git a/runTests.luau b/runTests.luau new file mode 100644 index 00000000..85205052 --- /dev/null +++ b/runTests.luau @@ -0,0 +1,13 @@ +--!strict + +local ServerStorage = game:GetService("ServerStorage"); + +local IJW = require(ServerStorage.DialogueMakerPlugin.roblox_packages.ijw); +local TestRunner = IJW.TestRunner; + +local tests = TestRunner:findTestsFromAncestors({ServerStorage.DialogueMakerPlugin:Clone()}, ".test"); +local results = TestRunner:runTests(tests); + +TestRunner:displayResults(results); + +return results; \ No newline at end of file diff --git a/src/DialogueEditor/components/Explorer/components/DialogueGroupContainer/components/DialogueGroup/components/DialogueItem/DialogueItem.test.luau b/src/DialogueEditor/components/Explorer/components/DialogueGroupContainer/components/DialogueGroup/components/DialogueItem/DialogueItem.test.luau new file mode 100644 index 00000000..50144478 --- /dev/null +++ b/src/DialogueEditor/components/Explorer/components/DialogueGroupContainer/components/DialogueGroup/components/DialogueItem/DialogueItem.test.luau @@ -0,0 +1,270 @@ +--!strict + +local root = script.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent; +local DialogueItem = require(script.Parent); +local VirtualService = require(root.VirtualService); +local React = require(root.roblox_packages.react); +local ReactRoblox = require(root.roblox_packages["react-roblox"]); +local IJW = require(root.roblox_packages.ijw); +local expect = IJW.expect; +local describe = IJW.describe; +local it = IJW.it; + +local screenGui: ScreenGui?; +local reactRoot: ReactRoblox.RootType?; + +return { + describe("DialogueItem", function() + + local function MockComponent(properties: any) + + local dialogueType = properties.type or "Message"; + local onRendered = properties.onRendered; + local dialogueScript = properties.dialogueScript or Instance.new("ModuleScript"); + local layoutOrder = properties.layoutOrder or 1; + + React.useEffect(function() + + if onRendered then + + onRendered(); + + end; + + end, {}); + + return React.createElement(DialogueItem, { + type = dialogueType; + dialogueScript = dialogueScript; + dialogueScriptCount = if dialogueScript.Parent then #dialogueScript.Parent:GetChildren() else 1; + layoutOrder = layoutOrder; + setSettingsTarget = function() end; + }); + + end; + + local function generateDialogueContainer() + + local dialogueContainer = Instance.new("Folder"); + local dialogueScript = Instance.new("ModuleScript"); + dialogueScript.Name = "1"; + dialogueScript:AddTag("DialogueMakerDialogueScript"); + dialogueScript.Parent = dialogueContainer; + + local secondDialogueScript = Instance.new("ModuleScript"); + secondDialogueScript.Name = "2"; + secondDialogueScript:AddTag("DialogueMakerDialogueScript"); + secondDialogueScript.Parent = dialogueContainer; + + return dialogueContainer; + + end; + + return { + + it("can set selection on click", function() + + expect(function() + + -- 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."); + + local renderedEvent = Instance.new("BindableEvent"); + + local element = React.createElement(MockComponent, { + onRendered = function() + + renderedEvent:Fire(); + + end; + }); + + reactRoot:render(element); + renderedEvent.Event:Wait(); + + -- Simulate a click on the component. + local dialogueItem = screenGui:FindFirstChildOfClass("Frame"); + assert(dialogueItem, "DialogueItem should be rendered in the ScreenGui."); + + local viewButton = dialogueItem:FindFirstChild("ViewButton"); + assert(viewButton and viewButton:IsA("GuiButton"), "ViewButton should be present in the DialogueItem."); + + local didSelectionChange = false; + VirtualService.mocks.services.Selection.SelectionChanged:Once(function() + + didSelectionChange = true; + + end); + + VirtualService.events.GuiButton.Activated:fireEvent(viewButton); + + while not didSelectionChange do task.wait() end; + + end).toFinishBeforeSeconds(1); + + end); + + it("can reduce dialogue priority", function() + + expect(function() + + -- 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."); + + local renderedEvent = Instance.new("BindableEvent"); + local dialogueContainer = generateDialogueContainer(); + local dialogueScript = dialogueContainer:FindFirstChild("1"); + assert(dialogueScript, "Dialogue script should be present in the dialogue container."); + + local element = React.createElement(MockComponent, { + dialogueScript = dialogueScript; + layoutOrder = 1; + onRendered = function() + + renderedEvent:Fire(); + + end; + }); + + reactRoot:render(element); + renderedEvent.Event:Wait(); + + -- Simulate a click on the component. + local dialogueItem = screenGui:FindFirstChildOfClass("Frame"); + assert(dialogueItem, "DialogueItem should be rendered in the ScreenGui."); + + local decreasePriorityButton = dialogueItem:FindFirstChild("DecreasePriorityButton"); + assert(decreasePriorityButton and decreasePriorityButton:IsA("GuiButton"), "DecreasePriorityButton should be present in the DialogueItem."); + + local didPriorityChange = false; + dialogueScript:GetPropertyChangedSignal("Name"):Once(function() + + expect(dialogueScript.Name).toBe("2"); + didPriorityChange = true; + + end); + + VirtualService.events.GuiButton.Activated:fireEvent(decreasePriorityButton); + + while not didPriorityChange do task.wait() end; + + end).toFinishBeforeSeconds(1); + + end); + + it("can increase dialogue priority", function() + + expect(function() + + -- 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."); + + local renderedEvent = Instance.new("BindableEvent"); + local dialogueContainer = generateDialogueContainer(); + local dialogueScript = dialogueContainer:FindFirstChild("2"); + assert(dialogueScript, "Dialogue script should be present in the dialogue container."); + + local element = React.createElement(MockComponent, { + dialogueScript = dialogueScript; + layoutOrder = 2; + onRendered = function() + + renderedEvent:Fire(); + + end; + }); + + reactRoot:render(element); + renderedEvent.Event:Wait(); + + -- Simulate a click on the component. + local dialogueItem = screenGui:FindFirstChildOfClass("Frame"); + assert(dialogueItem, "DialogueItem should be rendered in the ScreenGui."); + + local increasePriorityButton = dialogueItem:FindFirstChild("IncreasePriorityButton"); + assert(increasePriorityButton and increasePriorityButton:IsA("GuiButton"), "IncreasePriorityButton should be present in the DialogueItem."); + + local didPriorityChange = false; + dialogueScript:GetPropertyChangedSignal("Name"):Once(function() + + expect(dialogueScript.Name).toBe("1"); + didPriorityChange = true; + + end); + + VirtualService.events.GuiButton.Activated:fireEvent(increasePriorityButton); + + while not didPriorityChange do task.wait() end; + + end).toFinishBeforeSeconds(1); + + end); + + it("cannot modify priority of conversations", function() + + expect(function() + + -- 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."); + + local renderedEvent = Instance.new("BindableEvent"); + local element = React.createElement(MockComponent, { + type = "Conversation"; + onRendered = function() + + renderedEvent:Fire(); + + end; + }); + + reactRoot:render(element); + renderedEvent.Event:Wait(); + + -- Verify that the increase and decrease priority buttons are not present. + local dialogueItem = screenGui:FindFirstChildOfClass("Frame"); + assert(dialogueItem, "DialogueItem should be rendered in the ScreenGui."); + + local increasePriorityButton = dialogueItem:FindFirstChild("IncreasePriorityButton"); + expect(increasePriorityButton).toBeNil(); + + local decreasePriorityButton = dialogueItem:FindFirstChild("DecreasePriorityButton"); + expect(decreasePriorityButton).toBeNil(); + + end).toFinishBeforeSeconds(1); + + end); + } + + end, { + beforeEach = function() + + VirtualService.mocks.isEnabled = true; + + local newScreenGui = Instance.new("ScreenGui"); + screenGui = newScreenGui; + reactRoot = ReactRoblox.createRoot(newScreenGui); + + end; + afterEach = function() + + VirtualService.mocks.isEnabled = false; + + if reactRoot then + + reactRoot:unmount(); + + end; + + if screenGui then + + screenGui:Destroy(); + + end; + + end; + }) +}; \ No newline at end of file diff --git a/src/DialogueEditor/components/Explorer/components/DialogueGroupContainer/components/DialogueGroup/components/Dialogue/init.luau b/src/DialogueEditor/components/Explorer/components/DialogueGroupContainer/components/DialogueGroup/components/DialogueItem/init.luau similarity index 73% rename from src/DialogueEditor/components/Explorer/components/DialogueGroupContainer/components/DialogueGroup/components/Dialogue/init.luau rename to src/DialogueEditor/components/Explorer/components/DialogueGroupContainer/components/DialogueGroup/components/DialogueItem/init.luau index 7f914172..46e9ab68 100644 --- a/src/DialogueEditor/components/Explorer/components/DialogueGroupContainer/components/DialogueGroup/components/Dialogue/init.luau +++ b/src/DialogueEditor/components/Explorer/components/DialogueGroupContainer/components/DialogueGroup/components/DialogueItem/init.luau @@ -3,6 +3,7 @@ local Selection = game:GetService("Selection"); local root = script.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent; +local VirtualService = require(root.VirtualService); local React = require(root.roblox_packages.react); local useStudioColors = require(root.DialogueEditor.hooks.useStudioColors); local useStudioIcons = require(root.DialogueEditor.hooks.useStudioIcons); @@ -15,12 +16,15 @@ export type DialogueItemProperties = { dialogueScript: ModuleScript; dialogueScriptCount: number; layoutOrder: number; - plugin: Plugin; setSettingsTarget: (target: ModuleScript?) -> (); } local function DialogueItem(props: DialogueItemProperties) + -- Replace services with VirtualService services for testing. + -- VirtualService services are used to mock specific Roblox services in tests. + Selection = if VirtualService.mocks.isEnabled then VirtualService.mocks.services.Selection else Selection; + local dialogueType = props.type; local dialogueScript = props.dialogueScript; local dialogueScriptCount = props.dialogueScriptCount; @@ -34,7 +38,7 @@ local function DialogueItem(props: DialogueItemProperties) local incrementPriority = React.useCallback(function(increment: number) - if not dialogueScript.Parent then + if not dialogueScript.Parent or (increment < 0 and layoutOrder <= 1) or (increment > 0 and layoutOrder >= dialogueScriptCount) then return; @@ -78,10 +82,75 @@ local function DialogueItem(props: DialogueItemProperties) finishHistoryRecording(historyIdentifier); - end, {dialogueType :: unknown, dialogueScript, dialogueScriptCount}); + end, {dialogueType :: unknown, dialogueScript, dialogueScriptCount, layoutOrder}); + + local increasePriority = React.useCallback(function() + + incrementPriority(-1); + + end, {incrementPriority}); + + local decreasePriority = React.useCallback(function() + + incrementPriority(1); + + end, {incrementPriority}); + + local viewDialogue = React.useCallback(function() + + Selection:Set({dialogueScript}); + + end, {Selection, dialogueScript}); local isDeprioritized = (dialogueType == "Message" or dialogueType == "Redirect") and layoutOrder > 1; + local increasePriorityButtonRef = React.useRef(nil :: ImageButton?); + local decreasePriorityButtonRef = React.useRef(nil :: ImageButton?); + local viewButtonRef = React.useRef(nil :: TextButton?); + + React.useEffect(function() + + local increasePriorityButton = increasePriorityButtonRef.current; + local increasePriorityButtonConnection; + if increasePriorityButton then + + increasePriorityButtonConnection = VirtualService.events.GuiButton.Activated:getSignal(increasePriorityButton):Connect(increasePriority); + + end; + + local decreasePriorityButton = decreasePriorityButtonRef.current; + local decreasePriorityButtonConnection; + if decreasePriorityButton then + + decreasePriorityButtonConnection = VirtualService.events.GuiButton.Activated:getSignal(decreasePriorityButton):Connect(decreasePriority); + + end; + + local viewButton = viewButtonRef.current; + assert(viewButton, "View button reference is nil"); + + local viewButtonConnection = VirtualService.events.GuiButton.Activated:getSignal(viewButton):Connect(viewDialogue); + + return function() + + if increasePriorityButtonConnection then + + increasePriorityButtonConnection:Disconnect(); + + end; + + if decreasePriorityButtonConnection then + + decreasePriorityButtonConnection:Disconnect(); + + end; + + viewButtonConnection:Disconnect(); + + end; + + end, {increasePriority, decreasePriority, viewDialogue}); + return React.createElement("Frame", { AutomaticSize = Enum.AutomaticSize.Y; BackgroundTransparency = 1; @@ -101,17 +170,7 @@ local function DialogueItem(props: DialogueItemProperties) Image = icons.increasePriority; ImageTransparency = if layoutOrder <= 1 then 0.5 else 0; LayoutOrder = 1; - [React.Event.Activated] = function() - - if layoutOrder <= 1 then - - return; - - end; - - incrementPriority(-1); - - end; + ref = increasePriorityButtonRef; }) else nil; DecreasePriorityButton = if dialogueType ~= "Conversation" then @@ -121,17 +180,7 @@ local function DialogueItem(props: DialogueItemProperties) Image = icons.decreasePriority; ImageTransparency = if layoutOrder >= dialogueScriptCount then 0.5 else 0; LayoutOrder = 2; - [React.Event.Activated] = function() - - if layoutOrder >= dialogueScriptCount then - - return; - - end; - - incrementPriority(1); - - end; + ref = decreasePriorityButtonRef; }) else nil; ViewButton = React.createElement("TextButton", { @@ -140,11 +189,7 @@ local function DialogueItem(props: DialogueItemProperties) AutomaticSize = Enum.AutomaticSize.Y; Text = ""; LayoutOrder = 3; - [React.Event.Activated] = function() - - Selection:Set({dialogueScript}); - - end; + ref = viewButtonRef; }, { UIFlexItem = React.createElement("UIFlexItem", { FlexMode = Enum.UIFlexMode.Fill; diff --git a/src/DialogueEditor/components/Explorer/components/DialogueGroupContainer/components/DialogueGroup/init.luau b/src/DialogueEditor/components/Explorer/components/DialogueGroupContainer/components/DialogueGroup/init.luau index 42143792..26c9fe4a 100644 --- a/src/DialogueEditor/components/Explorer/components/DialogueGroupContainer/components/DialogueGroup/init.luau +++ b/src/DialogueEditor/components/Explorer/components/DialogueGroupContainer/components/DialogueGroup/init.luau @@ -2,9 +2,9 @@ local root = script.Parent.Parent.Parent.Parent.Parent.Parent.Parent; local React = require(root.roblox_packages.react); -local Dialogue = require(script.components.Dialogue); +local DialogueItem = require(script.components.DialogueItem); -export type DialogueItemType = Dialogue.DialogueItemType; +export type DialogueItemType = DialogueItem.DialogueItemType; export type DialogueGroupProperties = { name: DialogueItemType; @@ -24,7 +24,7 @@ local function SettingGroup(properties: DialogueGroupProperties) local scriptComponents = {}; for index, dialogueScript in scriptList do - local settingComponent = React.createElement(Dialogue, { + local settingComponent = React.createElement(DialogueItem, { key = dialogueScript:GetFullName(); plugin = properties.plugin; dialogueScript = dialogueScript; diff --git a/src/DialogueEditor/hooks/useChangeHistory.luau b/src/DialogueEditor/hooks/useChangeHistory.luau index cbb90312..35d62084 100644 --- a/src/DialogueEditor/hooks/useChangeHistory.luau +++ b/src/DialogueEditor/hooks/useChangeHistory.luau @@ -4,9 +4,12 @@ local ChangeHistoryService = game:GetService("ChangeHistoryService"); local root = script.Parent.Parent.Parent; local React = require(root.roblox_packages.react); +local VirtualService = require(root.VirtualService); local function useChangeHistory() + ChangeHistoryService = if VirtualService.mocks.isEnabled then VirtualService.mocks.services.ChangeHistoryService else ChangeHistoryService; + local startRecording = React.useCallback(function(label: string) if ChangeHistoryService:IsRecordingInProgress() then diff --git a/src/DialogueEditor/hooks/useStudioColors.luau b/src/DialogueEditor/hooks/useStudioColors.luau index bf9de771..4c330248 100644 --- a/src/DialogueEditor/hooks/useStudioColors.luau +++ b/src/DialogueEditor/hooks/useStudioColors.luau @@ -1,17 +1,39 @@ --!strict +local RunService = game:GetService("RunService"); + local React = require(script.Parent.Parent.Parent.roblox_packages.react); local Colors = require(script.Parent.Parent.Parent.Colors); type ColorDictionary = Colors.ColorDictionary; local function useStudioColors(): ColorDictionary - local themeName, setThemeName = React.useState(settings().Studio.Theme.Name) + local getColors = React.useCallback(function() + + if not RunService:IsStudio() then + + return Colors.Dark; + + end + + local themeName = settings().Studio.Theme.Name + return Colors[themeName] or Colors.Dark + + end, {}); + + local colors, setColors = React.useState(getColors()) + + React.useEffect(function(): () + + if not RunService:IsStudio() then + + return; + + end - React.useEffect(function() local function onThemeChanged() - setThemeName(settings().Studio.Theme.Name); + setColors(getColors()); end @@ -23,9 +45,9 @@ local function useStudioColors(): ColorDictionary end - end, {}) + end, {getColors}) - return Colors[themeName] or Colors.Dark; + return colors; end diff --git a/src/DialogueEditor/hooks/useStudioIcons.luau b/src/DialogueEditor/hooks/useStudioIcons.luau index f98b364f..b02d4515 100644 --- a/src/DialogueEditor/hooks/useStudioIcons.luau +++ b/src/DialogueEditor/hooks/useStudioIcons.luau @@ -1,18 +1,39 @@ --!strict +local RunService = game:GetService("RunService"); + local root = script.Parent.Parent.Parent; local React = require(root.roblox_packages.react); local Icons = require(root.Icons); local function useStudioIcons() - local themeName, setThemeName = React.useState(settings().Studio.Theme.Name) + local getIcons = React.useCallback(function() + + if not RunService:IsStudio() then + + return Icons.Dark; + + end + + local themeName = settings().Studio.Theme.Name + return Icons[themeName] or Icons.Dark - React.useEffect(function() + end, {}); + + local icons, setIcons = React.useState(getIcons()) + + React.useEffect(function(): () + if not RunService:IsStudio() then + + return; + + end + local function onThemeChanged() - setThemeName(settings().Studio.Theme.Name); + setIcons(getIcons()); end @@ -24,9 +45,9 @@ local function useStudioIcons() end - end, {}) + end, {getIcons}) - return Icons[themeName] or Icons.Dark; + return icons; end diff --git a/src/DialogueMakerKit b/src/DialogueMakerKit index 8290edb7..20e7aee9 160000 --- a/src/DialogueMakerKit +++ b/src/DialogueMakerKit @@ -1 +1 @@ -Subproject commit 8290edb744cfec37e790d403c5e2f2710d956c49 +Subproject commit 20e7aee97317ceb9861740433bed233a704f7e48 diff --git a/src/VirtualService/EventProvider.luau b/src/VirtualService/EventProvider.luau new file mode 100644 index 00000000..b8d61b28 --- /dev/null +++ b/src/VirtualService/EventProvider.luau @@ -0,0 +1,62 @@ +--!strict + +local EventProvider = {}; + +export type EventProvider = { + getSignal: (self: EventProvider, instance: InstanceType) -> RBXScriptSignal; + fireEvent: (self: EventProvider, instance: InstanceType, ...T) -> (); +}; + +function EventProvider.new(instanceType: InstanceType, eventName: string, ...: T) + + local events = {}; + local connections = {}; + local eventProvider: EventProvider = {} :: EventProvider; + + local function getBindableEvent(instance: InstanceType): BindableEvent + + if events[instance] then + + return events[instance]; + + end; + + local bindableEvent = Instance.new("BindableEvent"); + events[instance] = bindableEvent; + + if not connections[instance] then + + connections[instance] = ((instance :: any)[eventName] :: RBXScriptSignal):Connect(function(...) + + eventProvider:fireEvent(instance, ...); + + end); + + end; + + return bindableEvent; + + end; + + local function getSignal(self, instance: InstanceType): RBXScriptSignal + + return getBindableEvent(instance).Event; + + end; + + local function fireEvent(self, instance: InstanceType, ...): () + + getBindableEvent(instance):Fire(...); + + end; + + eventProvider = { + getSignal = getSignal; + fireEvent = fireEvent; + } + + return eventProvider; + +end; + +return EventProvider; \ No newline at end of file diff --git a/src/VirtualService/init.luau b/src/VirtualService/init.luau new file mode 100644 index 00000000..8fdb5cf3 --- /dev/null +++ b/src/VirtualService/init.luau @@ -0,0 +1,23 @@ +--!strict + +local EventProvider = require(script.EventProvider); + +local VirtualService = { + mocks = { + isEnabled = false; + services = { + ChangeHistoryService = require(script.mocks.services.ChangeHistoryService); + Selection = require(script.mocks.services.Selection); + }; + globals = { + plugin = require(script.mocks.globals.plugin); + }; + }; + events = { + GuiButton = { + Activated = EventProvider.new((nil :: unknown) :: GuiButton, "Activated"); + } + } +}; + +return VirtualService; \ No newline at end of file diff --git a/src/VirtualService/mocks/globals/plugin.luau b/src/VirtualService/mocks/globals/plugin.luau new file mode 100644 index 00000000..d74a3c33 --- /dev/null +++ b/src/VirtualService/mocks/globals/plugin.luau @@ -0,0 +1,5 @@ +--!strict + +local VirtualPlugin = {}; + +return (VirtualPlugin :: unknown) :: Plugin; \ No newline at end of file diff --git a/src/VirtualService/mocks/services/ChangeHistoryService.luau b/src/VirtualService/mocks/services/ChangeHistoryService.luau new file mode 100644 index 00000000..188190b0 --- /dev/null +++ b/src/VirtualService/mocks/services/ChangeHistoryService.luau @@ -0,0 +1,46 @@ +--!strict + +local HttpService = game:GetService("HttpService"); + +local VirtualChangeHistoryService = { + currentHistory = {}; + currentIdentifier = nil; +}; + +function VirtualChangeHistoryService:TryBeginRecording() + + if self.currentIdentifier then + + return; + + end + + self.currentIdentifier = HttpService:GenerateGUID(false); + self.currentHistory[self.currentIdentifier] = true; -- Simulate a successful start + return self.currentIdentifier; + +end; + +function VirtualChangeHistoryService:IsRecordingInProgress() + + return self.currentIdentifier ~= nil; + +end; + +function VirtualChangeHistoryService:FinishRecording(identifier, operation) + + if not self.currentHistory[identifier] then + + return; + + end + + if operation == Enum.FinishRecordingOperation.Cancel then + self.currentHistory[identifier] = nil; + end + + self.currentIdentifier = nil; + +end; + +return (VirtualChangeHistoryService :: unknown) :: ChangeHistoryService; \ No newline at end of file diff --git a/src/VirtualService/mocks/services/Selection.luau b/src/VirtualService/mocks/services/Selection.luau new file mode 100644 index 00000000..95aa70f8 --- /dev/null +++ b/src/VirtualService/mocks/services/Selection.luau @@ -0,0 +1,102 @@ +--!strict + +local selectionChangedEvent = Instance.new("BindableEvent"); +local VirtualSelection = { + currentSelection = {}; + SelectionChanged = selectionChangedEvent.Event; +}; + +function VirtualSelection:Set(selection: {Instance}): () + + local oldSelection = self.currentSelection; + self.currentSelection = selection; + + if not oldSelection[1] and #selection > 0 then + + selectionChangedEvent:Fire(); + return; + + end; + + local checkedInstances = {}; + for _, instance in oldSelection do + + if not table.find(selection, instance) then + + selectionChangedEvent:Fire(); + return; + + end; + + table.insert(checkedInstances, instance); + + end; + + for _, instance in selection do + + if not table.find(checkedInstances, instance) then + + selectionChangedEvent:Fire(); + return; + + end; + + end; + +end; + +function VirtualSelection:Get(): {Instance} + + return self.currentSelection; + +end; + +function VirtualSelection:Add(instances: {Instance}): () + + local shouldFire = false; + + for _, instance in instances do + + if not table.find(self.currentSelection, instance) then + + table.insert(self.currentSelection, instance); + shouldFire = true; + + end; + + end; + + if shouldFire then + + selectionChangedEvent:Fire(); + + end; + +end; + +function VirtualSelection:Remove(instances: {Instance}): () + + local shouldFire = false; + + for _, instance in instances do + + local index = table.find(self.currentSelection, instance); + + if index then + + table.remove(self.currentSelection, index); + shouldFire = true; + + end; + + end; + + if shouldFire then + + selectionChangedEvent:Fire(); + + end; + +end; + +return (VirtualSelection :: unknown) :: Selection; \ No newline at end of file