diff --git a/CHANGELOG.md b/CHANGELOG.md
index b9d199c77..2d08d02c6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
* Added Never option to Confirmation ([#893])
* Added popout diff visualizer for table properties like Attributes and Tags ([#834])
* Updated Theme to use Studio colors ([#838])
+* Improved patch visualizer UX ([#883])
* Added experimental setting for Auto Connect in playtests ([#840])
* Projects may now specify rules for syncing files as if they had a different file extension. ([#813])
This is specified via a new field on project files, `syncRules`:
@@ -56,6 +57,7 @@
[#834]: https://github.com/rojo-rbx/rojo/pull/834
[#838]: https://github.com/rojo-rbx/rojo/pull/838
[#840]: https://github.com/rojo-rbx/rojo/pull/840
+[#883]: https://github.com/rojo-rbx/rojo/pull/883
[#893]: https://github.com/rojo-rbx/rojo/pull/893
## [7.4.1] - February 20, 2024
diff --git a/assets/images/syncsuccess.png b/assets/images/syncsuccess.png
new file mode 100644
index 000000000..74958cc13
Binary files /dev/null and b/assets/images/syncsuccess.png differ
diff --git a/assets/images/syncwarning.png b/assets/images/syncwarning.png
new file mode 100644
index 000000000..eafb93d36
Binary files /dev/null and b/assets/images/syncwarning.png differ
diff --git a/plugin/src/App/Components/ClassIcon.lua b/plugin/src/App/Components/ClassIcon.lua
new file mode 100644
index 000000000..c3e46e0d2
--- /dev/null
+++ b/plugin/src/App/Components/ClassIcon.lua
@@ -0,0 +1,126 @@
+local StudioService = game:GetService("StudioService")
+local AssetService = game:GetService("AssetService")
+
+local Rojo = script:FindFirstAncestor("Rojo")
+local Plugin = Rojo.Plugin
+local Packages = Rojo.Packages
+
+local Roact = require(Packages.Roact)
+
+local e = Roact.createElement
+
+local EditableImage = require(Plugin.App.Components.EditableImage)
+
+local imageCache = {}
+local function getImageSizeAndPixels(image)
+ if not imageCache[image] then
+ local editableImage = AssetService:CreateEditableImageAsync(image)
+ imageCache[image] = {
+ Size = editableImage.Size,
+ Pixels = editableImage:ReadPixels(Vector2.zero, editableImage.Size),
+ }
+ end
+
+ return imageCache[image].Size, table.clone(imageCache[image].Pixels)
+end
+
+local function getRecoloredClassIcon(className, color)
+ local iconProps = StudioService:GetClassIcon(className)
+
+ if iconProps and color then
+ local success, editableImageSize, editableImagePixels = pcall(function()
+ local size, pixels = getImageSizeAndPixels(iconProps.Image)
+
+ local minVal, maxVal = math.huge, -math.huge
+ for i = 1, #pixels, 4 do
+ if pixels[i + 3] == 0 then
+ continue
+ end
+ local pixelVal = math.max(pixels[i], pixels[i + 1], pixels[i + 2])
+
+ minVal = math.min(minVal, pixelVal)
+ maxVal = math.max(maxVal, pixelVal)
+ end
+
+ local hue, sat, val = color:ToHSV()
+ for i = 1, #pixels, 4 do
+ if pixels[i + 3] == 0 then
+ continue
+ end
+
+ local pixelVal = math.max(pixels[i], pixels[i + 1], pixels[i + 2])
+ local newVal = val
+ if minVal < maxVal then
+ -- Remap minVal - maxVal to val*0.9 - val
+ newVal = val * (0.9 + 0.1 * (pixelVal - minVal) / (maxVal - minVal))
+ end
+
+ local newPixelColor = Color3.fromHSV(hue, sat, newVal)
+ pixels[i], pixels[i + 1], pixels[i + 2] = newPixelColor.R, newPixelColor.G, newPixelColor.B
+ end
+ return size, pixels
+ end)
+ if success then
+ iconProps.EditableImagePixels = editableImagePixels
+ iconProps.EditableImageSize = editableImageSize
+ end
+ end
+
+ return iconProps
+end
+
+local ClassIcon = Roact.PureComponent:extend("ClassIcon")
+
+function ClassIcon:init()
+ self.state = {
+ iconProps = nil,
+ }
+end
+
+function ClassIcon:updateIcon()
+ local props = self.props
+ local iconProps = getRecoloredClassIcon(props.className, props.color)
+ self:setState({
+ iconProps = iconProps,
+ })
+end
+
+function ClassIcon:didMount()
+ self:updateIcon()
+end
+
+function ClassIcon:didUpdate(lastProps)
+ if lastProps.className ~= self.props.className or lastProps.color ~= self.props.color then
+ self:updateIcon()
+ end
+end
+
+function ClassIcon:render()
+ local iconProps = self.state.iconProps
+ if not iconProps then
+ return nil
+ end
+
+ return e(
+ "ImageLabel",
+ {
+ Size = self.props.size,
+ Position = self.props.position,
+ LayoutOrder = self.props.layoutOrder,
+ AnchorPoint = self.props.anchorPoint,
+ ImageTransparency = self.props.transparency,
+ Image = iconProps.Image,
+ ImageRectOffset = iconProps.ImageRectOffset,
+ ImageRectSize = iconProps.ImageRectSize,
+ BackgroundTransparency = 1,
+ },
+ if iconProps.EditableImagePixels
+ then e(EditableImage, {
+ size = iconProps.EditableImageSize,
+ pixels = iconProps.EditableImagePixels,
+ })
+ else nil
+ )
+end
+
+return ClassIcon
diff --git a/plugin/src/App/Components/EditableImage.lua b/plugin/src/App/Components/EditableImage.lua
new file mode 100644
index 000000000..501a37eee
--- /dev/null
+++ b/plugin/src/App/Components/EditableImage.lua
@@ -0,0 +1,41 @@
+local Rojo = script:FindFirstAncestor("Rojo")
+local Packages = Rojo.Packages
+
+local Roact = require(Packages.Roact)
+
+local e = Roact.createElement
+
+local EditableImage = Roact.PureComponent:extend("EditableImage")
+
+function EditableImage:init()
+ self.ref = Roact.createRef()
+end
+
+function EditableImage:writePixels()
+ local image = self.ref.current
+ if not image then
+ return
+ end
+ if not self.props.pixels then
+ return
+ end
+
+ image:WritePixels(Vector2.zero, self.props.size, self.props.pixels)
+end
+
+function EditableImage:render()
+ return e("EditableImage", {
+ Size = self.props.size,
+ [Roact.Ref] = self.ref,
+ })
+end
+
+function EditableImage:didMount()
+ self:writePixels()
+end
+
+function EditableImage:didUpdate()
+ self:writePixels()
+end
+
+return EditableImage
diff --git a/plugin/src/App/Components/PatchVisualizer/ChangeList.lua b/plugin/src/App/Components/PatchVisualizer/ChangeList.lua
index ce01b8010..2f10d4587 100644
--- a/plugin/src/App/Components/PatchVisualizer/ChangeList.lua
+++ b/plugin/src/App/Components/PatchVisualizer/ChangeList.lua
@@ -155,7 +155,7 @@ function ChangeList:render()
local headerRow = changes[1]
local headers = e("Frame", {
- Size = UDim2.new(1, 0, 0, 30),
+ Size = UDim2.new(1, 0, 0, 24),
BackgroundTransparency = rowTransparency,
BackgroundColor3 = theme.Diff.Row,
LayoutOrder = 0,
@@ -214,7 +214,7 @@ function ChangeList:render()
local isWarning = metadata.isWarning
rows[row] = e("Frame", {
- Size = UDim2.new(1, 0, 0, 30),
+ Size = UDim2.new(1, 0, 0, 24),
BackgroundTransparency = row % 2 ~= 0 and rowTransparency or 1,
BackgroundColor3 = theme.Diff.Row,
BorderSizePixel = 0,
@@ -269,8 +269,8 @@ function ChangeList:render()
}, {
Headers = headers,
Values = e(ScrollingFrame, {
- size = UDim2.new(1, 0, 1, -30),
- position = UDim2.new(0, 0, 0, 30),
+ size = UDim2.new(1, 0, 1, -24),
+ position = UDim2.new(0, 0, 0, 24),
contentSize = self.contentSize,
transparency = props.transparency,
}, rows),
diff --git a/plugin/src/App/Components/PatchVisualizer/DomLabel.lua b/plugin/src/App/Components/PatchVisualizer/DomLabel.lua
index 41ec0b8e9..a842dcbc5 100644
--- a/plugin/src/App/Components/PatchVisualizer/DomLabel.lua
+++ b/plugin/src/App/Components/PatchVisualizer/DomLabel.lua
@@ -1,5 +1,4 @@
local SelectionService = game:GetService("Selection")
-local StudioService = game:GetService("StudioService")
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
@@ -15,7 +14,8 @@ local bindingUtil = require(Plugin.App.bindingUtil)
local e = Roact.createElement
local ChangeList = require(script.Parent.ChangeList)
-local Tooltip = require(script.Parent.Parent.Tooltip)
+local Tooltip = require(Plugin.App.Components.Tooltip)
+local ClassIcon = require(Plugin.App.Components.ClassIcon)
local Expansion = Roact.Component:extend("Expansion")
@@ -28,8 +28,8 @@ function Expansion:render()
return e("Frame", {
BackgroundTransparency = 1,
- Size = UDim2.new(1, -props.indent, 1, -30),
- Position = UDim2.new(0, props.indent, 0, 30),
+ Size = UDim2.new(1, -props.indent, 1, -24),
+ Position = UDim2.new(0, props.indent, 0, 24),
}, {
ChangeList = e(ChangeList, {
changes = props.changeList,
@@ -44,7 +44,7 @@ local DomLabel = Roact.Component:extend("DomLabel")
function DomLabel:init()
local initHeight = self.props.elementHeight:getValue()
- self.expanded = initHeight > 30
+ self.expanded = initHeight > 24
self.motor = Flipper.SingleMotor.new(initHeight)
self.binding = bindingUtil.fromMotor(self.motor)
@@ -53,7 +53,7 @@ function DomLabel:init()
renderExpansion = self.expanded,
})
self.motor:onStep(function(value)
- local renderExpansion = value > 30
+ local renderExpansion = value > 24
self.props.setElementHeight(value)
if self.props.updateEvent then
@@ -81,7 +81,7 @@ function DomLabel:didUpdate(prevProps)
then
-- Close the expansion when the domlabel is changed to a different thing
self.expanded = false
- self.motor:setGoal(Flipper.Spring.new(30, {
+ self.motor:setGoal(Flipper.Spring.new(24, {
frequency = 5,
dampingRatio = 1,
}))
@@ -90,17 +90,49 @@ end
function DomLabel:render()
local props = self.props
+ local depth = props.depth or 1
return Theme.with(function(theme)
- local iconProps = StudioService:GetClassIcon(props.className)
- local indent = (props.depth or 0) * 20 + 25
+ local color = if props.isWarning
+ then theme.Diff.Warning
+ elseif props.patchType then theme.Diff[props.patchType]
+ else theme.TextColor
+
+ local indent = (depth - 1) * 12 + 15
-- Line guides help indent depth remain readable
local lineGuides = {}
- for i = 1, props.depth or 0 do
- lineGuides["Line_" .. i] = e("Frame", {
- Size = UDim2.new(0, 2, 1, 2),
- Position = UDim2.new(0, (20 * i) + 15, 0, -1),
+ for i = 2, depth do
+ if props.depthsComplete[i] then
+ continue
+ end
+ if props.isFinalChild and i == depth then
+ -- This line stops halfway down to merge with our connector for the right angle
+ lineGuides["Line_" .. i] = e("Frame", {
+ Size = UDim2.new(0, 2, 0, 15),
+ Position = UDim2.new(0, (12 * (i - 1)) + 6, 0, -1),
+ BorderSizePixel = 0,
+ BackgroundTransparency = props.transparency,
+ BackgroundColor3 = theme.BorderedContainer.BorderColor,
+ })
+ else
+ -- All other lines go all the way
+ -- with the exception of the final element, which stops halfway down
+ lineGuides["Line_" .. i] = e("Frame", {
+ Size = UDim2.new(0, 2, 1, if props.isFinalElement then -9 else 2),
+ Position = UDim2.new(0, (12 * (i - 1)) + 6, 0, -1),
+ BorderSizePixel = 0,
+ BackgroundTransparency = props.transparency,
+ BackgroundColor3 = theme.BorderedContainer.BorderColor,
+ })
+ end
+ end
+
+ if depth ~= 1 then
+ lineGuides["Connector"] = e("Frame", {
+ Size = UDim2.new(0, 8, 0, 2),
+ Position = UDim2.new(0, 2 + (12 * props.depth), 0, 12),
+ AnchorPoint = Vector2.xAxis,
BorderSizePixel = 0,
BackgroundTransparency = props.transparency,
BackgroundColor3 = theme.BorderedContainer.BorderColor,
@@ -109,9 +141,8 @@ function DomLabel:render()
return e("Frame", {
ClipsDescendants = true,
- BackgroundColor3 = if props.patchType then theme.Diff[props.patchType] else nil,
- BorderSizePixel = 0,
- BackgroundTransparency = props.patchType and props.transparency or 1,
+ BackgroundTransparency = if props.elementIndex % 2 == 0 then 0.985 else 1,
+ BackgroundColor3 = theme.Diff.Row,
Size = self.binding:map(function(expand)
return UDim2.new(1, 0, 0, expand)
end),
@@ -141,8 +172,8 @@ function DomLabel:render()
if props.changeList then
self.expanded = not self.expanded
- local goalHeight = 30
- + (if self.expanded then math.clamp(#props.changeList * 30, 30, 30 * 6) else 0)
+ local goalHeight = 24
+ + (if self.expanded then math.clamp(#props.changeList * 24, 24, 24 * 6) else 0)
self.motor:setGoal(Flipper.Spring.new(goalHeight, {
frequency = 5,
dampingRatio = 1,
@@ -174,40 +205,74 @@ function DomLabel:render()
DiffIcon = if props.patchType
then e("ImageLabel", {
Image = Assets.Images.Diff[props.patchType],
- ImageColor3 = theme.AddressEntry.PlaceholderColor,
+ ImageColor3 = color,
ImageTransparency = props.transparency,
BackgroundTransparency = 1,
- Size = UDim2.new(0, 20, 0, 20),
- Position = UDim2.new(0, 0, 0, 15),
+ Size = UDim2.new(0, 14, 0, 14),
+ Position = UDim2.new(0, 0, 0, 12),
AnchorPoint = Vector2.new(0, 0.5),
})
else nil,
- ClassIcon = e("ImageLabel", {
- Image = iconProps.Image,
- ImageTransparency = props.transparency,
- ImageRectOffset = iconProps.ImageRectOffset,
- ImageRectSize = iconProps.ImageRectSize,
- BackgroundTransparency = 1,
- Size = UDim2.new(0, 20, 0, 20),
- Position = UDim2.new(0, indent, 0, 15),
- AnchorPoint = Vector2.new(0, 0.5),
+ ClassIcon = e(ClassIcon, {
+ className = props.className,
+ color = color,
+ transparency = props.transparency,
+ size = UDim2.new(0, 16, 0, 16),
+ position = UDim2.new(0, indent + 2, 0, 12),
+ anchorPoint = Vector2.new(0, 0.5),
}),
InstanceName = e("TextLabel", {
- Text = (if props.isWarning then "⚠ " else "") .. props.name .. (props.hint and string.format(
- ' %s',
- theme.AddressEntry.PlaceholderColor:ToHex(),
- props.hint
- ) or ""),
+ Text = (if props.isWarning then "⚠ " else "") .. props.name,
RichText = true,
BackgroundTransparency = 1,
- Font = Enum.Font.GothamMedium,
+ Font = if props.patchType then Enum.Font.GothamBold else Enum.Font.GothamMedium,
TextSize = 14,
- TextColor3 = if props.isWarning then theme.Diff.Warning else theme.TextColor,
+ TextColor3 = color,
TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = props.transparency,
TextTruncate = Enum.TextTruncate.AtEnd,
- Size = UDim2.new(1, -indent - 50, 0, 30),
- Position = UDim2.new(0, indent + 30, 0, 0),
+ Size = UDim2.new(1, -indent - 50, 0, 24),
+ Position = UDim2.new(0, indent + 22, 0, 0),
+ }),
+ ChangeInfo = e("Frame", {
+ BackgroundTransparency = 1,
+ Size = UDim2.new(1, -indent - 80, 0, 24),
+ Position = UDim2.new(1, -2, 0, 0),
+ AnchorPoint = Vector2.new(1, 0),
+ }, {
+ Layout = e("UIListLayout", {
+ FillDirection = Enum.FillDirection.Horizontal,
+ HorizontalAlignment = Enum.HorizontalAlignment.Right,
+ VerticalAlignment = Enum.VerticalAlignment.Center,
+ SortOrder = Enum.SortOrder.LayoutOrder,
+ Padding = UDim.new(0, 4),
+ }),
+ Edits = if props.changeInfo and props.changeInfo.edits
+ then e("TextLabel", {
+ Text = props.changeInfo.edits .. if props.changeInfo.failed then "," else "",
+ BackgroundTransparency = 1,
+ Font = Enum.Font.Gotham,
+ TextSize = 14,
+ TextColor3 = theme.SubTextColor,
+ TextTransparency = props.transparency,
+ Size = UDim2.new(0, 0, 0, 16),
+ AutomaticSize = Enum.AutomaticSize.X,
+ LayoutOrder = 2,
+ })
+ else nil,
+ Failed = if props.changeInfo and props.changeInfo.failed
+ then e("TextLabel", {
+ Text = props.changeInfo.failed,
+ BackgroundTransparency = 1,
+ Font = Enum.Font.Gotham,
+ TextSize = 14,
+ TextColor3 = theme.Diff.Warning,
+ TextTransparency = props.transparency,
+ Size = UDim2.new(0, 0, 0, 16),
+ AutomaticSize = Enum.AutomaticSize.X,
+ LayoutOrder = 6,
+ })
+ else nil,
}),
LineGuides = e("Folder", nil, lineGuides),
})
diff --git a/plugin/src/App/Components/PatchVisualizer/init.lua b/plugin/src/App/Components/PatchVisualizer/init.lua
index 67a4d7b20..a87d499bb 100644
--- a/plugin/src/App/Components/PatchVisualizer/init.lua
+++ b/plugin/src/App/Components/PatchVisualizer/init.lua
@@ -8,8 +8,8 @@ local PatchTree = require(Plugin.PatchTree)
local PatchSet = require(Plugin.PatchSet)
local Theme = require(Plugin.App.Theme)
-local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
local VirtualScroller = require(Plugin.App.Components.VirtualScroller)
+local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
local e = Roact.createElement
@@ -55,34 +55,60 @@ function PatchVisualizer:render()
end
-- Recusively draw tree
- local scrollElements, elementHeights = {}, {}
+ local scrollElements, elementHeights, elementIndex = {}, {}, 0
if patchTree then
+ local elementTotal = patchTree:getCount()
+ local depthsComplete = {}
local function drawNode(node, depth)
- local elementHeight, setElementHeight = Roact.createBinding(30)
- table.insert(elementHeights, elementHeight)
- table.insert(
- scrollElements,
- e(DomLabel, {
- updateEvent = self.updateEvent,
- elementHeight = elementHeight,
- setElementHeight = setElementHeight,
- patchType = node.patchType,
- className = node.className,
- isWarning = node.isWarning,
- instance = node.instance,
- name = node.name,
- hint = node.hint,
- changeList = node.changeList,
- depth = depth,
- transparency = self.props.transparency,
- showStringDiff = self.props.showStringDiff,
- showTableDiff = self.props.showTableDiff,
- })
- )
+ elementIndex += 1
+
+ local parentNode = patchTree:getNode(node.parentId)
+ local isFinalChild = true
+ if parentNode then
+ for _id, sibling in parentNode.children do
+ if type(sibling) == "table" and sibling.name and sibling.name > node.name then
+ isFinalChild = false
+ break
+ end
+ end
+ end
+
+ local elementHeight, setElementHeight = Roact.createBinding(24)
+ elementHeights[elementIndex] = elementHeight
+ scrollElements[elementIndex] = e(DomLabel, {
+ transparency = self.props.transparency,
+ showStringDiff = self.props.showStringDiff,
+ showTableDiff = self.props.showTableDiff,
+ updateEvent = self.updateEvent,
+ elementHeight = elementHeight,
+ setElementHeight = setElementHeight,
+ elementIndex = elementIndex,
+ isFinalElement = elementIndex == elementTotal,
+ depth = depth,
+ depthsComplete = table.clone(depthsComplete),
+ hasChildren = (node.children ~= nil and next(node.children) ~= nil),
+ isFinalChild = isFinalChild,
+ patchType = node.patchType,
+ className = node.className,
+ isWarning = node.isWarning,
+ instance = node.instance,
+ name = node.name,
+ changeInfo = node.changeInfo,
+ changeList = node.changeList,
+ })
+
+ if isFinalChild then
+ depthsComplete[depth] = true
+ end
end
patchTree:forEach(function(node, depth)
+ depthsComplete[depth] = false
+ for i = depth + 1, #depthsComplete do
+ depthsComplete[i] = nil
+ end
+
drawNode(node, depth)
end)
end
@@ -92,6 +118,7 @@ function PatchVisualizer:render()
transparency = self.props.transparency,
size = self.props.size,
position = self.props.position,
+ anchorPoint = self.props.anchorPoint,
layoutOrder = self.props.layoutOrder,
}, {
CleanMerge = e("TextLabel", {
@@ -106,7 +133,8 @@ function PatchVisualizer:render()
}),
VirtualScroller = e(VirtualScroller, {
- size = UDim2.new(1, 0, 1, 0),
+ size = UDim2.new(1, 0, 1, -2),
+ position = UDim2.new(0, 0, 0, 2),
transparency = self.props.transparency,
count = #scrollElements,
updateEvent = self.updateEvent.Event,
diff --git a/plugin/src/App/Components/VirtualScroller.lua b/plugin/src/App/Components/VirtualScroller.lua
index 69bcf5f10..466da8a4f 100644
--- a/plugin/src/App/Components/VirtualScroller.lua
+++ b/plugin/src/App/Components/VirtualScroller.lua
@@ -131,8 +131,8 @@ function VirtualScroller:render()
Position = props.position,
AnchorPoint = props.anchorPoint,
BackgroundTransparency = props.backgroundTransparency or 1,
- BackgroundColor3 = props.backgroundColor3,
- BorderColor3 = props.borderColor3,
+ BackgroundColor3 = props.backgroundColor3 or theme.BorderedContainer.BackgroundColor,
+ BorderColor3 = props.borderColor3 or theme.BorderedContainer.BorderColor,
CanvasSize = self.totalCanvas:map(function(s)
return UDim2.fromOffset(0, s)
end),
diff --git a/plugin/src/App/StatusPages/Confirming.lua b/plugin/src/App/StatusPages/Confirming.lua
index bd0fd0a13..df8bf5b49 100644
--- a/plugin/src/App/StatusPages/Confirming.lua
+++ b/plugin/src/App/StatusPages/Confirming.lua
@@ -9,7 +9,6 @@ local PatchTree = require(Plugin.PatchTree)
local Settings = require(Plugin.Settings)
local Theme = require(Plugin.App.Theme)
local TextButton = require(Plugin.App.Components.TextButton)
-local Header = require(Plugin.App.Components.Header)
local StudioPluginGui = require(Plugin.App.Components.Studio.StudioPluginGui)
local Tooltip = require(Plugin.App.Components.Tooltip)
local PatchVisualizer = require(Plugin.App.Components.PatchVisualizer)
@@ -60,17 +59,11 @@ end
function ConfirmingPage:render()
return Theme.with(function(theme)
local pageContent = Roact.createFragment({
- Header = e(Header, {
- transparency = self.props.transparency,
- layoutOrder = 1,
- }),
-
Title = e("TextLabel", {
Text = string.format(
"Sync changes for project '%s':",
self.props.confirmData.serverInfo.projectName or "UNKNOWN"
),
- LayoutOrder = 2,
Font = Enum.Font.Gotham,
LineHeight = 1.2,
TextSize = 14,
@@ -82,7 +75,7 @@ function ConfirmingPage:render()
}),
PatchVisualizer = e(PatchVisualizer, {
- size = UDim2.new(1, 0, 1, -150),
+ size = UDim2.new(1, 0, 1, -100),
transparency = self.props.transparency,
layoutOrder = 3,
@@ -155,6 +148,11 @@ function ConfirmingPage:render()
}),
}),
+ Padding = e("UIPadding", {
+ PaddingLeft = UDim.new(0, 8),
+ PaddingRight = UDim.new(0, 8),
+ }),
+
Layout = e("UIListLayout", {
HorizontalAlignment = Enum.HorizontalAlignment.Center,
VerticalAlignment = Enum.VerticalAlignment.Center,
@@ -163,11 +161,6 @@ function ConfirmingPage:render()
Padding = UDim.new(0, 10),
}),
- Padding = e("UIPadding", {
- PaddingLeft = UDim.new(0, 20),
- PaddingRight = UDim.new(0, 20),
- }),
-
StringDiff = e(StudioPluginGui, {
id = "Rojo_ConfirmingStringDiff",
title = "String diff",
diff --git a/plugin/src/App/StatusPages/Connected.lua b/plugin/src/App/StatusPages/Connected.lua
index d8055b3f3..3f84d15f4 100644
--- a/plugin/src/App/StatusPages/Connected.lua
+++ b/plugin/src/App/StatusPages/Connected.lua
@@ -3,9 +3,7 @@ local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Packages.Roact)
-local Flipper = require(Packages.Flipper)
-local bindingUtil = require(Plugin.App.bindingUtil)
local Theme = require(Plugin.App.Theme)
local Assets = require(Plugin.Assets)
local PatchSet = require(Plugin.PatchSet)
@@ -23,28 +21,20 @@ local TableDiffVisualizer = require(Plugin.App.Components.TableDiffVisualizer)
local e = Roact.createElement
local AGE_UNITS = {
- { 31556909, "year" },
- { 2629743, "month" },
- { 604800, "week" },
- { 86400, "day" },
- { 3600, "hour" },
- {
- 60,
- "minute",
- },
+ { 31556909, "y" },
+ { 2629743, "mon" },
+ { 604800, "w" },
+ { 86400, "d" },
+ { 3600, "h" },
+ { 60, "m" },
}
function timeSinceText(elapsed: number): string
- if elapsed < 3 then
- return "just now"
- end
-
- local ageText = string.format("%d seconds ago", elapsed)
+ local ageText = string.format("%ds", elapsed)
for _, UnitData in ipairs(AGE_UNITS) do
local UnitSeconds, UnitName = UnitData[1], UnitData[2]
if elapsed > UnitSeconds then
- local c = math.floor(elapsed / UnitSeconds)
- ageText = string.format("%d %s%s ago", c, UnitName, c > 1 and "s" or "")
+ ageText = elapsed // UnitSeconds .. UnitName
break
end
end
@@ -52,49 +42,179 @@ function timeSinceText(elapsed: number): string
return ageText
end
-local ChangesDrawer = Roact.Component:extend("ChangesDrawer")
+local ChangesViewer = Roact.Component:extend("ChangesViewer")
-function ChangesDrawer:init()
+function ChangesViewer:init()
-- Hold onto the serve session during the lifecycle of this component
-- so that it can still render during the fade out after disconnecting
self.serveSession = self.props.serveSession
end
-function ChangesDrawer:render()
- if self.props.rendered == false or self.serveSession == nil then
+function ChangesViewer:render()
+ if self.props.rendered == false or self.serveSession == nil or self.props.patchData == nil then
return nil
end
+ local unapplied = PatchSet.countChanges(self.props.patchData.unapplied)
+ local applied = PatchSet.countChanges(self.props.patchData.patch) - unapplied
+
return Theme.with(function(theme)
- return e(BorderedContainer, {
- transparency = self.props.transparency,
- size = self.props.height:map(function(y)
- return UDim2.new(1, 0, y, -220 * y)
- end),
- position = UDim2.new(0, 0, 1, 0),
- anchorPoint = Vector2.new(0, 1),
- layoutOrder = self.props.layoutOrder,
- }, {
- Close = e(IconButton, {
- icon = Assets.Images.Icons.Close,
- iconSize = 24,
- color = theme.ConnectionDetails.DisconnectColor,
- transparency = self.props.transparency,
+ return Roact.createFragment({
+ Navbar = e("Frame", {
+ Size = UDim2.new(1, 0, 0, 40),
+ BackgroundTransparency = 1,
+ }, {
+ Close = e(IconButton, {
+ icon = Assets.Images.Icons.Close,
+ iconSize = 24,
+ color = theme.Settings.Navbar.BackButtonColor,
+ transparency = self.props.transparency,
- position = UDim2.new(1, 0, 0, 0),
- anchorPoint = Vector2.new(1, 0),
+ position = UDim2.new(0, 0, 0.5, 0),
+ anchorPoint = Vector2.new(0, 0.5),
- onClick = self.props.onClose,
- }, {
- Tip = e(Tooltip.Trigger, {
- text = "Close the patch visualizer",
+ onClick = self.props.onBack,
+ }, {
+ Tip = e(Tooltip.Trigger, {
+ text = "Close",
+ }),
+ }),
+
+ Title = e("TextLabel", {
+ Text = "Sync",
+ Font = Enum.Font.GothamMedium,
+ TextSize = 17,
+ TextXAlignment = Enum.TextXAlignment.Left,
+ TextColor3 = theme.TextColor,
+ TextTransparency = self.props.transparency,
+ Size = UDim2.new(1, -40, 0, 20),
+ Position = UDim2.new(0, 40, 0, 0),
+ BackgroundTransparency = 1,
+ }),
+
+ Subtitle = e("TextLabel", {
+ Text = DateTime.fromUnixTimestamp(self.props.patchData.timestamp):FormatLocalTime("LTS", "en-us"),
+ TextXAlignment = Enum.TextXAlignment.Left,
+ Font = Enum.Font.Gotham,
+ TextSize = 15,
+ TextColor3 = theme.SubTextColor,
+ TextTruncate = Enum.TextTruncate.AtEnd,
+ TextTransparency = self.props.transparency,
+ Size = UDim2.new(1, -40, 0, 16),
+ Position = UDim2.new(0, 40, 0, 20),
+ BackgroundTransparency = 1,
+ }),
+
+ Info = e("Frame", {
+ BackgroundTransparency = 1,
+ Size = UDim2.new(0, 10, 0, 24),
+ AutomaticSize = Enum.AutomaticSize.X,
+ Position = UDim2.new(1, -5, 0.5, 0),
+ AnchorPoint = Vector2.new(1, 0.5),
+ }, {
+ Tooltip = e(Tooltip.Trigger, {
+ text = `{applied} changes applied`
+ .. (if unapplied > 0 then `, {unapplied} changes failed` else ""),
+ }),
+ Content = e("Frame", {
+ BackgroundTransparency = 1,
+ Size = UDim2.new(0, 0, 1, 0),
+ AutomaticSize = Enum.AutomaticSize.X,
+ }, {
+ Layout = e("UIListLayout", {
+ FillDirection = Enum.FillDirection.Horizontal,
+ HorizontalAlignment = Enum.HorizontalAlignment.Right,
+ VerticalAlignment = Enum.VerticalAlignment.Center,
+ SortOrder = Enum.SortOrder.LayoutOrder,
+ Padding = UDim.new(0, 4),
+ }),
+
+ StatusIcon = e("ImageLabel", {
+ BackgroundTransparency = 1,
+ Image = if unapplied > 0
+ then Assets.Images.Icons.SyncWarning
+ else Assets.Images.Icons.SyncSuccess,
+ ImageColor3 = if unapplied > 0 then theme.Diff.Warning else theme.TextColor,
+ Size = UDim2.new(0, 24, 0, 24),
+ LayoutOrder = 10,
+ }),
+ StatusSpacer = e("Frame", {
+ BackgroundTransparency = 1,
+ Size = UDim2.new(0, 6, 0, 4),
+ LayoutOrder = 9,
+ }),
+ AppliedIcon = e("ImageLabel", {
+ BackgroundTransparency = 1,
+ Image = Assets.Images.Icons.Checkmark,
+ ImageColor3 = theme.TextColor,
+ Size = UDim2.new(0, 16, 0, 16),
+ LayoutOrder = 1,
+ }),
+ AppliedText = e("TextLabel", {
+ Text = applied,
+ Font = Enum.Font.Gotham,
+ TextSize = 15,
+ TextColor3 = theme.TextColor,
+ TextTransparency = self.props.transparency,
+ Size = UDim2.new(0, 0, 1, 0),
+ AutomaticSize = Enum.AutomaticSize.X,
+ BackgroundTransparency = 1,
+ LayoutOrder = 2,
+ }),
+ Warnings = if unapplied > 0
+ then Roact.createFragment({
+ WarningsSpacer = e("Frame", {
+ BackgroundTransparency = 1,
+ Size = UDim2.new(0, 4, 0, 4),
+ LayoutOrder = 3,
+ }),
+ UnappliedIcon = e("ImageLabel", {
+ BackgroundTransparency = 1,
+ Image = Assets.Images.Icons.Exclamation,
+ ImageColor3 = theme.Diff.Warning,
+ Size = UDim2.new(0, 4, 0, 16),
+ LayoutOrder = 4,
+ }),
+ UnappliedText = e("TextLabel", {
+ Text = unapplied,
+ Font = Enum.Font.Gotham,
+ TextSize = 15,
+ TextColor3 = theme.Diff.Warning,
+ TextTransparency = self.props.transparency,
+ Size = UDim2.new(0, 0, 1, 0),
+ AutomaticSize = Enum.AutomaticSize.X,
+ BackgroundTransparency = 1,
+ LayoutOrder = 5,
+ }),
+ })
+ else nil,
+ }),
+ }),
+
+ Divider = e("Frame", {
+ BackgroundColor3 = theme.Settings.DividerColor,
+ BackgroundTransparency = self.props.transparency,
+ Size = UDim2.new(1, 0, 0, 1),
+ Position = UDim2.new(0, 0, 1, 0),
+ BorderSizePixel = 0,
+ }, {
+ Gradient = e("UIGradient", {
+ Transparency = NumberSequence.new({
+ NumberSequenceKeypoint.new(0, 1),
+ NumberSequenceKeypoint.new(0.1, 0),
+ NumberSequenceKeypoint.new(0.9, 0),
+ NumberSequenceKeypoint.new(1, 1),
+ }),
+ }),
}),
}),
- PatchVisualizer = e(PatchVisualizer, {
- size = UDim2.new(1, 0, 1, 0),
+ Patch = e(PatchVisualizer, {
+ size = UDim2.new(1, -10, 1, -65),
+ position = UDim2.new(0, 5, 1, -5),
+ anchorPoint = Vector2.new(0, 1),
transparency = self.props.transparency,
- layoutOrder = 3,
+ layoutOrder = self.props.layoutOrder,
patchTree = self.props.patchTree,
@@ -167,20 +287,7 @@ function ConnectedPage:getChangeInfoText()
if patchData == nil then
return ""
end
-
- local elapsed = os.time() - patchData.timestamp
- local unapplied = PatchSet.countChanges(patchData.unapplied)
-
- return "Synced "
- .. timeSinceText(elapsed)
- .. (if unapplied > 0
- then string.format(
- ', but %d change%s failed to apply',
- unapplied,
- unapplied == 1 and "" or "s"
- )
- else "")
- .. ""
+ return timeSinceText(DateTime.now().UnixTimestamp - patchData.timestamp)
end
function ConnectedPage:startChangeInfoTextUpdater()
@@ -190,13 +297,9 @@ function ConnectedPage:startChangeInfoTextUpdater()
-- Start a new updater
self.changeInfoTextUpdater = task.defer(function()
while true do
- if self.state.hoveringChangeInfo then
- self.setChangeInfoText("" .. self:getChangeInfoText() .. "")
- else
- self.setChangeInfoText(self:getChangeInfoText())
- end
+ self.setChangeInfoText(self:getChangeInfoText())
- local elapsed = os.time() - self.props.patchData.timestamp
+ local elapsed = DateTime.now().UnixTimestamp - self.props.patchData.timestamp
local updateInterval = 1
-- Update timestamp text as frequently as currently needed
@@ -221,23 +324,6 @@ function ConnectedPage:stopChangeInfoTextUpdater()
end
function ConnectedPage:init()
- self.changeDrawerMotor = Flipper.SingleMotor.new(0)
- self.changeDrawerHeight = bindingUtil.fromMotor(self.changeDrawerMotor)
-
- self.changeDrawerMotor:onStep(function(value)
- local renderChanges = value > 0.05
-
- self:setState(function(state)
- if state.renderChanges == renderChanges then
- return nil
- end
-
- return {
- renderChanges = renderChanges,
- }
- end)
- end)
-
self:setState({
renderChanges = false,
hoveringChangeInfo = false,
@@ -266,6 +352,10 @@ function ConnectedPage:didUpdate(previousProps)
end
function ConnectedPage:render()
+ local syncWarning = self.props.patchData
+ and self.props.patchData.unapplied
+ and PatchSet.countChanges(self.props.patchData.unapplied) > 0
+
return Theme.with(function(theme)
return Roact.createFragment({
Padding = e("UIPadding", {
@@ -280,9 +370,88 @@ function ConnectedPage:render()
Padding = UDim.new(0, 10),
}),
- Header = e(Header, {
- transparency = self.props.transparency,
- layoutOrder = 1,
+ Heading = e("Frame", {
+ BackgroundTransparency = 1,
+ Size = UDim2.new(1, 0, 0, 32),
+ }, {
+ Header = e(Header, {
+ transparency = self.props.transparency,
+ }),
+
+ ChangeInfo = e("TextButton", {
+ Text = "",
+ Size = UDim2.new(0, 0, 1, 0),
+ AutomaticSize = Enum.AutomaticSize.X,
+ BackgroundColor3 = theme.BorderedContainer.BorderedColor,
+ BackgroundTransparency = if self.state.hoveringChangeInfo then 0.7 else 1,
+ BorderSizePixel = 0,
+ Position = UDim2.new(1, -5, 0.5, 0),
+ AnchorPoint = Vector2.new(1, 0.5),
+ [Roact.Event.MouseEnter] = function()
+ self:setState({
+ hoveringChangeInfo = true,
+ })
+ end,
+ [Roact.Event.MouseLeave] = function()
+ self:setState({
+ hoveringChangeInfo = false,
+ })
+ end,
+ [Roact.Event.Activated] = function()
+ self:setState(function(prevState)
+ prevState = prevState or {}
+ return {
+ renderChanges = not prevState.renderChanges,
+ }
+ end)
+ end,
+ }, {
+ Corner = e("UICorner", {
+ CornerRadius = UDim.new(0, 5),
+ }),
+ Tooltip = e(Tooltip.Trigger, {
+ text = if self.state.renderChanges then "Hide changes" else "View changes",
+ }),
+ Content = e("Frame", {
+ BackgroundTransparency = 1,
+ Size = UDim2.new(0, 0, 1, 0),
+ AutomaticSize = Enum.AutomaticSize.X,
+ }, {
+ Layout = e("UIListLayout", {
+ FillDirection = Enum.FillDirection.Horizontal,
+ HorizontalAlignment = Enum.HorizontalAlignment.Center,
+ VerticalAlignment = Enum.VerticalAlignment.Center,
+ SortOrder = Enum.SortOrder.LayoutOrder,
+ Padding = UDim.new(0, 5),
+ }),
+ Padding = e("UIPadding", {
+ PaddingLeft = UDim.new(0, 5),
+ PaddingRight = UDim.new(0, 5),
+ }),
+ Text = e("TextLabel", {
+ BackgroundTransparency = 1,
+ Text = self.changeInfoText,
+ Font = Enum.Font.Gotham,
+ TextSize = 15,
+ TextColor3 = if syncWarning then theme.Diff.Warning else theme.Header.VersionColor,
+ TextTransparency = self.props.transparency,
+ TextXAlignment = Enum.TextXAlignment.Right,
+ Size = UDim2.new(0, 0, 1, 0),
+ AutomaticSize = Enum.AutomaticSize.X,
+ LayoutOrder = 1,
+ }),
+ Icon = e("ImageLabel", {
+ BackgroundTransparency = 1,
+ Image = if syncWarning
+ then Assets.Images.Icons.SyncWarning
+ else Assets.Images.Icons.SyncSuccess,
+ ImageColor3 = if syncWarning then theme.Diff.Warning else theme.Header.VersionColor,
+ ImageTransparency = self.props.transparency,
+ Size = UDim2.new(0, 24, 0, 24),
+ LayoutOrder = 2,
+ }),
+ }),
+ }),
}),
ConnectionDetails = e(ConnectionDetails, {
@@ -332,86 +501,61 @@ function ConnectedPage:render()
}),
}),
- ChangeInfo = e("TextButton", {
- Text = self.changeInfoText,
- Font = Enum.Font.Gotham,
- TextSize = 14,
- TextWrapped = true,
- RichText = true,
- TextColor3 = theme.Header.VersionColor,
- TextXAlignment = Enum.TextXAlignment.Left,
- TextYAlignment = Enum.TextYAlignment.Top,
- TextTransparency = self.props.transparency,
-
- Size = UDim2.new(1, 0, 0, 28),
+ ChangesViewer = e(StudioPluginGui, {
+ id = "Rojo_ChangesViewer",
+ title = "View changes",
+ active = self.state.renderChanges,
+ isEphemeral = true,
- LayoutOrder = 4,
- BackgroundTransparency = 1,
+ initDockState = Enum.InitialDockState.Float,
+ overridePreviousState = true,
+ floatingSize = Vector2.new(400, 500),
+ minimumSize = Vector2.new(300, 300),
- [Roact.Event.MouseEnter] = function()
- self:setState({
- hoveringChangeInfo = true,
- })
- self.setChangeInfoText("" .. self:getChangeInfoText() .. "")
- end,
+ zIndexBehavior = Enum.ZIndexBehavior.Sibling,
- [Roact.Event.MouseLeave] = function()
+ onClose = function()
self:setState({
- hoveringChangeInfo = false,
+ renderChanges = false,
})
- self.setChangeInfoText(self:getChangeInfoText())
- end,
-
- [Roact.Event.Activated] = function()
- if self.state.renderChanges then
- self.changeDrawerMotor:setGoal(Flipper.Spring.new(0, {
- frequency = 4,
- dampingRatio = 1,
- }))
- else
- self.changeDrawerMotor:setGoal(Flipper.Spring.new(1, {
- frequency = 3,
- dampingRatio = 1,
- }))
- end
end,
}, {
- Tooltip = e(Tooltip.Trigger, {
- text = if self.state.renderChanges then "Hide the changes" else "View the changes",
+ TooltipsProvider = e(Tooltip.Provider, nil, {
+ Tooltips = e(Tooltip.Container, nil),
+ Content = e("Frame", {
+ Size = UDim2.fromScale(1, 1),
+ BackgroundTransparency = 1,
+ }, {
+ Changes = e(ChangesViewer, {
+ transparency = self.props.transparency,
+ rendered = self.state.renderChanges,
+ patchData = self.props.patchData,
+ patchTree = self.props.patchTree,
+ serveSession = self.props.serveSession,
+ showStringDiff = function(oldString: string, newString: string)
+ self:setState({
+ showingStringDiff = true,
+ oldString = oldString,
+ newString = newString,
+ })
+ end,
+ showTableDiff = function(oldTable: { [any]: any? }, newTable: { [any]: any? })
+ self:setState({
+ showingTableDiff = true,
+ oldTable = oldTable,
+ newTable = newTable,
+ })
+ end,
+ onBack = function()
+ self:setState({
+ renderChanges = false,
+ })
+ end,
+ }),
+ }),
}),
}),
- ChangesDrawer = e(ChangesDrawer, {
- rendered = self.state.renderChanges,
- transparency = self.props.transparency,
- patchTree = self.props.patchTree,
- serveSession = self.props.serveSession,
- height = self.changeDrawerHeight,
- layoutOrder = 5,
-
- showStringDiff = function(oldString: string, newString: string)
- self:setState({
- showingStringDiff = true,
- oldString = oldString,
- newString = newString,
- })
- end,
- showTableDiff = function(oldTable: { [any]: any? }, newTable: { [any]: any? })
- self:setState({
- showingTableDiff = true,
- oldTable = oldTable,
- newTable = newTable,
- })
- end,
-
- onClose = function()
- self.changeDrawerMotor:setGoal(Flipper.Spring.new(0, {
- frequency = 4,
- dampingRatio = 1,
- }))
- end,
- }),
-
StringDiff = e(StudioPluginGui, {
id = "Rojo_ConnectedStringDiff",
title = "String diff",
diff --git a/plugin/src/App/Theme.lua b/plugin/src/App/Theme.lua
index f1e9c3971..84a429c6d 100644
--- a/plugin/src/App/Theme.lua
+++ b/plugin/src/App/Theme.lua
@@ -32,9 +32,12 @@ local StudioProvider = Roact.Component:extend("StudioProvider")
function StudioProvider:updateTheme()
local studioTheme = getStudio().Theme
+ local isDark = studioTheme.Name == "Dark"
+
local theme = strict(studioTheme.Name .. "Theme", {
BackgroundColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.MainBackground),
TextColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.MainText),
+ SubTextColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.SubText),
Button = {
Solid = {
-- Solid uses brand theming, not Studio theming.
@@ -139,9 +142,10 @@ function StudioProvider:updateTheme()
BackgroundColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.InputFieldBackground),
},
Diff = {
- Add = studioTheme:GetColor(Enum.StudioStyleGuideColor.DiffTextAdditionBackground),
- Remove = studioTheme:GetColor(Enum.StudioStyleGuideColor.DiffTextDeletionBackground),
- Edit = studioTheme:GetColor(Enum.StudioStyleGuideColor.DiffLineNumSeparatorBackground),
+ -- Studio doesn't have good colors since their diffs use backgrounds, not text
+ Add = if isDark then Color3.fromRGB(143, 227, 154) else Color3.fromRGB(41, 164, 45),
+ Remove = if isDark then Color3.fromRGB(242, 125, 125) else Color3.fromRGB(150, 29, 29),
+ Edit = if isDark then Color3.fromRGB(120, 154, 248) else Color3.fromRGB(0, 70, 160),
Row = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
Warning = studioTheme:GetColor(Enum.StudioStyleGuideColor.WarningText),
},
diff --git a/plugin/src/App/init.lua b/plugin/src/App/init.lua
index a9c20ce95..d80bf0169 100644
--- a/plugin/src/App/init.lua
+++ b/plugin/src/App/init.lua
@@ -457,7 +457,7 @@ function App:startSession()
end)
serveSession:hookPostcommit(function(patch, _instanceMap, unapplied)
- local now = os.time()
+ local now = DateTime.now().UnixTimestamp
local old = self.state.patchData
if PatchSet.isEmpty(patch) then
diff --git a/plugin/src/Assets.lua b/plugin/src/Assets.lua
index dbc60a166..91f82d1ab 100644
--- a/plugin/src/Assets.lua
+++ b/plugin/src/Assets.lua
@@ -25,6 +25,10 @@ local Assets = {
Back = "rbxassetid://6017213752",
Reset = "rbxassetid://10142422327",
Expand = "rbxassetid://12045401097",
+ Checkmark = "rbxassetid://16571012729",
+ Exclamation = "rbxassetid://16571172190",
+ SyncSuccess = "rbxassetid://16565035221",
+ SyncWarning = "rbxassetid://16565325171",
},
Diff = {
Add = "rbxassetid://10434145835",
diff --git a/plugin/src/PatchSet.lua b/plugin/src/PatchSet.lua
index abbac0cbb..064def36c 100644
--- a/plugin/src/PatchSet.lua
+++ b/plugin/src/PatchSet.lua
@@ -211,9 +211,11 @@ end
function PatchSet.countChanges(patch)
local count = 0
- for _ in patch.added do
- -- Adding an instance is 1 change
- count += 1
+ for _, add in patch.added do
+ -- Adding an instance is 1 change per property
+ for _ in add.Properties do
+ count += 1
+ end
end
for _ in patch.removed do
-- Removing an instance is 1 change
diff --git a/plugin/src/PatchTree.lua b/plugin/src/PatchTree.lua
index 6d871001d..c1c5f91ca 100644
--- a/plugin/src/PatchTree.lua
+++ b/plugin/src/PatchTree.lua
@@ -79,6 +79,15 @@ function Tree.new()
return setmetatable(tree, Tree)
end
+-- Iterates over all nodes and counts them up
+function Tree:getCount()
+ local count = 0
+ self:forEach(function()
+ count += 1
+ end)
+ return count
+end
+
-- Iterates over all sub-nodes, depth first
-- node is where to start from, defaults to root
-- depth is used for recursion but can be used to set the starting depth
@@ -219,42 +228,14 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
tree:buildAncestryNodes(previousId, ancestryIds, patch, instanceMap)
-- Gather detail text
- local changeList, hint = nil, nil
+ local changeList, changeInfo = nil, nil
if next(change.changedProperties) or change.changedName then
changeList = {}
- local hintBuffer, hintBufferSize, hintOverflow = table.create(3), 0, 0
local changeIndex = 0
local function addProp(prop: string, current: any?, incoming: any?, metadata: any?)
changeIndex += 1
changeList[changeIndex] = { prop, current, incoming, metadata }
-
- if hintBufferSize < 3 then
- hintBufferSize += 1
- hintBuffer[hintBufferSize] = prop
- return
- end
-
- -- We only want to have 3 hints
- -- to keep it deterministic, we sort them alphabetically
-
- -- Either this prop overflows, or it makes another one move to overflow
- hintOverflow += 1
-
- -- Shortcut for the common case
- if hintBuffer[3] <= prop then
- -- This prop is below the last hint, no need to insert
- return
- end
-
- -- Find the first available spot
- for i, hintItem in hintBuffer do
- if prop < hintItem then
- -- This prop is before the currently selected hint,
- -- so take its place and then continue to find a spot for the old hint
- hintBuffer[i], prop = prop, hintBuffer[i]
- end
- end
end
-- Gather the changes
@@ -274,8 +255,9 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
)
end
- -- Finalize detail values
- hint = table.concat(hintBuffer, ", ") .. (if hintOverflow == 0 then "" else ", " .. hintOverflow .. " more")
+ changeInfo = {
+ edits = changeIndex,
+ }
-- Sort changes and add header
table.sort(changeList, function(a, b)
@@ -291,7 +273,7 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
className = instance.ClassName,
name = instance.Name,
instance = instance,
- hint = hint,
+ changeInfo = changeInfo,
changeList = changeList,
})
end
@@ -376,42 +358,14 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
tree:buildAncestryNodes(previousId, ancestryIds, patch, instanceMap)
-- Gather detail text
- local changeList, hint = nil, nil
+ local changeList, changeInfo = nil, nil
if next(change.Properties) then
changeList = {}
- local hintBuffer, hintBufferSize, hintOverflow = table.create(3), 0, 0
local changeIndex = 0
local function addProp(prop: string, incoming: any)
changeIndex += 1
changeList[changeIndex] = { prop, "N/A", incoming }
-
- if hintBufferSize < 3 then
- hintBufferSize += 1
- hintBuffer[hintBufferSize] = prop
- return
- end
-
- -- We only want to have 3 hints
- -- to keep it deterministic, we sort them alphabetically
-
- -- Either this prop overflows, or it makes another one move to overflow
- hintOverflow += 1
-
- -- Shortcut for the common case
- if hintBuffer[3] <= prop then
- -- This prop is below the last hint, no need to insert
- return
- end
-
- -- Find the first available spot
- for i, hintItem in hintBuffer do
- if prop < hintItem then
- -- This prop is before the currently selected hint,
- -- so take its place and then continue to find a spot for the old hint
- hintBuffer[i], prop = prop, hintBuffer[i]
- end
- end
end
for prop, incoming in change.Properties do
@@ -419,8 +373,9 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
addProp(prop, if success then incomingValue else select(2, next(incoming)))
end
- -- Finalize detail values
- hint = table.concat(hintBuffer, ", ") .. (if hintOverflow == 0 then "" else ", " .. hintOverflow .. " more")
+ changeInfo = {
+ edits = changeIndex,
+ }
-- Sort changes and add header
table.sort(changeList, function(a, b)
@@ -435,7 +390,7 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
patchType = "Add",
className = change.ClassName,
name = change.Name,
- hint = hint,
+ changeInfo = changeInfo,
changeList = changeList,
instance = instanceMap.fromIds[id],
})
@@ -473,6 +428,8 @@ function PatchTree.updateMetadata(tree, patch, instanceMap, unappliedPatch)
if not node.changeList then
continue
end
+
+ local warnings = 0
for _, change in node.changeList do
local property = change[1]
local propertyFailedToApply = if property == "Name"
@@ -483,6 +440,8 @@ function PatchTree.updateMetadata(tree, patch, instanceMap, unappliedPatch)
-- This change didn't fail, no need to mark
continue
end
+
+ warnings += 1
if change[4] == nil then
change[4] = { isWarning = true }
else
@@ -490,6 +449,11 @@ function PatchTree.updateMetadata(tree, patch, instanceMap, unappliedPatch)
end
Log.trace(" Marked property as warning: {}.{}", node.name, property)
end
+
+ node.changeInfo = {
+ edits = (node.changeInfo.edits or (#node.changeList - 1)) - warnings,
+ failed = if warnings > 0 then warnings else nil,
+ }
end
for failedAdditionId in unappliedPatch.added do
local node = tree:getNode(failedAdditionId)
@@ -503,6 +467,7 @@ function PatchTree.updateMetadata(tree, patch, instanceMap, unappliedPatch)
if not node.changeList then
continue
end
+
for _, change in node.changeList do
-- Failed addition means that all properties failed to be added
if change[4] == nil then
@@ -512,6 +477,10 @@ function PatchTree.updateMetadata(tree, patch, instanceMap, unappliedPatch)
end
Log.trace(" Marked property as warning: {}.{}", node.name, change[1])
end
+
+ node.changeInfo = {
+ failed = node.changeInfo.edits or (#node.changeList - 1),
+ }
end
for _, failedRemovalIdOrInstance in unappliedPatch.removed do
local failedRemovalId = if Types.RbxId(failedRemovalIdOrInstance)