Skip to content

Commit cea70dc

Browse files
committed
fix(editor): configure reverb at runtime and match the legacy shell
1 parent 76e67c8 commit cea70dc

17 files changed

Lines changed: 358 additions & 135 deletions

app/Http/Controllers/EditorController.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace App\Http\Controllers;
44

5+
use App\Services\RealtimeConfig;
56
use Illuminate\Http\Response;
67

78
/**
@@ -10,10 +11,13 @@
1011
*/
1112
class EditorController extends Controller
1213
{
13-
public function show(string $sessionId): Response
14+
public function show(string $sessionId, RealtimeConfig $realtime): Response
1415
{
1516
return response()
16-
->view('editor', ['sessionId' => $sessionId])
17+
->view('editor', [
18+
'sessionId' => $sessionId,
19+
'realtime' => $realtime->toArray(),
20+
])
1721
->header('Cache-Control', 'no-store');
1822
}
1923
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace App\Http\Controllers;
4+
5+
use App\Services\RealtimeConfig;
6+
use Illuminate\Contracts\View\View;
7+
8+
/**
9+
* The landing page runs the editor in demo mode, on the sample menus.
10+
*/
11+
class HomeController extends Controller
12+
{
13+
public function __invoke(RealtimeConfig $realtime): View
14+
{
15+
return view('welcome', ['realtime' => $realtime->toArray()]);
16+
}
17+
}

resources/css/editor-extras.css

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
/* Pieces the React editor adds, in the language of editor.css */
22

3+
.app-shell {
4+
display: flex;
5+
flex-direction: column;
6+
height: 100vh;
7+
overflow: hidden;
8+
}
9+
10+
.app-shell .ide-container {
11+
flex: 1;
12+
min-height: 0;
13+
height: auto;
14+
}
15+
316
.menu-section {
417
border-bottom: 1px solid var(--border-color);
518
}

resources/js/EditorApp.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ import { useEditorSession } from './session/useEditorSession';
66

77
interface EditorAppProps {
88
sessionId: string;
9+
realtimeReady: boolean;
910
}
1011

11-
export function EditorApp({ sessionId }: EditorAppProps) {
12+
export function EditorApp({ sessionId, realtimeReady }: EditorAppProps) {
1213
const session = useEditorSession(sessionId);
1314

1415
if (session.phase !== 'ready') {
@@ -18,7 +19,7 @@ export function EditorApp({ sessionId }: EditorAppProps) {
1819
return (
1920
<ToastProvider>
2021
<EditorStoreProvider>
21-
<EditorWorkspace sessionId={sessionId} session={session} />
22+
<EditorWorkspace sessionId={sessionId} session={session} realtimeReady={realtimeReady} />
2223
</EditorStoreProvider>
2324
</ToastProvider>
2425
);

resources/js/app.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,24 @@
11
import { StrictMode } from 'react';
22
import { createRoot } from 'react-dom/client';
3-
import { configureEcho } from '@laravel/echo-react';
43
import { DemoWorkspace } from './components/DemoWorkspace';
54
import { ToastProvider } from './components/Toasts';
65
import { EditorStoreProvider } from './editor/store';
6+
import { readRealtimeSettings, startRealtime } from './editor/realtime';
77
import { EditorApp } from './EditorApp';
88

9-
configureEcho({
10-
broadcaster: 'reverb',
11-
});
12-
139
const rootElement = document.getElementById('app');
1410

1511
if (!rootElement) {
1612
throw new Error('React root element was not found.');
1713
}
1814

15+
const realtimeReady = startRealtime(readRealtimeSettings(rootElement));
1916
const sessionId = rootElement.dataset.sessionId;
2017

2118
createRoot(rootElement).render(
2219
<StrictMode>
2320
{sessionId ? (
24-
<EditorApp sessionId={sessionId} />
21+
<EditorApp sessionId={sessionId} realtimeReady={realtimeReady} />
2522
) : (
2623
<ToastProvider>
2724
<EditorStoreProvider>

resources/js/components/DemoWorkspace.tsx

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useCallback, useEffect, useState } from 'react';
2+
import { EDITOR_VERSION } from '../editor/config';
23
import { DEMO_MENUS, fetchDemoMenu } from '../editor/demo';
34
import { menuKey } from '../editor/menus';
45
import { activeTab, useEditorStore } from '../editor/store';
@@ -44,15 +45,9 @@ export function DemoWorkspace() {
4445

4546
return (
4647
<WorkspaceLayout
47-
subtitle="Menu Editor"
48-
connectionLabel="Demo mode"
48+
connectionLabel="Demo Mode"
4949
connectionState="demo"
50-
banner={
51-
<div className="demo-banner">
52-
<span className="demo-banner-title">Demo</span>
53-
<span>Sample menus, nothing is saved. Run /bm editor on your server to edit for real.</span>
54-
</div>
55-
}
50+
sessionInfo="Session: Demo mode"
5651
serverVersion={null}
5752
diagnostics={diagnostics}
5853
visualMode={visualMode}
@@ -64,7 +59,10 @@ export function DemoWorkspace() {
6459
}
6560
onSave={() => notify('info', 'The demo cannot save. Open the editor from your server to keep changes.')}
6661
onOpen={menu => void openMenu(menu)}
62+
onCreate={() => notify('info', 'The demo cannot create menus. Open the editor from your server.')}
6763
onDelete={null}
64+
statusLeft={`${store.menus.filter(menu => menu.platform !== 'CONFIG').length} menus loaded`}
65+
statusRight={EDITOR_VERSION}
6866
onCloseDiagnostics={() => setDiagnostics(null)}
6967
/>
7068
);

resources/js/components/EditorWorkspace.tsx

Lines changed: 77 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,24 @@
11
import { useCallback, useEffect, useState } from 'react';
2-
import { useEcho } from '@laravel/echo-react';
32
import { ApiError } from '../api/client';
43
import { deleteMenu, fetchMenu, fetchMenus, menuKey, saveMenu, SETTINGS_DESCRIPTOR } from '../editor/menus';
4+
import { EDITOR_VERSION } from '../editor/config';
55
import { activeTab, hasUnsavedWork, useEditorStore } from '../editor/store';
66
import { validateMenuSource, type MenuDiagnostics } from '../editor/validator';
77
import { AdminTerminal } from './AdminTerminal';
88
import { MaintenanceBanner } from './MaintenanceBanner';
9+
import { RealtimeBridge } from './RealtimeBridge';
910
import { WorkspaceLayout } from './WorkspaceLayout';
1011
import { useToast } from './Toasts';
1112
import type { MenuDescriptor, SessionState } from '../types/editor';
1213

1314
interface EditorWorkspaceProps {
1415
sessionId: string;
1516
session: SessionState;
17+
realtimeReady: boolean;
1618
}
1719

1820
/** The live editor, bound to one confirmed session and its Minecraft server. */
19-
export function EditorWorkspace({ sessionId, session }: EditorWorkspaceProps) {
21+
export function EditorWorkspace({ sessionId, session, realtimeReady }: EditorWorkspaceProps) {
2022
const { store, dispatch } = useEditorStore();
2123
const notify = useToast();
2224
const [diagnostics, setDiagnostics] = useState<MenuDiagnostics | null>(null);
@@ -117,29 +119,50 @@ export function EditorWorkspace({ sessionId, session }: EditorWorkspaceProps) {
117119
[dispatch, loadMenus, notify, report],
118120
);
119121

120-
useSaveShortcut(save);
121-
useUnsavedWarning(hasUnsavedWork(store));
122+
const create = useCallback(
123+
async (platform: string): Promise<void> => {
124+
const fileName = window.prompt(`New ${platform.toLowerCase()} menu file name`, 'new_menu.yml');
122125

123-
useEcho<{ connected: boolean }>(`session.${sessionId}`, '.plugin.status', payload => {
124-
dispatch({ type: 'plugin/status', online: payload.connected });
126+
if (fileName === null || fileName.trim() === '') {
127+
return;
128+
}
125129

126-
if (payload.connected) {
127-
void loadMenus();
128-
}
129-
});
130+
try {
131+
await saveMenu(platform, fileName.trim(), starterMenu(platform, fileName.trim()));
132+
notify('success', `${fileName.trim()} created`);
133+
await loadMenus();
134+
} catch (error) {
135+
report(error, `Could not create ${fileName}`);
136+
}
137+
},
138+
[loadMenus, notify, report],
139+
);
130140

131-
useEcho<{ platform: string; fileName: string; change: string }>(`session.${sessionId}`, '.menu.changed', () => {
132-
void loadMenus();
133-
});
141+
useSaveShortcut(save);
142+
useUnsavedWarning(hasUnsavedWork(store));
134143

135144
return (
136-
<WorkspaceLayout
137-
subtitle="Menu Editor"
138-
connectionLabel={store.pluginOnline ? 'Connected' : 'Server offline'}
139-
connectionState={store.pluginOnline ? 'connected' : 'offline'}
140-
sessionInfo={`session ${sessionId.slice(0, 8)}`}
141-
banner={<MaintenanceBanner />}
142-
toolbar={
145+
<>
146+
{realtimeReady && (
147+
<RealtimeBridge
148+
sessionId={sessionId}
149+
onPluginStatus={online => {
150+
dispatch({ type: 'plugin/status', online });
151+
152+
if (online) {
153+
void loadMenus();
154+
}
155+
}}
156+
onMenuChanged={() => void loadMenus()}
157+
/>
158+
)}
159+
160+
<WorkspaceLayout
161+
connectionLabel={store.pluginOnline ? 'Connected' : 'Server offline'}
162+
connectionState={store.pluginOnline ? 'connected' : 'offline'}
163+
sessionInfo={sessionInfo(sessionId, session.serverVersion)}
164+
banner={<MaintenanceBanner />}
165+
toolbar={
143166
<button
144167
type="button"
145168
className="btn btn-secondary btn-sm"
@@ -148,24 +171,45 @@ export function EditorWorkspace({ sessionId, session }: EditorWorkspaceProps) {
148171
⌨ Terminal
149172
</button>
150173
}
151-
serverVersion={session.serverVersion}
152-
diagnostics={diagnostics}
153-
visualMode={visualMode}
154-
busy={busy}
155-
canSave
156-
onToggleVisual={() => setVisualMode(mode => !mode)}
157-
onValidate={() =>
174+
serverVersion={session.serverVersion}
175+
diagnostics={diagnostics}
176+
visualMode={visualMode}
177+
busy={busy}
178+
canSave
179+
onToggleVisual={() => setVisualMode(mode => !mode)}
180+
onValidate={() =>
158181
current !== null && setDiagnostics(validateMenuSource(current.content, current.menu.platform))
159182
}
160-
onSave={() => void save()}
161-
onOpen={menu => void openMenu(menu)}
162-
onDelete={menu => void remove(menu)}
163-
onCloseDiagnostics={() => setDiagnostics(null)}
164-
footer={terminalOpen ? <AdminTerminal onClose={() => setTerminalOpen(false)} /> : null}
165-
/>
183+
onSave={() => void save()}
184+
onOpen={menu => void openMenu(menu)}
185+
onCreate={platform => void create(platform)}
186+
onDelete={menu => void remove(menu)}
187+
statusLeft={`${store.menus.filter(menu => menu.platform !== 'CONFIG').length} menus loaded`}
188+
statusRight={EDITOR_VERSION}
189+
onCloseDiagnostics={() => setDiagnostics(null)}
190+
footer={terminalOpen ? <AdminTerminal onClose={() => setTerminalOpen(false)} /> : null}
191+
/>
192+
</>
166193
);
167194
}
168195

196+
function sessionInfo(sessionId: string, serverVersion: string | null): string {
197+
const suffix = serverVersion === null ? '' : ` · MC ${serverVersion}`;
198+
199+
return `Session: ${sessionId.slice(0, 8)}${suffix}`;
200+
}
201+
202+
/** A new menu starts from the smallest file the plugin will actually load. */
203+
function starterMenu(platform: string, fileName: string): string {
204+
const name = fileName.replace(/\.ya?ml$/i, '');
205+
206+
if (platform.toUpperCase() === 'BEDROCK') {
207+
return ['file_version: 1', `menuName: '${name}'`, 'type: SIMPLE', 'content:', ` - '${name}'`, 'buttons: {}', ''].join('\n');
208+
}
209+
210+
return ['file_version: 1', `menuName: '${name}'`, 'type: CHEST', 'menuSize: 27', 'items: {}', ''].join('\n');
211+
}
212+
169213
/** Ctrl+S and Cmd+S save, which is what anyone editing a file will reach for. */
170214
function useSaveShortcut(save: () => Promise<void>): void {
171215
useEffect(() => {

0 commit comments

Comments
 (0)