mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-26 10:34:07 +00:00
one-shot + new commit dialogue
This commit is contained in:
parent
ad61bfa903
commit
298fc43987
10 changed files with 428 additions and 163 deletions
|
|
@ -51,6 +51,103 @@ local function ChangeTag(props)
|
|||
end)
|
||||
end
|
||||
|
||||
-- Radio button option for selection
|
||||
local function SelectionOption(props)
|
||||
return Theme.with(function(theme)
|
||||
local isSelected = props.isSelected
|
||||
local bgColor = if isSelected
|
||||
then (if props.optionType == "push" then Color3.fromHex("27AE60")
|
||||
elseif props.optionType == "pull" then Color3.fromHex("E74C3C")
|
||||
else Color3.fromHex("7F8C8D"))
|
||||
else theme.BorderedContainer.BackgroundColor
|
||||
local textColor = if isSelected then Color3.new(1, 1, 1) else theme.TextColor
|
||||
|
||||
return e("TextButton", {
|
||||
Size = UDim2.new(0, 36, 0, 18),
|
||||
BackgroundColor3 = bgColor,
|
||||
BackgroundTransparency = props.transparency:map(function(t)
|
||||
return if isSelected then 0.1 + (0.9 * t) else 0.7 + (0.3 * t)
|
||||
end),
|
||||
BorderSizePixel = 0,
|
||||
Text = props.text,
|
||||
FontFace = if isSelected then theme.Font.Bold else theme.Font.Main,
|
||||
TextSize = theme.TextSize.Small,
|
||||
TextColor3 = textColor,
|
||||
TextTransparency = props.transparency,
|
||||
LayoutOrder = props.layoutOrder,
|
||||
ZIndex = 10,
|
||||
[Roact.Event.Activated] = function()
|
||||
if props.onClick then
|
||||
props.onClick()
|
||||
end
|
||||
end,
|
||||
}, {
|
||||
Corner = e("UICorner", {
|
||||
CornerRadius = UDim.new(0, 3),
|
||||
}),
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
-- Selection radio group component
|
||||
local function SelectionRadio(props)
|
||||
if not props.visible then
|
||||
return nil
|
||||
end
|
||||
|
||||
return e("Frame", {
|
||||
Size = UDim2.new(0, 118, 0, 18),
|
||||
BackgroundTransparency = 1,
|
||||
Position = UDim2.new(1, -5, 0, 3),
|
||||
AnchorPoint = Vector2.new(1, 0),
|
||||
ZIndex = 10,
|
||||
}, {
|
||||
Layout = e("UIListLayout", {
|
||||
FillDirection = Enum.FillDirection.Horizontal,
|
||||
HorizontalAlignment = Enum.HorizontalAlignment.Right,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
Padding = UDim.new(0, 4),
|
||||
}),
|
||||
Pull = e(SelectionOption, {
|
||||
text = "Pull",
|
||||
optionType = "pull",
|
||||
isSelected = props.selection == "pull",
|
||||
transparency = props.transparency,
|
||||
layoutOrder = 1,
|
||||
onClick = function()
|
||||
if props.onSelectionChange then
|
||||
props.onSelectionChange(props.nodeId, "pull")
|
||||
end
|
||||
end,
|
||||
}),
|
||||
Skip = e(SelectionOption, {
|
||||
text = "Skip",
|
||||
optionType = "ignore",
|
||||
isSelected = props.selection == "ignore",
|
||||
transparency = props.transparency,
|
||||
layoutOrder = 2,
|
||||
onClick = function()
|
||||
if props.onSelectionChange then
|
||||
props.onSelectionChange(props.nodeId, "ignore")
|
||||
end
|
||||
end,
|
||||
}),
|
||||
Push = e(SelectionOption, {
|
||||
text = "Push",
|
||||
optionType = "push",
|
||||
isSelected = props.selection == "push",
|
||||
transparency = props.transparency,
|
||||
layoutOrder = 3,
|
||||
onClick = function()
|
||||
if props.onSelectionChange then
|
||||
props.onSelectionChange(props.nodeId, "push")
|
||||
end
|
||||
end,
|
||||
}),
|
||||
})
|
||||
end
|
||||
|
||||
local Expansion = Roact.Component:extend("Expansion")
|
||||
|
||||
function Expansion:render()
|
||||
|
|
@ -230,16 +327,6 @@ function DomLabel:render()
|
|||
end
|
||||
end
|
||||
end,
|
||||
}, {
|
||||
StateTip = if (props.instance or props.changeList)
|
||||
then e(Tooltip.Trigger, {
|
||||
text = (if props.changeList
|
||||
then "Click to " .. (if self.expanded then "hide" else "view") .. " changes"
|
||||
else "") .. (if props.instance
|
||||
then (if props.changeList then " & d" else "D") .. "ouble click to open in Explorer"
|
||||
else ""),
|
||||
})
|
||||
else nil,
|
||||
}),
|
||||
Expansion = if props.changeList
|
||||
then e(Expansion, {
|
||||
|
|
@ -285,9 +372,10 @@ function DomLabel:render()
|
|||
}),
|
||||
ChangeInfo = e("Frame", {
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, -indent - 80, 0, 24),
|
||||
Position = UDim2.new(1, -2, 0, 0),
|
||||
Size = UDim2.new(1, -indent - (if props.patchType and props.onSelectionChange then 210 else 80), 0, 24),
|
||||
Position = UDim2.new(1, if props.patchType and props.onSelectionChange then -130 else -2, 0, 0),
|
||||
AnchorPoint = Vector2.new(1, 0),
|
||||
ClipsDescendants = true,
|
||||
}, {
|
||||
Layout = e("UIListLayout", {
|
||||
FillDirection = Enum.FillDirection.Horizontal,
|
||||
|
|
@ -324,6 +412,13 @@ function DomLabel:render()
|
|||
})
|
||||
else nil,
|
||||
}),
|
||||
SelectionRadio = e(SelectionRadio, {
|
||||
visible = props.patchType ~= nil and props.onSelectionChange ~= nil,
|
||||
nodeId = props.nodeId,
|
||||
selection = props.selection,
|
||||
transparency = props.transparency,
|
||||
onSelectionChange = props.onSelectionChange,
|
||||
}),
|
||||
LineGuides = e("Folder", nil, lineGuides),
|
||||
})
|
||||
end)
|
||||
|
|
|
|||
|
|
@ -32,6 +32,10 @@ function PatchVisualizer:shouldUpdate(nextProps)
|
|||
return true
|
||||
end
|
||||
|
||||
if self.props.selections ~= nextProps.selections then
|
||||
return true
|
||||
end
|
||||
|
||||
local currentPatch, nextPatch = self.props.patch, nextProps.patch
|
||||
if currentPatch ~= nil or nextPatch ~= nil then
|
||||
return not PatchSet.isEqual(currentPatch, nextPatch)
|
||||
|
|
@ -96,6 +100,10 @@ function PatchVisualizer:render()
|
|||
name = node.name,
|
||||
changeInfo = node.changeInfo,
|
||||
changeList = node.changeList,
|
||||
-- Selection props
|
||||
nodeId = node.id,
|
||||
selection = self.props.selections and self.props.selections[node.id],
|
||||
onSelectionChange = self.props.onSelectionChange,
|
||||
})
|
||||
|
||||
if isFinalChild then
|
||||
|
|
|
|||
|
|
@ -130,7 +130,7 @@ function TextButton:render()
|
|||
zIndex = -1,
|
||||
}),
|
||||
|
||||
Background = (style == "Solid" or style == "Warning") and e(SlicedImage, {
|
||||
Background = (style == "Solid" or style == "Warning" or style == "Primary" or style == "Success" or style == "Danger" or style == "Neutral") and e(SlicedImage, {
|
||||
slice = Assets.Slices.RoundedBackground,
|
||||
color = bindingUtil.mapLerp(
|
||||
bindingEnabled,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ local Roact = require(Packages.Roact)
|
|||
|
||||
local Settings = require(Plugin.Settings)
|
||||
local Theme = require(Plugin.App.Theme)
|
||||
local PatchTree = require(Plugin.PatchTree)
|
||||
local TextButton = require(Plugin.App.Components.TextButton)
|
||||
local StudioPluginGui = require(Plugin.App.Components.Studio.StudioPluginGui)
|
||||
local Tooltip = require(Plugin.App.Components.Tooltip)
|
||||
|
|
@ -21,6 +22,12 @@ function ConfirmingPage:init()
|
|||
self.contentSize, self.setContentSize = Roact.createBinding(0)
|
||||
self.containerSize, self.setContainerSize = Roact.createBinding(Vector2.new(0, 0))
|
||||
|
||||
-- Initialize selections from patchTree defaults
|
||||
local initialSelections = {}
|
||||
if self.props.patchTree then
|
||||
initialSelections = PatchTree.buildInitialSelections(self.props.patchTree)
|
||||
end
|
||||
|
||||
self:setState({
|
||||
showingStringDiff = false,
|
||||
currentString = "",
|
||||
|
|
@ -29,10 +36,48 @@ function ConfirmingPage:init()
|
|||
oldTable = {},
|
||||
newTable = {},
|
||||
showingRejectConfirm = false,
|
||||
selections = initialSelections,
|
||||
})
|
||||
|
||||
-- Callback to update individual selection
|
||||
self.onSelectionChange = function(nodeId, selection)
|
||||
self:setState(function(state)
|
||||
local newSelections = table.clone(state.selections)
|
||||
newSelections[nodeId] = selection
|
||||
return { selections = newSelections }
|
||||
end)
|
||||
end
|
||||
|
||||
-- Set all selections to a specific value
|
||||
self.setAllSelections = function(value)
|
||||
if not self.props.patchTree then
|
||||
return
|
||||
end
|
||||
local newSelections = {}
|
||||
self.props.patchTree:forEach(function(node)
|
||||
if node.patchType then
|
||||
newSelections[node.id] = value
|
||||
end
|
||||
end)
|
||||
self:setState({ selections = newSelections })
|
||||
end
|
||||
end
|
||||
|
||||
function ConfirmingPage:didUpdate(prevProps)
|
||||
-- If patchTree changed, reinitialize selections
|
||||
if prevProps.patchTree ~= self.props.patchTree and self.props.patchTree then
|
||||
local initialSelections = PatchTree.buildInitialSelections(self.props.patchTree)
|
||||
self:setState({ selections = initialSelections })
|
||||
end
|
||||
end
|
||||
|
||||
function ConfirmingPage:render()
|
||||
-- Check if all items have a selection
|
||||
local allSelected = true
|
||||
if self.props.patchTree then
|
||||
allSelected = PatchTree.allNodesSelected(self.props.patchTree, self.state.selections)
|
||||
end
|
||||
|
||||
return Theme.with(function(theme)
|
||||
local pageContent = Roact.createFragment({
|
||||
Title = e("TextLabel", {
|
||||
|
|
@ -56,6 +101,8 @@ function ConfirmingPage:render()
|
|||
layoutOrder = 3,
|
||||
|
||||
patchTree = self.props.patchTree,
|
||||
selections = self.state.selections,
|
||||
onSelectionChange = self.onSelectionChange,
|
||||
|
||||
showStringDiff = function(currentString: string, incomingString: string)
|
||||
self:setState({
|
||||
|
|
@ -84,38 +131,51 @@ function ConfirmingPage:render()
|
|||
transparency = self.props.transparency,
|
||||
layoutOrder = 1,
|
||||
onClick = self.props.onAbort,
|
||||
}, {
|
||||
Tip = e(Tooltip.Trigger, {
|
||||
text = "Stop the connection process",
|
||||
}),
|
||||
}),
|
||||
|
||||
Reject = if Settings:get("twoWaySync")
|
||||
PullAll = if Settings:get("twoWaySync")
|
||||
then e(TextButton, {
|
||||
text = "Reject",
|
||||
style = "Warning",
|
||||
text = "Pull All",
|
||||
style = "Danger",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = 0,
|
||||
layoutOrder = 2,
|
||||
onClick = function()
|
||||
self:setState({ showingRejectConfirm = true })
|
||||
self.setAllSelections("pull")
|
||||
end,
|
||||
}, {
|
||||
Tip = e(Tooltip.Trigger, {
|
||||
text = "Push Studio changes to the Rojo server",
|
||||
}),
|
||||
})
|
||||
else nil,
|
||||
|
||||
Accept = e(TextButton, {
|
||||
text = "Accept",
|
||||
style = "Solid",
|
||||
SkipAll = e(TextButton, {
|
||||
text = "Skip All",
|
||||
style = "Neutral",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = 3,
|
||||
onClick = self.props.onAccept,
|
||||
}, {
|
||||
Tip = e(Tooltip.Trigger, {
|
||||
text = "Pull Rojo server changes to Studio",
|
||||
onClick = function()
|
||||
self.setAllSelections("ignore")
|
||||
end,
|
||||
}),
|
||||
|
||||
PushAll = e(TextButton, {
|
||||
text = "Push All",
|
||||
style = "Success",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = 4,
|
||||
onClick = function()
|
||||
self.setAllSelections("push")
|
||||
end,
|
||||
}),
|
||||
|
||||
Commit = e(TextButton, {
|
||||
text = "Commit",
|
||||
style = "Primary",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = 5,
|
||||
enabled = allSelected,
|
||||
onClick = function()
|
||||
if allSelected and self.props.onConfirm then
|
||||
self.props.onConfirm(self.state.selections)
|
||||
end
|
||||
end,
|
||||
}),
|
||||
|
||||
Layout = e("UIListLayout", {
|
||||
|
|
@ -215,73 +275,6 @@ function ConfirmingPage:render()
|
|||
}),
|
||||
}),
|
||||
|
||||
RejectConfirm = e(StudioPluginGui, {
|
||||
id = "Rojo_RejectConfirm",
|
||||
title = "Confirm Reject",
|
||||
active = self.state.showingRejectConfirm,
|
||||
isEphemeral = true,
|
||||
|
||||
initDockState = Enum.InitialDockState.Float,
|
||||
overridePreviousState = true,
|
||||
floatingSize = Vector2.new(340, 120),
|
||||
minimumSize = Vector2.new(300, 100),
|
||||
|
||||
zIndexBehavior = Enum.ZIndexBehavior.Sibling,
|
||||
|
||||
onClose = function()
|
||||
self:setState({ showingRejectConfirm = false })
|
||||
end,
|
||||
}, {
|
||||
Content = e("Frame", {
|
||||
Size = UDim2.fromScale(1, 1),
|
||||
BackgroundColor3 = theme.BackgroundColor,
|
||||
}, {
|
||||
Message = e("TextLabel", {
|
||||
Text = "Are you sure you want to reject incoming changes and push Studio changes to the server?",
|
||||
FontFace = theme.Font.Main,
|
||||
TextSize = theme.TextSize.Body,
|
||||
TextColor3 = theme.TextColor,
|
||||
TextWrapped = true,
|
||||
Size = UDim2.new(1, -20, 0, 50),
|
||||
Position = UDim2.new(0, 10, 0, 10),
|
||||
BackgroundTransparency = 1,
|
||||
}),
|
||||
|
||||
Buttons = e("Frame", {
|
||||
Size = UDim2.new(1, -20, 0, 34),
|
||||
Position = UDim2.new(0, 10, 1, -44),
|
||||
BackgroundTransparency = 1,
|
||||
}, {
|
||||
Layout = e("UIListLayout", {
|
||||
HorizontalAlignment = Enum.HorizontalAlignment.Right,
|
||||
FillDirection = Enum.FillDirection.Horizontal,
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
Padding = UDim.new(0, 10),
|
||||
}),
|
||||
|
||||
Cancel = e(TextButton, {
|
||||
text = "Cancel",
|
||||
style = "Bordered",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = 1,
|
||||
onClick = function()
|
||||
self:setState({ showingRejectConfirm = false })
|
||||
end,
|
||||
}),
|
||||
|
||||
Confirm = e(TextButton, {
|
||||
text = "Reject",
|
||||
style = "Warning",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = 2,
|
||||
onClick = function()
|
||||
self:setState({ showingRejectConfirm = false })
|
||||
self.props.onReject()
|
||||
end,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
if self.props.createPopup then
|
||||
|
|
|
|||
|
|
@ -237,6 +237,14 @@ function SettingsPage:render()
|
|||
layoutOrder = layoutIncrement(),
|
||||
}),
|
||||
|
||||
OneShotSync = e(Setting, {
|
||||
id = "oneShotSync",
|
||||
name = "One-Shot Sync",
|
||||
description = "Disconnect automatically after the initial sync completes",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = layoutIncrement(),
|
||||
}),
|
||||
|
||||
LogLevel = e(Setting, {
|
||||
id = "logLevel",
|
||||
name = "Log Level",
|
||||
|
|
|
|||
|
|
@ -75,8 +75,60 @@ function StudioProvider:updateTheme()
|
|||
BackgroundColor = BRAND_COLOR,
|
||||
},
|
||||
},
|
||||
Primary = {
|
||||
-- Primary uses blue theming for main actions.
|
||||
ActionFillColor = Color3.fromHex("FFFFFF"),
|
||||
ActionFillTransparency = 0.8,
|
||||
Enabled = {
|
||||
TextColor = Color3.fromHex("FFFFFF"),
|
||||
BackgroundColor = Color3.fromHex("335FFF"),
|
||||
},
|
||||
Disabled = {
|
||||
TextColor = Color3.fromHex("FFFFFF"),
|
||||
BackgroundColor = Color3.fromHex("2A4FCC"),
|
||||
},
|
||||
},
|
||||
Success = {
|
||||
-- Success uses green theming for positive actions.
|
||||
ActionFillColor = Color3.fromHex("FFFFFF"),
|
||||
ActionFillTransparency = 0.8,
|
||||
Enabled = {
|
||||
TextColor = Color3.fromHex("FFFFFF"),
|
||||
BackgroundColor = Color3.fromHex("27AE60"),
|
||||
},
|
||||
Disabled = {
|
||||
TextColor = Color3.fromHex("FFFFFF"),
|
||||
BackgroundColor = Color3.fromHex("1E8449"),
|
||||
},
|
||||
},
|
||||
Danger = {
|
||||
-- Danger uses red theming for destructive actions.
|
||||
ActionFillColor = Color3.fromHex("FFFFFF"),
|
||||
ActionFillTransparency = 0.8,
|
||||
Enabled = {
|
||||
TextColor = Color3.fromHex("FFFFFF"),
|
||||
BackgroundColor = Color3.fromHex("E74C3C"),
|
||||
},
|
||||
Disabled = {
|
||||
TextColor = Color3.fromHex("FFFFFF"),
|
||||
BackgroundColor = Color3.fromHex("B03A2E"),
|
||||
},
|
||||
},
|
||||
Neutral = {
|
||||
-- Neutral uses gray theming for skip/ignore actions.
|
||||
ActionFillColor = Color3.fromHex("FFFFFF"),
|
||||
ActionFillTransparency = 0.8,
|
||||
Enabled = {
|
||||
TextColor = Color3.fromHex("FFFFFF"),
|
||||
BackgroundColor = Color3.fromHex("7F8C8D"),
|
||||
},
|
||||
Disabled = {
|
||||
TextColor = Color3.fromHex("FFFFFF"),
|
||||
BackgroundColor = Color3.fromHex("616A6B"),
|
||||
},
|
||||
},
|
||||
Warning = {
|
||||
-- Warning uses orange theming for destructive actions.
|
||||
-- Warning uses orange theming for caution actions.
|
||||
ActionFillColor = Color3.fromHex("FFFFFF"),
|
||||
ActionFillTransparency = 0.8,
|
||||
Enabled = {
|
||||
|
|
|
|||
|
|
@ -709,14 +709,28 @@ function App:startSession()
|
|||
end)
|
||||
|
||||
serveSession:setConfirmCallback(function(instanceMap, patch, serverInfo)
|
||||
-- Helper to schedule one-shot disconnect after auto-accept
|
||||
local function scheduleOneShotDisconnect()
|
||||
if Settings:get("oneShotSync") then
|
||||
task.delay(0.5, function()
|
||||
if self.serveSession then
|
||||
Log.info("One-shot sync: disconnecting after sync")
|
||||
self:endSession()
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
if PatchSet.isEmpty(patch) then
|
||||
Log.trace("Accepting patch without confirmation because it is empty")
|
||||
scheduleOneShotDisconnect()
|
||||
return "Accept"
|
||||
end
|
||||
|
||||
-- Play solo auto-connect does not require confirmation
|
||||
if self:isAutoConnectPlaytestServerAvailable() then
|
||||
Log.trace("Accepting patch without confirmation because play solo auto-connect is enabled")
|
||||
scheduleOneShotDisconnect()
|
||||
return "Accept"
|
||||
end
|
||||
|
||||
|
|
@ -728,6 +742,7 @@ function App:startSession()
|
|||
Log.trace(
|
||||
"Accepting patch without confirmation because project has already been connected and behavior is set to Initial"
|
||||
)
|
||||
scheduleOneShotDisconnect()
|
||||
return "Accept"
|
||||
end
|
||||
elseif confirmationBehavior == "Large Changes" then
|
||||
|
|
@ -736,6 +751,7 @@ function App:startSession()
|
|||
Log.trace(
|
||||
"Accepting patch without confirmation because patch is small and behavior is set to Large Changes"
|
||||
)
|
||||
scheduleOneShotDisconnect()
|
||||
return "Accept"
|
||||
end
|
||||
elseif confirmationBehavior == "Unlisted PlaceId" then
|
||||
|
|
@ -746,11 +762,13 @@ function App:startSession()
|
|||
Log.trace(
|
||||
"Accepting patch without confirmation because placeId is listed and behavior is set to Unlisted PlaceId"
|
||||
)
|
||||
scheduleOneShotDisconnect()
|
||||
return "Accept"
|
||||
end
|
||||
end
|
||||
elseif confirmationBehavior == "Never" then
|
||||
Log.trace("Accepting patch without confirmation because behavior is set to Never")
|
||||
scheduleOneShotDisconnect()
|
||||
return "Accept"
|
||||
end
|
||||
end
|
||||
|
|
@ -769,6 +787,7 @@ function App:startSession()
|
|||
and datamodelUpdates.changedClassName == nil
|
||||
then
|
||||
Log.trace("Accepting patch without confirmation because it only contains a datamodel name change")
|
||||
scheduleOneShotDisconnect()
|
||||
return "Accept"
|
||||
end
|
||||
end
|
||||
|
|
@ -786,15 +805,15 @@ function App:startSession()
|
|||
})
|
||||
|
||||
self:addNotification({
|
||||
text = string.format(
|
||||
"Please accept%sor abort the initializing sync session.",
|
||||
Settings:get("twoWaySync") and ", reject, " or " "
|
||||
),
|
||||
text = "Please review and confirm the sync changes, or disconnect.",
|
||||
timeout = 7,
|
||||
})
|
||||
|
||||
local result = self.confirmationEvent:Wait()
|
||||
|
||||
-- Track if this was the initial sync for this project
|
||||
local isInitialSyncForProject = not self.knownProjects[serverInfo.projectName]
|
||||
|
||||
-- Reset UI state back to Connected after confirmation
|
||||
-- This is needed for ongoing WebSocket patches where the session
|
||||
-- is already connected and won't trigger a status change
|
||||
|
|
@ -808,6 +827,20 @@ function App:startSession()
|
|||
})
|
||||
end
|
||||
|
||||
-- One-shot sync: disconnect after confirmation
|
||||
if Settings:get("oneShotSync") then
|
||||
local resultType = if type(result) == "table" then result.type else result
|
||||
if resultType == "Confirm" or resultType == "Accept" then
|
||||
-- Delay slightly to allow patch to be applied
|
||||
task.delay(0.5, function()
|
||||
if self.serveSession then
|
||||
Log.info("One-shot sync: disconnecting after sync")
|
||||
self:endSession()
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end)
|
||||
|
||||
|
|
@ -815,7 +848,8 @@ function App:startSession()
|
|||
-- If all changes have been reverted, auto-accept the empty patch
|
||||
if PatchSet.isEmpty(patch) then
|
||||
Log.trace("Patch became empty after merging, auto-accepting")
|
||||
self.confirmationBindable:Fire("Accept")
|
||||
-- Return empty selections to accept all (nothing to do)
|
||||
self.confirmationBindable:Fire({ type = "Confirm", selections = {} })
|
||||
return
|
||||
end
|
||||
|
||||
|
|
@ -925,11 +959,8 @@ function App:render()
|
|||
onAbort = function()
|
||||
self.confirmationBindable:Fire("Abort")
|
||||
end,
|
||||
onAccept = function()
|
||||
self.confirmationBindable:Fire("Accept")
|
||||
end,
|
||||
onReject = function()
|
||||
self.confirmationBindable:Fire("Reject")
|
||||
onConfirm = function(selections)
|
||||
self.confirmationBindable:Fire({ type = "Confirm", selections = selections })
|
||||
end,
|
||||
}),
|
||||
|
||||
|
|
|
|||
|
|
@ -313,6 +313,7 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
|
|||
end
|
||||
|
||||
-- Add this node to tree
|
||||
-- For Edit nodes, default to "push" (apply Rojo changes to Studio)
|
||||
tree:addNode(instanceMap.fromInstances[instance.Parent], {
|
||||
id = change.id,
|
||||
patchType = "Edit",
|
||||
|
|
@ -321,6 +322,7 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
|
|||
instance = instance,
|
||||
changeInfo = changeInfo,
|
||||
changeList = changeList,
|
||||
defaultSelection = "push",
|
||||
})
|
||||
end
|
||||
Timer.stop()
|
||||
|
|
@ -364,6 +366,7 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
|
|||
tree:buildAncestryNodes(previousId, ancestryIds, patch, instanceMap)
|
||||
|
||||
-- Add this node to tree
|
||||
-- For Remove nodes, default to "push" (apply Rojo removal to Studio)
|
||||
local nodeId = instanceMap.fromInstances[instance] or HttpService:GenerateGUID(false)
|
||||
instanceMap:insert(nodeId, instance)
|
||||
tree:addNode(instanceMap.fromInstances[instance.Parent], {
|
||||
|
|
@ -372,6 +375,7 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
|
|||
className = instance.ClassName,
|
||||
name = instance.Name,
|
||||
instance = instance,
|
||||
defaultSelection = "push",
|
||||
})
|
||||
end
|
||||
Timer.stop()
|
||||
|
|
@ -440,6 +444,7 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
|
|||
end
|
||||
|
||||
-- Add this node to tree
|
||||
-- For Add nodes, default to "push" (apply Rojo addition to Studio)
|
||||
tree:addNode(change.Parent, {
|
||||
id = change.Id,
|
||||
patchType = "Add",
|
||||
|
|
@ -448,6 +453,7 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
|
|||
changeInfo = changeInfo,
|
||||
changeList = changeList,
|
||||
instance = instanceMap.fromIds[id],
|
||||
defaultSelection = "push",
|
||||
})
|
||||
end
|
||||
Timer.stop()
|
||||
|
|
@ -456,6 +462,41 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
|
|||
return tree
|
||||
end
|
||||
|
||||
-- Builds initial selections map from the tree using defaultSelection values
|
||||
-- Returns a table mapping node id -> selection ("push", "pull", or "ignore")
|
||||
function PatchTree.buildInitialSelections(tree)
|
||||
local selections = {}
|
||||
tree:forEach(function(node)
|
||||
if node.patchType and node.defaultSelection then
|
||||
selections[node.id] = node.defaultSelection
|
||||
end
|
||||
end)
|
||||
return selections
|
||||
end
|
||||
|
||||
-- Gets all selectable node IDs (nodes that have a patchType)
|
||||
-- Returns an array of node IDs
|
||||
function PatchTree.getSelectableNodeIds(tree)
|
||||
local ids = {}
|
||||
tree:forEach(function(node)
|
||||
if node.patchType then
|
||||
table.insert(ids, node.id)
|
||||
end
|
||||
end)
|
||||
return ids
|
||||
end
|
||||
|
||||
-- Checks if all selectable nodes have a selection
|
||||
function PatchTree.allNodesSelected(tree, selections)
|
||||
local allSelected = true
|
||||
tree:forEach(function(node)
|
||||
if node.patchType and not selections[node.id] then
|
||||
allSelected = false
|
||||
end
|
||||
end)
|
||||
return allSelected
|
||||
end
|
||||
|
||||
-- Updates the metadata of a tree with the unapplied patch and currently existing instances
|
||||
-- Builds a new tree from the data if one isn't provided
|
||||
-- Always returns a new tree for immutability purposes in Roact
|
||||
|
|
|
|||
|
|
@ -590,55 +590,91 @@ function ServeSession:__confirmAndApplyInitialPatch(catchUpPatch, serverInfo)
|
|||
|
||||
if userDecision == "Abort" then
|
||||
return Promise.reject("Aborted Rojo sync operation")
|
||||
elseif userDecision == "Reject" then
|
||||
if not self.__twoWaySync then
|
||||
return Promise.reject("Cannot reject sync operation without two-way sync enabled")
|
||||
end
|
||||
-- The user wants their studio DOM to write back to their Rojo DOM
|
||||
-- so we will reverse the patch and send it back
|
||||
elseif userDecision == "Accept" then
|
||||
-- Legacy: Accept all changes (push all to Studio)
|
||||
self:__setStatus(Status.Connected, serverInfo.projectName)
|
||||
self:__applyGameAndPlaceId(serverInfo)
|
||||
self:__applyPatch(catchUpPatch)
|
||||
return Promise.resolve()
|
||||
elseif type(userDecision) == "table" and userDecision.type == "Confirm" then
|
||||
-- New: Apply based on per-item selections
|
||||
local selections = userDecision.selections or {}
|
||||
|
||||
local inversePatch = PatchSet.newEmpty()
|
||||
-- Build partial patches based on selections
|
||||
local pushPatch = PatchSet.newEmpty() -- Items to apply to Studio
|
||||
local pullPatch = PatchSet.newEmpty() -- Items to send back to Rojo
|
||||
|
||||
-- Send back the current properties
|
||||
-- Process updated items
|
||||
for _, change in catchUpPatch.updated do
|
||||
local selection = selections[change.id]
|
||||
if selection == "push" then
|
||||
-- Apply Rojo changes to Studio
|
||||
table.insert(pushPatch.updated, change)
|
||||
elseif selection == "pull" and self.__twoWaySync then
|
||||
-- Send Studio state back to Rojo
|
||||
local instance = self.__instanceMap.fromIds[change.id]
|
||||
if not instance then
|
||||
continue
|
||||
end
|
||||
|
||||
if instance then
|
||||
local propertiesToSync = change.changedProperties
|
||||
if self.__syncSourceOnly then
|
||||
-- Filter to only Source property when server is in source-only mode
|
||||
if propertiesToSync.Source then
|
||||
propertiesToSync = { Source = propertiesToSync.Source }
|
||||
else
|
||||
continue
|
||||
end
|
||||
end
|
||||
|
||||
local update = encodePatchUpdate(instance, change.id, propertiesToSync)
|
||||
if update then
|
||||
table.insert(inversePatch.updated, update)
|
||||
table.insert(pullPatch.updated, update)
|
||||
end
|
||||
end
|
||||
-- Add the removed instances back to Rojo
|
||||
-- selene:allow(empty_if, unused_variable, empty_loop)
|
||||
for _, instance in catchUpPatch.removed do
|
||||
-- TODO: Generate ID for our instance and add it to inversePatch.added
|
||||
end
|
||||
-- Remove the additions we've rejected
|
||||
for id, _change in catchUpPatch.added do
|
||||
table.insert(inversePatch.removed, id)
|
||||
-- "ignore" items are skipped
|
||||
end
|
||||
|
||||
return self.__apiContext:write(inversePatch)
|
||||
elseif userDecision == "Accept" then
|
||||
-- Process removed items
|
||||
for _, idOrInstance in catchUpPatch.removed do
|
||||
local id = if type(idOrInstance) == "string" then idOrInstance else self.__instanceMap.fromInstances[idOrInstance]
|
||||
local selection = selections[id]
|
||||
if selection == "push" then
|
||||
-- Apply Rojo removal to Studio
|
||||
table.insert(pushPatch.removed, idOrInstance)
|
||||
elseif selection == "pull" and self.__twoWaySync then
|
||||
-- Don't remove in Studio, but we can't easily "add back" to Rojo
|
||||
-- For now, just skip (similar to ignore)
|
||||
end
|
||||
-- "ignore" items are skipped
|
||||
end
|
||||
|
||||
-- Process added items
|
||||
for id, change in catchUpPatch.added do
|
||||
local selection = selections[id]
|
||||
if selection == "push" then
|
||||
-- Apply Rojo addition to Studio
|
||||
pushPatch.added[id] = change
|
||||
elseif selection == "pull" and self.__twoWaySync then
|
||||
-- Don't add in Studio, remove from Rojo
|
||||
table.insert(pullPatch.removed, id)
|
||||
end
|
||||
-- "ignore" items are skipped
|
||||
end
|
||||
|
||||
-- Apply the changes
|
||||
self:__setStatus(Status.Connected, serverInfo.projectName)
|
||||
self:__applyGameAndPlaceId(serverInfo)
|
||||
self:__applyPatch(catchUpPatch)
|
||||
|
||||
-- Apply push items to Studio
|
||||
if not PatchSet.isEmpty(pushPatch) then
|
||||
self:__applyPatch(pushPatch)
|
||||
end
|
||||
|
||||
-- Send pull items back to Rojo server
|
||||
if self.__twoWaySync and not PatchSet.isEmpty(pullPatch) then
|
||||
return self.__apiContext:write(pullPatch)
|
||||
end
|
||||
|
||||
return Promise.resolve()
|
||||
else
|
||||
return Promise.reject("Invalid user decision: " .. userDecision)
|
||||
return Promise.reject("Invalid user decision: " .. tostring(userDecision))
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ local Roact = require(Packages.Roact)
|
|||
local defaultSettings = {
|
||||
openScriptsExternally = false,
|
||||
twoWaySync = false,
|
||||
oneShotSync = false,
|
||||
autoReconnect = false,
|
||||
showNotifications = true,
|
||||
enableSyncFallback = true,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue