From ea6048b73ece221088bf430826136e0c1db98ff4 Mon Sep 17 00:00:00 2001 From: Marin Minnerly Date: Wed, 18 Dec 2024 20:07:43 -0800 Subject: [PATCH] Display unavailable storybooks --- img/Alert.png | Bin 0 -> 409 bytes src/Common/CodeBlock.luau | 79 +++++++++++++++++ src/Navigation/Screen.luau | 9 +- src/Panels/Sidebar.luau | 8 +- src/Plugin/PluginApp.luau | 20 +++-- src/Storybook/StoryError.luau | 27 +++++- src/Storybook/StorybookError.luau | 112 ++++++++++++++++++++++++ src/Storybook/StorybookError.story.luau | 31 +++++++ src/Storybook/StorybookTreeView.luau | 93 +++++++++++++++----- src/TreeView/types.luau | 3 +- src/TreeView/useTreeNodeIcon.luau | 4 +- 11 files changed, 345 insertions(+), 41 deletions(-) create mode 100644 img/Alert.png create mode 100644 src/Common/CodeBlock.luau create mode 100644 src/Storybook/StorybookError.luau create mode 100644 src/Storybook/StorybookError.story.luau diff --git a/img/Alert.png b/img/Alert.png new file mode 100644 index 0000000000000000000000000000000000000000..23743d46efa0e16ce0ab6fe7a7df028d3aab857d GIT binary patch literal 409 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjoCO|{#S9GG!XV7ZFl!D-1!HlL zyA#8@b22Z19F}xPUq=Rpjs4tz5?O(AdY&$hAr*{QLj!paIq%=9GR1ylpp^W_R-{f?ok`VRO0D5u|sPj&pua&W4V8Fiq}1v_(M)ATdV)> ziDcWYlAjsfU0(^Uy;^do)Kg{qy%jH`TV#Hyv75?r_m?eQmY-?ub3aXK?_Q(SV!g!~ zH+}_Wt~(RpUK;&tzKcc0%mVIv2aF6V>zl (), - storybooks: { LoadedStorybook }, + onShowErrorPage: (unavailableStorybook: UnavailableStorybook) -> (), + storybooks: { + avialable: { LoadedStorybook }, + unavailable: { UnavailableStorybook }, + }, } local function Sidebar(props: Props) @@ -82,6 +87,7 @@ local function Sidebar(props: Props) searchTerm = search, storybooks = props.storybooks, onStoryChanged = props.onStoryChanged, + onShowErrorPage = props.onShowErrorPage, }), }), }) diff --git a/src/Plugin/PluginApp.luau b/src/Plugin/PluginApp.luau index 53877344..f57c9e0b 100644 --- a/src/Plugin/PluginApp.luau +++ b/src/Plugin/PluginApp.luau @@ -1,4 +1,3 @@ -local ModuleLoader = require("@pkg/ModuleLoader") local React = require("@pkg/React") local Storyteller = require("@pkg/Storyteller") @@ -15,6 +14,7 @@ local useTheme = require("@root/Common/useTheme") local TOPBAR_HEIGHT_PX = 32 type LoadedStorybook = Storyteller.LoadedStorybook +type UnavailableStorybook = Storyteller.UnavailableStorybook export type Props = { loader: ModuleLoader.ModuleLoader, @@ -26,6 +26,7 @@ local function App(props: Props) local storybooks = Storyteller.useStorybooks(game, props.loader) local storyModule: ModuleScript?, setStoryModule = React.useState(nil :: ModuleScript?) local storybook, setStorybook = React.useState(nil :: LoadedStorybook?) + local unavailableStorybook, setUnavailableStorybook = React.useState(nil :: LoadedStorybook?) local initialSidebarWidth = settingsContext.getSetting("sidebarWidth") local sidebarWidth, setSidebarWidth = React.useState(initialSidebarWidth) local navigation = NavigationContext.use() @@ -33,13 +34,17 @@ local function App(props: Props) local onStoryChanged = React.useCallback(function(newStoryModule: ModuleScript?, newStorybook: LoadedStorybook?) navigation.navigateTo("Home") - setStoryModule(function(prev: ModuleScript?) - return if prev ~= newStoryModule then newStoryModule else nil - end) - + setUnavailableStorybook(nil) + setStoryModule(newStoryModule) setStorybook(newStorybook) end, { navigation.navigateTo } :: { unknown }) + local onShowErrorPage = React.useCallback(function(newUnavailableStorybook: UnavailableStorybook) + setStoryModule(nil) + setStorybook(nil) + setUnavailableStorybook(newUnavailableStorybook) + end, {}) + local onSidebarResized = React.useCallback(function(newSize: Vector2) setSidebarWidth(newSize.X) end, {}) @@ -67,7 +72,8 @@ local function App(props: Props) }, { Sidebar = React.createElement(Sidebar, { onStoryChanged = onStoryChanged, - storybooks = storybooks.available, + onShowErrorPage = onShowErrorPage, + storybooks = storybooks, }), }), @@ -91,9 +97,9 @@ local function App(props: Props) BackgroundTransparency = 1, }, { Screen = React.createElement(Screen, { - loader = props.loader, story = storyModule, storybook = storybook, + unavailableStorybook = unavailableStorybook, }), }), }), diff --git a/src/Storybook/StoryError.luau b/src/Storybook/StoryError.luau index 97878ad7..646c5213 100644 --- a/src/Storybook/StoryError.luau +++ b/src/Storybook/StoryError.luau @@ -1,5 +1,7 @@ local React = require("@pkg/React") -local SelectableTextLabel = require("@root/Forms/SelectableTextLabel") + +local CodeBlock = require("@root/Common/CodeBlock") +local ScrollingFrame = require("@root/Common/ScrollingFrame") local useTheme = require("@root/Common/useTheme") export type Props = { @@ -10,10 +12,27 @@ export type Props = { local function StoryError(props: Props) local theme = useTheme() - return React.createElement(SelectableTextLabel, { + return React.createElement(ScrollingFrame, { + ScrollingDirection = Enum.ScrollingDirection.XY, LayoutOrder = props.layoutOrder, - Text = props.err, - TextColor3 = theme.alert, + }, { + Layout = React.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Horizontal, + Padding = theme.padding, + }), + + Padding = React.createElement("UIPadding", { + PaddingTop = theme.paddingSmall, + PaddingRight = theme.paddingSmall, + PaddingBottom = theme.paddingSmall, + PaddingLeft = theme.paddingSmall, + }), + + CodeBlock = React.createElement(CodeBlock, { + source = props.err, + sourceColor = theme.alert, + }), }) end diff --git a/src/Storybook/StorybookError.luau b/src/Storybook/StorybookError.luau new file mode 100644 index 00000000..952ef40f --- /dev/null +++ b/src/Storybook/StorybookError.luau @@ -0,0 +1,112 @@ +local React = require("@pkg/React") +local Sift = require("@pkg/Sift") +local Storyteller = require("@pkg/Storyteller") + +local CodeBlock = require("@root/Common/CodeBlock") +local ScrollingFrame = require("@root/Common/ScrollingFrame") +local nextLayoutOrder = require("@root/Common/nextLayoutOrder") +local useTheme = require("@root/Common/useTheme") + +type UnavailableStorybook = Storyteller.UnavailableStorybook + +export type Props = { + unavailableStorybook: UnavailableStorybook, + layoutOrder: number?, +} + +local function StoryError(props: Props) + local theme = useTheme() + + local storybookSource = props.unavailableStorybook.storybook.source.Source + + return React.createElement(ScrollingFrame, { + ScrollingDirection = Enum.ScrollingDirection.XY, + LayoutOrder = props.layoutOrder, + }, { + Layout = React.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Vertical, + Padding = theme.paddingLarge, + }), + + Padding = React.createElement("UIPadding", { + PaddingTop = theme.paddingLarge, + PaddingRight = theme.paddingLarge, + PaddingBottom = theme.paddingLarge, + PaddingLeft = theme.paddingLarge, + }), + + MainText = React.createElement("TextLabel", { + LayoutOrder = nextLayoutOrder(), + Text = `Failed to load {props.unavailableStorybook.storybook.name}`, + AutomaticSize = Enum.AutomaticSize.XY, + BackgroundTransparency = 1, + Font = theme.font, + TextColor3 = theme.text, + TextSize = theme.textSize, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Center, + }), + + Problem = React.createElement("Frame", { + AutomaticSize = Enum.AutomaticSize.XY, + BackgroundTransparency = 1, + LayoutOrder = nextLayoutOrder(), + }, { + Layout = React.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Vertical, + Padding = theme.padding, + }), + + Title = React.createElement("TextLabel", { + LayoutOrder = nextLayoutOrder(), + Text = "Error", + AutomaticSize = Enum.AutomaticSize.XY, + BackgroundTransparency = 1, + Font = theme.headerFont, + TextColor3 = theme.text, + TextSize = theme.headerTextSize, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Center, + }), + + CodeBlock = React.createElement(CodeBlock, { + source = props.unavailableStorybook.problem, + sourceColor = theme.alert, + layoutOrder = nextLayoutOrder(), + }), + }), + + StorybookSource = React.createElement("Frame", { + AutomaticSize = Enum.AutomaticSize.XY, + BackgroundTransparency = 1, + LayoutOrder = nextLayoutOrder(), + }, { + Layout = React.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Vertical, + Padding = theme.padding, + }), + + Title = React.createElement("TextLabel", { + LayoutOrder = nextLayoutOrder(), + Text = "Storybook Source", + AutomaticSize = Enum.AutomaticSize.XY, + BackgroundTransparency = 1, + Font = theme.headerFont, + TextColor3 = theme.text, + TextSize = theme.headerTextSize, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Center, + }), + + CodeBlock = React.createElement(CodeBlock, { + source = storybookSource, + layoutOrder = nextLayoutOrder(), + }), + }), + }) +end + +return StoryError diff --git a/src/Storybook/StorybookError.story.luau b/src/Storybook/StorybookError.story.luau new file mode 100644 index 00000000..7f42759b --- /dev/null +++ b/src/Storybook/StorybookError.story.luau @@ -0,0 +1,31 @@ +local React = require("@pkg/React") +local Storyteller = require("@pkg/Storyteller") + +local ContextProviders = require("@root/Common/ContextProviders") +local MockPlugin = require("@root/Testing/MockPlugin") +local StorybookError = require("@root/Storybook/StorybookError") + +type UnavailableStorybook = Storyteller.UnavailableStorybook + +return { + summary = "Component for displaying error messages to the user", + story = function() + local storybookModule = script.Parent.Parent["init.storybook"] + local unavailableStorybook: UnavailableStorybook = { + problem = "Something went wrong!", + storybook = { + name = storybookModule.Name, + source = storybookModule, + loader = {} :: any, + }, + } + + return React.createElement(ContextProviders, { + plugin = MockPlugin.new() :: any, + }, { + StorybookError = React.createElement(StorybookError, { + unavailableStorybook = unavailableStorybook, + }), + }) + end, +} diff --git a/src/Storybook/StorybookTreeView.luau b/src/Storybook/StorybookTreeView.luau index 13a3c4e5..7dce4083 100644 --- a/src/Storybook/StorybookTreeView.luau +++ b/src/Storybook/StorybookTreeView.luau @@ -1,3 +1,5 @@ +local HttpService = game:GetService("HttpService") + local React = require("@pkg/React") local Storyteller = require("@pkg/Storyteller") local TreeView = require("@root/TreeView") @@ -8,14 +10,19 @@ local usePrevious = require("@root/Common/usePrevious") type TreeNode = TreeView.TreeNode type LoadedStorybook = Storyteller.LoadedStorybook +type UnavailableStorybook = Storyteller.UnavailableStorybook local useEffect = React.useEffect local useRef = React.useRef export type Props = { searchTerm: string?, - storybooks: { LoadedStorybook }, + storybooks: { + avialable: { LoadedStorybook }, + unavailable: { UnavailableStorybook }, + }, onStoryChanged: ((storyModule: ModuleScript?, storybook: LoadedStorybook?) -> ())?, + onShowErrorPage: ((unavailableStorybook: UnavailableStorybook) -> ())?, layoutOrder: number?, } @@ -25,22 +32,48 @@ local function StorybookTreeView(props: Props) local selectedNode = treeViewContext.getSelectedNode() local prevSelectedNode = usePrevious(selectedNode) local storybookByNodeId = useRef({} :: { [string]: LoadedStorybook }) + local unavailableStorybookByNodeId = useRef({} :: { [string]: UnavailableStorybook }) local lastOpenedStory, setLastOpenedStory = useLastOpenedStory() useEffect(function() - storybookByNodeId.current = {} local roots: { TreeNode } = {} - for _, storybook in props.storybooks do + + for _, storybook in props.storybooks.available do local root = createTreeNodesForStorybook(storybook) table.insert(roots, root) storybookByNodeId.current[root.id] = storybook end + + if #props.storybooks.unavailable > 0 then + local unavailableStorybooks: TreeNode = { + id = HttpService:GenerateGUID(), + label = "Unavailable Storybooks", + icon = "folder", + isExpanded = false, + children = {}, + } + + for _, unavailableStorybook in props.storybooks.unavailable do + local root = { + id = HttpService:GenerateGUID(), + label = unavailableStorybook.storybook.name, + icon = "alert", + isExpanded = false, + children = {}, + } + table.insert(unavailableStorybooks.children, root) + unavailableStorybookByNodeId.current[root.id] = unavailableStorybook + end + + table.insert(roots, unavailableStorybooks) + end + treeViewContext.setRoots(roots) return function() treeViewContext.setRoots({}) end - end, { props.storybooks, treeViewContext.setRoots } :: { unknown }) + end, { props.storybooks.available, props.storybooks.unavailable, treeViewContext.setRoots } :: { unknown }) useEffect(function() treeViewContext.search(props.searchTerm) @@ -62,28 +95,42 @@ local function StorybookTreeView(props: Props) end end, { lastOpenedStory, treeViewContext.getNodeByInstance, treeViewContext.activateNode } :: { unknown }) - useEffect(function() - if props.onStoryChanged and selectedNode ~= prevSelectedNode then - if selectedNode then - if - selectedNode.icon == TreeView.TreeNodeIcon.Story - and selectedNode.instance - and selectedNode.instance:IsA("ModuleScript") - then - local ancestry = TreeView.getAncestry(selectedNode) - local root = ancestry[#ancestry] - local storybook = storybookByNodeId.current[root.id] - - if storybook then - props.onStoryChanged(selectedNode.instance, storybook) - setLastOpenedStory(selectedNode.instance) + useEffect( + function() + if selectedNode ~= prevSelectedNode then + if props.onStoryChanged then + if selectedNode then + if + selectedNode.icon == TreeView.TreeNodeIcon.Story + and selectedNode.instance + and selectedNode.instance:IsA("ModuleScript") + then + local ancestry = TreeView.getAncestry(selectedNode) + local root = ancestry[#ancestry] + local storybook = storybookByNodeId.current[root.id] + + if storybook then + props.onStoryChanged(selectedNode.instance, storybook) + setLastOpenedStory(selectedNode.instance) + end + end + else + props.onStoryChanged(nil, nil) + end + end + + if props.onShowErrorPage then + if selectedNode and selectedNode.icon == TreeView.TreeNodeIcon.Alert then + local unavailableStorybook = unavailableStorybookByNodeId.current[selectedNode.id] + if unavailableStorybook then + props.onShowErrorPage(unavailableStorybook) + end end end - else - props.onStoryChanged(nil, nil) end - end - end, { props.onStoryChanged, selectedNode, prevSelectedNode, setLastOpenedStory } :: { unknown }) + end, + { props.onShowErrorPage, props.onStoryChanged, selectedNode, prevSelectedNode, setLastOpenedStory } :: { unknown } + ) return React.createElement(TreeView.TreeView, { layoutOrder = props.layoutOrder, diff --git a/src/TreeView/types.luau b/src/TreeView/types.luau index f84293fc..08f0c0d1 100644 --- a/src/TreeView/types.luau +++ b/src/TreeView/types.luau @@ -1,12 +1,13 @@ local types = {} -export type TreeNodeIcon = "none" | "story" | "storybook" | "folder" +export type TreeNodeIcon = "none" | "story" | "storybook" | "folder" | "alert" types.TreeNodeIcon = { None = "none" :: "none", Story = "story" :: "story", Storybook = "storybook" :: "storybook", Folder = "folder" :: "folder", + Alert = "alert" :: "alert", } export type PartialTreeNode = { diff --git a/src/TreeView/useTreeNodeIcon.luau b/src/TreeView/useTreeNodeIcon.luau index 92525320..f8ff2cda 100644 --- a/src/TreeView/useTreeNodeIcon.luau +++ b/src/TreeView/useTreeNodeIcon.luau @@ -1,6 +1,6 @@ local assets = require("@root/assets") -local useTheme = require("@root/Common/useTheme") local types = require("./types") +local useTheme = require("@root/Common/useTheme") type TreeNodeIcon = types.TreeNodeIcon @@ -19,6 +19,8 @@ local function useTreeNodeIcon(icon: TreeNodeIcon): (Sprite, Color3) return assets.Storybook, theme.textFaded elseif icon == types.TreeNodeIcon.Folder then return assets.Folder, theme.directory + elseif icon == types.TreeNodeIcon.Alert then + return assets.Alert, theme.alert else return assets.Folder, theme.textFaded end