one-shot + new commit dialogue

This commit is contained in:
Joe 2026-01-27 06:34:40 -06:00
parent ad61bfa903
commit 298fc43987
10 changed files with 428 additions and 163 deletions

View file

@ -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)

View file

@ -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

View file

@ -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,

View file

@ -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

View file

@ -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",

View file

@ -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 = {

View file

@ -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,
}),

View file

@ -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

View file

@ -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

View file

@ -12,6 +12,7 @@ local Roact = require(Packages.Roact)
local defaultSettings = {
openScriptsExternally = false,
twoWaySync = false,
oneShotSync = false,
autoReconnect = false,
showNotifications = true,
enableSyncFallback = true,