mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-21 20:21:38 +00:00
Merge branch 'master' of https://github.com/boatbomber/rojo into headless-api
This commit is contained in:
commit
1220b5c0d6
57 changed files with 1332 additions and 613 deletions
23
.github/workflows/changelog.yml
vendored
Normal file
23
.github/workflows/changelog.yml
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
name: Changelog Check
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [assigned, opened, synchronize, reopened, labeled, unlabeled]
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Check Actions
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Changelog check
|
||||||
|
uses: Zomzog/changelog-checker@v1.3.0
|
||||||
|
with:
|
||||||
|
fileName: CHANGELOG.md
|
||||||
|
noChangelogLabel: skip changelog
|
||||||
|
checkNotification: Simple
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
|
|
@ -67,3 +67,4 @@ jobs:
|
||||||
|
|
||||||
- name: Clippy
|
- name: Clippy
|
||||||
run: cargo clippy
|
run: cargo clippy
|
||||||
|
|
||||||
|
|
|
||||||
22
CHANGELOG.md
22
CHANGELOG.md
|
|
@ -8,6 +8,16 @@
|
||||||
* Skip confirming patches that contain only a datamodel name change. ([#688])
|
* Skip confirming patches that contain only a datamodel name change. ([#688])
|
||||||
* Added sync reminder notification. ([#689])
|
* Added sync reminder notification. ([#689])
|
||||||
* Added protection against syncing a model to a place. ([#691])
|
* Added protection against syncing a model to a place. ([#691])
|
||||||
|
* Select Instances from diff tree view ([#709])
|
||||||
|
* Fix Rojo breaking when users undo/redo in Studio ([#708])
|
||||||
|
* Improved sync info text on Connected page. ([#692])
|
||||||
|
* Fix patch visualizer breaking when instances are removed during sync ([#713])
|
||||||
|
* Patch visualizer now indicates what changes failed to apply. ([#717])
|
||||||
|
* Add buttons for navigation on the Connected page ([#722])
|
||||||
|
* Improve tooltip behavior ([#723])
|
||||||
|
* Better settings controls ([#725])
|
||||||
|
* Rework patch visualizer with many fixes and improvements ([#726])
|
||||||
|
* Added support for syncing in `.toml` files ([#633])
|
||||||
|
|
||||||
[#631]: https://github.com/rojo-rbx/rojo/pull/631
|
[#631]: https://github.com/rojo-rbx/rojo/pull/631
|
||||||
[#668]: https://github.com/rojo-rbx/rojo/pull/668
|
[#668]: https://github.com/rojo-rbx/rojo/pull/668
|
||||||
|
|
@ -16,6 +26,16 @@
|
||||||
[#688]: https://github.com/rojo-rbx/rojo/pull/688
|
[#688]: https://github.com/rojo-rbx/rojo/pull/688
|
||||||
[#689]: https://github.com/rojo-rbx/rojo/pull/689
|
[#689]: https://github.com/rojo-rbx/rojo/pull/689
|
||||||
[#691]: https://github.com/rojo-rbx/rojo/pull/691
|
[#691]: https://github.com/rojo-rbx/rojo/pull/691
|
||||||
|
[#709]: https://github.com/rojo-rbx/rojo/pull/709
|
||||||
|
[#708]: https://github.com/rojo-rbx/rojo/pull/708
|
||||||
|
[#692]: https://github.com/rojo-rbx/rojo/pull/692
|
||||||
|
[#713]: https://github.com/rojo-rbx/rojo/pull/713
|
||||||
|
[#717]: https://github.com/rojo-rbx/rojo/pull/717
|
||||||
|
[#722]: https://github.com/rojo-rbx/rojo/pull/722
|
||||||
|
[#723]: https://github.com/rojo-rbx/rojo/pull/723
|
||||||
|
[#725]: https://github.com/rojo-rbx/rojo/pull/725
|
||||||
|
[#726]: https://github.com/rojo-rbx/rojo/pull/726
|
||||||
|
[#633]: https://github.com/rojo-rbx/rojo/pull/633
|
||||||
|
|
||||||
## [7.3.0] - April 22, 2023
|
## [7.3.0] - April 22, 2023
|
||||||
* Added `$attributes` to project format. ([#574])
|
* Added `$attributes` to project format. ([#574])
|
||||||
|
|
@ -581,4 +601,4 @@ This is a general maintenance release for the Rojo 0.5.x release series.
|
||||||
* More robust syncing with a new reconciler
|
* More robust syncing with a new reconciler
|
||||||
|
|
||||||
## [0.1.0](https://github.com/rojo-rbx/rojo/releases/tag/v0.1.0) (November 29, 2017)
|
## [0.1.0](https://github.com/rojo-rbx/rojo/releases/tag/v0.1.0) (November 29, 2017)
|
||||||
* Initial release, functionally very similar to [rbxfs](https://github.com/LPGhatguy/rbxfs)
|
* Initial release, functionally very similar to [rbxfs](https://github.com/LPGhatguy/rbxfs)
|
||||||
|
|
|
||||||
9
Cargo.lock
generated
9
Cargo.lock
generated
|
|
@ -1308,9 +1308,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl"
|
name = "openssl"
|
||||||
version = "0.10.51"
|
version = "0.10.55"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "97ea2d98598bf9ada7ea6ee8a30fb74f9156b63bbe495d64ec2b87c269d2dda3"
|
checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"cfg-if 1.0.0",
|
"cfg-if 1.0.0",
|
||||||
|
|
@ -1349,9 +1349,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl-sys"
|
name = "openssl-sys"
|
||||||
version = "0.9.86"
|
version = "0.9.90"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "992bac49bdbab4423199c654a5515bd2a6c6a23bf03f2dd3bdb7e5ae6259bc69"
|
checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"libc",
|
"libc",
|
||||||
|
|
@ -1922,6 +1922,7 @@ dependencies = [
|
||||||
"termcolor",
|
"termcolor",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"toml",
|
||||||
"tracy-client 0.13.2",
|
"tracy-client 0.13.2",
|
||||||
"uuid",
|
"uuid",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,7 @@ ritz = "0.1.0"
|
||||||
roblox_install = "1.0.0"
|
roblox_install = "1.0.0"
|
||||||
serde = { version = "1.0.130", features = ["derive", "rc"] }
|
serde = { version = "1.0.130", features = ["derive", "rc"] }
|
||||||
serde_json = "1.0.68"
|
serde_json = "1.0.68"
|
||||||
|
toml = "0.5.9"
|
||||||
termcolor = "1.1.2"
|
termcolor = "1.1.2"
|
||||||
thiserror = "1.0.30"
|
thiserror = "1.0.30"
|
||||||
tokio = { version = "1.12.0", features = ["rt", "rt-multi-thread"] }
|
tokio = { version = "1.12.0", features = ["rt", "rt-multi-thread"] }
|
||||||
|
|
|
||||||
|
|
@ -11,13 +11,6 @@ local validateApiInfo = Types.ifEnabled(Types.ApiInfoResponse)
|
||||||
local validateApiRead = Types.ifEnabled(Types.ApiReadResponse)
|
local validateApiRead = Types.ifEnabled(Types.ApiReadResponse)
|
||||||
local validateApiSubscribe = Types.ifEnabled(Types.ApiSubscribeResponse)
|
local validateApiSubscribe = Types.ifEnabled(Types.ApiSubscribeResponse)
|
||||||
|
|
||||||
--[[
|
|
||||||
Returns a promise that will never resolve nor reject.
|
|
||||||
]]
|
|
||||||
local function hangingPromise()
|
|
||||||
return Promise.new(function() end)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function rejectFailedRequests(response)
|
local function rejectFailedRequests(response)
|
||||||
if response.code >= 400 then
|
if response.code >= 400 then
|
||||||
local message = string.format("HTTP %s:\n%s", tostring(response.code), response.body)
|
local message = string.format("HTTP %s:\n%s", tostring(response.code), response.body)
|
||||||
|
|
@ -212,12 +205,8 @@ function ApiContext:retrieveMessages()
|
||||||
local function sendRequest()
|
local function sendRequest()
|
||||||
local request = Http.get(url)
|
local request = Http.get(url)
|
||||||
:catch(function(err)
|
:catch(function(err)
|
||||||
if err.type == Http.Error.Kind.Timeout then
|
if err.type == Http.Error.Kind.Timeout and self.__connected then
|
||||||
if self.__connected then
|
return sendRequest()
|
||||||
return sendRequest()
|
|
||||||
else
|
|
||||||
return hangingPromise()
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return Promise.reject(err)
|
return Promise.reject(err)
|
||||||
|
|
|
||||||
|
|
@ -51,10 +51,15 @@ function Checkbox:render()
|
||||||
ZIndex = self.props.zIndex,
|
ZIndex = self.props.zIndex,
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
|
|
||||||
[Roact.Event.Activated] = self.props.onClick,
|
[Roact.Event.Activated] = function()
|
||||||
|
if self.props.locked then return end
|
||||||
|
self.props.onClick()
|
||||||
|
end,
|
||||||
}, {
|
}, {
|
||||||
StateTip = e(Tooltip.Trigger, {
|
StateTip = e(Tooltip.Trigger, {
|
||||||
text = if self.props.active then "Enabled" else "Disabled",
|
text =
|
||||||
|
(if self.props.locked then "[LOCKED] " else "")
|
||||||
|
.. (if self.props.active then "Enabled" else "Disabled"),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Active = e(SlicedImage, {
|
Active = e(SlicedImage, {
|
||||||
|
|
@ -65,7 +70,7 @@ function Checkbox:render()
|
||||||
zIndex = 2,
|
zIndex = 2,
|
||||||
}, {
|
}, {
|
||||||
Icon = e("ImageLabel", {
|
Icon = e("ImageLabel", {
|
||||||
Image = Assets.Images.Checkbox.Active,
|
Image = if self.props.locked then Assets.Images.Checkbox.Locked else Assets.Images.Checkbox.Active,
|
||||||
ImageColor3 = theme.Active.IconColor,
|
ImageColor3 = theme.Active.IconColor,
|
||||||
ImageTransparency = activeTransparency,
|
ImageTransparency = activeTransparency,
|
||||||
|
|
||||||
|
|
@ -84,7 +89,7 @@ function Checkbox:render()
|
||||||
size = UDim2.new(1, 0, 1, 0),
|
size = UDim2.new(1, 0, 1, 0),
|
||||||
}, {
|
}, {
|
||||||
Icon = e("ImageLabel", {
|
Icon = e("ImageLabel", {
|
||||||
Image = Assets.Images.Checkbox.Inactive,
|
Image = if self.props.locked then Assets.Images.Checkbox.Locked else Assets.Images.Checkbox.Inactive,
|
||||||
ImageColor3 = theme.Inactive.IconColor,
|
ImageColor3 = theme.Inactive.IconColor,
|
||||||
ImageTransparency = self.props.transparency,
|
ImageTransparency = self.props.transparency,
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,13 @@ function Dropdown:init()
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
function Dropdown:didUpdate()
|
function Dropdown:didUpdate(prevProps)
|
||||||
|
if self.props.locked and not prevProps.locked then
|
||||||
|
self:setState({
|
||||||
|
open = false,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
self.openMotor:setGoal(
|
self.openMotor:setGoal(
|
||||||
Flipper.Spring.new(self.state.open and 1 or 0, {
|
Flipper.Spring.new(self.state.open and 1 or 0, {
|
||||||
frequency = 6,
|
frequency = 6,
|
||||||
|
|
@ -68,6 +74,7 @@ function Dropdown:render()
|
||||||
Font = Enum.Font.GothamMedium,
|
Font = Enum.Font.GothamMedium,
|
||||||
|
|
||||||
[Roact.Event.Activated] = function()
|
[Roact.Event.Activated] = function()
|
||||||
|
if self.props.locked then return end
|
||||||
self:setState({
|
self:setState({
|
||||||
open = false,
|
open = false,
|
||||||
})
|
})
|
||||||
|
|
@ -89,6 +96,7 @@ function Dropdown:render()
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
|
|
||||||
[Roact.Event.Activated] = function()
|
[Roact.Event.Activated] = function()
|
||||||
|
if self.props.locked then return end
|
||||||
self:setState({
|
self:setState({
|
||||||
open = not self.state.open,
|
open = not self.state.open,
|
||||||
})
|
})
|
||||||
|
|
@ -101,7 +109,7 @@ function Dropdown:render()
|
||||||
size = UDim2.new(1, 0, 1, 0),
|
size = UDim2.new(1, 0, 1, 0),
|
||||||
}, {
|
}, {
|
||||||
DropArrow = e("ImageLabel", {
|
DropArrow = e("ImageLabel", {
|
||||||
Image = Assets.Images.Dropdown.Arrow,
|
Image = if self.props.locked then Assets.Images.Dropdown.Locked else Assets.Images.Dropdown.Arrow,
|
||||||
ImageColor3 = self.openBinding:map(function(a)
|
ImageColor3 = self.openBinding:map(function(a)
|
||||||
return theme.Closed.IconColor:Lerp(theme.Open.IconColor, a)
|
return theme.Closed.IconColor:Lerp(theme.Open.IconColor, a)
|
||||||
end),
|
end),
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ local Theme = require(Plugin.App.Theme)
|
||||||
local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame)
|
local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame)
|
||||||
local DisplayValue = require(script.Parent.DisplayValue)
|
local DisplayValue = require(script.Parent.DisplayValue)
|
||||||
|
|
||||||
|
local EMPTY_TABLE = {}
|
||||||
|
|
||||||
local e = Roact.createElement
|
local e = Roact.createElement
|
||||||
|
|
||||||
local ChangeList = Roact.Component:extend("ChangeList")
|
local ChangeList = Roact.Component:extend("ChangeList")
|
||||||
|
|
@ -26,8 +28,6 @@ function ChangeList:render()
|
||||||
return 0.93 + (0.07 * t)
|
return 0.93 + (0.07 * t)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
local columnVisibility = props.columnVisibility
|
|
||||||
|
|
||||||
local rows = {}
|
local rows = {}
|
||||||
local pad = {
|
local pad = {
|
||||||
PaddingLeft = UDim.new(0, 5),
|
PaddingLeft = UDim.new(0, 5),
|
||||||
|
|
@ -48,7 +48,6 @@ function ChangeList:render()
|
||||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||||
}),
|
}),
|
||||||
A = e("TextLabel", {
|
A = e("TextLabel", {
|
||||||
Visible = columnVisibility[1],
|
|
||||||
Text = tostring(changes[1][1]),
|
Text = tostring(changes[1][1]),
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Font = Enum.Font.GothamBold,
|
Font = Enum.Font.GothamBold,
|
||||||
|
|
@ -61,7 +60,6 @@ function ChangeList:render()
|
||||||
LayoutOrder = 1,
|
LayoutOrder = 1,
|
||||||
}),
|
}),
|
||||||
B = e("TextLabel", {
|
B = e("TextLabel", {
|
||||||
Visible = columnVisibility[2],
|
|
||||||
Text = tostring(changes[1][2]),
|
Text = tostring(changes[1][2]),
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Font = Enum.Font.GothamBold,
|
Font = Enum.Font.GothamBold,
|
||||||
|
|
@ -74,7 +72,6 @@ function ChangeList:render()
|
||||||
LayoutOrder = 2,
|
LayoutOrder = 2,
|
||||||
}),
|
}),
|
||||||
C = e("TextLabel", {
|
C = e("TextLabel", {
|
||||||
Visible = columnVisibility[3],
|
|
||||||
Text = tostring(changes[1][3]),
|
Text = tostring(changes[1][3]),
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Font = Enum.Font.GothamBold,
|
Font = Enum.Font.GothamBold,
|
||||||
|
|
@ -93,6 +90,9 @@ function ChangeList:render()
|
||||||
continue -- Skip headers, already handled above
|
continue -- Skip headers, already handled above
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local metadata = values[4] or EMPTY_TABLE
|
||||||
|
local isWarning = metadata.isWarning
|
||||||
|
|
||||||
rows[row] = e("Frame", {
|
rows[row] = e("Frame", {
|
||||||
Size = UDim2.new(1, 0, 0, 30),
|
Size = UDim2.new(1, 0, 0, 30),
|
||||||
BackgroundTransparency = row % 2 ~= 0 and rowTransparency or 1,
|
BackgroundTransparency = row % 2 ~= 0 and rowTransparency or 1,
|
||||||
|
|
@ -108,12 +108,11 @@ function ChangeList:render()
|
||||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||||
}),
|
}),
|
||||||
A = e("TextLabel", {
|
A = e("TextLabel", {
|
||||||
Visible = columnVisibility[1],
|
Text = (if isWarning then "⚠ " else "") .. tostring(values[1]),
|
||||||
Text = tostring(values[1]),
|
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Font = Enum.Font.GothamMedium,
|
Font = Enum.Font.GothamMedium,
|
||||||
TextSize = 14,
|
TextSize = 14,
|
||||||
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
TextColor3 = if isWarning then theme.Diff.Warning else theme.Settings.Setting.DescriptionColor,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
TextTransparency = props.transparency,
|
TextTransparency = props.transparency,
|
||||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
|
|
@ -123,7 +122,6 @@ function ChangeList:render()
|
||||||
B = e(
|
B = e(
|
||||||
"Frame",
|
"Frame",
|
||||||
{
|
{
|
||||||
Visible = columnVisibility[2],
|
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Size = UDim2.new(0.35, 0, 1, 0),
|
Size = UDim2.new(0.35, 0, 1, 0),
|
||||||
LayoutOrder = 2,
|
LayoutOrder = 2,
|
||||||
|
|
@ -131,12 +129,12 @@ function ChangeList:render()
|
||||||
e(DisplayValue, {
|
e(DisplayValue, {
|
||||||
value = values[2],
|
value = values[2],
|
||||||
transparency = props.transparency,
|
transparency = props.transparency,
|
||||||
|
textColor = if isWarning then theme.Diff.Warning else theme.Settings.Setting.DescriptionColor,
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
C = e(
|
C = e(
|
||||||
"Frame",
|
"Frame",
|
||||||
{
|
{
|
||||||
Visible = columnVisibility[3],
|
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Size = UDim2.new(0.35, 0, 1, 0),
|
Size = UDim2.new(0.35, 0, 1, 0),
|
||||||
LayoutOrder = 3,
|
LayoutOrder = 3,
|
||||||
|
|
@ -144,6 +142,7 @@ function ChangeList:render()
|
||||||
e(DisplayValue, {
|
e(DisplayValue, {
|
||||||
value = values[3],
|
value = values[3],
|
||||||
transparency = props.transparency,
|
transparency = props.transparency,
|
||||||
|
textColor = if isWarning then theme.Diff.Warning else theme.Settings.Setting.DescriptionColor,
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ local function DisplayValue(props)
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Font = Enum.Font.GothamMedium,
|
Font = Enum.Font.GothamMedium,
|
||||||
TextSize = 14,
|
TextSize = 14,
|
||||||
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
TextColor3 = props.textColor,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
TextTransparency = props.transparency,
|
TextTransparency = props.transparency,
|
||||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
|
|
@ -78,7 +78,7 @@ local function DisplayValue(props)
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Font = Enum.Font.GothamMedium,
|
Font = Enum.Font.GothamMedium,
|
||||||
TextSize = 14,
|
TextSize = 14,
|
||||||
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
TextColor3 = props.textColor,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
TextTransparency = props.transparency,
|
TextTransparency = props.transparency,
|
||||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
|
|
@ -95,7 +95,7 @@ local function DisplayValue(props)
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Font = Enum.Font.GothamMedium,
|
Font = Enum.Font.GothamMedium,
|
||||||
TextSize = 14,
|
TextSize = 14,
|
||||||
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
TextColor3 = props.textColor,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
TextTransparency = props.transparency,
|
TextTransparency = props.transparency,
|
||||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
local SelectionService = game:GetService("Selection")
|
||||||
local StudioService = game:GetService("StudioService")
|
local StudioService = game:GetService("StudioService")
|
||||||
|
|
||||||
local Rojo = script:FindFirstAncestor("Rojo")
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
|
|
@ -14,6 +15,7 @@ local bindingUtil = require(Plugin.App.bindingUtil)
|
||||||
local e = Roact.createElement
|
local e = Roact.createElement
|
||||||
|
|
||||||
local ChangeList = require(script.Parent.ChangeList)
|
local ChangeList = require(script.Parent.ChangeList)
|
||||||
|
local Tooltip = require(script.Parent.Parent.Tooltip)
|
||||||
|
|
||||||
local Expansion = Roact.Component:extend("Expansion")
|
local Expansion = Roact.Component:extend("Expansion")
|
||||||
|
|
||||||
|
|
@ -32,7 +34,6 @@ function Expansion:render()
|
||||||
ChangeList = e(ChangeList, {
|
ChangeList = e(ChangeList, {
|
||||||
changes = props.changeList,
|
changes = props.changeList,
|
||||||
transparency = props.transparency,
|
transparency = props.transparency,
|
||||||
columnVisibility = props.columnVisibility,
|
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
@ -69,6 +70,22 @@ function DomLabel:init()
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function DomLabel:didUpdate(prevProps)
|
||||||
|
if
|
||||||
|
prevProps.instance ~= self.props.instance
|
||||||
|
or prevProps.patchType ~= self.props.patchType
|
||||||
|
or prevProps.name ~= self.props.name
|
||||||
|
or prevProps.changeList ~= self.props.changeList
|
||||||
|
then
|
||||||
|
-- Close the expansion when the domlabel is changed to a different thing
|
||||||
|
self.expanded = false
|
||||||
|
self.motor:setGoal(Flipper.Spring.new(30, {
|
||||||
|
frequency = 5,
|
||||||
|
dampingRatio = 1,
|
||||||
|
}))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
function DomLabel:render()
|
function DomLabel:render()
|
||||||
local props = self.props
|
local props = self.props
|
||||||
|
|
||||||
|
|
@ -106,28 +123,53 @@ function DomLabel:render()
|
||||||
PaddingLeft = UDim.new(0, 10),
|
PaddingLeft = UDim.new(0, 10),
|
||||||
PaddingRight = UDim.new(0, 10),
|
PaddingRight = UDim.new(0, 10),
|
||||||
}),
|
}),
|
||||||
ExpandButton = if props.changeList
|
Button = e("TextButton", {
|
||||||
then e("TextButton", {
|
BackgroundTransparency = 1,
|
||||||
BackgroundTransparency = 1,
|
Text = "",
|
||||||
Text = "",
|
Size = UDim2.new(1, 0, 1, 0),
|
||||||
Size = UDim2.new(1, 0, 1, 0),
|
[Roact.Event.Activated] = function(_rbx: Instance, _input: InputObject, clickCount: number)
|
||||||
[Roact.Event.Activated] = function()
|
if clickCount == 1 then
|
||||||
self.expanded = not self.expanded
|
-- Double click opens the instance in explorer
|
||||||
local goalHeight = 30 + (if self.expanded then math.clamp(#self.props.changeList * 30, 30, 30 * 6) else 0)
|
self.lastDoubleClickTime = os.clock()
|
||||||
self.motor:setGoal(Flipper.Spring.new(goalHeight, {
|
if props.instance then
|
||||||
frequency = 5,
|
SelectionService:Set({ props.instance })
|
||||||
dampingRatio = 1,
|
end
|
||||||
}))
|
elseif clickCount == 0 then
|
||||||
end,
|
-- Single click expands the changes
|
||||||
})
|
task.wait(0.25)
|
||||||
else nil,
|
if os.clock() - (self.lastDoubleClickTime or 0) <= 0.25 then
|
||||||
|
-- This is a double click, so don't expand
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
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)
|
||||||
|
self.motor:setGoal(Flipper.Spring.new(goalHeight, {
|
||||||
|
frequency = 5,
|
||||||
|
dampingRatio = 1,
|
||||||
|
}))
|
||||||
|
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
|
Expansion = if props.changeList
|
||||||
then e(Expansion, {
|
then e(Expansion, {
|
||||||
rendered = self.state.renderExpansion,
|
rendered = self.state.renderExpansion,
|
||||||
indent = indent,
|
indent = indent,
|
||||||
transparency = props.transparency,
|
transparency = props.transparency,
|
||||||
changeList = props.changeList,
|
changeList = props.changeList,
|
||||||
columnVisibility = props.columnVisibility,
|
|
||||||
})
|
})
|
||||||
else nil,
|
else nil,
|
||||||
DiffIcon = if props.patchType
|
DiffIcon = if props.patchType
|
||||||
|
|
@ -152,7 +194,7 @@ function DomLabel:render()
|
||||||
AnchorPoint = Vector2.new(0, 0.5),
|
AnchorPoint = Vector2.new(0, 0.5),
|
||||||
}),
|
}),
|
||||||
InstanceName = e("TextLabel", {
|
InstanceName = e("TextLabel", {
|
||||||
Text = props.name .. (props.hint and string.format(
|
Text = (if props.isWarning then "⚠ " else "") .. props.name .. (props.hint and string.format(
|
||||||
' <font color="#%s">%s</font>',
|
' <font color="#%s">%s</font>',
|
||||||
theme.AddressEntry.PlaceholderColor:ToHex(),
|
theme.AddressEntry.PlaceholderColor:ToHex(),
|
||||||
props.hint
|
props.hint
|
||||||
|
|
@ -161,7 +203,7 @@ function DomLabel:render()
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Font = Enum.Font.GothamMedium,
|
Font = Enum.Font.GothamMedium,
|
||||||
TextSize = 14,
|
TextSize = 14,
|
||||||
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
TextColor3 = if props.isWarning then theme.Diff.Warning else theme.Settings.Setting.DescriptionColor,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
TextTransparency = props.transparency,
|
TextTransparency = props.transparency,
|
||||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
|
|
|
||||||
|
|
@ -1,143 +1,18 @@
|
||||||
local HttpService = game:GetService("HttpService")
|
|
||||||
|
|
||||||
local Rojo = script:FindFirstAncestor("Rojo")
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
local Plugin = Rojo.Plugin
|
local Plugin = Rojo.Plugin
|
||||||
local Packages = Rojo.Packages
|
local Packages = Rojo.Packages
|
||||||
|
|
||||||
local Roact = require(Packages.Roact)
|
local Roact = require(Packages.Roact)
|
||||||
local Log = require(Packages.Log)
|
|
||||||
|
|
||||||
|
local PatchTree = require(Plugin.PatchTree)
|
||||||
local PatchSet = require(Plugin.PatchSet)
|
local PatchSet = require(Plugin.PatchSet)
|
||||||
local decodeValue = require(Plugin.Reconciler.decodeValue)
|
|
||||||
local getProperty = require(Plugin.Reconciler.getProperty)
|
|
||||||
|
|
||||||
|
local Theme = require(Plugin.App.Theme)
|
||||||
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
||||||
local VirtualScroller = require(Plugin.App.Components.VirtualScroller)
|
local VirtualScroller = require(Plugin.App.Components.VirtualScroller)
|
||||||
|
|
||||||
local e = Roact.createElement
|
local e = Roact.createElement
|
||||||
|
|
||||||
local function alphabeticalNext(t, state)
|
|
||||||
-- Equivalent of the next function, but returns the keys in the alphabetic
|
|
||||||
-- order of node names. We use a temporary ordered key table that is stored in the
|
|
||||||
-- table being iterated.
|
|
||||||
|
|
||||||
local key = nil
|
|
||||||
if state == nil then
|
|
||||||
-- First iteration, generate the index
|
|
||||||
local orderedIndex, i = table.create(5), 0
|
|
||||||
for k in t do
|
|
||||||
i += 1
|
|
||||||
orderedIndex[i] = k
|
|
||||||
end
|
|
||||||
table.sort(orderedIndex, function(a, b)
|
|
||||||
local nodeA, nodeB = t[a], t[b]
|
|
||||||
return (nodeA.name or "") < (nodeB.name or "")
|
|
||||||
end)
|
|
||||||
|
|
||||||
t.__orderedIndex = orderedIndex
|
|
||||||
key = orderedIndex[1]
|
|
||||||
else
|
|
||||||
-- Fetch the next value
|
|
||||||
for i, orderedState in t.__orderedIndex do
|
|
||||||
if orderedState == state then
|
|
||||||
key = t.__orderedIndex[i + 1]
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if key then
|
|
||||||
return key, t[key]
|
|
||||||
end
|
|
||||||
|
|
||||||
-- No more value to return, cleanup
|
|
||||||
t.__orderedIndex = nil
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local function alphabeticalPairs(t)
|
|
||||||
-- Equivalent of the pairs() iterator, but sorted
|
|
||||||
return alphabeticalNext, t, nil
|
|
||||||
end
|
|
||||||
|
|
||||||
local function Tree()
|
|
||||||
local tree = {
|
|
||||||
idToNode = {},
|
|
||||||
ROOT = {
|
|
||||||
className = "DataModel",
|
|
||||||
name = "ROOT",
|
|
||||||
children = {},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
-- Add ROOT to idToNode or it won't be found by getNode since that searches *within* ROOT
|
|
||||||
tree.idToNode["ROOT"] = tree.ROOT
|
|
||||||
|
|
||||||
function tree:getNode(id, target)
|
|
||||||
if self.idToNode[id] then
|
|
||||||
return self.idToNode[id]
|
|
||||||
end
|
|
||||||
|
|
||||||
for nodeId, node in target or tree.ROOT.children do
|
|
||||||
if nodeId == id then
|
|
||||||
self.idToNode[id] = node
|
|
||||||
return node
|
|
||||||
end
|
|
||||||
local descendant = self:getNode(id, node.children)
|
|
||||||
if descendant then
|
|
||||||
return descendant
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
function tree:addNode(parent, props)
|
|
||||||
parent = parent or "ROOT"
|
|
||||||
|
|
||||||
local node = self:getNode(props.id)
|
|
||||||
if node then
|
|
||||||
for k, v in props do
|
|
||||||
node[k] = v
|
|
||||||
end
|
|
||||||
return node
|
|
||||||
end
|
|
||||||
|
|
||||||
node = table.clone(props)
|
|
||||||
node.children = {}
|
|
||||||
|
|
||||||
local parentNode = self:getNode(parent)
|
|
||||||
if not parentNode then
|
|
||||||
Log.warn("Failed to create node since parent doesnt exist: {}, {}", parent, props)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
parentNode.children[node.id] = node
|
|
||||||
self.idToNode[node.id] = node
|
|
||||||
|
|
||||||
return node
|
|
||||||
end
|
|
||||||
|
|
||||||
function tree:buildAncestryNodes(ancestry, patch, instanceMap)
|
|
||||||
-- Build nodes for ancestry by going up the tree
|
|
||||||
local previousId = "ROOT"
|
|
||||||
for _, ancestorId in ancestry do
|
|
||||||
local value = instanceMap.fromIds[ancestorId] or patch.added[ancestorId]
|
|
||||||
if not value then
|
|
||||||
Log.warn("Failed to find ancestor object for " .. ancestorId)
|
|
||||||
continue
|
|
||||||
end
|
|
||||||
self:addNode(previousId, {
|
|
||||||
id = ancestorId,
|
|
||||||
className = value.ClassName,
|
|
||||||
name = value.Name,
|
|
||||||
})
|
|
||||||
previousId = ancestorId
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return tree
|
|
||||||
end
|
|
||||||
|
|
||||||
local DomLabel = require(script.DomLabel)
|
local DomLabel = require(script.DomLabel)
|
||||||
|
|
||||||
local PatchVisualizer = Roact.Component:extend("PatchVisualizer")
|
local PatchVisualizer = Roact.Component:extend("PatchVisualizer")
|
||||||
|
|
@ -153,250 +28,90 @@ function PatchVisualizer:willUnmount()
|
||||||
end
|
end
|
||||||
|
|
||||||
function PatchVisualizer:shouldUpdate(nextProps)
|
function PatchVisualizer:shouldUpdate(nextProps)
|
||||||
|
if self.props.patchTree ~= nextProps.patchTree then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
local currentPatch, nextPatch = self.props.patch, nextProps.patch
|
local currentPatch, nextPatch = self.props.patch, nextProps.patch
|
||||||
|
if currentPatch ~= nil or nextPatch ~= nil then
|
||||||
return not PatchSet.isEqual(currentPatch, nextPatch)
|
return not PatchSet.isEqual(currentPatch, nextPatch)
|
||||||
end
|
|
||||||
|
|
||||||
function PatchVisualizer:buildTree(patch, instanceMap)
|
|
||||||
local tree = Tree()
|
|
||||||
|
|
||||||
for _, change in patch.updated do
|
|
||||||
local instance = instanceMap.fromIds[change.id]
|
|
||||||
if not instance then
|
|
||||||
continue
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Gather ancestors from existing DOM
|
|
||||||
local ancestry = {}
|
|
||||||
local parentObject = instance.Parent
|
|
||||||
local parentId = instanceMap.fromInstances[parentObject]
|
|
||||||
while parentObject do
|
|
||||||
table.insert(ancestry, 1, parentId)
|
|
||||||
parentObject = parentObject.Parent
|
|
||||||
parentId = instanceMap.fromInstances[parentObject]
|
|
||||||
end
|
|
||||||
|
|
||||||
tree:buildAncestryNodes(ancestry, patch, instanceMap)
|
|
||||||
|
|
||||||
-- Gather detail text
|
|
||||||
local changeList, hint = nil, nil
|
|
||||||
if next(change.changedProperties) or change.changedName then
|
|
||||||
changeList = {}
|
|
||||||
|
|
||||||
local hintBuffer, i = {}, 0
|
|
||||||
local function addProp(prop: string, current: any?, incoming: any?)
|
|
||||||
i += 1
|
|
||||||
hintBuffer[i] = prop
|
|
||||||
changeList[i] = { prop, current, incoming }
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Gather the changes
|
|
||||||
|
|
||||||
if change.changedName then
|
|
||||||
addProp("Name", instance.Name, change.changedName)
|
|
||||||
end
|
|
||||||
|
|
||||||
for prop, incoming in change.changedProperties do
|
|
||||||
local incomingSuccess, incomingValue = decodeValue(incoming, instanceMap)
|
|
||||||
local currentSuccess, currentValue = getProperty(instance, prop)
|
|
||||||
|
|
||||||
addProp(
|
|
||||||
prop,
|
|
||||||
if currentSuccess then currentValue else "[Error]",
|
|
||||||
if incomingSuccess then incomingValue else next(incoming)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Finalize detail values
|
|
||||||
|
|
||||||
-- Trim hint to top 3
|
|
||||||
table.sort(hintBuffer)
|
|
||||||
if #hintBuffer > 3 then
|
|
||||||
hintBuffer = {
|
|
||||||
hintBuffer[1],
|
|
||||||
hintBuffer[2],
|
|
||||||
hintBuffer[3],
|
|
||||||
i - 3 .. " more",
|
|
||||||
}
|
|
||||||
end
|
|
||||||
hint = table.concat(hintBuffer, ", ")
|
|
||||||
|
|
||||||
-- Sort changes and add header
|
|
||||||
table.sort(changeList, function(a, b)
|
|
||||||
return a[1] < b[1]
|
|
||||||
end)
|
|
||||||
table.insert(changeList, 1, { "Property", "Current", "Incoming" })
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Add this node to tree
|
|
||||||
tree:addNode(instanceMap.fromInstances[instance.Parent], {
|
|
||||||
id = change.id,
|
|
||||||
patchType = "Edit",
|
|
||||||
className = instance.ClassName,
|
|
||||||
name = instance.Name,
|
|
||||||
hint = hint,
|
|
||||||
changeList = changeList,
|
|
||||||
})
|
|
||||||
end
|
end
|
||||||
|
|
||||||
for _, instance in patch.removed do
|
return false
|
||||||
-- Gather ancestors from existing DOM
|
|
||||||
-- (note that they may have no ID if they're being removed as unknown)
|
|
||||||
local ancestry = {}
|
|
||||||
local parentObject = instance.Parent
|
|
||||||
local parentId = instanceMap.fromInstances[parentObject] or HttpService:GenerateGUID(false)
|
|
||||||
while parentObject do
|
|
||||||
instanceMap:insert(parentId, parentObject)
|
|
||||||
table.insert(ancestry, 1, parentId)
|
|
||||||
parentObject = parentObject.Parent
|
|
||||||
parentId = instanceMap.fromInstances[parentObject] or HttpService:GenerateGUID(false)
|
|
||||||
end
|
|
||||||
|
|
||||||
tree:buildAncestryNodes(ancestry, patch, instanceMap)
|
|
||||||
|
|
||||||
-- Add this node to tree
|
|
||||||
local nodeId = instanceMap.fromInstances[instance] or HttpService:GenerateGUID(false)
|
|
||||||
instanceMap:insert(nodeId, instance)
|
|
||||||
tree:addNode(instanceMap.fromInstances[instance.Parent], {
|
|
||||||
id = nodeId,
|
|
||||||
patchType = "Remove",
|
|
||||||
className = instance.ClassName,
|
|
||||||
name = instance.Name,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
for _, change in patch.added do
|
|
||||||
-- Gather ancestors from existing DOM or future additions
|
|
||||||
local ancestry = {}
|
|
||||||
local parentId = change.Parent
|
|
||||||
local parentData = patch.added[parentId]
|
|
||||||
local parentObject = instanceMap.fromIds[parentId]
|
|
||||||
while parentId do
|
|
||||||
table.insert(ancestry, 1, parentId)
|
|
||||||
parentId = nil
|
|
||||||
|
|
||||||
if parentData then
|
|
||||||
parentId = parentData.Parent
|
|
||||||
parentData = patch.added[parentId]
|
|
||||||
parentObject = instanceMap.fromIds[parentId]
|
|
||||||
elseif parentObject then
|
|
||||||
parentObject = parentObject.Parent
|
|
||||||
parentId = instanceMap.fromInstances[parentObject]
|
|
||||||
parentData = patch.added[parentId]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
tree:buildAncestryNodes(ancestry, patch, instanceMap)
|
|
||||||
|
|
||||||
-- Gather detail text
|
|
||||||
local changeList, hint = nil, nil
|
|
||||||
if next(change.Properties) then
|
|
||||||
changeList = {}
|
|
||||||
|
|
||||||
local hintBuffer, i = {}, 0
|
|
||||||
for prop, incoming in change.Properties do
|
|
||||||
i += 1
|
|
||||||
hintBuffer[i] = prop
|
|
||||||
|
|
||||||
local success, incomingValue = decodeValue(incoming, instanceMap)
|
|
||||||
if success then
|
|
||||||
table.insert(changeList, { prop, "N/A", incomingValue })
|
|
||||||
else
|
|
||||||
table.insert(changeList, { prop, "N/A", next(incoming) })
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Finalize detail values
|
|
||||||
|
|
||||||
-- Trim hint to top 3
|
|
||||||
table.sort(hintBuffer)
|
|
||||||
if #hintBuffer > 3 then
|
|
||||||
hintBuffer = {
|
|
||||||
hintBuffer[1],
|
|
||||||
hintBuffer[2],
|
|
||||||
hintBuffer[3],
|
|
||||||
i - 3 .. " more",
|
|
||||||
}
|
|
||||||
end
|
|
||||||
hint = table.concat(hintBuffer, ", ")
|
|
||||||
|
|
||||||
-- Sort changes and add header
|
|
||||||
table.sort(changeList, function(a, b)
|
|
||||||
return a[1] < b[1]
|
|
||||||
end)
|
|
||||||
table.insert(changeList, 1, { "Property", "Current", "Incoming" })
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Add this node to tree
|
|
||||||
tree:addNode(change.Parent, {
|
|
||||||
id = change.Id,
|
|
||||||
patchType = "Add",
|
|
||||||
className = change.ClassName,
|
|
||||||
name = change.Name,
|
|
||||||
hint = hint,
|
|
||||||
changeList = changeList,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
return tree
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function PatchVisualizer:render()
|
function PatchVisualizer:render()
|
||||||
local patch = self.props.patch
|
local patchTree = self.props.patchTree
|
||||||
local instanceMap = self.props.instanceMap
|
if patchTree == nil and self.props.patch ~= nil then
|
||||||
|
patchTree = PatchTree.build(self.props.patch, self.props.instanceMap, self.props.changeListHeaders or { "Property", "Current", "Incoming" })
|
||||||
local tree = self:buildTree(patch, instanceMap)
|
if self.props.unappliedPatch then
|
||||||
|
patchTree = PatchTree.updateMetadata(patchTree, self.props.patch, self.props.instanceMap, self.props.unappliedPatch)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- Recusively draw tree
|
-- Recusively draw tree
|
||||||
local scrollElements, elementHeights = {}, {}
|
local scrollElements, elementHeights = {}, {}
|
||||||
local function drawNode(node, depth)
|
|
||||||
local elementHeight, setElementHeight = Roact.createBinding(30)
|
|
||||||
table.insert(elementHeights, elementHeight)
|
|
||||||
table.insert(
|
|
||||||
scrollElements,
|
|
||||||
e(DomLabel, {
|
|
||||||
columnVisibility = self.props.columnVisibility,
|
|
||||||
updateEvent = self.updateEvent,
|
|
||||||
elementHeight = elementHeight,
|
|
||||||
setElementHeight = setElementHeight,
|
|
||||||
patchType = node.patchType,
|
|
||||||
className = node.className,
|
|
||||||
name = node.name,
|
|
||||||
hint = node.hint,
|
|
||||||
changeList = node.changeList,
|
|
||||||
depth = depth,
|
|
||||||
transparency = self.props.transparency,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
for _, childNode in alphabeticalPairs(node.children) do
|
if patchTree then
|
||||||
drawNode(childNode, depth + 1)
|
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,
|
||||||
|
})
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
|
||||||
for _, node in alphabeticalPairs(tree.ROOT.children) do
|
patchTree:forEach(function(node, depth)
|
||||||
drawNode(node, 0)
|
drawNode(node, depth)
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
return e(BorderedContainer, {
|
return Theme.with(function(theme)
|
||||||
transparency = self.props.transparency,
|
return e(BorderedContainer, {
|
||||||
size = self.props.size,
|
|
||||||
position = self.props.position,
|
|
||||||
layoutOrder = self.props.layoutOrder,
|
|
||||||
}, {
|
|
||||||
VirtualScroller = e(VirtualScroller, {
|
|
||||||
size = UDim2.new(1, 0, 1, 0),
|
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
count = #scrollElements,
|
size = self.props.size,
|
||||||
updateEvent = self.updateEvent.Event,
|
position = self.props.position,
|
||||||
render = function(i)
|
layoutOrder = self.props.layoutOrder,
|
||||||
return scrollElements[i]
|
}, {
|
||||||
end,
|
CleanMerge = e("TextLabel", {
|
||||||
getHeightBinding = function(i)
|
Visible = #scrollElements == 0,
|
||||||
return elementHeights[i]
|
Text = "No changes to sync, project is up to date.",
|
||||||
end,
|
Font = Enum.Font.GothamMedium,
|
||||||
}),
|
TextSize = 15,
|
||||||
})
|
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
||||||
|
TextWrapped = true,
|
||||||
|
Size = UDim2.new(1, 0, 1, 0),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
}),
|
||||||
|
|
||||||
|
VirtualScroller = e(VirtualScroller, {
|
||||||
|
size = UDim2.new(1, 0, 1, 0),
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
count = #scrollElements,
|
||||||
|
updateEvent = self.updateEvent.Event,
|
||||||
|
render = function(i)
|
||||||
|
return scrollElements[i]
|
||||||
|
end,
|
||||||
|
getHeightBinding = function(i)
|
||||||
|
return elementHeights[i]
|
||||||
|
end,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
return PatchVisualizer
|
return PatchVisualizer
|
||||||
|
|
|
||||||
|
|
@ -165,46 +165,94 @@ function Trigger:init()
|
||||||
self.id = HttpService:GenerateGUID(false)
|
self.id = HttpService:GenerateGUID(false)
|
||||||
self.ref = Roact.createRef()
|
self.ref = Roact.createRef()
|
||||||
self.mousePos = Vector2.zero
|
self.mousePos = Vector2.zero
|
||||||
|
self.showingPopup = false
|
||||||
|
|
||||||
self.destroy = function()
|
self.destroy = function()
|
||||||
self.props.context.removeTip(self.id)
|
self.props.context.removeTip(self.id)
|
||||||
|
self.showingPopup = false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function Trigger:willUnmount()
|
function Trigger:willUnmount()
|
||||||
if self.showDelayThread then
|
if self.showDelayThread then
|
||||||
task.cancel(self.showDelayThread)
|
pcall(task.cancel, self.showDelayThread)
|
||||||
end
|
end
|
||||||
if self.destroy then
|
if self.destroy then
|
||||||
self.destroy()
|
self.destroy()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function Trigger:didUpdate(prevProps)
|
||||||
|
if prevProps.text ~= self.props.text then
|
||||||
|
-- Any existing popup is now invalid
|
||||||
|
self.props.context.removeTip(self.id)
|
||||||
|
self.showingPopup = false
|
||||||
|
|
||||||
|
-- Let the new text propagate
|
||||||
|
self:managePopup()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Trigger:isHovering()
|
||||||
|
local rbx = self.ref.current
|
||||||
|
if rbx then
|
||||||
|
local pos = rbx.AbsolutePosition
|
||||||
|
local size = rbx.AbsoluteSize
|
||||||
|
local mousePos = self.mousePos
|
||||||
|
|
||||||
|
return
|
||||||
|
mousePos.X >= pos.X and mousePos.X <= pos.X + size.X
|
||||||
|
and mousePos.Y >= pos.Y and mousePos.Y <= pos.Y + size.Y
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function Trigger:managePopup()
|
||||||
|
if self:isHovering() then
|
||||||
|
if self.showingPopup or self.showDelayThread then
|
||||||
|
-- Don't duplicate popups
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
self.showDelayThread = task.delay(DELAY, function()
|
||||||
|
self.props.context.addTip(self.id, {
|
||||||
|
Text = self.props.text,
|
||||||
|
Position = self.mousePos,
|
||||||
|
Trigger = self.ref,
|
||||||
|
})
|
||||||
|
self.showDelayThread = nil
|
||||||
|
self.showingPopup = true
|
||||||
|
end)
|
||||||
|
else
|
||||||
|
if self.showDelayThread then
|
||||||
|
pcall(task.cancel, self.showDelayThread)
|
||||||
|
self.showDelayThread = nil
|
||||||
|
end
|
||||||
|
self.props.context.removeTip(self.id)
|
||||||
|
self.showingPopup = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
function Trigger:render()
|
function Trigger:render()
|
||||||
|
local function recalculate(rbx)
|
||||||
|
local widget = rbx:FindFirstAncestorOfClass("DockWidgetPluginGui")
|
||||||
|
if not widget then return end
|
||||||
|
self.mousePos = widget:GetRelativeMousePosition()
|
||||||
|
|
||||||
|
self:managePopup()
|
||||||
|
end
|
||||||
|
|
||||||
return e("Frame", {
|
return e("Frame", {
|
||||||
Size = UDim2.fromScale(1, 1),
|
Size = UDim2.fromScale(1, 1),
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
ZIndex = self.props.zIndex or 100,
|
ZIndex = self.props.zIndex or 100,
|
||||||
[Roact.Ref] = self.ref,
|
[Roact.Ref] = self.ref,
|
||||||
|
|
||||||
[Roact.Event.MouseMoved] = function(_rbx, x, y)
|
[Roact.Change.AbsolutePosition] = recalculate,
|
||||||
self.mousePos = Vector2.new(x, y)
|
[Roact.Change.AbsoluteSize] = recalculate,
|
||||||
end,
|
[Roact.Event.MouseMoved] = recalculate,
|
||||||
[Roact.Event.MouseEnter] = function()
|
[Roact.Event.MouseLeave] = recalculate,
|
||||||
self.showDelayThread = task.delay(DELAY, function()
|
[Roact.Event.MouseEnter] = recalculate,
|
||||||
self.props.context.addTip(self.id, {
|
|
||||||
Text = self.props.text,
|
|
||||||
Position = self.mousePos,
|
|
||||||
Trigger = self.ref,
|
|
||||||
})
|
|
||||||
end)
|
|
||||||
end,
|
|
||||||
[Roact.Event.MouseLeave] = function()
|
|
||||||
if self.showDelayThread then
|
|
||||||
task.cancel(self.showDelayThread)
|
|
||||||
end
|
|
||||||
self.props.context.removeTip(self.id)
|
|
||||||
end,
|
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ function ConfirmingPage:render()
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
layoutOrder = 3,
|
layoutOrder = 3,
|
||||||
|
|
||||||
columnVisibility = {true, true, true},
|
changeListHeaders = { "Property", "Current", "Incoming" },
|
||||||
patch = self.props.confirmData.patch,
|
patch = self.props.confirmData.patch,
|
||||||
instanceMap = self.props.confirmData.instanceMap,
|
instanceMap = self.props.confirmData.instanceMap,
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ local PatchSet = require(Plugin.PatchSet)
|
||||||
|
|
||||||
local Header = require(Plugin.App.Components.Header)
|
local Header = require(Plugin.App.Components.Header)
|
||||||
local IconButton = require(Plugin.App.Components.IconButton)
|
local IconButton = require(Plugin.App.Components.IconButton)
|
||||||
|
local TextButton = require(Plugin.App.Components.TextButton)
|
||||||
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
||||||
local Tooltip = require(Plugin.App.Components.Tooltip)
|
local Tooltip = require(Plugin.App.Components.Tooltip)
|
||||||
local PatchVisualizer = require(Plugin.App.Components.PatchVisualizer)
|
local PatchVisualizer = require(Plugin.App.Components.PatchVisualizer)
|
||||||
|
|
@ -55,7 +56,7 @@ function ChangesDrawer:render()
|
||||||
return e(BorderedContainer, {
|
return e(BorderedContainer, {
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
size = self.props.height:map(function(y)
|
size = self.props.height:map(function(y)
|
||||||
return UDim2.new(1, 0, y, -180 * y)
|
return UDim2.new(1, 0, y, -220 * y)
|
||||||
end),
|
end),
|
||||||
position = UDim2.new(0, 0, 1, 0),
|
position = UDim2.new(0, 0, 1, 0),
|
||||||
anchorPoint = Vector2.new(0, 1),
|
anchorPoint = Vector2.new(0, 1),
|
||||||
|
|
@ -82,9 +83,7 @@ function ChangesDrawer:render()
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
layoutOrder = 3,
|
layoutOrder = 3,
|
||||||
|
|
||||||
columnVisibility = { true, false, true },
|
patchTree = self.props.patchTree,
|
||||||
patch = self.props.patch,
|
|
||||||
instanceMap = self.serveSession.__instanceMap,
|
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
end)
|
end)
|
||||||
|
|
@ -137,22 +136,6 @@ local function ConnectionDetails(props)
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Disconnect = e(IconButton, {
|
|
||||||
icon = Assets.Images.Icons.Close,
|
|
||||||
iconSize = 24,
|
|
||||||
color = theme.ConnectionDetails.DisconnectColor,
|
|
||||||
transparency = props.transparency,
|
|
||||||
|
|
||||||
position = UDim2.new(1, 0, 0.5, 0),
|
|
||||||
anchorPoint = Vector2.new(1, 0.5),
|
|
||||||
|
|
||||||
onClick = props.onDisconnect,
|
|
||||||
}, {
|
|
||||||
Tip = e(Tooltip.Trigger, {
|
|
||||||
text = "Disconnect from the Rojo sync server",
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
|
|
||||||
Padding = e("UIPadding", {
|
Padding = e("UIPadding", {
|
||||||
PaddingLeft = UDim.new(0, 17),
|
PaddingLeft = UDim.new(0, 17),
|
||||||
PaddingRight = UDim.new(0, 15),
|
PaddingRight = UDim.new(0, 15),
|
||||||
|
|
@ -170,9 +153,56 @@ function ConnectedPage:getChangeInfoText()
|
||||||
end
|
end
|
||||||
|
|
||||||
local elapsed = os.time() - patchData.timestamp
|
local elapsed = os.time() - patchData.timestamp
|
||||||
local changes = PatchSet.countChanges(patchData.patch)
|
local unapplied = PatchSet.countChanges(patchData.unapplied)
|
||||||
|
|
||||||
return string.format("<i>Synced %d change%s %s</i>", changes, changes == 1 and "" or "s", timeSinceText(elapsed))
|
return
|
||||||
|
"<i>Synced "
|
||||||
|
.. timeSinceText(elapsed)
|
||||||
|
.. (if unapplied > 0 then
|
||||||
|
string.format(
|
||||||
|
", <font color=\"#FF8E3C\">but %d change%s failed to apply</font>",
|
||||||
|
unapplied,
|
||||||
|
unapplied == 1 and "" or "s"
|
||||||
|
)
|
||||||
|
else "")
|
||||||
|
.. "</i>"
|
||||||
|
end
|
||||||
|
|
||||||
|
function ConnectedPage:startChangeInfoTextUpdater()
|
||||||
|
-- Cancel any existing updater
|
||||||
|
self:stopChangeInfoTextUpdater()
|
||||||
|
|
||||||
|
-- Start a new updater
|
||||||
|
self.changeInfoTextUpdater = task.defer(function()
|
||||||
|
while true do
|
||||||
|
if self.state.hoveringChangeInfo then
|
||||||
|
self.setChangeInfoText("<u>" .. self:getChangeInfoText() .. "</u>")
|
||||||
|
else
|
||||||
|
self.setChangeInfoText(self:getChangeInfoText())
|
||||||
|
end
|
||||||
|
|
||||||
|
local elapsed = os.time() - self.props.patchData.timestamp
|
||||||
|
local updateInterval = 1
|
||||||
|
|
||||||
|
-- Update timestamp text as frequently as currently needed
|
||||||
|
for _, UnitData in ipairs(AGE_UNITS) do
|
||||||
|
local UnitSeconds = UnitData[1]
|
||||||
|
if elapsed > UnitSeconds then
|
||||||
|
updateInterval = UnitSeconds
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
task.wait(updateInterval)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
function ConnectedPage:stopChangeInfoTextUpdater()
|
||||||
|
if self.changeInfoTextUpdater then
|
||||||
|
task.cancel(self.changeInfoTextUpdater)
|
||||||
|
self.changeInfoTextUpdater = nil
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function ConnectedPage:init()
|
function ConnectedPage:init()
|
||||||
|
|
@ -195,34 +225,21 @@ function ConnectedPage:init()
|
||||||
|
|
||||||
self:setState({
|
self:setState({
|
||||||
renderChanges = false,
|
renderChanges = false,
|
||||||
|
hoveringChangeInfo = false,
|
||||||
})
|
})
|
||||||
|
|
||||||
self.changeInfoText, self.setChangeInfoText = Roact.createBinding("")
|
self.changeInfoText, self.setChangeInfoText = Roact.createBinding("")
|
||||||
|
|
||||||
self.changeInfoTextUpdater = task.defer(function()
|
self:startChangeInfoTextUpdater()
|
||||||
while true do
|
|
||||||
self.setChangeInfoText(self:getChangeInfoText())
|
|
||||||
|
|
||||||
local elapsed = os.time() - self.props.patchData.timestamp
|
|
||||||
local updateInterval = 1
|
|
||||||
|
|
||||||
-- Update timestamp text as frequently as currently needed
|
|
||||||
for _, UnitData in ipairs(AGE_UNITS) do
|
|
||||||
local UnitSeconds = UnitData[1]
|
|
||||||
if elapsed >= UnitSeconds then
|
|
||||||
updateInterval = UnitSeconds
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
task.wait(updateInterval)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function ConnectedPage:willUnmount()
|
function ConnectedPage:willUnmount()
|
||||||
if self.changeInfoTextUpdater then
|
self:stopChangeInfoTextUpdater()
|
||||||
task.cancel(self.changeInfoTextUpdater)
|
end
|
||||||
|
|
||||||
|
function ConnectedPage:didUpdate(previousProps)
|
||||||
|
if self.props.patchData.timestamp ~= previousProps.patchData.timestamp then
|
||||||
|
self:startChangeInfoTextUpdater()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -255,6 +272,44 @@ function ConnectedPage:render()
|
||||||
onDisconnect = self.props.onDisconnect,
|
onDisconnect = self.props.onDisconnect,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
Buttons = e("Frame", {
|
||||||
|
Size = UDim2.new(1, 0, 0, 34),
|
||||||
|
LayoutOrder = 3,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
ZIndex = 2,
|
||||||
|
}, {
|
||||||
|
Settings = e(TextButton, {
|
||||||
|
text = "Settings",
|
||||||
|
style = "Bordered",
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
layoutOrder = 1,
|
||||||
|
onClick = self.props.onNavigateSettings,
|
||||||
|
}, {
|
||||||
|
Tip = e(Tooltip.Trigger, {
|
||||||
|
text = "View and modify plugin settings"
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
|
Disconnect = e(TextButton, {
|
||||||
|
text = "Disconnect",
|
||||||
|
style = "Solid",
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
layoutOrder = 2,
|
||||||
|
onClick = self.props.onDisconnect,
|
||||||
|
}, {
|
||||||
|
Tip = e(Tooltip.Trigger, {
|
||||||
|
text = "Disconnect from the Rojo sync server"
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
|
Layout = e("UIListLayout", {
|
||||||
|
HorizontalAlignment = Enum.HorizontalAlignment.Right,
|
||||||
|
FillDirection = Enum.FillDirection.Horizontal,
|
||||||
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||||
|
Padding = UDim.new(0, 10),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
ChangeInfo = e("TextButton", {
|
ChangeInfo = e("TextButton", {
|
||||||
Text = self.changeInfoText,
|
Text = self.changeInfoText,
|
||||||
Font = Enum.Font.Gotham,
|
Font = Enum.Font.Gotham,
|
||||||
|
|
@ -268,9 +323,23 @@ function ConnectedPage:render()
|
||||||
|
|
||||||
Size = UDim2.new(1, 0, 0, 28),
|
Size = UDim2.new(1, 0, 0, 28),
|
||||||
|
|
||||||
LayoutOrder = 3,
|
LayoutOrder = 4,
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
|
|
||||||
|
[Roact.Event.MouseEnter] = function()
|
||||||
|
self:setState({
|
||||||
|
hoveringChangeInfo = true,
|
||||||
|
})
|
||||||
|
self.setChangeInfoText("<u>" .. self:getChangeInfoText() .. "</u>")
|
||||||
|
end,
|
||||||
|
|
||||||
|
[Roact.Event.MouseLeave] = function()
|
||||||
|
self:setState({
|
||||||
|
hoveringChangeInfo = false,
|
||||||
|
})
|
||||||
|
self.setChangeInfoText(self:getChangeInfoText())
|
||||||
|
end,
|
||||||
|
|
||||||
[Roact.Event.Activated] = function()
|
[Roact.Event.Activated] = function()
|
||||||
if self.state.renderChanges then
|
if self.state.renderChanges then
|
||||||
self.changeDrawerMotor:setGoal(Flipper.Spring.new(0, {
|
self.changeDrawerMotor:setGoal(Flipper.Spring.new(0, {
|
||||||
|
|
@ -284,15 +353,19 @@ function ConnectedPage:render()
|
||||||
}))
|
}))
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
|
}, {
|
||||||
|
Tooltip = e(Tooltip.Trigger, {
|
||||||
|
text = if self.state.renderChanges then "Hide the changes" else "View the changes",
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
ChangesDrawer = e(ChangesDrawer, {
|
ChangesDrawer = e(ChangesDrawer, {
|
||||||
rendered = self.state.renderChanges,
|
rendered = self.state.renderChanges,
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
patch = self.props.patchData.patch,
|
patchTree = self.props.patchTree,
|
||||||
serveSession = self.props.serveSession,
|
serveSession = self.props.serveSession,
|
||||||
height = self.changeDrawerHeight,
|
height = self.changeDrawerHeight,
|
||||||
layoutOrder = 4,
|
layoutOrder = 5,
|
||||||
|
|
||||||
onClose = function()
|
onClose = function()
|
||||||
self.changeDrawerMotor:setGoal(Flipper.Spring.new(0, {
|
self.changeDrawerMotor:setGoal(Flipper.Spring.new(0, {
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@ function Setting:render()
|
||||||
self.props.customInput
|
self.props.customInput
|
||||||
elseif self.props.options ~= nil then
|
elseif self.props.options ~= nil then
|
||||||
e(Dropdown, {
|
e(Dropdown, {
|
||||||
|
locked = self.props.locked,
|
||||||
options = self.props.options,
|
options = self.props.options,
|
||||||
active = self.state.setting,
|
active = self.state.setting,
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
|
|
@ -83,6 +84,7 @@ function Setting:render()
|
||||||
})
|
})
|
||||||
else
|
else
|
||||||
e(Checkbox, {
|
e(Checkbox, {
|
||||||
|
locked = self.props.locked,
|
||||||
active = self.state.setting,
|
active = self.state.setting,
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
position = UDim2.new(1, 0, 0.5, 0),
|
position = UDim2.new(1, 0, 0.5, 0),
|
||||||
|
|
@ -111,12 +113,13 @@ function Setting:render()
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
}, {
|
}, {
|
||||||
Name = e("TextLabel", {
|
Name = e("TextLabel", {
|
||||||
Text = self.props.name,
|
Text = (if self.props.experimental then "<font color=\"#FF8E3C\">⚠ </font>" else "") .. self.props.name,
|
||||||
Font = Enum.Font.GothamBold,
|
Font = Enum.Font.GothamBold,
|
||||||
TextSize = 17,
|
TextSize = 17,
|
||||||
TextColor3 = theme.Setting.NameColor,
|
TextColor3 = theme.Setting.NameColor,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
TextTransparency = self.props.transparency,
|
TextTransparency = self.props.transparency,
|
||||||
|
RichText = true,
|
||||||
|
|
||||||
Size = UDim2.new(1, 0, 0, 17),
|
Size = UDim2.new(1, 0, 0, 17),
|
||||||
|
|
||||||
|
|
@ -125,7 +128,7 @@ function Setting:render()
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Description = e("TextLabel", {
|
Description = e("TextLabel", {
|
||||||
Text = self.props.description,
|
Text = (if self.props.experimental then "<font color=\"#FF8E3C\">[Experimental] </font>" else "") .. self.props.description,
|
||||||
Font = Enum.Font.Gotham,
|
Font = Enum.Font.Gotham,
|
||||||
LineHeight = 1.2,
|
LineHeight = 1.2,
|
||||||
TextSize = 14,
|
TextSize = 14,
|
||||||
|
|
@ -133,11 +136,13 @@ function Setting:render()
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
TextTransparency = self.props.transparency,
|
TextTransparency = self.props.transparency,
|
||||||
TextWrapped = true,
|
TextWrapped = true,
|
||||||
|
RichText = true,
|
||||||
|
|
||||||
Size = self.containerSize:map(function(value)
|
Size = self.containerSize:map(function(value)
|
||||||
|
local desc = (if self.props.experimental then "[Experimental] " else "") .. self.props.description
|
||||||
local offset = (self.props.onReset and 34 or 0) + (self.props.options ~= nil and 120 or 40)
|
local offset = (self.props.onReset and 34 or 0) + (self.props.options ~= nil and 120 or 40)
|
||||||
local textBounds = getTextBounds(
|
local textBounds = getTextBounds(
|
||||||
self.props.description, 14, Enum.Font.Gotham, 1.2,
|
desc, 14, Enum.Font.Gotham, 1.2,
|
||||||
Vector2.new(value.X - offset, math.huge)
|
Vector2.new(value.X - offset, math.huge)
|
||||||
)
|
)
|
||||||
return UDim2.new(1, -offset, 0, textBounds.Y)
|
return UDim2.new(1, -offset, 0, textBounds.Y)
|
||||||
|
|
|
||||||
|
|
@ -156,7 +156,9 @@ function SettingsPage:render()
|
||||||
OpenScriptsExternally = e(Setting, {
|
OpenScriptsExternally = e(Setting, {
|
||||||
id = "openScriptsExternally",
|
id = "openScriptsExternally",
|
||||||
name = "Open Scripts Externally",
|
name = "Open Scripts Externally",
|
||||||
description = "EXPERIMENTAL! Attempt to open scripts in an external editor",
|
description = "Attempt to open scripts in an external editor",
|
||||||
|
locked = self.props.syncActive,
|
||||||
|
experimental = true,
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
layoutOrder = 5,
|
layoutOrder = 5,
|
||||||
}),
|
}),
|
||||||
|
|
@ -164,7 +166,9 @@ function SettingsPage:render()
|
||||||
TwoWaySync = e(Setting, {
|
TwoWaySync = e(Setting, {
|
||||||
id = "twoWaySync",
|
id = "twoWaySync",
|
||||||
name = "Two-Way Sync",
|
name = "Two-Way Sync",
|
||||||
description = "EXPERIMENTAL! Editing files in Studio will sync them into the filesystem",
|
description = "Editing files in Studio will sync them into the filesystem",
|
||||||
|
locked = self.props.syncActive,
|
||||||
|
experimental = true,
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
layoutOrder = 6,
|
layoutOrder = 6,
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,7 @@ local lightTheme = strict("LightTheme", {
|
||||||
Remove = hexColor(0xffbdba),
|
Remove = hexColor(0xffbdba),
|
||||||
Edit = hexColor(0xbacdff),
|
Edit = hexColor(0xbacdff),
|
||||||
Row = hexColor(0x000000),
|
Row = hexColor(0x000000),
|
||||||
|
Warning = hexColor(0xFF8E3C),
|
||||||
},
|
},
|
||||||
ConnectionDetails = {
|
ConnectionDetails = {
|
||||||
ProjectNameColor = hexColor(0x00000),
|
ProjectNameColor = hexColor(0x00000),
|
||||||
|
|
@ -195,6 +196,7 @@ local darkTheme = strict("DarkTheme", {
|
||||||
Remove = hexColor(0x3F2D32),
|
Remove = hexColor(0x3F2D32),
|
||||||
Edit = hexColor(0x193345),
|
Edit = hexColor(0x193345),
|
||||||
Row = hexColor(0xFFFFFF),
|
Row = hexColor(0xFFFFFF),
|
||||||
|
Warning = hexColor(0xFF8E3C),
|
||||||
},
|
},
|
||||||
ConnectionDetails = {
|
ConnectionDetails = {
|
||||||
ProjectNameColor = hexColor(0xFFFFFF),
|
ProjectNameColor = hexColor(0xFFFFFF),
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
local ChangeHistoryService = game:GetService("ChangeHistoryService")
|
||||||
local Players = game:GetService("Players")
|
local Players = game:GetService("Players")
|
||||||
local ServerStorage = game:GetService("ServerStorage")
|
local ServerStorage = game:GetService("ServerStorage")
|
||||||
local RunService = game:GetService("RunService")
|
local RunService = game:GetService("RunService")
|
||||||
|
|
@ -19,6 +20,7 @@ local ServeSession = require(Plugin.ServeSession)
|
||||||
local ApiContext = require(Plugin.ApiContext)
|
local ApiContext = require(Plugin.ApiContext)
|
||||||
local HeadlessAPI = require(Plugin.HeadlessAPI)
|
local HeadlessAPI = require(Plugin.HeadlessAPI)
|
||||||
local PatchSet = require(Plugin.PatchSet)
|
local PatchSet = require(Plugin.PatchSet)
|
||||||
|
local PatchTree = require(Plugin.PatchTree)
|
||||||
local preloadAssets = require(Plugin.preloadAssets)
|
local preloadAssets = require(Plugin.preloadAssets)
|
||||||
local soundPlayer = require(Plugin.soundPlayer)
|
local soundPlayer = require(Plugin.soundPlayer)
|
||||||
local ignorePlaceIds = require(Plugin.ignorePlaceIds)
|
local ignorePlaceIds = require(Plugin.ignorePlaceIds)
|
||||||
|
|
@ -61,12 +63,71 @@ function App:init()
|
||||||
self.confirmationEvent = self.confirmationBindable.Event
|
self.confirmationEvent = self.confirmationBindable.Event
|
||||||
self.notifId = 0
|
self.notifId = 0
|
||||||
|
|
||||||
|
self.waypointConnection = ChangeHistoryService.OnUndo:Connect(function(action: string)
|
||||||
|
if not string.find(action, "^Rojo: Patch") then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local undoConnection, redoConnection = nil, nil
|
||||||
|
local function cleanup()
|
||||||
|
undoConnection:Disconnect()
|
||||||
|
redoConnection:Disconnect()
|
||||||
|
end
|
||||||
|
|
||||||
|
Log.warn(
|
||||||
|
string.format(
|
||||||
|
"You've undone '%s'.\nIf this was not intended, please Redo in the topbar or with Ctrl/⌘+Y.",
|
||||||
|
action
|
||||||
|
)
|
||||||
|
)
|
||||||
|
local dismissNotif = self:addNotification(
|
||||||
|
string.format("You've undone '%s'.\nIf this was not intended, please restore.", action),
|
||||||
|
10,
|
||||||
|
{
|
||||||
|
Restore = {
|
||||||
|
text = "Restore",
|
||||||
|
style = "Solid",
|
||||||
|
layoutOrder = 1,
|
||||||
|
onClick = function(notification)
|
||||||
|
cleanup()
|
||||||
|
notification:dismiss()
|
||||||
|
ChangeHistoryService:Redo()
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
Dismiss = {
|
||||||
|
text = "Dismiss",
|
||||||
|
style = "Bordered",
|
||||||
|
layoutOrder = 2,
|
||||||
|
onClick = function(notification)
|
||||||
|
cleanup()
|
||||||
|
notification:dismiss()
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
undoConnection = ChangeHistoryService.OnUndo:Once(function()
|
||||||
|
-- Our notif is now out of date- redoing will not restore the patch
|
||||||
|
-- since we've undone even further. Dismiss the notif.
|
||||||
|
cleanup()
|
||||||
|
dismissNotif()
|
||||||
|
end)
|
||||||
|
redoConnection = ChangeHistoryService.OnRedo:Once(function(redoneAction: string)
|
||||||
|
if redoneAction == action then
|
||||||
|
-- The user has restored the patch, so we can dismiss the notif
|
||||||
|
cleanup()
|
||||||
|
dismissNotif()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
self:setState({
|
self:setState({
|
||||||
appStatus = AppStatus.NotConnected,
|
appStatus = AppStatus.NotConnected,
|
||||||
guiEnabled = false,
|
guiEnabled = false,
|
||||||
confirmData = {},
|
confirmData = {},
|
||||||
patchData = {
|
patchData = {
|
||||||
patch = PatchSet.newEmpty(),
|
patch = PatchSet.newEmpty(),
|
||||||
|
unapplied = PatchSet.newEmpty(),
|
||||||
timestamp = os.time(),
|
timestamp = os.time(),
|
||||||
},
|
},
|
||||||
notifications = {},
|
notifications = {},
|
||||||
|
|
@ -132,7 +193,7 @@ function App:init()
|
||||||
onClick = function(notification)
|
onClick = function(notification)
|
||||||
notification:dismiss()
|
notification:dismiss()
|
||||||
self:startSession()
|
self:startSession()
|
||||||
end
|
end,
|
||||||
},
|
},
|
||||||
Dismiss = {
|
Dismiss = {
|
||||||
text = "Dismiss",
|
text = "Dismiss",
|
||||||
|
|
@ -146,7 +207,16 @@ function App:init()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function App:addNotification(text: string, timeout: number?, actions: { [string]: {text: string, style: string, layoutOrder: number, onClick: (any) -> ()} }?)
|
function App:willUnmount()
|
||||||
|
self.waypointConnection:Disconnect()
|
||||||
|
self.confirmationBindable:Destroy()
|
||||||
|
end
|
||||||
|
|
||||||
|
function App:addNotification(
|
||||||
|
text: string,
|
||||||
|
timeout: number?,
|
||||||
|
actions: { [string]: { text: string, style: string, layoutOrder: number, onClick: (any) -> () } }?
|
||||||
|
)
|
||||||
if not Settings:get("showNotifications") then
|
if not Settings:get("showNotifications") then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
@ -199,6 +269,10 @@ function App:addThirdPartyNotification(source: string, text: string, timeout: nu
|
||||||
end
|
end
|
||||||
|
|
||||||
function App:closeNotification(id: number)
|
function App:closeNotification(id: number)
|
||||||
|
if not self.state.notifications[id] then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
local notifications = table.clone(self.state.notifications)
|
local notifications = table.clone(self.state.notifications)
|
||||||
notifications[id] = nil
|
notifications[id] = nil
|
||||||
|
|
||||||
|
|
@ -209,26 +283,38 @@ end
|
||||||
|
|
||||||
function App:getPriorEndpoint()
|
function App:getPriorEndpoint()
|
||||||
local priorEndpoints = Settings:get("priorEndpoints")
|
local priorEndpoints = Settings:get("priorEndpoints")
|
||||||
if not priorEndpoints then return end
|
if not priorEndpoints then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
local id = tostring(game.PlaceId)
|
local id = tostring(game.PlaceId)
|
||||||
if ignorePlaceIds[id] then return end
|
if ignorePlaceIds[id] then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
local place = priorEndpoints[id]
|
local place = priorEndpoints[id]
|
||||||
if not place then return end
|
if not place then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
return place.host, place.port
|
return place.host, place.port
|
||||||
end
|
end
|
||||||
|
|
||||||
function App:getLastSyncTimestamp()
|
function App:getLastSyncTimestamp()
|
||||||
local priorEndpoints = Settings:get("priorEndpoints")
|
local priorEndpoints = Settings:get("priorEndpoints")
|
||||||
if not priorEndpoints then return end
|
if not priorEndpoints then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
local id = tostring(game.PlaceId)
|
local id = tostring(game.PlaceId)
|
||||||
if ignorePlaceIds[id] then return end
|
if ignorePlaceIds[id] then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
local place = priorEndpoints[id]
|
local place = priorEndpoints[id]
|
||||||
if not place then return end
|
if not place then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
return place.timestamp
|
return place.timestamp
|
||||||
end
|
end
|
||||||
|
|
@ -248,7 +334,9 @@ function App:setPriorEndpoint(host: string, port: string)
|
||||||
end
|
end
|
||||||
|
|
||||||
local id = tostring(game.PlaceId)
|
local id = tostring(game.PlaceId)
|
||||||
if ignorePlaceIds[id] then return end
|
if ignorePlaceIds[id] then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
priorEndpoints[id] = {
|
priorEndpoints[id] = {
|
||||||
host = if host ~= Config.defaultHost then host else nil,
|
host = if host ~= Config.defaultHost then host else nil,
|
||||||
|
|
@ -257,7 +345,6 @@ function App:setPriorEndpoint(host: string, port: string)
|
||||||
}
|
}
|
||||||
Log.trace("Saved last used endpoint for {}", game.PlaceId)
|
Log.trace("Saved last used endpoint for {}", game.PlaceId)
|
||||||
|
|
||||||
|
|
||||||
Settings:set("priorEndpoints", priorEndpoints)
|
Settings:set("priorEndpoints", priorEndpoints)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -393,35 +480,51 @@ function App:startSession(host: string?, port: string?)
|
||||||
twoWaySync = sessionOptions.twoWaySync,
|
twoWaySync = sessionOptions.twoWaySync,
|
||||||
})
|
})
|
||||||
|
|
||||||
serveSession:onPatchApplied(function(patch, _unapplied)
|
self.cleanupPrecommit = serveSession.__reconciler:hookPrecommit(function(patch, instanceMap)
|
||||||
|
-- Build new tree for patch
|
||||||
|
self:setState({
|
||||||
|
patchTree = PatchTree.build(patch, instanceMap, { "Property", "Old", "New" }),
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
self.cleanupPostcommit = serveSession.__reconciler:hookPostcommit(function(patch, instanceMap, unappliedPatch)
|
||||||
|
-- Update tree with unapplied metadata
|
||||||
|
self:setState(function(prevState)
|
||||||
|
return {
|
||||||
|
patchTree = PatchTree.updateMetadata(prevState.patchTree, patch, instanceMap, unappliedPatch),
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
serveSession:hookPostcommit(function(patch, _instanceMap, unapplied)
|
||||||
|
local now = os.time()
|
||||||
|
local old = self.state.patchData
|
||||||
|
|
||||||
if PatchSet.isEmpty(patch) then
|
if PatchSet.isEmpty(patch) then
|
||||||
-- Ignore empty patches
|
-- Ignore empty patch, but update timestamp
|
||||||
|
self:setState({
|
||||||
|
patchData = {
|
||||||
|
patch = old.patch,
|
||||||
|
unapplied = old.unapplied,
|
||||||
|
timestamp = now,
|
||||||
|
},
|
||||||
|
})
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local now = os.time()
|
|
||||||
|
|
||||||
local old = self.state.patchData
|
|
||||||
if now - old.timestamp < 2 then
|
if now - old.timestamp < 2 then
|
||||||
-- Patches that apply in the same second are
|
-- Patches that apply in the same second are
|
||||||
-- considered to be part of the same change for human clarity
|
-- considered to be part of the same change for human clarity
|
||||||
local merged = PatchSet.newEmpty()
|
patch = PatchSet.assign(PatchSet.newEmpty(), old.patch, patch)
|
||||||
PatchSet.assign(merged, old.patch, patch)
|
unapplied = PatchSet.assign(PatchSet.newEmpty(), old.unapplied, unapplied)
|
||||||
|
|
||||||
self:setState({
|
|
||||||
patchData = {
|
|
||||||
patch = merged,
|
|
||||||
timestamp = now,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
else
|
|
||||||
self:setState({
|
|
||||||
patchData = {
|
|
||||||
patch = patch,
|
|
||||||
timestamp = now,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
self:setState({
|
||||||
|
patchData = {
|
||||||
|
patch = patch,
|
||||||
|
unapplied = unapplied,
|
||||||
|
timestamp = now,
|
||||||
|
},
|
||||||
|
})
|
||||||
end)
|
end)
|
||||||
|
|
||||||
serveSession:onStatusChanged(function(status, details)
|
serveSession:onStatusChanged(function(status, details)
|
||||||
|
|
@ -449,6 +552,13 @@ function App:startSession(host: string?, port: string?)
|
||||||
elseif status == ServeSession.Status.Disconnected then
|
elseif status == ServeSession.Status.Disconnected then
|
||||||
self.serveSession = nil
|
self.serveSession = nil
|
||||||
self:releaseSyncLock()
|
self:releaseSyncLock()
|
||||||
|
self:setState({
|
||||||
|
patchData = {
|
||||||
|
patch = PatchSet.newEmpty(),
|
||||||
|
unapplied = PatchSet.newEmpty(),
|
||||||
|
timestamp = os.time(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
-- Details being present indicates that this
|
-- Details being present indicates that this
|
||||||
-- disconnection was from an error.
|
-- disconnection was from an error.
|
||||||
|
|
@ -540,6 +650,13 @@ function App:endSession()
|
||||||
appStatus = AppStatus.NotConnected,
|
appStatus = AppStatus.NotConnected,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if self.cleanupPrecommit ~= nil then
|
||||||
|
self.cleanupPrecommit()
|
||||||
|
end
|
||||||
|
if self.cleanupPostcommit ~= nil then
|
||||||
|
self.cleanupPostcommit()
|
||||||
|
end
|
||||||
|
|
||||||
Log.trace("Session terminated by user")
|
Log.trace("Session terminated by user")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -602,8 +719,8 @@ function App:render()
|
||||||
initDockState = Enum.InitialDockState.Right,
|
initDockState = Enum.InitialDockState.Right,
|
||||||
initEnabled = false,
|
initEnabled = false,
|
||||||
overridePreviousState = false,
|
overridePreviousState = false,
|
||||||
floatingSize = Vector2.new(300, 200),
|
floatingSize = Vector2.new(320, 210),
|
||||||
minimumSize = Vector2.new(300, 120),
|
minimumSize = Vector2.new(300, 210),
|
||||||
|
|
||||||
zIndexBehavior = Enum.ZIndexBehavior.Sibling,
|
zIndexBehavior = Enum.ZIndexBehavior.Sibling,
|
||||||
|
|
||||||
|
|
@ -632,6 +749,7 @@ function App:render()
|
||||||
end,
|
end,
|
||||||
|
|
||||||
onNavigateSettings = function()
|
onNavigateSettings = function()
|
||||||
|
self.backPage = AppStatus.NotConnected
|
||||||
self:setState({
|
self:setState({
|
||||||
appStatus = AppStatus.Settings,
|
appStatus = AppStatus.Settings,
|
||||||
})
|
})
|
||||||
|
|
@ -658,18 +776,28 @@ function App:render()
|
||||||
Connected = createPageElement(AppStatus.Connected, {
|
Connected = createPageElement(AppStatus.Connected, {
|
||||||
projectName = self.state.projectName,
|
projectName = self.state.projectName,
|
||||||
address = self.state.address,
|
address = self.state.address,
|
||||||
|
patchTree = self.state.patchTree,
|
||||||
patchData = self.state.patchData,
|
patchData = self.state.patchData,
|
||||||
serveSession = self.serveSession,
|
serveSession = self.serveSession,
|
||||||
|
|
||||||
onDisconnect = function()
|
onDisconnect = function()
|
||||||
self:endSession()
|
self:endSession()
|
||||||
end,
|
end,
|
||||||
|
|
||||||
|
onNavigateSettings = function()
|
||||||
|
self.backPage = AppStatus.Connected
|
||||||
|
self:setState({
|
||||||
|
appStatus = AppStatus.Settings,
|
||||||
|
})
|
||||||
|
end,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Settings = createPageElement(AppStatus.Settings, {
|
Settings = createPageElement(AppStatus.Settings, {
|
||||||
|
syncActive = self.serveSession ~= nil and self.serveSession:getStatus() == ServeSession.Status.Connected,
|
||||||
|
|
||||||
onBack = function()
|
onBack = function()
|
||||||
self:setState({
|
self:setState({
|
||||||
appStatus = AppStatus.NotConnected,
|
appStatus = self.backPage or AppStatus.NotConnected,
|
||||||
})
|
})
|
||||||
end,
|
end,
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,9 +36,11 @@ local Assets = {
|
||||||
Checkbox = {
|
Checkbox = {
|
||||||
Active = "rbxassetid://6016251644",
|
Active = "rbxassetid://6016251644",
|
||||||
Inactive = "rbxassetid://6016251963",
|
Inactive = "rbxassetid://6016251963",
|
||||||
|
Locked = "rbxassetid://14011257320",
|
||||||
},
|
},
|
||||||
Dropdown = {
|
Dropdown = {
|
||||||
Arrow = "rbxassetid://10131770538",
|
Arrow = "rbxassetid://10131770538",
|
||||||
|
Locked = "rbxassetid://14011257320",
|
||||||
},
|
},
|
||||||
Spinner = {
|
Spinner = {
|
||||||
Foreground = "rbxassetid://3222731032",
|
Foreground = "rbxassetid://3222731032",
|
||||||
|
|
|
||||||
|
|
@ -212,13 +212,24 @@ function PatchSet.countChanges(patch)
|
||||||
local count = 0
|
local count = 0
|
||||||
|
|
||||||
for _ in patch.added do
|
for _ in patch.added do
|
||||||
|
-- Adding an instance is 1 change
|
||||||
count += 1
|
count += 1
|
||||||
end
|
end
|
||||||
for _ in patch.removed do
|
for _ in patch.removed do
|
||||||
|
-- Removing an instance is 1 change
|
||||||
count += 1
|
count += 1
|
||||||
end
|
end
|
||||||
for _ in patch.updated do
|
for _, update in patch.updated do
|
||||||
count += 1
|
-- Updating an instance is 1 change per property updated
|
||||||
|
for _ in update.changedProperties do
|
||||||
|
count += 1
|
||||||
|
end
|
||||||
|
if update.changedName ~= nil then
|
||||||
|
count += 1
|
||||||
|
end
|
||||||
|
if update.changedClassName ~= nil then
|
||||||
|
count += 1
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return count
|
return count
|
||||||
|
|
|
||||||
434
plugin/src/PatchTree.lua
Normal file
434
plugin/src/PatchTree.lua
Normal file
|
|
@ -0,0 +1,434 @@
|
||||||
|
--[[
|
||||||
|
Methods to turn PatchSets into trees matching the DataModel containing
|
||||||
|
the changes and metadata for use in the PatchVisualizer component.
|
||||||
|
]]
|
||||||
|
|
||||||
|
local HttpService = game:GetService("HttpService")
|
||||||
|
|
||||||
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
|
local Plugin = Rojo.Plugin
|
||||||
|
local Packages = Rojo.Packages
|
||||||
|
|
||||||
|
local Log = require(Packages.Log)
|
||||||
|
|
||||||
|
local Types = require(Plugin.Types)
|
||||||
|
local decodeValue = require(Plugin.Reconciler.decodeValue)
|
||||||
|
local getProperty = require(Plugin.Reconciler.getProperty)
|
||||||
|
|
||||||
|
local function alphabeticalNext(t, state)
|
||||||
|
-- Equivalent of the next function, but returns the keys in the alphabetic
|
||||||
|
-- order of node names. We use a temporary ordered key table that is stored in the
|
||||||
|
-- table being iterated.
|
||||||
|
|
||||||
|
local key = nil
|
||||||
|
if state == nil then
|
||||||
|
-- First iteration, generate the index
|
||||||
|
local orderedIndex, i = table.create(5), 0
|
||||||
|
for k in t do
|
||||||
|
i += 1
|
||||||
|
orderedIndex[i] = k
|
||||||
|
end
|
||||||
|
table.sort(orderedIndex, function(a, b)
|
||||||
|
local nodeA, nodeB = t[a], t[b]
|
||||||
|
return (nodeA.name or "") < (nodeB.name or "")
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.__orderedIndex = orderedIndex
|
||||||
|
key = orderedIndex[1]
|
||||||
|
else
|
||||||
|
-- Fetch the next value
|
||||||
|
for i, orderedState in t.__orderedIndex do
|
||||||
|
if orderedState == state then
|
||||||
|
key = t.__orderedIndex[i + 1]
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if key then
|
||||||
|
return key, t[key]
|
||||||
|
end
|
||||||
|
|
||||||
|
-- No more value to return, cleanup
|
||||||
|
t.__orderedIndex = nil
|
||||||
|
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local function alphabeticalPairs(t)
|
||||||
|
-- Equivalent of the pairs() iterator, but sorted
|
||||||
|
return alphabeticalNext, t, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local Tree = {}
|
||||||
|
Tree.__index = Tree
|
||||||
|
|
||||||
|
function Tree.new()
|
||||||
|
local tree = {
|
||||||
|
idToNode = {},
|
||||||
|
ROOT = {
|
||||||
|
className = "DataModel",
|
||||||
|
name = "ROOT",
|
||||||
|
children = {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
-- Add ROOT to idToNode or it won't be found by getNode since that searches *within* ROOT
|
||||||
|
tree.idToNode["ROOT"] = tree.ROOT
|
||||||
|
|
||||||
|
return setmetatable(tree, Tree)
|
||||||
|
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
|
||||||
|
function Tree:forEach(callback, node, depth)
|
||||||
|
depth = depth or 1
|
||||||
|
for _, child in alphabeticalPairs(if node then node.children else self.ROOT.children) do
|
||||||
|
callback(child, depth)
|
||||||
|
if type(child.children) == "table" then
|
||||||
|
self:forEach(callback, child, depth + 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Finds a node by id, depth first
|
||||||
|
-- searchNode is the node to start the search within, defaults to root
|
||||||
|
function Tree:getNode(id, searchNode)
|
||||||
|
if self.idToNode[id] then
|
||||||
|
return self.idToNode[id]
|
||||||
|
end
|
||||||
|
|
||||||
|
local searchChildren = (searchNode or self.ROOT).children
|
||||||
|
for nodeId, node in searchChildren do
|
||||||
|
if nodeId == id then
|
||||||
|
self.idToNode[id] = node
|
||||||
|
return node
|
||||||
|
end
|
||||||
|
local descendant = self:getNode(id, node)
|
||||||
|
if descendant then
|
||||||
|
return descendant
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Adds a node to the tree as a child of the node with id == parent
|
||||||
|
-- If parent is nil, it defaults to root
|
||||||
|
-- props must contain id, and cannot contain children or parentId
|
||||||
|
-- other than those three, it can hold anything
|
||||||
|
function Tree:addNode(parent, props)
|
||||||
|
assert(props.id, "props must contain id")
|
||||||
|
|
||||||
|
parent = parent or "ROOT"
|
||||||
|
|
||||||
|
local node = self:getNode(props.id)
|
||||||
|
if node then
|
||||||
|
-- Update existing node
|
||||||
|
for k, v in props do
|
||||||
|
node[k] = v
|
||||||
|
end
|
||||||
|
return node
|
||||||
|
end
|
||||||
|
|
||||||
|
node = table.clone(props)
|
||||||
|
node.children = {}
|
||||||
|
node.parentId = parent
|
||||||
|
|
||||||
|
local parentNode = self:getNode(parent)
|
||||||
|
if not parentNode then
|
||||||
|
Log.warn("Failed to create node since parent doesnt exist: {}, {}", parent, props)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
parentNode.children[node.id] = node
|
||||||
|
self.idToNode[node.id] = node
|
||||||
|
|
||||||
|
return node
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Given a list of ancestor ids in descending order, builds the nodes for them
|
||||||
|
-- using the patch and instanceMap info
|
||||||
|
function Tree:buildAncestryNodes(ancestryIds: { string }, patch, instanceMap)
|
||||||
|
-- Build nodes for ancestry by going up the tree
|
||||||
|
local previousId = "ROOT"
|
||||||
|
for _, ancestorId in ancestryIds do
|
||||||
|
local value = instanceMap.fromIds[ancestorId] or patch.added[ancestorId]
|
||||||
|
if not value then
|
||||||
|
Log.warn("Failed to find ancestor object for " .. ancestorId)
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
self:addNode(previousId, {
|
||||||
|
id = ancestorId,
|
||||||
|
className = value.ClassName,
|
||||||
|
name = value.Name,
|
||||||
|
instance = if typeof(value) == "Instance" then value else nil,
|
||||||
|
})
|
||||||
|
previousId = ancestorId
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local PatchTree = {}
|
||||||
|
|
||||||
|
-- Builds a new tree from a patch and instanceMap
|
||||||
|
-- uses changeListHeaders in node.changeList
|
||||||
|
function PatchTree.build(patch, instanceMap, changeListHeaders)
|
||||||
|
local tree = Tree.new()
|
||||||
|
|
||||||
|
for _, change in patch.updated do
|
||||||
|
local instance = instanceMap.fromIds[change.id]
|
||||||
|
if not instance then
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Gather ancestors from existing DOM
|
||||||
|
local ancestryIds = {}
|
||||||
|
local parentObject = instance.Parent
|
||||||
|
local parentId = instanceMap.fromInstances[parentObject]
|
||||||
|
while parentObject do
|
||||||
|
table.insert(ancestryIds, 1, parentId)
|
||||||
|
parentObject = parentObject.Parent
|
||||||
|
parentId = instanceMap.fromInstances[parentObject]
|
||||||
|
end
|
||||||
|
|
||||||
|
tree:buildAncestryNodes(ancestryIds, patch, instanceMap)
|
||||||
|
|
||||||
|
-- Gather detail text
|
||||||
|
local changeList, hint = nil, nil
|
||||||
|
if next(change.changedProperties) or change.changedName then
|
||||||
|
changeList = {}
|
||||||
|
|
||||||
|
local hintBuffer, i = {}, 0
|
||||||
|
local function addProp(prop: string, current: any?, incoming: any?, metadata: any?)
|
||||||
|
i += 1
|
||||||
|
hintBuffer[i] = prop
|
||||||
|
changeList[i] = { prop, current, incoming, metadata }
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Gather the changes
|
||||||
|
|
||||||
|
if change.changedName then
|
||||||
|
addProp("Name", instance.Name, change.changedName)
|
||||||
|
end
|
||||||
|
|
||||||
|
for prop, incoming in change.changedProperties do
|
||||||
|
local incomingSuccess, incomingValue = decodeValue(incoming, instanceMap)
|
||||||
|
local currentSuccess, currentValue = getProperty(instance, prop)
|
||||||
|
|
||||||
|
addProp(
|
||||||
|
prop,
|
||||||
|
if currentSuccess then currentValue else "[Error]",
|
||||||
|
if incomingSuccess then incomingValue else next(incoming)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Finalize detail values
|
||||||
|
|
||||||
|
-- Trim hint to top 3
|
||||||
|
table.sort(hintBuffer)
|
||||||
|
if #hintBuffer > 3 then
|
||||||
|
hintBuffer = {
|
||||||
|
hintBuffer[1],
|
||||||
|
hintBuffer[2],
|
||||||
|
hintBuffer[3],
|
||||||
|
i - 3 .. " more",
|
||||||
|
}
|
||||||
|
end
|
||||||
|
hint = table.concat(hintBuffer, ", ")
|
||||||
|
|
||||||
|
-- Sort changes and add header
|
||||||
|
table.sort(changeList, function(a, b)
|
||||||
|
return a[1] < b[1]
|
||||||
|
end)
|
||||||
|
table.insert(changeList, 1, changeListHeaders)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Add this node to tree
|
||||||
|
tree:addNode(instanceMap.fromInstances[instance.Parent], {
|
||||||
|
id = change.id,
|
||||||
|
patchType = "Edit",
|
||||||
|
className = instance.ClassName,
|
||||||
|
name = instance.Name,
|
||||||
|
instance = instance,
|
||||||
|
hint = hint,
|
||||||
|
changeList = changeList,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, idOrInstance in patch.removed do
|
||||||
|
local instance = if Types.RbxId(idOrInstance) then instanceMap.fromIds[idOrInstance] else idOrInstance
|
||||||
|
if not instance then
|
||||||
|
-- If we're viewing a past patch, the instance is already removed
|
||||||
|
-- and we therefore cannot get the tree for it anymore
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Gather ancestors from existing DOM
|
||||||
|
-- (note that they may have no ID if they're being removed as unknown)
|
||||||
|
local ancestryIds = {}
|
||||||
|
local parentObject = instance.Parent
|
||||||
|
local parentId = instanceMap.fromInstances[parentObject] or HttpService:GenerateGUID(false)
|
||||||
|
while parentObject do
|
||||||
|
instanceMap:insert(parentId, parentObject) -- This ensures we can find the parent later
|
||||||
|
table.insert(ancestryIds, 1, parentId)
|
||||||
|
parentObject = parentObject.Parent
|
||||||
|
parentId = instanceMap.fromInstances[parentObject] or HttpService:GenerateGUID(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
tree:buildAncestryNodes(ancestryIds, patch, instanceMap)
|
||||||
|
|
||||||
|
-- Add this node to tree
|
||||||
|
local nodeId = instanceMap.fromInstances[instance] or HttpService:GenerateGUID(false)
|
||||||
|
instanceMap:insert(nodeId, instance)
|
||||||
|
tree:addNode(instanceMap.fromInstances[instance.Parent], {
|
||||||
|
id = nodeId,
|
||||||
|
patchType = "Remove",
|
||||||
|
className = instance.ClassName,
|
||||||
|
name = instance.Name,
|
||||||
|
instance = instance,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
for id, change in patch.added do
|
||||||
|
-- Gather ancestors from existing DOM or future additions
|
||||||
|
local ancestryIds = {}
|
||||||
|
local parentId = change.Parent
|
||||||
|
local parentData = patch.added[parentId]
|
||||||
|
local parentObject = instanceMap.fromIds[parentId]
|
||||||
|
while parentId do
|
||||||
|
table.insert(ancestryIds, 1, parentId)
|
||||||
|
parentId = nil
|
||||||
|
|
||||||
|
if parentData then
|
||||||
|
-- object is parented to an instance that does not exist yet
|
||||||
|
parentId = parentData.Parent
|
||||||
|
parentData = patch.added[parentId]
|
||||||
|
parentObject = instanceMap.fromIds[parentId]
|
||||||
|
elseif parentObject then
|
||||||
|
-- object is parented to an instance that exists
|
||||||
|
parentObject = parentObject.Parent
|
||||||
|
parentId = instanceMap.fromInstances[parentObject]
|
||||||
|
parentData = patch.added[parentId]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
tree:buildAncestryNodes(ancestryIds, patch, instanceMap)
|
||||||
|
|
||||||
|
-- Gather detail text
|
||||||
|
local changeList, hint = nil, nil
|
||||||
|
if next(change.Properties) then
|
||||||
|
changeList = {}
|
||||||
|
|
||||||
|
local hintBuffer, i = {}, 0
|
||||||
|
for prop, incoming in change.Properties do
|
||||||
|
i += 1
|
||||||
|
hintBuffer[i] = prop
|
||||||
|
|
||||||
|
local success, incomingValue = decodeValue(incoming, instanceMap)
|
||||||
|
if success then
|
||||||
|
table.insert(changeList, { prop, "N/A", incomingValue })
|
||||||
|
else
|
||||||
|
table.insert(changeList, { prop, "N/A", next(incoming) })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Finalize detail values
|
||||||
|
|
||||||
|
-- Trim hint to top 3
|
||||||
|
table.sort(hintBuffer)
|
||||||
|
if #hintBuffer > 3 then
|
||||||
|
hintBuffer = {
|
||||||
|
hintBuffer[1],
|
||||||
|
hintBuffer[2],
|
||||||
|
hintBuffer[3],
|
||||||
|
i - 3 .. " more",
|
||||||
|
}
|
||||||
|
end
|
||||||
|
hint = table.concat(hintBuffer, ", ")
|
||||||
|
|
||||||
|
-- Sort changes and add header
|
||||||
|
table.sort(changeList, function(a, b)
|
||||||
|
return a[1] < b[1]
|
||||||
|
end)
|
||||||
|
table.insert(changeList, 1, changeListHeaders)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Add this node to tree
|
||||||
|
tree:addNode(change.Parent, {
|
||||||
|
id = change.Id,
|
||||||
|
patchType = "Add",
|
||||||
|
className = change.ClassName,
|
||||||
|
name = change.Name,
|
||||||
|
hint = hint,
|
||||||
|
changeList = changeList,
|
||||||
|
instance = instanceMap.fromIds[id],
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
return tree
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Creates a deep copy of a tree for immutability purposes in Roact
|
||||||
|
function PatchTree.clone(tree)
|
||||||
|
if not tree then return end
|
||||||
|
|
||||||
|
local newTree = Tree.new()
|
||||||
|
tree:forEach(function(node)
|
||||||
|
newTree:addNode(node.parentId, table.clone(node))
|
||||||
|
end)
|
||||||
|
|
||||||
|
return newTree
|
||||||
|
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
|
||||||
|
function PatchTree.updateMetadata(tree, patch, instanceMap, unappliedPatch)
|
||||||
|
if tree then
|
||||||
|
tree = PatchTree.clone(tree)
|
||||||
|
else
|
||||||
|
tree = PatchTree.build(patch, instanceMap)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Update isWarning metadata
|
||||||
|
for _, failedChange in unappliedPatch.updated do
|
||||||
|
local node = tree:getNode(failedChange.id)
|
||||||
|
if node then
|
||||||
|
node.isWarning = true
|
||||||
|
Log.trace("Marked node as warning: {} {}", node.id, node.name)
|
||||||
|
|
||||||
|
if node.changeList then
|
||||||
|
for _, change in node.changeList do
|
||||||
|
if failedChange.changedProperties[change[1]] then
|
||||||
|
Log.trace(" Marked property as warning: {}", change[1])
|
||||||
|
if change[4] == nil then
|
||||||
|
change[4] = {}
|
||||||
|
end
|
||||||
|
change[4].isWarning = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Update if instances exist
|
||||||
|
tree:forEach(function(node)
|
||||||
|
if node.instance then
|
||||||
|
if node.instance.Parent == nil and node.instance ~= game then
|
||||||
|
-- This instance has been removed
|
||||||
|
Log.trace("Removed instance from node: {} {}", node.id, node.name)
|
||||||
|
node.instance = nil
|
||||||
|
end
|
||||||
|
else
|
||||||
|
-- This instance may have been added
|
||||||
|
node.instance = instanceMap.fromIds[node.id]
|
||||||
|
if node.instance then
|
||||||
|
Log.trace("Added instance to node: {} {}", node.id, node.name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
return tree
|
||||||
|
end
|
||||||
|
|
||||||
|
return PatchTree
|
||||||
|
|
@ -5,6 +5,8 @@
|
||||||
Patches can come from the server or be generated by the client.
|
Patches can come from the server or be generated by the client.
|
||||||
]]
|
]]
|
||||||
|
|
||||||
|
local ChangeHistoryService = game:GetService("ChangeHistoryService")
|
||||||
|
|
||||||
local Packages = script.Parent.Parent.Parent.Packages
|
local Packages = script.Parent.Parent.Parent.Packages
|
||||||
local Log = require(Packages.Log)
|
local Log = require(Packages.Log)
|
||||||
|
|
||||||
|
|
@ -17,6 +19,8 @@ local reify = require(script.Parent.reify)
|
||||||
local setProperty = require(script.Parent.setProperty)
|
local setProperty = require(script.Parent.setProperty)
|
||||||
|
|
||||||
local function applyPatch(instanceMap, patch)
|
local function applyPatch(instanceMap, patch)
|
||||||
|
local patchTimestamp = DateTime.now():FormatLocalTime("LTS", "en-us")
|
||||||
|
|
||||||
-- Tracks any portions of the patch that could not be applied to the DOM.
|
-- Tracks any portions of the patch that could not be applied to the DOM.
|
||||||
local unappliedPatch = PatchSet.newEmpty()
|
local unappliedPatch = PatchSet.newEmpty()
|
||||||
|
|
||||||
|
|
@ -199,6 +203,8 @@ local function applyPatch(instanceMap, patch)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
ChangeHistoryService:SetWaypoint("Rojo: Patch " .. patchTimestamp)
|
||||||
|
|
||||||
return unappliedPatch
|
return unappliedPatch
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,9 @@
|
||||||
and mutating the Roblox DOM.
|
and mutating the Roblox DOM.
|
||||||
]]
|
]]
|
||||||
|
|
||||||
|
local Packages = script.Parent.Parent.Packages
|
||||||
|
local Log = require(Packages.Log)
|
||||||
|
|
||||||
local applyPatch = require(script.applyPatch)
|
local applyPatch = require(script.applyPatch)
|
||||||
local hydrate = require(script.hydrate)
|
local hydrate = require(script.hydrate)
|
||||||
local diff = require(script.diff)
|
local diff = require(script.diff)
|
||||||
|
|
@ -14,13 +17,63 @@ function Reconciler.new(instanceMap)
|
||||||
local self = {
|
local self = {
|
||||||
-- Tracks all of the instances known by the reconciler by ID.
|
-- Tracks all of the instances known by the reconciler by ID.
|
||||||
__instanceMap = instanceMap,
|
__instanceMap = instanceMap,
|
||||||
|
__precommitCallbacks = {},
|
||||||
|
__postcommitCallbacks = {},
|
||||||
}
|
}
|
||||||
|
|
||||||
return setmetatable(self, Reconciler)
|
return setmetatable(self, Reconciler)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function Reconciler:hookPrecommit(callback: (patch: any, instanceMap: any) -> ()): () -> ()
|
||||||
|
table.insert(self.__precommitCallbacks, callback)
|
||||||
|
Log.trace("Added precommit callback: {}", callback)
|
||||||
|
|
||||||
|
return function()
|
||||||
|
-- Remove the callback from the list
|
||||||
|
for i, cb in self.__precommitCallbacks do
|
||||||
|
if cb == callback then
|
||||||
|
table.remove(self.__precommitCallbacks, i)
|
||||||
|
Log.trace("Removed precommit callback: {}", callback)
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Reconciler:hookPostcommit(callback: (patch: any, instanceMap: any, unappliedPatch: any) -> ()): () -> ()
|
||||||
|
table.insert(self.__postcommitCallbacks, callback)
|
||||||
|
Log.trace("Added postcommit callback: {}", callback)
|
||||||
|
|
||||||
|
return function()
|
||||||
|
-- Remove the callback from the list
|
||||||
|
for i, cb in self.__postcommitCallbacks do
|
||||||
|
if cb == callback then
|
||||||
|
table.remove(self.__postcommitCallbacks, i)
|
||||||
|
Log.trace("Removed postcommit callback: {}", callback)
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
function Reconciler:applyPatch(patch)
|
function Reconciler:applyPatch(patch)
|
||||||
return applyPatch(self.__instanceMap, patch)
|
for _, callback in self.__precommitCallbacks do
|
||||||
|
local success, err = pcall(callback, patch, self.__instanceMap)
|
||||||
|
if not success then
|
||||||
|
Log.warn("Precommit hook errored: {}", err)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local unappliedPatch = applyPatch(self.__instanceMap, patch)
|
||||||
|
|
||||||
|
for _, callback in self.__postcommitCallbacks do
|
||||||
|
local success, err = pcall(callback, patch, self.__instanceMap, unappliedPatch)
|
||||||
|
if not success then
|
||||||
|
Log.warn("Postcommit hook errored: {}", err)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return unappliedPatch
|
||||||
end
|
end
|
||||||
|
|
||||||
function Reconciler:hydrate(virtualInstances, rootId, rootInstance)
|
function Reconciler:hydrate(virtualInstances, rootId, rootInstance)
|
||||||
|
|
@ -31,4 +84,4 @@ function Reconciler:diff(virtualInstances, rootId)
|
||||||
return diff(self.__instanceMap, virtualInstances, rootId)
|
return diff(self.__instanceMap, virtualInstances, rootId)
|
||||||
end
|
end
|
||||||
|
|
||||||
return Reconciler
|
return Reconciler
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,6 @@ function ServeSession.new(options)
|
||||||
__instanceMap = instanceMap,
|
__instanceMap = instanceMap,
|
||||||
__changeBatcher = changeBatcher,
|
__changeBatcher = changeBatcher,
|
||||||
__statusChangedCallback = nil,
|
__statusChangedCallback = nil,
|
||||||
__patchAppliedCallback = nil,
|
|
||||||
__connections = connections,
|
__connections = connections,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -129,8 +128,12 @@ function ServeSession:setConfirmCallback(callback)
|
||||||
self.__userConfirmCallback = callback
|
self.__userConfirmCallback = callback
|
||||||
end
|
end
|
||||||
|
|
||||||
function ServeSession:onPatchApplied(callback)
|
function ServeSession:hookPrecommit(callback)
|
||||||
self.__patchAppliedCallback = callback
|
return self.__reconciler:hookPrecommit(callback)
|
||||||
|
end
|
||||||
|
|
||||||
|
function ServeSession:hookPostcommit(callback)
|
||||||
|
return self.__reconciler:hookPostcommit(callback)
|
||||||
end
|
end
|
||||||
|
|
||||||
function ServeSession:start()
|
function ServeSession:start()
|
||||||
|
|
@ -291,35 +294,45 @@ function ServeSession:__initialSync(serverInfo)
|
||||||
Log.warn("Could not apply all changes requested by the Rojo server:\n{}",
|
Log.warn("Could not apply all changes requested by the Rojo server:\n{}",
|
||||||
PatchSet.humanSummary(self.__instanceMap, unappliedPatch))
|
PatchSet.humanSummary(self.__instanceMap, unappliedPatch))
|
||||||
end
|
end
|
||||||
if self.__patchAppliedCallback then
|
|
||||||
pcall(self.__patchAppliedCallback, catchUpPatch, unappliedPatch)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
function ServeSession:__mainSyncLoop()
|
function ServeSession:__mainSyncLoop()
|
||||||
return self.__apiContext:retrieveMessages()
|
return Promise.new(function(resolve, reject)
|
||||||
:andThen(function(messages)
|
while self.__status == Status.Connected do
|
||||||
Log.trace("Serve session {} retrieved {} messages", tostring(self), #messages)
|
local success, result = self.__apiContext:retrieveMessages()
|
||||||
|
:andThen(function(messages)
|
||||||
|
if self.__status == Status.Disconnected then
|
||||||
|
-- In the time it took to retrieve messages, we disconnected
|
||||||
|
-- so we just resolve immediately without patching anything
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
for _, message in ipairs(messages) do
|
Log.trace("Serve session {} retrieved {} messages", tostring(self), #messages)
|
||||||
local unappliedPatch = self.__reconciler:applyPatch(message)
|
|
||||||
|
|
||||||
if not PatchSet.isEmpty(unappliedPatch) then
|
for _, message in messages do
|
||||||
Log.warn("Could not apply all changes requested by the Rojo server:\n{}",
|
local unappliedPatch = self.__reconciler:applyPatch(message)
|
||||||
PatchSet.humanSummary(self.__instanceMap, unappliedPatch))
|
|
||||||
end
|
|
||||||
|
|
||||||
if self.__patchAppliedCallback then
|
if not PatchSet.isEmpty(unappliedPatch) then
|
||||||
pcall(self.__patchAppliedCallback, message, unappliedPatch)
|
Log.warn("Could not apply all changes requested by the Rojo server:\n{}",
|
||||||
end
|
PatchSet.humanSummary(self.__instanceMap, unappliedPatch))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end):await()
|
||||||
|
|
||||||
|
if self.__status == Status.Disconnected then
|
||||||
|
-- If we are no longer connected after applying, we stop silently
|
||||||
|
-- without checking for errors as they are no longer relevant
|
||||||
|
break
|
||||||
|
elseif success == false then
|
||||||
|
reject(result)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
if self.__status ~= Status.Disconnected then
|
-- We are no longer connected, so we resolve the promise
|
||||||
return self:__mainSyncLoop()
|
resolve()
|
||||||
end
|
end)
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function ServeSession:__stopInternal(err)
|
function ServeSession:__stopInternal(err)
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,8 @@ use super::InstanceMetadata;
|
||||||
// - Replace use of Variant with a sum of Variant + borrowed value
|
// - Replace use of Variant with a sum of Variant + borrowed value
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct InstanceSnapshot {
|
pub struct InstanceSnapshot {
|
||||||
// FIXME: Don't use Option<Ref> anymore!
|
|
||||||
/// A temporary ID applied to the snapshot that's used for Ref properties.
|
/// A temporary ID applied to the snapshot that's used for Ref properties.
|
||||||
pub snapshot_id: Option<Ref>,
|
pub snapshot_id: Ref,
|
||||||
|
|
||||||
/// Rojo-specific metadata associated with the instance.
|
/// Rojo-specific metadata associated with the instance.
|
||||||
pub metadata: InstanceMetadata,
|
pub metadata: InstanceMetadata,
|
||||||
|
|
@ -42,7 +41,7 @@ pub struct InstanceSnapshot {
|
||||||
impl InstanceSnapshot {
|
impl InstanceSnapshot {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
snapshot_id: None,
|
snapshot_id: Ref::none(),
|
||||||
metadata: InstanceMetadata::default(),
|
metadata: InstanceMetadata::default(),
|
||||||
name: Cow::Borrowed("DEFAULT"),
|
name: Cow::Borrowed("DEFAULT"),
|
||||||
class_name: Cow::Borrowed("DEFAULT"),
|
class_name: Cow::Borrowed("DEFAULT"),
|
||||||
|
|
@ -88,7 +87,7 @@ impl InstanceSnapshot {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn snapshot_id(self, snapshot_id: Option<Ref>) -> Self {
|
pub fn snapshot_id(self, snapshot_id: Ref) -> Self {
|
||||||
Self {
|
Self {
|
||||||
snapshot_id,
|
snapshot_id,
|
||||||
..self
|
..self
|
||||||
|
|
@ -120,7 +119,7 @@ impl InstanceSnapshot {
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
snapshot_id: Some(id),
|
snapshot_id: id,
|
||||||
metadata: InstanceMetadata::default(),
|
metadata: InstanceMetadata::default(),
|
||||||
name: Cow::Owned(instance.name),
|
name: Cow::Owned(instance.name),
|
||||||
class_name: Cow::Owned(instance.class),
|
class_name: Cow::Owned(instance.class),
|
||||||
|
|
|
||||||
|
|
@ -135,7 +135,7 @@ fn apply_add_child(
|
||||||
context.has_refs_to_rewrite.insert(id);
|
context.has_refs_to_rewrite.insert(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(snapshot_id) = snapshot_id {
|
if snapshot_id.is_some() {
|
||||||
context.snapshot_id_to_instance_id.insert(snapshot_id, id);
|
context.snapshot_id_to_instance_id.insert(snapshot_id, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -231,7 +231,7 @@ mod test {
|
||||||
let root_id = tree.get_root_id();
|
let root_id = tree.get_root_id();
|
||||||
|
|
||||||
let snapshot = InstanceSnapshot {
|
let snapshot = InstanceSnapshot {
|
||||||
snapshot_id: None,
|
snapshot_id: Ref::none(),
|
||||||
metadata: Default::default(),
|
metadata: Default::default(),
|
||||||
name: Cow::Borrowed("Foo"),
|
name: Cow::Borrowed("Foo"),
|
||||||
class_name: Cow::Borrowed("Bar"),
|
class_name: Cow::Borrowed("Bar"),
|
||||||
|
|
|
||||||
|
|
@ -77,8 +77,10 @@ fn compute_patch_set_internal(
|
||||||
id: Ref,
|
id: Ref,
|
||||||
patch_set: &mut PatchSet,
|
patch_set: &mut PatchSet,
|
||||||
) {
|
) {
|
||||||
if let Some(snapshot_id) = snapshot.snapshot_id {
|
if snapshot.snapshot_id.is_some() {
|
||||||
context.snapshot_id_to_instance_id.insert(snapshot_id, id);
|
context
|
||||||
|
.snapshot_id_to_instance_id
|
||||||
|
.insert(snapshot.snapshot_id, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
let instance = tree
|
let instance = tree
|
||||||
|
|
@ -244,7 +246,7 @@ mod test {
|
||||||
// addition of a prop named Self, which is a self-referential Ref.
|
// addition of a prop named Self, which is a self-referential Ref.
|
||||||
let snapshot_id = Ref::new();
|
let snapshot_id = Ref::new();
|
||||||
let snapshot = InstanceSnapshot {
|
let snapshot = InstanceSnapshot {
|
||||||
snapshot_id: Some(snapshot_id),
|
snapshot_id: snapshot_id,
|
||||||
properties: hashmap! {
|
properties: hashmap! {
|
||||||
"Self".to_owned() => Variant::Ref(snapshot_id),
|
"Self".to_owned() => Variant::Ref(snapshot_id),
|
||||||
},
|
},
|
||||||
|
|
@ -286,13 +288,13 @@ mod test {
|
||||||
// This patch describes the existing instance with a new child added.
|
// This patch describes the existing instance with a new child added.
|
||||||
let snapshot_id = Ref::new();
|
let snapshot_id = Ref::new();
|
||||||
let snapshot = InstanceSnapshot {
|
let snapshot = InstanceSnapshot {
|
||||||
snapshot_id: Some(snapshot_id),
|
snapshot_id: snapshot_id,
|
||||||
children: vec![InstanceSnapshot {
|
children: vec![InstanceSnapshot {
|
||||||
properties: hashmap! {
|
properties: hashmap! {
|
||||||
"Self".to_owned() => Variant::Ref(snapshot_id),
|
"Self".to_owned() => Variant::Ref(snapshot_id),
|
||||||
},
|
},
|
||||||
|
|
||||||
snapshot_id: None,
|
snapshot_id: Ref::none(),
|
||||||
metadata: Default::default(),
|
metadata: Default::default(),
|
||||||
name: Cow::Borrowed("child"),
|
name: Cow::Borrowed("child"),
|
||||||
class_name: Cow::Borrowed("child"),
|
class_name: Cow::Borrowed("child"),
|
||||||
|
|
@ -311,7 +313,7 @@ mod test {
|
||||||
added_instances: vec![PatchAdd {
|
added_instances: vec![PatchAdd {
|
||||||
parent_id: root_id,
|
parent_id: root_id,
|
||||||
instance: InstanceSnapshot {
|
instance: InstanceSnapshot {
|
||||||
snapshot_id: None,
|
snapshot_id: Ref::none(),
|
||||||
metadata: Default::default(),
|
metadata: Default::default(),
|
||||||
properties: hashmap! {
|
properties: hashmap! {
|
||||||
"Self".to_owned() => Variant::Ref(root_id),
|
"Self".to_owned() => Variant::Ref(root_id),
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ use std::borrow::Cow;
|
||||||
use insta::assert_yaml_snapshot;
|
use insta::assert_yaml_snapshot;
|
||||||
use maplit::hashmap;
|
use maplit::hashmap;
|
||||||
|
|
||||||
|
use rbx_dom_weak::types::Ref;
|
||||||
use rojo_insta_ext::RedactionMap;
|
use rojo_insta_ext::RedactionMap;
|
||||||
|
|
||||||
use crate::snapshot::{compute_patch_set, InstanceSnapshot, RojoTree};
|
use crate::snapshot::{compute_patch_set, InstanceSnapshot, RojoTree};
|
||||||
|
|
@ -15,7 +16,7 @@ fn set_name_and_class_name() {
|
||||||
redactions.intern(tree.get_root_id());
|
redactions.intern(tree.get_root_id());
|
||||||
|
|
||||||
let snapshot = InstanceSnapshot {
|
let snapshot = InstanceSnapshot {
|
||||||
snapshot_id: None,
|
snapshot_id: Ref::none(),
|
||||||
metadata: Default::default(),
|
metadata: Default::default(),
|
||||||
name: Cow::Borrowed("Some Folder"),
|
name: Cow::Borrowed("Some Folder"),
|
||||||
class_name: Cow::Borrowed("Folder"),
|
class_name: Cow::Borrowed("Folder"),
|
||||||
|
|
@ -37,7 +38,7 @@ fn set_property() {
|
||||||
redactions.intern(tree.get_root_id());
|
redactions.intern(tree.get_root_id());
|
||||||
|
|
||||||
let snapshot = InstanceSnapshot {
|
let snapshot = InstanceSnapshot {
|
||||||
snapshot_id: None,
|
snapshot_id: Ref::none(),
|
||||||
metadata: Default::default(),
|
metadata: Default::default(),
|
||||||
name: Cow::Borrowed("ROOT"),
|
name: Cow::Borrowed("ROOT"),
|
||||||
class_name: Cow::Borrowed("ROOT"),
|
class_name: Cow::Borrowed("ROOT"),
|
||||||
|
|
@ -70,7 +71,7 @@ fn remove_property() {
|
||||||
}
|
}
|
||||||
|
|
||||||
let snapshot = InstanceSnapshot {
|
let snapshot = InstanceSnapshot {
|
||||||
snapshot_id: None,
|
snapshot_id: Ref::none(),
|
||||||
metadata: Default::default(),
|
metadata: Default::default(),
|
||||||
name: Cow::Borrowed("ROOT"),
|
name: Cow::Borrowed("ROOT"),
|
||||||
class_name: Cow::Borrowed("ROOT"),
|
class_name: Cow::Borrowed("ROOT"),
|
||||||
|
|
@ -92,13 +93,13 @@ fn add_child() {
|
||||||
redactions.intern(tree.get_root_id());
|
redactions.intern(tree.get_root_id());
|
||||||
|
|
||||||
let snapshot = InstanceSnapshot {
|
let snapshot = InstanceSnapshot {
|
||||||
snapshot_id: None,
|
snapshot_id: Ref::none(),
|
||||||
metadata: Default::default(),
|
metadata: Default::default(),
|
||||||
name: Cow::Borrowed("ROOT"),
|
name: Cow::Borrowed("ROOT"),
|
||||||
class_name: Cow::Borrowed("ROOT"),
|
class_name: Cow::Borrowed("ROOT"),
|
||||||
properties: Default::default(),
|
properties: Default::default(),
|
||||||
children: vec![InstanceSnapshot {
|
children: vec![InstanceSnapshot {
|
||||||
snapshot_id: None,
|
snapshot_id: Ref::none(),
|
||||||
metadata: Default::default(),
|
metadata: Default::default(),
|
||||||
name: Cow::Borrowed("New"),
|
name: Cow::Borrowed("New"),
|
||||||
class_name: Cow::Borrowed("Folder"),
|
class_name: Cow::Borrowed("Folder"),
|
||||||
|
|
@ -131,7 +132,7 @@ fn remove_child() {
|
||||||
}
|
}
|
||||||
|
|
||||||
let snapshot = InstanceSnapshot {
|
let snapshot = InstanceSnapshot {
|
||||||
snapshot_id: None,
|
snapshot_id: Ref::none(),
|
||||||
metadata: Default::default(),
|
metadata: Default::default(),
|
||||||
name: Cow::Borrowed("ROOT"),
|
name: Cow::Borrowed("ROOT"),
|
||||||
class_name: Cow::Borrowed("ROOT"),
|
class_name: Cow::Borrowed("ROOT"),
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ removed_instances: []
|
||||||
added_instances:
|
added_instances:
|
||||||
- parent_id: id-1
|
- parent_id: id-1
|
||||||
instance:
|
instance:
|
||||||
snapshot_id: ~
|
snapshot_id: "00000000000000000000000000000000"
|
||||||
metadata:
|
metadata:
|
||||||
ignore_unknown_instances: false
|
ignore_unknown_instances: false
|
||||||
relevant_paths: []
|
relevant_paths: []
|
||||||
|
|
@ -16,3 +16,4 @@ added_instances:
|
||||||
properties: {}
|
properties: {}
|
||||||
children: []
|
children: []
|
||||||
updated_instances: []
|
updated_instances: []
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use std::{borrow::Cow, collections::HashMap, path::Path, str};
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use memofs::Vfs;
|
use memofs::Vfs;
|
||||||
use rbx_dom_weak::types::Attributes;
|
use rbx_dom_weak::types::{Attributes, Ref};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
@ -112,7 +112,7 @@ impl JsonModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(InstanceSnapshot {
|
Ok(InstanceSnapshot {
|
||||||
snapshot_id: None,
|
snapshot_id: Ref::none(),
|
||||||
metadata: Default::default(),
|
metadata: Default::default(),
|
||||||
name: Cow::Owned(name),
|
name: Cow::Owned(name),
|
||||||
class_name: Cow::Owned(class_name),
|
class_name: Cow::Owned(class_name),
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ mod meta_file;
|
||||||
mod project;
|
mod project;
|
||||||
mod rbxm;
|
mod rbxm;
|
||||||
mod rbxmx;
|
mod rbxmx;
|
||||||
|
mod toml;
|
||||||
mod txt;
|
mod txt;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
|
|
@ -32,6 +33,7 @@ use self::{
|
||||||
project::snapshot_project,
|
project::snapshot_project,
|
||||||
rbxm::snapshot_rbxm,
|
rbxm::snapshot_rbxm,
|
||||||
rbxmx::snapshot_rbxmx,
|
rbxmx::snapshot_rbxmx,
|
||||||
|
toml::snapshot_toml,
|
||||||
txt::snapshot_txt,
|
txt::snapshot_txt,
|
||||||
util::PathExt,
|
util::PathExt,
|
||||||
};
|
};
|
||||||
|
|
@ -117,6 +119,8 @@ pub fn snapshot_from_vfs(
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
} else if path.file_name_ends_with(".json") {
|
} else if path.file_name_ends_with(".json") {
|
||||||
return snapshot_json(context, vfs, path);
|
return snapshot_json(context, vfs, path);
|
||||||
|
} else if path.file_name_ends_with(".toml") {
|
||||||
|
return snapshot_toml(context, vfs, path);
|
||||||
} else if let Ok(name) = csv_name {
|
} else if let Ok(name) = csv_name {
|
||||||
match name {
|
match name {
|
||||||
// init csv are handled elsewhere and should not turn into
|
// init csv are handled elsewhere and should not turn into
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use std::{borrow::Cow, collections::HashMap, path::Path};
|
||||||
|
|
||||||
use anyhow::{bail, Context};
|
use anyhow::{bail, Context};
|
||||||
use memofs::Vfs;
|
use memofs::Vfs;
|
||||||
use rbx_dom_weak::types::Attributes;
|
use rbx_dom_weak::types::{Attributes, Ref};
|
||||||
use rbx_reflection::ClassTag;
|
use rbx_reflection::ClassTag;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
@ -271,7 +271,7 @@ pub fn snapshot_project_node(
|
||||||
));
|
));
|
||||||
|
|
||||||
Ok(Some(InstanceSnapshot {
|
Ok(Some(InstanceSnapshot {
|
||||||
snapshot_id: None,
|
snapshot_id: Ref::none(),
|
||||||
name,
|
name,
|
||||||
class_name,
|
class_name,
|
||||||
properties,
|
properties,
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
---
|
---
|
||||||
source: src/snapshot_middleware/csv.rs
|
source: src/snapshot_middleware/csv.rs
|
||||||
expression: instance_snapshot
|
expression: instance_snapshot
|
||||||
|
|
||||||
---
|
---
|
||||||
snapshot_id: ~
|
snapshot_id: "00000000000000000000000000000000"
|
||||||
metadata:
|
metadata:
|
||||||
ignore_unknown_instances: false
|
ignore_unknown_instances: false
|
||||||
instigating_source:
|
instigating_source:
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
---
|
---
|
||||||
source: src/snapshot_middleware/csv.rs
|
source: src/snapshot_middleware/csv.rs
|
||||||
expression: instance_snapshot
|
expression: instance_snapshot
|
||||||
|
|
||||||
---
|
---
|
||||||
snapshot_id: ~
|
snapshot_id: "00000000000000000000000000000000"
|
||||||
metadata:
|
metadata:
|
||||||
ignore_unknown_instances: true
|
ignore_unknown_instances: true
|
||||||
instigating_source:
|
instigating_source:
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
---
|
---
|
||||||
source: src/snapshot_middleware/dir.rs
|
source: src/snapshot_middleware/dir.rs
|
||||||
assertion_line: 127
|
|
||||||
expression: instance_snapshot
|
expression: instance_snapshot
|
||||||
---
|
---
|
||||||
snapshot_id: ~
|
snapshot_id: "00000000000000000000000000000000"
|
||||||
metadata:
|
metadata:
|
||||||
ignore_unknown_instances: false
|
ignore_unknown_instances: false
|
||||||
instigating_source:
|
instigating_source:
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
---
|
---
|
||||||
source: src/snapshot_middleware/dir.rs
|
source: src/snapshot_middleware/dir.rs
|
||||||
assertion_line: 148
|
|
||||||
expression: instance_snapshot
|
expression: instance_snapshot
|
||||||
---
|
---
|
||||||
snapshot_id: ~
|
snapshot_id: "00000000000000000000000000000000"
|
||||||
metadata:
|
metadata:
|
||||||
ignore_unknown_instances: false
|
ignore_unknown_instances: false
|
||||||
instigating_source:
|
instigating_source:
|
||||||
|
|
@ -23,7 +22,7 @@ name: foo
|
||||||
class_name: Folder
|
class_name: Folder
|
||||||
properties: {}
|
properties: {}
|
||||||
children:
|
children:
|
||||||
- snapshot_id: ~
|
- snapshot_id: "00000000000000000000000000000000"
|
||||||
metadata:
|
metadata:
|
||||||
ignore_unknown_instances: false
|
ignore_unknown_instances: false
|
||||||
instigating_source:
|
instigating_source:
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
---
|
---
|
||||||
source: src/snapshot_middleware/json.rs
|
source: src/snapshot_middleware/json.rs
|
||||||
expression: instance_snapshot
|
expression: instance_snapshot
|
||||||
|
|
||||||
---
|
---
|
||||||
snapshot_id: ~
|
snapshot_id: "00000000000000000000000000000000"
|
||||||
metadata:
|
metadata:
|
||||||
ignore_unknown_instances: false
|
ignore_unknown_instances: false
|
||||||
instigating_source:
|
instigating_source:
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
---
|
---
|
||||||
source: src/snapshot_middleware/json_model.rs
|
source: src/snapshot_middleware/json_model.rs
|
||||||
expression: instance_snapshot
|
expression: instance_snapshot
|
||||||
|
|
||||||
---
|
---
|
||||||
snapshot_id: ~
|
snapshot_id: "00000000000000000000000000000000"
|
||||||
metadata:
|
metadata:
|
||||||
ignore_unknown_instances: false
|
ignore_unknown_instances: false
|
||||||
instigating_source:
|
instigating_source:
|
||||||
|
|
@ -17,7 +16,7 @@ properties:
|
||||||
Value:
|
Value:
|
||||||
Int64: 5
|
Int64: 5
|
||||||
children:
|
children:
|
||||||
- snapshot_id: ~
|
- snapshot_id: "00000000000000000000000000000000"
|
||||||
metadata:
|
metadata:
|
||||||
ignore_unknown_instances: false
|
ignore_unknown_instances: false
|
||||||
relevant_paths: []
|
relevant_paths: []
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
---
|
---
|
||||||
source: src/snapshot_middleware/json_model.rs
|
source: src/snapshot_middleware/json_model.rs
|
||||||
assertion_line: 186
|
|
||||||
expression: instance_snapshot
|
expression: instance_snapshot
|
||||||
---
|
---
|
||||||
snapshot_id: ~
|
snapshot_id: "00000000000000000000000000000000"
|
||||||
metadata:
|
metadata:
|
||||||
ignore_unknown_instances: false
|
ignore_unknown_instances: false
|
||||||
instigating_source:
|
instigating_source:
|
||||||
|
|
@ -17,7 +16,7 @@ properties:
|
||||||
Value:
|
Value:
|
||||||
Int64: 5
|
Int64: 5
|
||||||
children:
|
children:
|
||||||
- snapshot_id: ~
|
- snapshot_id: "00000000000000000000000000000000"
|
||||||
metadata:
|
metadata:
|
||||||
ignore_unknown_instances: false
|
ignore_unknown_instances: false
|
||||||
relevant_paths: []
|
relevant_paths: []
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
---
|
---
|
||||||
source: src/snapshot_middleware/lua.rs
|
source: src/snapshot_middleware/lua.rs
|
||||||
expression: instance_snapshot
|
expression: instance_snapshot
|
||||||
|
|
||||||
---
|
---
|
||||||
snapshot_id: ~
|
snapshot_id: "00000000000000000000000000000000"
|
||||||
metadata:
|
metadata:
|
||||||
ignore_unknown_instances: false
|
ignore_unknown_instances: false
|
||||||
instigating_source:
|
instigating_source:
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
---
|
---
|
||||||
source: src/snapshot_middleware/lua.rs
|
source: src/snapshot_middleware/lua.rs
|
||||||
expression: instance_snapshot
|
expression: instance_snapshot
|
||||||
|
|
||||||
---
|
---
|
||||||
snapshot_id: ~
|
snapshot_id: "00000000000000000000000000000000"
|
||||||
metadata:
|
metadata:
|
||||||
ignore_unknown_instances: false
|
ignore_unknown_instances: false
|
||||||
instigating_source:
|
instigating_source:
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
---
|
---
|
||||||
source: src/snapshot_middleware/lua.rs
|
source: src/snapshot_middleware/lua.rs
|
||||||
expression: instance_snapshot
|
expression: instance_snapshot
|
||||||
|
|
||||||
---
|
---
|
||||||
snapshot_id: ~
|
snapshot_id: "00000000000000000000000000000000"
|
||||||
metadata:
|
metadata:
|
||||||
ignore_unknown_instances: true
|
ignore_unknown_instances: true
|
||||||
instigating_source:
|
instigating_source:
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
---
|
---
|
||||||
source: src/snapshot_middleware/lua.rs
|
source: src/snapshot_middleware/lua.rs
|
||||||
expression: instance_snapshot
|
expression: instance_snapshot
|
||||||
|
|
||||||
---
|
---
|
||||||
snapshot_id: ~
|
snapshot_id: "00000000000000000000000000000000"
|
||||||
metadata:
|
metadata:
|
||||||
ignore_unknown_instances: false
|
ignore_unknown_instances: false
|
||||||
instigating_source:
|
instigating_source:
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
---
|
---
|
||||||
source: src/snapshot_middleware/lua.rs
|
source: src/snapshot_middleware/lua.rs
|
||||||
expression: instance_snapshot
|
expression: instance_snapshot
|
||||||
|
|
||||||
---
|
---
|
||||||
snapshot_id: ~
|
snapshot_id: "00000000000000000000000000000000"
|
||||||
metadata:
|
metadata:
|
||||||
ignore_unknown_instances: true
|
ignore_unknown_instances: true
|
||||||
instigating_source:
|
instigating_source:
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
---
|
---
|
||||||
source: src/snapshot_middleware/lua.rs
|
source: src/snapshot_middleware/lua.rs
|
||||||
expression: instance_snapshot
|
expression: instance_snapshot
|
||||||
|
|
||||||
---
|
---
|
||||||
snapshot_id: ~
|
snapshot_id: "00000000000000000000000000000000"
|
||||||
metadata:
|
metadata:
|
||||||
ignore_unknown_instances: false
|
ignore_unknown_instances: false
|
||||||
instigating_source:
|
instigating_source:
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
source: src/snapshot_middleware/project.rs
|
source: src/snapshot_middleware/project.rs
|
||||||
expression: instance_snapshot
|
expression: instance_snapshot
|
||||||
---
|
---
|
||||||
snapshot_id: ~
|
snapshot_id: "00000000000000000000000000000000"
|
||||||
metadata:
|
metadata:
|
||||||
ignore_unknown_instances: true
|
ignore_unknown_instances: true
|
||||||
instigating_source:
|
instigating_source:
|
||||||
|
|
@ -14,3 +14,4 @@ name: direct-project
|
||||||
class_name: Model
|
class_name: Model
|
||||||
properties: {}
|
properties: {}
|
||||||
children: []
|
children: []
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
---
|
---
|
||||||
source: src/snapshot_middleware/project.rs
|
source: src/snapshot_middleware/project.rs
|
||||||
expression: instance_snapshot
|
expression: instance_snapshot
|
||||||
|
|
||||||
---
|
---
|
||||||
snapshot_id: ~
|
snapshot_id: "00000000000000000000000000000000"
|
||||||
metadata:
|
metadata:
|
||||||
ignore_unknown_instances: true
|
ignore_unknown_instances: true
|
||||||
instigating_source:
|
instigating_source:
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
source: src/snapshot_middleware/project.rs
|
source: src/snapshot_middleware/project.rs
|
||||||
expression: instance_snapshot
|
expression: instance_snapshot
|
||||||
---
|
---
|
||||||
snapshot_id: ~
|
snapshot_id: "00000000000000000000000000000000"
|
||||||
metadata:
|
metadata:
|
||||||
ignore_unknown_instances: true
|
ignore_unknown_instances: true
|
||||||
instigating_source:
|
instigating_source:
|
||||||
|
|
@ -14,7 +14,7 @@ name: children
|
||||||
class_name: Folder
|
class_name: Folder
|
||||||
properties: {}
|
properties: {}
|
||||||
children:
|
children:
|
||||||
- snapshot_id: ~
|
- snapshot_id: "00000000000000000000000000000000"
|
||||||
metadata:
|
metadata:
|
||||||
ignore_unknown_instances: true
|
ignore_unknown_instances: true
|
||||||
instigating_source:
|
instigating_source:
|
||||||
|
|
@ -29,3 +29,4 @@ children:
|
||||||
class_name: Model
|
class_name: Model
|
||||||
properties: {}
|
properties: {}
|
||||||
children: []
|
children: []
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
source: src/snapshot_middleware/project.rs
|
source: src/snapshot_middleware/project.rs
|
||||||
expression: instance_snapshot
|
expression: instance_snapshot
|
||||||
---
|
---
|
||||||
snapshot_id: ~
|
snapshot_id: "00000000000000000000000000000000"
|
||||||
metadata:
|
metadata:
|
||||||
ignore_unknown_instances: true
|
ignore_unknown_instances: true
|
||||||
instigating_source:
|
instigating_source:
|
||||||
|
|
@ -15,3 +15,4 @@ name: path-project
|
||||||
class_name: Model
|
class_name: Model
|
||||||
properties: {}
|
properties: {}
|
||||||
children: []
|
children: []
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
source: src/snapshot_middleware/project.rs
|
source: src/snapshot_middleware/project.rs
|
||||||
expression: instance_snapshot
|
expression: instance_snapshot
|
||||||
---
|
---
|
||||||
snapshot_id: ~
|
snapshot_id: "00000000000000000000000000000000"
|
||||||
metadata:
|
metadata:
|
||||||
ignore_unknown_instances: true
|
ignore_unknown_instances: true
|
||||||
instigating_source:
|
instigating_source:
|
||||||
|
|
@ -15,7 +15,7 @@ name: path-child-project
|
||||||
class_name: Folder
|
class_name: Folder
|
||||||
properties: {}
|
properties: {}
|
||||||
children:
|
children:
|
||||||
- snapshot_id: ~
|
- snapshot_id: "00000000000000000000000000000000"
|
||||||
metadata:
|
metadata:
|
||||||
ignore_unknown_instances: true
|
ignore_unknown_instances: true
|
||||||
instigating_source:
|
instigating_source:
|
||||||
|
|
@ -30,3 +30,4 @@ children:
|
||||||
class_name: Model
|
class_name: Model
|
||||||
properties: {}
|
properties: {}
|
||||||
children: []
|
children: []
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
---
|
---
|
||||||
source: src/snapshot_middleware/project.rs
|
source: src/snapshot_middleware/project.rs
|
||||||
expression: instance_snapshot
|
expression: instance_snapshot
|
||||||
|
|
||||||
---
|
---
|
||||||
snapshot_id: ~
|
snapshot_id: "00000000000000000000000000000000"
|
||||||
metadata:
|
metadata:
|
||||||
ignore_unknown_instances: false
|
ignore_unknown_instances: false
|
||||||
instigating_source:
|
instigating_source:
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
---
|
---
|
||||||
source: src/snapshot_middleware/project.rs
|
source: src/snapshot_middleware/project.rs
|
||||||
expression: instance_snapshot
|
expression: instance_snapshot
|
||||||
|
|
||||||
---
|
---
|
||||||
snapshot_id: ~
|
snapshot_id: "00000000000000000000000000000000"
|
||||||
metadata:
|
metadata:
|
||||||
ignore_unknown_instances: true
|
ignore_unknown_instances: true
|
||||||
instigating_source:
|
instigating_source:
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
---
|
---
|
||||||
source: src/snapshot_middleware/project.rs
|
source: src/snapshot_middleware/project.rs
|
||||||
expression: instance_snapshot
|
expression: instance_snapshot
|
||||||
|
|
||||||
---
|
---
|
||||||
snapshot_id: ~
|
snapshot_id: "00000000000000000000000000000000"
|
||||||
metadata:
|
metadata:
|
||||||
ignore_unknown_instances: true
|
ignore_unknown_instances: true
|
||||||
instigating_source:
|
instigating_source:
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
---
|
||||||
|
source: src/snapshot_middleware/toml.rs
|
||||||
|
expression: instance_snapshot
|
||||||
|
---
|
||||||
|
snapshot_id: "00000000000000000000000000000000"
|
||||||
|
metadata:
|
||||||
|
ignore_unknown_instances: false
|
||||||
|
instigating_source:
|
||||||
|
Path: /foo.toml
|
||||||
|
relevant_paths:
|
||||||
|
- /foo.toml
|
||||||
|
- /foo.meta.json
|
||||||
|
context: {}
|
||||||
|
name: foo
|
||||||
|
class_name: ModuleScript
|
||||||
|
properties:
|
||||||
|
Source:
|
||||||
|
String: "return {\n\t[\"1invalidident\"] = \"nice\",\n\tarray = {1, 2, 3},\n\tdates = {\n\t\tlocaldate = \"1979-05-27\",\n\t\tlocaldatetime = \"1979-05-27T07:32:00\",\n\t\tlocaltime = \"00:32:00.999999\",\n\t\toffset1 = \"1979-05-27T00:32:00.999999-07:00\",\n\t\toffset2 = \"1979-05-27T07:32:00Z\",\n\t},\n\t[\"false\"] = false,\n\tfloat = 1234.5452,\n\tint = 1234,\n\tobject = {\n\t\thello = \"world\",\n\t},\n\t[\"true\"] = true,\n}"
|
||||||
|
children: []
|
||||||
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
---
|
---
|
||||||
source: src/snapshot_middleware/txt.rs
|
source: src/snapshot_middleware/txt.rs
|
||||||
expression: instance_snapshot
|
expression: instance_snapshot
|
||||||
|
|
||||||
---
|
---
|
||||||
snapshot_id: ~
|
snapshot_id: "00000000000000000000000000000000"
|
||||||
metadata:
|
metadata:
|
||||||
ignore_unknown_instances: false
|
ignore_unknown_instances: false
|
||||||
instigating_source:
|
instigating_source:
|
||||||
|
|
|
||||||
123
src/snapshot_middleware/toml.rs
Normal file
123
src/snapshot_middleware/toml.rs
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
use maplit::hashmap;
|
||||||
|
use memofs::{IoResultExt, Vfs};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
lua_ast::{Expression, Statement},
|
||||||
|
snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{meta_file::AdjacentMetadata, util::PathExt};
|
||||||
|
|
||||||
|
pub fn snapshot_toml(
|
||||||
|
context: &InstanceContext,
|
||||||
|
vfs: &Vfs,
|
||||||
|
path: &Path,
|
||||||
|
) -> anyhow::Result<Option<InstanceSnapshot>> {
|
||||||
|
let name = path.file_name_trim_end(".toml")?;
|
||||||
|
let contents = vfs.read(path)?;
|
||||||
|
|
||||||
|
let value: toml::Value = toml::from_slice(&contents)
|
||||||
|
.with_context(|| format!("File contains malformed TOML: {}", path.display()))?;
|
||||||
|
|
||||||
|
let as_lua = toml_to_lua(value).to_string();
|
||||||
|
|
||||||
|
let properties = hashmap! {
|
||||||
|
"Source".to_owned() => as_lua.into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let meta_path = path.with_file_name(format!("{}.meta.json", name));
|
||||||
|
|
||||||
|
let mut snapshot = InstanceSnapshot::new()
|
||||||
|
.name(name)
|
||||||
|
.class_name("ModuleScript")
|
||||||
|
.properties(properties)
|
||||||
|
.metadata(
|
||||||
|
InstanceMetadata::new()
|
||||||
|
.instigating_source(path)
|
||||||
|
.relevant_paths(vec![path.to_path_buf(), meta_path.clone()])
|
||||||
|
.context(context),
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(meta_contents) = vfs.read(&meta_path).with_not_found()? {
|
||||||
|
let mut metadata = AdjacentMetadata::from_slice(&meta_contents, meta_path)?;
|
||||||
|
metadata.apply_all(&mut snapshot)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Some(snapshot))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toml_to_lua(value: toml::Value) -> Statement {
|
||||||
|
Statement::Return(toml_to_lua_value(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toml_to_lua_value(value: toml::Value) -> Expression {
|
||||||
|
use toml::Value;
|
||||||
|
|
||||||
|
match value {
|
||||||
|
Value::Datetime(value) => Expression::String(value.to_string()),
|
||||||
|
Value::Boolean(value) => Expression::Bool(value),
|
||||||
|
Value::Float(value) => Expression::Number(value),
|
||||||
|
Value::Integer(value) => Expression::Number(value as f64),
|
||||||
|
Value::String(value) => Expression::String(value),
|
||||||
|
Value::Array(values) => {
|
||||||
|
Expression::Array(values.into_iter().map(toml_to_lua_value).collect())
|
||||||
|
}
|
||||||
|
Value::Table(values) => Expression::table(
|
||||||
|
values
|
||||||
|
.into_iter()
|
||||||
|
.map(|(key, value)| (key.into(), toml_to_lua_value(value)))
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
use memofs::{InMemoryFs, VfsSnapshot};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn instance_from_vfs() {
|
||||||
|
let mut imfs = InMemoryFs::new();
|
||||||
|
imfs.load_snapshot(
|
||||||
|
"/foo.toml",
|
||||||
|
VfsSnapshot::file(
|
||||||
|
r#"
|
||||||
|
array = [1, 2, 3]
|
||||||
|
true = true
|
||||||
|
false = false
|
||||||
|
int = 1234
|
||||||
|
float = 1234.5452
|
||||||
|
"1invalidident" = "nice"
|
||||||
|
|
||||||
|
[object]
|
||||||
|
hello = "world"
|
||||||
|
|
||||||
|
[dates]
|
||||||
|
offset1 = 1979-05-27T00:32:00.999999-07:00
|
||||||
|
offset2 = 1979-05-27 07:32:00Z
|
||||||
|
localdatetime = 1979-05-27T07:32:00
|
||||||
|
localdate = 1979-05-27
|
||||||
|
localtime = 00:32:00.999999
|
||||||
|
"#,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut vfs = Vfs::new(imfs.clone());
|
||||||
|
|
||||||
|
let instance_snapshot = snapshot_toml(
|
||||||
|
&InstanceContext::default(),
|
||||||
|
&mut vfs,
|
||||||
|
Path::new("/foo.toml"),
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
insta::assert_yaml_snapshot!(instance_snapshot);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue