Skip to content

Commit e465854

Browse files
committed
refactor: portal sidebar (#602)
* refactor: sidebar component * refactor: sidebar api
1 parent cda99eb commit e465854

File tree

9 files changed

+170
-150
lines changed

9 files changed

+170
-150
lines changed

internal/portal/src/app.tsx

Lines changed: 41 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import ErrorBoundary from "./common/ErrorBoundary/ErrorBoundary";
1010
import CONFIGS from "./config";
1111
import Destination from "./scenes/Destination/Destination";
1212
import { ToastProvider } from "./common/Toast/Toast";
13+
import { SidebarProvider } from "./common/Sidebar/Sidebar";
1314
import CreateDestination from "./scenes/CreateDestination/CreateDestination";
1415

1516
type ApiClient = {
@@ -87,45 +88,47 @@ export function App() {
8788
}}
8889
>
8990
<ToastProvider>
90-
<div className="layout">
91-
<ErrorBoundary>
92-
{tenant ? (
93-
<ApiContext.Provider value={apiClient}>
94-
<SWRConfig
95-
value={{
96-
fetcher: (path: string) => apiClient.fetch(path),
97-
}}
98-
>
99-
<Routes>
100-
<Route path="/" Component={DestinationList} />
101-
<Route path="/new/*" Component={CreateDestination} />
102-
<Route
103-
path="/destinations/:destination_id/*"
104-
Component={Destination}
105-
/>
106-
<Route path="*" Component={NotFound} />
107-
</Routes>
108-
</SWRConfig>
109-
</ApiContext.Provider>
110-
) : (
111-
<div>
112-
<Loading />
113-
</div>
114-
)}
115-
</ErrorBoundary>
116-
</div>
117-
{!CONFIGS.DISABLE_OUTPOST_BRANDING && (
118-
<div className="powered-by subtitle-s">
119-
Powered by{" "}
120-
<a
121-
href="https://github.com/hookdeck/outpost"
122-
target="_blank"
123-
rel="noreferrer"
124-
>
125-
Outpost
126-
</a>
91+
<SidebarProvider>
92+
<div className="layout">
93+
<ErrorBoundary>
94+
{tenant ? (
95+
<ApiContext.Provider value={apiClient}>
96+
<SWRConfig
97+
value={{
98+
fetcher: (path: string) => apiClient.fetch(path),
99+
}}
100+
>
101+
<Routes>
102+
<Route path="/" Component={DestinationList} />
103+
<Route path="/new/*" Component={CreateDestination} />
104+
<Route
105+
path="/destinations/:destination_id/*"
106+
Component={Destination}
107+
/>
108+
<Route path="*" Component={NotFound} />
109+
</Routes>
110+
</SWRConfig>
111+
</ApiContext.Provider>
112+
) : (
113+
<div>
114+
<Loading />
115+
</div>
116+
)}
117+
</ErrorBoundary>
127118
</div>
128-
)}
119+
{!CONFIGS.DISABLE_OUTPOST_BRANDING && (
120+
<div className="powered-by subtitle-s">
121+
Powered by{" "}
122+
<a
123+
href="https://github.com/hookdeck/outpost"
124+
target="_blank"
125+
rel="noreferrer"
126+
>
127+
Outpost
128+
</a>
129+
</div>
130+
)}
131+
</SidebarProvider>
129132
</ToastProvider>
130133
</BrowserRouter>
131134
);
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { DestinationTypeReference } from "../../typings/Destination";
2+
import Markdown from "react-markdown";
3+
4+
export const ConfigurationGuide = ({
5+
type,
6+
}: {
7+
type: DestinationTypeReference;
8+
}) => {
9+
return <Markdown>{type.instructions}</Markdown>;
10+
};
11+
12+
export default ConfigurationGuide;

internal/portal/src/common/ConfigurationModal/ConfigurationModal.tsx

Lines changed: 0 additions & 50 deletions
This file was deleted.

internal/portal/src/common/DestinationConfigFields/DestinationConfigFields.tsx

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import "./DestinationConfigFields.scss";
77
import { EditIcon, HelpIcon, CloseIcon } from "../Icons";
88
import Tooltip from "../Tooltip/Tooltip";
99
import Button from "../Button/Button";
10-
import ConfigurationModal from "../ConfigurationModal/ConfigurationModal";
10+
import { useSidebar } from "../Sidebar/Sidebar";
11+
import { ConfigurationGuide } from "../ConfigurationGuide/ConfigurationGuide";
1112
import { Checkbox } from "../Checkbox/Checkbox";
1213
import KeyValueMapField from "../KeyValueMapField/KeyValueMapField";
1314
import { isCheckedValue } from "../../utils/formHelper";
@@ -28,7 +29,7 @@ const DestinationConfigFields = ({
2829
const [lastUnlockedSensitiveField, setLastUnlockedSensitiveField] = useState<
2930
string | null
3031
>(null);
31-
const [showConfigModal, setShowConfigModal] = useState(false);
32+
const sidebar = useSidebar();
3233

3334
const inputRefs = useRef<Record<string, HTMLInputElement>>({});
3435

@@ -69,7 +70,9 @@ const DestinationConfigFields = ({
6970
<>
7071
{type.instructions && (
7172
<Button
72-
onClick={() => setShowConfigModal(!showConfigModal)}
73+
onClick={() =>
74+
sidebar.toggle("configuration", <ConfigurationGuide type={type} />)
75+
}
7376
className="config-guide-button"
7477
>
7578
<HelpIcon />
@@ -220,12 +223,6 @@ const DestinationConfigFields = ({
220223
</div>
221224
))}
222225

223-
{showConfigModal && (
224-
<ConfigurationModal
225-
type={type}
226-
onClose={() => setShowConfigModal(false)}
227-
/>
228-
)}
229226
</>
230227
);
231228
};

internal/portal/src/common/FilterSyntaxModal/FilterSyntaxModal.tsx renamed to internal/portal/src/common/FilterSyntaxGuide/FilterSyntaxGuide.tsx

Lines changed: 3 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,3 @@
1-
import { useEffect, useState } from "react";
2-
import { createPortal } from "react-dom";
3-
import { CollapseIcon } from "../Icons";
4-
import Button from "../Button/Button";
5-
import "../ConfigurationModal/ConfigurationModal.scss";
61
import Markdown from "react-markdown";
72

83
const FILTER_SYNTAX_GUIDE = `# Filter Syntax Guide
@@ -195,39 +190,8 @@ Check if a field exists (or doesn't):
195190
\`\`\`
196191
`;
197192

198-
const FilterSyntaxModal = ({ onClose }: { onClose: () => void }) => {
199-
const [portalRef, setPortalRef] = useState<HTMLDivElement | null>(null);
200-
201-
useEffect(() => {
202-
// Create portal container for sidebar
203-
const portal = document.createElement("div");
204-
portal.id = "sidebar";
205-
document.body.appendChild(portal);
206-
207-
// Add class to body to adjust main content
208-
document.body.classList.add("sidebar-open");
209-
210-
setPortalRef(portal);
211-
212-
return () => {
213-
portal.remove();
214-
document.body.classList.remove("sidebar-open");
215-
};
216-
}, []);
217-
218-
if (!portalRef) {
219-
return null;
220-
}
221-
222-
return createPortal(
223-
<>
224-
<Button minimal onClick={onClose} className="close-button">
225-
<CollapseIcon />
226-
</Button>
227-
<Markdown>{FILTER_SYNTAX_GUIDE}</Markdown>
228-
</>,
229-
portalRef
230-
);
193+
export const FilterSyntaxGuide = () => {
194+
return <Markdown>{FILTER_SYNTAX_GUIDE}</Markdown>;
231195
};
232196

233-
export default FilterSyntaxModal;
197+
export default FilterSyntaxGuide;

internal/portal/src/common/ConfigurationModal/ConfigurationModal.scss renamed to internal/portal/src/common/Sidebar/Sidebar.scss

File renamed without changes.
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import {
2+
createContext,
3+
useContext,
4+
useState,
5+
useEffect,
6+
ReactNode,
7+
useCallback,
8+
} from "react";
9+
import { createPortal } from "react-dom";
10+
import { CollapseIcon } from "../Icons";
11+
import Button from "../Button/Button";
12+
import "./Sidebar.scss";
13+
14+
type SidebarState = {
15+
id: string;
16+
content: ReactNode;
17+
} | null;
18+
19+
type SidebarContextType = {
20+
open: (id: string, content: ReactNode) => void;
21+
close: (id: string) => void;
22+
toggle: (id: string, content: ReactNode) => void;
23+
};
24+
25+
const SidebarContext = createContext<SidebarContextType | null>(null);
26+
27+
export const SidebarProvider = ({ children }: { children: ReactNode }) => {
28+
const [state, setState] = useState<SidebarState>(null);
29+
30+
const open = useCallback((id: string, content: ReactNode) => {
31+
setState({ id, content });
32+
}, []);
33+
34+
const close = useCallback((id: string) => {
35+
setState((current) => (current?.id === id ? null : current));
36+
}, []);
37+
38+
const toggle = useCallback(
39+
(id: string, content: ReactNode) => {
40+
setState((current) => {
41+
if (current?.id === id) {
42+
return null;
43+
}
44+
return { id, content };
45+
});
46+
},
47+
[]
48+
);
49+
50+
const handleClose = useCallback(() => {
51+
setState(null);
52+
}, []);
53+
54+
return (
55+
<SidebarContext.Provider value={{ open, close, toggle }}>
56+
{children}
57+
<SidebarPortal state={state} onClose={handleClose} />
58+
</SidebarContext.Provider>
59+
);
60+
};
61+
62+
const SidebarPortal = ({
63+
state,
64+
onClose,
65+
}: {
66+
state: SidebarState;
67+
onClose: () => void;
68+
}) => {
69+
const isOpen = state !== null;
70+
71+
useEffect(() => {
72+
if (isOpen) {
73+
document.body.classList.add("sidebar-open");
74+
} else {
75+
document.body.classList.remove("sidebar-open");
76+
}
77+
}, [isOpen]);
78+
79+
if (!state) {
80+
return null;
81+
}
82+
83+
return createPortal(
84+
<div id="sidebar">
85+
<Button minimal onClick={onClose} className="close-button">
86+
<CollapseIcon />
87+
</Button>
88+
{state.content}
89+
</div>,
90+
document.body
91+
);
92+
};
93+
94+
export const useSidebar = () => {
95+
const context = useContext(SidebarContext);
96+
if (!context) {
97+
throw new Error("useSidebar must be used within a SidebarProvider");
98+
}
99+
return context;
100+
};

internal/portal/src/scenes/CreateDestination/CreateDestination.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ import TopicPicker from "../../common/TopicPicker/TopicPicker";
1717
import { DestinationTypeReference, Filter } from "../../typings/Destination";
1818
import DestinationConfigFields from "../../common/DestinationConfigFields/DestinationConfigFields";
1919
import FilterField from "../../common/FilterField/FilterField";
20-
import FilterSyntaxModal from "../../common/FilterSyntaxModal/FilterSyntaxModal";
20+
import { FilterSyntaxGuide } from "../../common/FilterSyntaxGuide/FilterSyntaxGuide";
21+
import { useSidebar } from "../../common/Sidebar/Sidebar";
2122
import { getFormValues } from "../../utils/formHelper";
2223
import CONFIGS from "../../config";
2324

@@ -163,7 +164,7 @@ const CONFIGURATION_STEP: Step = {
163164
const [filter, setFilter] = useState<Filter>(defaultValue.filter || null);
164165
const [showFilter, setShowFilter] = useState(!!defaultValue.filter);
165166
const [filterValid, setFilterValid] = useState(true);
166-
const [showFilterSyntaxModal, setShowFilterSyntaxModal] = useState(false);
167+
const sidebar = useSidebar();
167168

168169
const isFilterEnabled = CONFIGS.ENABLE_DESTINATION_FILTER === "true";
169170

@@ -219,7 +220,7 @@ const CONFIGURATION_STEP: Step = {
219220
<Button
220221
type="button"
221222
onClick={() =>
222-
setShowFilterSyntaxModal(!showFilterSyntaxModal)
223+
sidebar.toggle("filter-syntax", <FilterSyntaxGuide />)
223224
}
224225
className="filter-section__guide-button"
225226
>
@@ -240,9 +241,6 @@ const CONFIGURATION_STEP: Step = {
240241
)}
241242
</div>
242243
)}
243-
{showFilterSyntaxModal && (
244-
<FilterSyntaxModal onClose={() => setShowFilterSyntaxModal(false)} />
245-
)}
246244
</>
247245
);
248246
},

0 commit comments

Comments
 (0)