дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 11:15:40 +02:00
feat(deployment): browse for the folders of a mapping (#1078)
Mapping paths had to be typed. The local path and deployment path cells now have a folder button: the local one picks a folder inside the project, the deployment one opens the server's folder chooser at its root path, and both fill in the path relative to their root (the deployment button needs an absolute root path). Fixes #799
Цей коміт міститься в:
1 джерело
555226a9bf
коміт
6eb5584beb
5 files changed
+91
-10
No files matched your search
@@ -123,8 +123,15 @@
|
||||
.mappingTable tbody tr.selectedRow {
|
||||
background: var(--bg-tree-selected);
|
||||
}
|
||||
.cellField {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
.cellInput {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Remote folder chooser. */
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useEffect, useId, useState, type KeyboardEvent } from "react";
|
||||
import clsx from "clsx";
|
||||
import { call, commands, IpcError, type FsConnection, type RemoteEntry } from "@/platform/ipc";
|
||||
import { useTx, tx, txTemplate } from "@/platform/i18n";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { deploymentConnection, type DeploymentServerValid } from "@/platform/remoteSettings";
|
||||
import { Badge, Button, Checkbox, Dialog, Icon, IconButton, Input, Select, Spinner, T, Tabs } from "@/ui";
|
||||
import { GlobListEditor, Row } from "../components/Form";
|
||||
@@ -15,6 +16,7 @@ import { useSetting } from "../components/context";
|
||||
import {
|
||||
addMapping,
|
||||
fixDefaultServer,
|
||||
folderWithin,
|
||||
formatExcludes,
|
||||
newServer,
|
||||
parseExcludes,
|
||||
@@ -22,6 +24,7 @@ import {
|
||||
removeMapping,
|
||||
updateMapping,
|
||||
} from "./deploymentModel";
|
||||
import { choosePath } from "../languages/api";
|
||||
import { BUILD_GROUP } from "./execution";
|
||||
import type { PageSpec } from "./spec";
|
||||
import { sshTestRemedy } from "./sshConnection";
|
||||
@@ -373,6 +376,8 @@ function ConnectionTab({
|
||||
);
|
||||
}
|
||||
|
||||
const isAbsolute = (p: string) => p.startsWith("/") || /^[A-Za-z]:[\\/]/.test(p);
|
||||
|
||||
const MAPPING_COLUMNS = [
|
||||
{ key: "local", title: "Local path", placeholder: "Project root" },
|
||||
{ key: "deployment", title: "Deployment path", placeholder: "Root path" },
|
||||
@@ -388,7 +393,22 @@ function MappingsTab({
|
||||
}) {
|
||||
const tx = useTx();
|
||||
const [selected, setSelected] = useState<number | null>(null);
|
||||
const [browsing, setBrowsing] = useState<number | null>(null);
|
||||
const projectRoot = useProject((s) => s.project?.root ?? null);
|
||||
const mappings = server.mappings;
|
||||
const remoteRoot = server.rootPath.trim().replace(/\/+$/, "");
|
||||
// Deployment paths are relative to the root path, so the remote chooser needs an absolute one.
|
||||
const canBrowseRemote = isAbsolute(remoteRoot) && (server.kind !== "sftp" || !!server.sshConfig);
|
||||
const set = (i: number, patch: Partial<(typeof mappings)[number]>) =>
|
||||
update({ mappings: updateMapping(mappings, i, patch) });
|
||||
const chooseLocal = async (i: number) => {
|
||||
if (!projectRoot) return;
|
||||
const current = mappings[i]?.local.trim() ?? "";
|
||||
const start = isAbsolute(current) ? current : current ? `${projectRoot}/${current}` : projectRoot;
|
||||
const picked = await choosePath({ directory: true, title: "Choose Folder", defaultPath: start });
|
||||
const rel = picked === null ? null : folderWithin(projectRoot, picked);
|
||||
if (rel !== null) set(i, { local: rel });
|
||||
};
|
||||
return (
|
||||
<div className={styles.panel}>
|
||||
<div className={workflow.tableBox}>
|
||||
@@ -440,15 +460,35 @@ function MappingsTab({
|
||||
>
|
||||
{MAPPING_COLUMNS.map((c) => (
|
||||
<td key={c.key}>
|
||||
<Input
|
||||
mono
|
||||
variant="bare"
|
||||
className={styles.cellInput}
|
||||
value={m[c.key]}
|
||||
placeholder={tx(c.placeholder)}
|
||||
aria-label={`${tx(c.title)} ${i + 1}`}
|
||||
onChange={(e) => update({ mappings: updateMapping(mappings, i, { [c.key]: e.target.value }) })}
|
||||
/>
|
||||
<span className={styles.cellField}>
|
||||
<Input
|
||||
mono
|
||||
variant="bare"
|
||||
className={styles.cellInput}
|
||||
value={m[c.key]}
|
||||
placeholder={tx(c.placeholder)}
|
||||
aria-label={`${tx(c.title)} ${i + 1}`}
|
||||
onChange={(e) => set(i, { [c.key]: e.target.value })}
|
||||
/>
|
||||
{c.key === "local" && (
|
||||
<IconButton
|
||||
icon="folder-open"
|
||||
size="small"
|
||||
title="Browse…"
|
||||
disabled={!projectRoot}
|
||||
onClick={() => void chooseLocal(i)}
|
||||
/>
|
||||
)}
|
||||
{c.key === "deployment" && (
|
||||
<IconButton
|
||||
icon="folder-open"
|
||||
size="small"
|
||||
title={canBrowseRemote ? "Browse…" : "Set an absolute root path on the Connection tab first"}
|
||||
disabled={!canBrowseRemote}
|
||||
onClick={() => setBrowsing(i)}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
@@ -463,6 +503,18 @@ function MappingsTab({
|
||||
}
|
||||
</T>
|
||||
</div>
|
||||
{browsing !== null && (
|
||||
<RemoteFolderDialog
|
||||
connection={deploymentConnection(server)}
|
||||
initial={[remoteRoot, mappings[browsing]?.deployment.trim().replace(/^\/+/, "")].filter(Boolean).join("/")}
|
||||
onClose={() => setBrowsing(null)}
|
||||
onChoose={(path) => {
|
||||
const rel = folderWithin(remoteRoot, path);
|
||||
if (rel !== null) set(browsing, { deployment: rel });
|
||||
setBrowsing(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { DEFAULT_DEPLOYMENT_SERVER } from "@/platform/remoteSettings";
|
||||
import {
|
||||
addMapping,
|
||||
fixDefaultServer,
|
||||
folderWithin,
|
||||
formatExcludes,
|
||||
newServer,
|
||||
nextServerName,
|
||||
@@ -83,3 +84,14 @@ describe("server list", () => {
|
||||
expect(remoteParent("/var")).toBe("/");
|
||||
});
|
||||
});
|
||||
|
||||
describe("chosen mapping folders", () => {
|
||||
it("are relative to the project or the server's root path", () => {
|
||||
expect(folderWithin("/work/shop", "/work/shop/web/public")).toBe("web/public");
|
||||
expect(folderWithin("/var/www/", "/var/www/site")).toBe("site");
|
||||
expect(folderWithin("/var/www", "/var/www/")).toBe("");
|
||||
expect(folderWithin("/var/www", "/var/wwwroot")).toBeNull();
|
||||
expect(folderWithin("/var/www", "/srv")).toBeNull();
|
||||
expect(folderWithin("", "/srv")).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -79,3 +79,12 @@ export function remoteParent(path: string): string {
|
||||
const i = trimmed.lastIndexOf("/");
|
||||
return i <= 0 ? "/" : trimmed.slice(0, i);
|
||||
}
|
||||
|
||||
/** `path` relative to the folder `root` ("" for `root` itself), for a chosen mapping folder; null outside it. */
|
||||
export function folderWithin(root: string, path: string): string | null {
|
||||
const r = root.replace(/\/+$/, "");
|
||||
const p = path.replace(/\/+$/, "");
|
||||
if (!r) return null;
|
||||
if (p === r) return "";
|
||||
return p.startsWith(`${r}/`) ? p.slice(r.length + 1) : null;
|
||||
}
|
||||
Reference in new issue
Заблокувати користувача