Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
1 change: 0 additions & 1 deletion src/profiles/web/dist/assets/index-BwJcCbGq.css

This file was deleted.

445 changes: 445 additions & 0 deletions src/profiles/web/dist/assets/index-CVDj4KUe.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/profiles/web/dist/assets/index-D1PFxxdY.css

Large diffs are not rendered by default.

440 changes: 0 additions & 440 deletions src/profiles/web/dist/assets/index-D366y7z9.js

This file was deleted.

4 changes: 2 additions & 2 deletions src/profiles/web/dist/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ProFiles</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<script type="module" crossorigin src="/assets/index-D366y7z9.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BwJcCbGq.css">
<script type="module" crossorigin src="/assets/index-CVDj4KUe.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D1PFxxdY.css">
<link rel="manifest" href="/manifest.webmanifest"><script id="vite-plugin-pwa:register-sw" src="/registerSW.js"></script></head>
<body class="antialiased selection:bg-indigo-500 selection:text-white">
<div id="root"></div>
Expand Down
2 changes: 1 addition & 1 deletion src/profiles/web/dist/sw.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

61 changes: 60 additions & 1 deletion src/profiles/web/src/App.configEditor.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,18 @@ import { EditorView } from "codemirror";
*/
import App from "./App";
import {
browseForConfigFiles,
executeScan,
fetchConfig,
fetchConfigContent,
fetchNotifications,
fetchScanResults,
fetchSystemInfo,
loadConfigContent,
loadConfigFile,
openConfigFile,
} from "./api/client";
import type { AppConfigData, ConfigContentData, ScanResultsResponse, SystemInfo } from "./api/types";
import type { AppConfigData, BrowseResult, ConfigContentData, ScanResultsResponse, SystemInfo } from "./api/types";

vi.mock("./api/client", async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
Expand All @@ -38,6 +40,8 @@ vi.mock("./api/client", async (importOriginal) => ({
fetchConfigContent: vi.fn(),
loadConfigContent: vi.fn(),
openConfigFile: vi.fn(),
browseForConfigFiles: vi.fn(),
loadConfigFile: vi.fn(),
}));

// `__APP_VERSION__` is injected by vite.config's `define` at build time;
Expand All @@ -53,6 +57,18 @@ const executeScanSpy = vi.mocked(executeScan);
const fetchContentSpy = vi.mocked(fetchConfigContent);
const loadContentSpy = vi.mocked(loadConfigContent);
const openInOSSpy = vi.mocked(openConfigFile);
const browseSpy = vi.mocked(browseForConfigFiles);
const loadFileSpy = vi.mocked(loadConfigFile);

/** Listing served by /api/fs/browse (include_hidden) inside the config picker. */
function makeBrowseResult(overrides: Partial<BrowseResult> = {}): BrowseResult {
return {
current_path: "C:/configs",
parent_path: "C:/",
items: [],
...overrides,
};
}

// jsdom gaps (same treatment as ConfigEditorModal.test.tsx):
// - CodeMirror's measurement code calls Range geometry APIs jsdom lacks.
Expand Down Expand Up @@ -221,4 +237,47 @@ describe("App config editor integration", () => {
});
expect(openInOSSpy).not.toHaveBeenCalled();
});

