feat(editor): the three-dot button in the simulation too
A tap on a device in the simulation now also chooses it, so its three-dot button shows as in the design, wherever the simulation's menu offers something (the desktop, each card's data exchange; a switch has none). Java's action of the tap stays (desktop, switch and NAT tables), a tap beside lets go. The device sheet stays closed — the design is locked — and the others see no frame. Java opens that menu only with a right click. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -270,6 +270,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand
|
|||||||
| `src/remote/remoteBlobs.ts`, `src/remote/SharedProject.tsx` (Blob-Seite), `src/desktop/fileExplorer.ts` (Bildbetrachter), `src/i18n/*.json` | Plan §5.1 | neu (F5.5): die Blob-Seite des Editors auf F — was das Dokument nennt, wird geholt (mit dem Keks; was F noch nicht hat, ein paar Mal im Abstand neu gefragt), was hier entsteht (eine importierte Datei), sofort gehalten und zu F gebracht (einmal wiederholt, dann gesagt); der Bildbetrachter zeichnet neu, wenn die Bytes kommen |
|
| `src/remote/remoteBlobs.ts`, `src/remote/SharedProject.tsx` (Blob-Seite), `src/desktop/fileExplorer.ts` (Bildbetrachter), `src/i18n/*.json` | Plan §5.1 | neu (F5.5): die Blob-Seite des Editors auf F — was das Dokument nennt, wird geholt (mit dem Keks; was F noch nicht hat, ein paar Mal im Abstand neu gefragt), was hier entsteht (eine importierte Datei), sofort gehalten und zu F gebracht (einmal wiederholt, dann gesagt); der Bildbetrachter zeichnet neu, wenn die Bytes kommen |
|
||||||
| `test/remoteBlobs.test.ts` | — | neu (F5.5): Hochladen, Wiederholen, Scheitern (auch bei falschem Namen), Holen je Blob einmal, nachfragen bei 404, Netzverlust |
|
| `test/remoteBlobs.test.ts` | — | neu (F5.5): Hochladen, Wiederholen, Scheitern (auch bei falschem Namen), Holen je Blob einmal, nachfragen bei 404, Netzverlust |
|
||||||
| `test/remoteSimHost.test.ts`, `browser/shared.spec.ts`, `browser/serverF.ts` | — | neu (F5.3b), erweitert (F5.5: ein großes Bild, von Mia importiert, bei Ben im Bildbetrachter, in F's Speicher unter seinem SHA-256): `RemoteSimHost` auf nachgebauten Sockets mit gestellter Uhr; drei Personen in drei Browsern an einem echten F (aus dem Bündel gestartet): verschieben, nur lesen, eine Simulation für alle mit Namen, ein Ping in einer Befehlszeile bei der anderen, Eintippen als Leser abgewiesen, eingefroren und aufgetaut, Zugang beendet; Absagen ohne Sitzung und ohne Projekt |
|
| `test/remoteSimHost.test.ts`, `browser/shared.spec.ts`, `browser/serverF.ts` | — | neu (F5.3b), erweitert (F5.5: ein großes Bild, von Mia importiert, bei Ben im Bildbetrachter, in F's Speicher unter seinem SHA-256): `RemoteSimHost` auf nachgebauten Sockets mit gestellter Uhr; drei Personen in drei Browsern an einem echten F (aus dem Bündel gestartet): verschieben, nur lesen, eine Simulation für alle mit Namen, ein Ping in einer Befehlszeile bei der anderen, Eintippen als Leser abgewiesen, eingefroren und aufgetaut, Zugang beendet; Absagen ohne Sitzung und ohne Projekt |
|
||||||
|
| `src/canvas/Canvas.tsx` (`hasMenu`), `src/Editor.tsx` (Aktionsmodus), `browser/capture.spec.ts`, `browser/shared.spec.ts` | Java `GUIEvents.kontextMenueAktionsmodus` | erweitert (F6.5, auf Wunsch des Nutzers): im Aktionsmodus wählt ein Tipp das Gerät auch aus, sein Dreipunkt-Knopf erscheint wie im Entwurf — nur wo das Menü des Aktionsmodus etwas anbietet (Desktop, Datenaustausch je Karte; ein Switch hat keins); Javas Aktion des Tipps bleibt (Desktop, Switch- und NAT-Tabelle), ein Tipp daneben lässt los. Das Geräteblatt bleibt zu (der Entwurf ist gesperrt), und die anderen sehen keinen Rahmen. Abweichend von Java: dort öffnet sich das Menü nur mit Rechtsklick |
|
||||||
|
|
||||||
## packages/server
|
## packages/server
|
||||||
|
|
||||||
|
|||||||
@@ -190,6 +190,50 @@ test("shows a card's frames as Java's data exchange window does, with their deta
|
|||||||
await expect(window).toHaveCount(0);
|
await expect(window).toHaveCount(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('chooses a device with a tap in the simulation, its three-dot button as in the design — only where it has a menu', async ({ page }) => {
|
||||||
|
await openPages(page);
|
||||||
|
await start(page);
|
||||||
|
const button = page.locator('.device-menu-button');
|
||||||
|
const sheet = page.locator('aside[aria-label="Einstellungen"]');
|
||||||
|
// A router: Java's tap does nothing else; the button opens the same menu as the long press.
|
||||||
|
const router = await onDevice(page, await idOf(page, 'Router'));
|
||||||
|
await page.mouse.click(router.x, router.y);
|
||||||
|
await expect(page.locator(`g.device[data-node="${await idOf(page, 'Router')}"]`)).toHaveAttribute('data-selected', 'true');
|
||||||
|
await button.click();
|
||||||
|
await expect(page.getByRole('menuitem')).toHaveText(['Datenaustausch anzeigen (192.168.0.1)', 'Datenaustausch anzeigen (10.0.0.1)']);
|
||||||
|
await page.getByRole('menuitem', { name: 'Datenaustausch anzeigen (10.0.0.1)' }).click();
|
||||||
|
await expect(page.locator('section[data-capture]').getByRole('tab', { selected: true })).toHaveText('Router - 10.0.0.1');
|
||||||
|
await page.locator('section[data-capture] .desktop-close').click();
|
||||||
|
// A computer: the tap still opens its desktop (Java), and chooses it; its settings stay closed, the design is locked.
|
||||||
|
const browser = await idOf(page, 'Browser');
|
||||||
|
const at = await onDevice(page, browser);
|
||||||
|
await page.mouse.click(at.x, at.y);
|
||||||
|
await expect(page.locator(`section[data-desktop="${browser}"]`)).toBeVisible();
|
||||||
|
await expect(button).toBeVisible();
|
||||||
|
await expect(sheet).toHaveCount(0);
|
||||||
|
await page.locator(`section[data-desktop="${browser}"] .desktop-close`).click();
|
||||||
|
await button.click();
|
||||||
|
await expect(page.getByRole('menuitem')).toHaveText(['Desktop anzeigen', 'Datenaustausch anzeigen (192.168.0.10)']);
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
// A switch has no menu in the simulation, so no button; a tap beside lets go.
|
||||||
|
const lanSwitch = (await drawing(page)).nodes.find((node) => node.kind === 'networkSwitch')!.id;
|
||||||
|
const hub = await onDevice(page, lanSwitch);
|
||||||
|
await page.mouse.click(hub.x, hub.y);
|
||||||
|
await expect(page.locator(`g.device[data-node="${lanSwitch}"]`)).toHaveAttribute('data-selected', 'true');
|
||||||
|
await expect(button).toHaveCount(0);
|
||||||
|
await page.locator(`section[data-viewer="${lanSwitch}"] .desktop-close`).click();
|
||||||
|
await page.mouse.click(at.x, at.y);
|
||||||
|
await expect(button).toBeVisible();
|
||||||
|
const surface = (await page.locator('svg.filius-canvas').boundingBox())!;
|
||||||
|
await page.mouse.click(surface.x + surface.width - 30, surface.y + surface.height - 80);
|
||||||
|
await expect(button).toHaveCount(0);
|
||||||
|
// Back in the design nothing stays chosen.
|
||||||
|
await page.mouse.click(at.x, at.y);
|
||||||
|
await page.getByRole('button', { name: 'Wechselt in den Entwurfsmodus' }).click();
|
||||||
|
await expect(button).toHaveCount(0);
|
||||||
|
await expect(sheet).toHaveCount(0);
|
||||||
|
});
|
||||||
|
|
||||||
test('fills a phone with the data exchange window, its bar in reach', async ({ page }) => {
|
test('fills a phone with the data exchange window, its bar in reach', async ({ page }) => {
|
||||||
await page.setViewportSize({ width: 360, height: 740 });
|
await page.setViewportSize({ width: 360, height: 740 });
|
||||||
await openPages(page);
|
await openPages(page);
|
||||||
|
|||||||
@@ -130,6 +130,9 @@ test('three people in one project of F: moving, reading only, one simulation for
|
|||||||
// Both have the notebook's desktop open: each sees the other's dot on it.
|
// Both have the notebook's desktop open: each sees the other's dot on it.
|
||||||
await expect(desktop(mia, notebook).locator(`[data-watcher="${benUser}"]`)).toBeVisible();
|
await expect(desktop(mia, notebook).locator(`[data-watcher="${benUser}"]`)).toBeVisible();
|
||||||
await expect(desktop(ben, notebook).locator(`[data-watcher="${miaUser}"]`)).toBeVisible();
|
await expect(desktop(ben, notebook).locator(`[data-watcher="${miaUser}"]`)).toBeVisible();
|
||||||
|
// Their taps chose the notebook too (its three-dot button), but in the simulation that is no frame for the others —
|
||||||
|
// told in the same message as the open desktop just seen.
|
||||||
|
for (const page of [mia, ben]) await expect(page.locator('rect.chosen-by')).toHaveCount(0);
|
||||||
// Rita may look, not type; she sees both on the desktop, and they see her.
|
// Rita may look, not type; she sees both on the desktop, and they see her.
|
||||||
const ritas = await commandLine(rita, notebook);
|
const ritas = await commandLine(rita, notebook);
|
||||||
await expect(desktop(rita, notebook).locator('[data-watcher]')).toHaveCount(2);
|
await expect(desktop(rita, notebook).locator('[data-watcher]')).toHaveCount(2);
|
||||||
|
|||||||
@@ -390,7 +390,8 @@ export function Editor({ doc, createSimHost, projectName, readOnly = false, froz
|
|||||||
|
|
||||||
// The others: told this editor's chosen device and open windows (a window is open only while the simulation runs).
|
// The others: told this editor's chosen device and open windows (a window is open only while the simulation runs).
|
||||||
const windows = simulating ? desktops.map((desktop) => (desktop.viewer === 'capture' ? windowKey('capture') : windowKey(desktop.viewer ?? 'desktop', desktop.id))) : [];
|
const windows = simulating ? desktops.map((desktop) => (desktop.viewer === 'capture' ? windowKey('capture') : windowKey(desktop.viewer ?? 'desktop', desktop.id))) : [];
|
||||||
const told = JSON.stringify([selected?.id ?? null, windows, moving]);
|
// A device chosen in the simulation only carries its three-dot button: the others see it chosen only in the design.
|
||||||
|
const told = JSON.stringify([simulating ? null : (selected?.id ?? null), windows, moving]);
|
||||||
const publish = presence?.publish;
|
const publish = presence?.publish;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const [device, open, drag] = JSON.parse(told) as [string | null, string[], Moving | null];
|
const [device, open, drag] = JSON.parse(told) as [string | null, string[], Moving | null];
|
||||||
@@ -484,6 +485,7 @@ export function Editor({ doc, createSimHost, projectName, readOnly = false, froz
|
|||||||
else if (kind === 'networkSwitch') openDesktop(id, 'sat');
|
else if (kind === 'networkSwitch') openDesktop(id, 'sat');
|
||||||
else if (kind === 'gateway') openDesktop(id, 'nat');
|
else if (kind === 'gateway') openDesktop(id, 'nat');
|
||||||
}}
|
}}
|
||||||
|
hasMenu={(target) => menuItems(target).length > 0}
|
||||||
onMenu={(target, at) => {
|
onMenu={(target, at) => {
|
||||||
if (readOnly && !simulating) return;
|
if (readOnly && !simulating) return;
|
||||||
if (editorMode === 'documentation') {
|
if (editorMode === 'documentation') {
|
||||||
@@ -640,8 +642,8 @@ export function Editor({ doc, createSimHost, projectName, readOnly = false, froz
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{/* Java shows the settings of the device that is chosen; closing them lets go of it, as a click beside it does. */}
|
{/* Java shows the settings of the device that is chosen (not in the simulation: the design is locked); closing them lets go of it, as a click beside it does. */}
|
||||||
{selected === undefined ? null : (
|
{selected === undefined || simulating ? null : (
|
||||||
<DeviceSheet
|
<DeviceSheet
|
||||||
key={selected.id}
|
key={selected.id}
|
||||||
doc={doc}
|
doc={doc}
|
||||||
|
|||||||
@@ -77,9 +77,10 @@ export interface CanvasProps {
|
|||||||
/** A cable drawn active, as Java draws the cable of the card whose tab is open (Kabel.setAktiv). */
|
/** A cable drawn active, as Java draws the cable of the card whose tab is open (Kabel.setAktiv). */
|
||||||
readonly highlightedLink?: string | null;
|
readonly highlightedLink?: string | null;
|
||||||
/**
|
/**
|
||||||
* The simulation: a tap on a device activates it (a computer's desktop),
|
* The simulation: a tap on a device activates it (a computer's desktop)
|
||||||
* a long press opens its menu; nothing moves, nothing is selected, as in
|
* and chooses it, so that its three-dot button shows as in the design; a
|
||||||
* Java's simulation mode. The documentation mode: see above.
|
* long press opens its menu; nothing moves, as in Java's simulation mode.
|
||||||
|
* The documentation mode: see above.
|
||||||
*/
|
*/
|
||||||
readonly mode?: 'design' | 'simulation' | 'documentation';
|
readonly mode?: 'design' | 'simulation' | 'documentation';
|
||||||
/**
|
/**
|
||||||
@@ -97,6 +98,8 @@ export interface CanvasProps {
|
|||||||
/** The cables that carry a frame now, drawn as Java draws them then (JCablePanel.farbeBlinken). */
|
/** The cables that carry a frame now, drawn as Java draws them then (JCablePanel.farbeBlinken). */
|
||||||
readonly activeLinks?: ReadonlySet<string>;
|
readonly activeLinks?: ReadonlySet<string>;
|
||||||
readonly onActivate?: (id: string) => void;
|
readonly onActivate?: (id: string) => void;
|
||||||
|
/** In the simulation: whether the chosen device has a menu at all (a switch has none) — only then its three-dot button. */
|
||||||
|
readonly hasMenu?: (target: Selection) => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const NO_LINKS: ReadonlySet<string> = new Set();
|
const NO_LINKS: ReadonlySet<string> = new Set();
|
||||||
@@ -274,6 +277,7 @@ export const Canvas = forwardRef<CanvasHandle, CanvasProps>(function Canvas(
|
|||||||
chosenBy = NO_CHOICES,
|
chosenBy = NO_CHOICES,
|
||||||
activeLinks = NO_LINKS,
|
activeLinks = NO_LINKS,
|
||||||
onActivate,
|
onActivate,
|
||||||
|
hasMenu,
|
||||||
onDocFrame,
|
onDocFrame,
|
||||||
renderDocText,
|
renderDocText,
|
||||||
},
|
},
|
||||||
@@ -542,7 +546,7 @@ export const Canvas = forwardRef<CanvasHandle, CanvasProps>(function Canvas(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (now.kind === 'background') {
|
if (now.kind === 'background') {
|
||||||
if (!now.moved && !simulating) {
|
if (!now.moved) {
|
||||||
onSelect?.(null);
|
onSelect?.(null);
|
||||||
if (cableFrom !== null) onCableStart?.(null);
|
if (cableFrom !== null) onCableStart?.(null);
|
||||||
}
|
}
|
||||||
@@ -554,7 +558,10 @@ export const Canvas = forwardRef<CanvasHandle, CanvasProps>(function Canvas(
|
|||||||
}
|
}
|
||||||
if (now.longPressed) return;
|
if (now.longPressed) return;
|
||||||
if (simulating) {
|
if (simulating) {
|
||||||
if (!now.moved) onActivate?.(now.id);
|
if (!now.moved) {
|
||||||
|
onSelect?.({ kind: 'device', id: now.id });
|
||||||
|
onActivate?.(now.id);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (locked) {
|
if (locked) {
|
||||||
@@ -706,7 +713,7 @@ export const Canvas = forwardRef<CanvasHandle, CanvasProps>(function Canvas(
|
|||||||
const corner = toScreen(current, { x: item.x, y: item.y });
|
const corner = toScreen(current, { x: item.x, y: item.y });
|
||||||
return renderDocText(item, { left: corner.x, top: corner.y, width: item.width * current.scale, height: item.height * current.scale, scale: current.scale });
|
return renderDocText(item, { left: corner.x, top: corner.y, width: item.width * current.scale, height: item.height * current.scale, scale: current.scale });
|
||||||
})()}
|
})()}
|
||||||
{menuButton === null || selection === null || simulating || locked ? null : (
|
{menuButton === null || selection === null || (simulating ? hasMenu?.(selection) !== true : locked) ? null : (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="device-menu-button"
|
className="device-menu-button"
|
||||||
|
|||||||
Reference in New Issue
Block a user