it("test_editor_load_config_opens_picker_above_editor_and_pick_reloads_content", async () => {
const user = userEvent.setup();
render(<App />);

await waitFor(() => expect(executeScanSpy).toHaveBeenCalledTimes(1));

// Open the in-app editor first.
await user.click(screen.getByRole("button", { name: /^config$/i }));
const editor = await screen.findByRole("dialog", { name: /configuration editor/i });
await waitFor(() => expect(fetchContentSpy).toHaveBeenCalledTimes(1));
await waitFor(() => expect(editor.querySelector(".cm-editor")).toBeTruthy());

// "Load config file…" raises the picker from INSIDE the editor (issue
// #101); the editor stays mounted underneath.
browseSpy.mockResolvedValue(
makeBrowseResult({ items: [{ name: "other.profiles", path: "C:/configs/other.profiles", is_dir: false, is_config: true }] }),
);
loadFileSpy.mockResolvedValue(
makeAppConfig({ config_path: "C:/configs/other.profiles", title: "other" }),
);
// After the pick, the editor's reload fetch serves the OTHER file.
fetchContentSpy.mockResolvedValueOnce(
makeContent({ filename: "other.profiles", content: 'release: "2.0"\nsearch_dir: "E:/other"\n', config_path: "C:/configs/other.profiles" }),
);
await user.click(within(editor).getByRole("button", { name: /load config file/i }));

const picker = await screen.findByRole("dialog", { name: /load configuration/i });
// The picker must stack ABOVE the editor: same explicit z-scale, so the
// DOM order decides (later sibling paints on top).
expect(editor.compareDocumentPosition(picker as Node) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();

// Pick the other config and confirm: onLoaded fires, the picker closes,
// and the editor reloads its content for the new file.
await user.click(await screen.findByRole("button", { name: /other\.profiles/i }));
await user.click(screen.getByRole("button", { name: /^load configuration$/i }));
await waitFor(() => expect(fetchContentSpy).toHaveBeenCalledTimes(2));
// The editor swapped to the freshly served file (header + footer both
// carry the name — check the header).
await waitFor(() =>
expect(within(editor).getByTestId("editor-header").textContent).toContain("other.profiles"),
);
});
});
43 changes: 30 additions & 13 deletions src/profiles/web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,13 @@ export default function App() {
// "Config" button. The old OS-launch behavior moved into the editor's
// header ("Open in OS").
const [showConfigEditor, setShowConfigEditor] = useState<boolean>(false);
/**
* Bumped when the config picker loads a different .profiles file (issue
* #101): an editor left open underneath re-runs its load-on-open fetch
* so it swaps to the freshly picked content. Apply/Save do NOT bump it —
* they already drive the editor through onApplied.
*/
const [editorReloadKey, setEditorReloadKey] = useState<number>(0);
const [showPropertiesModal, setShowPropertiesModal] = useState<boolean>(false);
// File awaiting delete confirmation (in-app dialog — window.confirm is a
// no-op inside the desktop webviews, issue #31).
Expand Down Expand Up @@ -1500,7 +1507,7 @@ export default function App() {
recursive={recursive}
onDisableRecursive={handleDisableRecursive}
onOpenDirectory={handleOpenActiveDirectory}
onOpenConfig={() => setShowConfigPicker(true)}
onLoadConfig={() => setShowConfigPicker(true)}
filterText={filterText}
onClearFilter={handleClearFilter}
columnHeaders={config?.column_headers || []}
Expand Down Expand Up @@ -1557,35 +1564,45 @@ export default function App() {
// this button used to perform now lives in the editor's header
// ("Open in OS"); the modal's onApplied drives handleConfigLoaded,
// the same refresh path as LoadConfigModal and the drag/drop trial.
// Config LOADING no longer lives here (issue #101): the editor's
// "Load config file…" raises the picker from inside the editor.
onOpenConfig={() => setShowConfigEditor(true)}
onLoadConfig={() => setShowConfigPicker(true)}
onShowShortcuts={() => setShowShortcuts(true)}
onReloadConfig={handleReloadConfig}
onExport={handleExport}
exportDisabled={files.length === 0 || scanning}
language={language}
onLanguageChange={setLanguage}
activeConfigName={config?.config_path ? config.config_path.split(/[\\/]/).pop() || config.config_path : undefined}
lanEnabled={systemInfo?.lan_enabled ?? null}
onToggleLan={toggleLan}
/>
</div>

{/* Config Picker Modal */}
<LoadConfigModal
isOpen={showConfigPicker}
activeConfigPath={config?.config_path}
configExists={config?.config_exists}
onClose={() => setShowConfigPicker(false)}
onLoaded={handleConfigLoaded}
/>

{/* YAML Configuration Editor (issue #95): Apply/Save/Save As hand the
fresh AppConfigData to the shared handleConfigLoaded refresh path. */}
fresh AppConfigData to the shared handleConfigLoaded refresh path.
Rendered BEFORE the picker so the picker — raised from inside the
editor via onLoadConfig (issue #101) — paints on top (same z-scale
token; DOM order decides). */}
<ConfigEditorModal
open={showConfigEditor}
onClose={() => setShowConfigEditor(false)}
onApplied={handleConfigLoaded}
onLoadConfig={() => setShowConfigPicker(true)}
reloadKey={editorReloadKey}
/>

{/* Config Picker Modal. onLoaded ALSO bumps the editor's reload key
so an editor left open underneath re-fetches the freshly picked
file instead of keeping stale text (issue #101). */}
<LoadConfigModal
isOpen={showConfigPicker}
activeConfigPath={config?.config_path}
configExists={config?.config_exists}
onClose={() => setShowConfigPicker(false)}
onLoaded={(cfg) => {
void handleConfigLoaded(cfg);
setEditorReloadKey((key) => key + 1);
}}
/>

{/* Dropped configuration files (session-only trial) */}
Expand Down
Loading
Loading