mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-09-05 09:27:29 +02:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64b9479deb | ||
|
|
6796569466 | ||
|
|
72d3bda769 | ||
|
|
bd932ce85f |
Generated
+1
@@ -11228,6 +11228,7 @@ dependencies = [
|
|||||||
"md5 0.8.0",
|
"md5 0.8.0",
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"sha2",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
|||||||
@@ -261,13 +261,20 @@ function LoadedImportDataDialog({
|
|||||||
const itemTree = useMemo(() => buildItemTree(items), [items]);
|
const itemTree = useMemo(() => buildItemTree(items), [items]);
|
||||||
|
|
||||||
// A folder row's checkbox aggregates its subtree the way the git commit tree does: creates and
|
// A folder row's checkbox aggregates its subtree the way the git commit tree does: creates and
|
||||||
// updates toggle together, while removals only ever cascade beneath a removed folder.
|
// updates toggle together, while removals only ever cascade beneath a removed folder. Checking
|
||||||
|
// anything also brings back the folders it needs to live in.
|
||||||
const toggleNode = (node: CheckboxTreeNode<ImportPlanItem>, checked: boolean) => {
|
const toggleNode = (node: CheckboxTreeNode<ImportPlanItem>, checked: boolean) => {
|
||||||
const targets = new Set(
|
const targets = new Set(
|
||||||
collectItems(node)
|
collectItems(node)
|
||||||
.filter((i) => togglesWith(node.data, i))
|
.filter((i) => togglesWith(node.data, i))
|
||||||
.map((i) => i.modelId),
|
.map((i) => i.modelId),
|
||||||
);
|
);
|
||||||
|
if (checked) {
|
||||||
|
const byId = new Map(items.map((i) => [i.modelId, i]));
|
||||||
|
for (const ancestor of ancestorsOf(node.data, byId)) {
|
||||||
|
if (ancestor.action === "not_imported") targets.add(ancestor.modelId);
|
||||||
|
}
|
||||||
|
}
|
||||||
setItems((prev) => prev.map((i) => (targets.has(i.modelId) ? { ...i, selected: checked } : i)));
|
setItems((prev) => prev.map((i) => (targets.has(i.modelId) ? { ...i, selected: checked } : i)));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -283,19 +290,17 @@ function LoadedImportDataDialog({
|
|||||||
const disabled = new Set<string>();
|
const disabled = new Set<string>();
|
||||||
const byId = new Map(items.map((i) => [i.modelId, i]));
|
const byId = new Map(items.map((i) => [i.modelId, i]));
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
const seen = new Set<string>();
|
for (const parent of ancestorsOf(item, byId)) {
|
||||||
let parentId = item.parentId;
|
if (parent.model !== "folder") break;
|
||||||
while (parentId != null && !seen.has(parentId)) {
|
const missing =
|
||||||
seen.add(parentId);
|
(parent.action === "create" || parent.action === "not_imported") && !parent.selected;
|
||||||
const parent = byId.get(parentId);
|
// A not-imported row stays checkable: checking it brings its folders back with it
|
||||||
if (parent == null || parent.model !== "folder") break;
|
if (missing && item.action !== "delete" && item.action !== "not_imported") {
|
||||||
if (parent.action === "create" && !parent.selected && item.action !== "delete") {
|
|
||||||
disabled.add(item.modelId);
|
disabled.add(item.modelId);
|
||||||
}
|
}
|
||||||
if (parent.action === "delete" && parent.selected && item.action === "delete") {
|
if (parent.action === "delete" && parent.selected && item.action === "delete") {
|
||||||
disabled.add(item.modelId);
|
disabled.add(item.modelId);
|
||||||
}
|
}
|
||||||
parentId = parent.parentId;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return disabled;
|
return disabled;
|
||||||
@@ -340,6 +345,7 @@ function LoadedImportDataDialog({
|
|||||||
modelId: existing?.id ?? planned?.id ?? "workspace",
|
modelId: existing?.id ?? planned?.id ?? "workspace",
|
||||||
name: existing?.name ?? planned?.name ?? "New workspace",
|
name: existing?.name ?? planned?.name ?? "New workspace",
|
||||||
selected: true,
|
selected: true,
|
||||||
|
changedFields: [],
|
||||||
},
|
},
|
||||||
children: itemTree,
|
children: itemTree,
|
||||||
};
|
};
|
||||||
@@ -358,6 +364,7 @@ function LoadedImportDataDialog({
|
|||||||
checked={nodeCheckedStatus}
|
checked={nodeCheckedStatus}
|
||||||
onCheck={toggleNode}
|
onCheck={toggleNode}
|
||||||
isCheckboxDisabled={(n) => disabledIds.has(n.key)}
|
isCheckboxDisabled={(n) => disabledIds.has(n.key)}
|
||||||
|
isCollapsedByDefault={(n) => n.data.action === "not_imported"}
|
||||||
isRelevant={(n) => n.data.model === "workspace" || n.data.action !== "unchanged"}
|
isRelevant={(n) => n.data.model === "workspace" || n.data.action !== "unchanged"}
|
||||||
renderRow={(n) => <ImportTreeRow item={n.data} onResolveConflict={resolveConflict} />}
|
renderRow={(n) => <ImportTreeRow item={n.data} onResolveConflict={resolveConflict} />}
|
||||||
/>
|
/>
|
||||||
@@ -402,7 +409,7 @@ function LoadedImportDataDialog({
|
|||||||
? "Importing"
|
? "Importing"
|
||||||
: changeCount > 0
|
: changeCount > 0
|
||||||
? `Apply ${changeCount} ${changeCount === 1 ? "Change" : "Changes"}`
|
? `Apply ${changeCount} ${changeCount === 1 ? "Change" : "Changes"}`
|
||||||
: "Apply"}
|
: "Done"}
|
||||||
</Button>
|
</Button>
|
||||||
</HStack>
|
</HStack>
|
||||||
</VStack>
|
</VStack>
|
||||||
@@ -565,11 +572,13 @@ function ImportTreeRow({
|
|||||||
)}
|
)}
|
||||||
<div className="truncate flex-1">{item.name}</div>
|
<div className="truncate flex-1">{item.name}</div>
|
||||||
{item.action === "conflict" ? (
|
{item.action === "conflict" ? (
|
||||||
<div className="shrink-0 flex items-center gap-1.5">
|
<div className="shrink-0">
|
||||||
<SegmentedControl
|
<SegmentedControl
|
||||||
name={`conflict-${item.modelId}`}
|
name={`conflict-${item.modelId}`}
|
||||||
label={`Resolve conflict for ${item.name}`}
|
label={`Resolve conflict for ${item.name}`}
|
||||||
hideLabel
|
hideLabel
|
||||||
|
size="2xs"
|
||||||
|
help={actionHelp(item)}
|
||||||
value={item.resolution ?? "keep_mine"}
|
value={item.resolution ?? "keep_mine"}
|
||||||
onChange={(v) => onResolveConflict(item.modelId, v)}
|
onChange={(v) => onResolveConflict(item.modelId, v)}
|
||||||
options={[
|
options={[
|
||||||
@@ -577,7 +586,6 @@ function ImportTreeRow({
|
|||||||
{ value: "take_source", label: "Take source" },
|
{ value: "take_source", label: "Take source" },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<IconTooltip content={actionHelp(item)} iconSize="sm" />
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
actionLabel(item) && (
|
actionLabel(item) && (
|
||||||
@@ -589,6 +597,7 @@ function ImportTreeRow({
|
|||||||
item.action === "update" && "text-info",
|
item.action === "update" && "text-info",
|
||||||
item.action === "delete" && "text-danger",
|
item.action === "delete" && "text-danger",
|
||||||
item.action === "keep_local" && item.selected && "text-warning",
|
item.action === "keep_local" && item.selected && "text-warning",
|
||||||
|
item.action === "not_imported" && "text-text-subtlest",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{actionLabel(item)}
|
{actionLabel(item)}
|
||||||
@@ -610,28 +619,57 @@ function actionLabel(item: ImportPlanItem): string | null {
|
|||||||
return "removed";
|
return "removed";
|
||||||
case "keep_local":
|
case "keep_local":
|
||||||
return "edited";
|
return "edited";
|
||||||
|
case "not_imported":
|
||||||
|
return "not imported";
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function actionHelp(item: ImportPlanItem): string | null {
|
function actionHelp(item: ImportPlanItem): string | null {
|
||||||
|
const help = (text: string) =>
|
||||||
|
item.changedFields.length > 0
|
||||||
|
? `${text} · ${item.changedFields.map(fieldLabel).join(", ")}`
|
||||||
|
: text;
|
||||||
switch (item.action) {
|
switch (item.action) {
|
||||||
case "create":
|
case "create":
|
||||||
return "Added since the last import";
|
return "Added since the last import";
|
||||||
case "update":
|
case "update":
|
||||||
return "Changed since the last import";
|
return help("Changed since the last import");
|
||||||
case "delete":
|
case "delete":
|
||||||
return "Deleted since the last import";
|
return item.reason === "moved_into_not_imported_folder"
|
||||||
|
? "Moved into a folder that isn't imported. Import that folder instead to follow the move"
|
||||||
|
: "Deleted since the last import";
|
||||||
case "keep_local":
|
case "keep_local":
|
||||||
return "Local edits made since the last import. Importing will revert them if checked";
|
return help("Local edits made since the last import. Importing will revert them if checked");
|
||||||
case "conflict":
|
case "conflict":
|
||||||
return "Changed both here and in the file since the last import";
|
return help("Changed both here and in the file since the last import");
|
||||||
|
case "not_imported":
|
||||||
|
return "In the file, but not imported. Check it to import it";
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fieldLabel(field: string): string {
|
||||||
|
return field.replace(/([A-Z])/g, " $1").toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Every plan item above `item`, nearest first. */
|
||||||
|
function ancestorsOf(item: ImportPlanItem, byId: Map<string, ImportPlanItem>): ImportPlanItem[] {
|
||||||
|
const ancestors: ImportPlanItem[] = [];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
let parentId = item.parentId;
|
||||||
|
while (parentId != null && !seen.has(parentId)) {
|
||||||
|
seen.add(parentId);
|
||||||
|
const parent = byId.get(parentId);
|
||||||
|
if (parent == null) break;
|
||||||
|
ancestors.push(parent);
|
||||||
|
parentId = parent.parentId;
|
||||||
|
}
|
||||||
|
return ancestors;
|
||||||
|
}
|
||||||
|
|
||||||
function buildItemTree(items: ImportPlanItem[]): CheckboxTreeNode<ImportPlanItem>[] {
|
function buildItemTree(items: ImportPlanItem[]): CheckboxTreeNode<ImportPlanItem>[] {
|
||||||
const byId = new Map(items.map((i) => [i.modelId, i]));
|
const byId = new Map(items.map((i) => [i.modelId, i]));
|
||||||
const childrenOf = new Map<string, ImportPlanItem[]>();
|
const childrenOf = new Map<string, ImportPlanItem[]>();
|
||||||
@@ -678,7 +716,7 @@ function togglesWith(root: ImportPlanItem, item: ImportPlanItem): boolean {
|
|||||||
if (item.action === "keep_local") {
|
if (item.action === "keep_local") {
|
||||||
return root.modelId === item.modelId && item.model !== "folder";
|
return root.modelId === item.modelId && item.model !== "folder";
|
||||||
}
|
}
|
||||||
return item.action === "create" || item.action === "update";
|
return item.action === "create" || item.action === "update" || item.action === "not_imported";
|
||||||
}
|
}
|
||||||
|
|
||||||
function nodeCheckedStatus(
|
function nodeCheckedStatus(
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ interface Props<T> {
|
|||||||
isCheckboxDisabled?: (node: CheckboxTreeNode<T>) => boolean;
|
isCheckboxDisabled?: (node: CheckboxTreeNode<T>) => boolean;
|
||||||
/** An irrelevant row is hidden unless one of its descendants is relevant */
|
/** An irrelevant row is hidden unless one of its descendants is relevant */
|
||||||
isRelevant: (node: CheckboxTreeNode<T>) => boolean;
|
isRelevant: (node: CheckboxTreeNode<T>) => boolean;
|
||||||
|
/** A node that starts collapsed, so a large subtree doesn't crowd out the rest */
|
||||||
|
isCollapsedByDefault?: (node: CheckboxTreeNode<T>) => boolean;
|
||||||
renderRow: (node: CheckboxTreeNode<T>) => ReactNode;
|
renderRow: (node: CheckboxTreeNode<T>) => ReactNode;
|
||||||
onSelectRow?: (node: CheckboxTreeNode<T>) => void;
|
onSelectRow?: (node: CheckboxTreeNode<T>) => void;
|
||||||
canSelectRow?: (node: CheckboxTreeNode<T>) => boolean;
|
canSelectRow?: (node: CheckboxTreeNode<T>) => boolean;
|
||||||
@@ -29,7 +31,9 @@ interface Props<T> {
|
|||||||
|
|
||||||
export function CheckboxTree<T>(props: Props<T>) {
|
export function CheckboxTree<T>(props: Props<T>) {
|
||||||
const { node, depth = 0 } = props;
|
const { node, depth = 0 } = props;
|
||||||
const [collapsed, setCollapsed] = useState<boolean>(false);
|
const [collapsed, setCollapsed] = useState<boolean>(
|
||||||
|
() => props.isCollapsedByDefault?.(node) ?? false,
|
||||||
|
);
|
||||||
if (!hasRelevantNode(node, props.isRelevant)) return null;
|
if (!hasRelevantNode(node, props.isRelevant)) return null;
|
||||||
|
|
||||||
const checked = props.checked(node);
|
const checked = props.checked(node);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useStateWithDeps } from "../../hooks/useStateWithDeps";
|
|||||||
import { generateId } from "../../lib/generateId";
|
import { generateId } from "../../lib/generateId";
|
||||||
import { Button } from "./Button";
|
import { Button } from "./Button";
|
||||||
import { IconButton, type IconButtonProps } from "./IconButton";
|
import { IconButton, type IconButtonProps } from "./IconButton";
|
||||||
|
import { IconTooltip } from "./IconTooltip";
|
||||||
import { Label } from "./Label";
|
import { Label } from "./Label";
|
||||||
|
|
||||||
interface Props<T extends string> {
|
interface Props<T extends string> {
|
||||||
@@ -36,11 +37,15 @@ export function SegmentedControl<T extends string>({
|
|||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const id = useRef(`input-${generateId()}`);
|
const id = useRef(`input-${generateId()}`);
|
||||||
|
|
||||||
|
// A visually hidden label has nowhere to show the help, so the last option carries it
|
||||||
|
const inlineHelp =
|
||||||
|
hideLabel && help ? <IconTooltip tabIndex={-1} content={help} iconSize="xs" /> : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full grid">
|
<div className="w-full grid">
|
||||||
<Label
|
<Label
|
||||||
htmlFor={id.current}
|
htmlFor={id.current}
|
||||||
help={help}
|
help={hideLabel ? undefined : help}
|
||||||
visuallyHidden={hideLabel}
|
visuallyHidden={hideLabel}
|
||||||
className={classNames(labelClassName)}
|
className={classNames(labelClassName)}
|
||||||
>
|
>
|
||||||
@@ -78,9 +83,10 @@ export function SegmentedControl<T extends string>({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{options.map((o) => {
|
{options.map((o, i) => {
|
||||||
const isSelected = selectedValue === o.value;
|
const isSelected = selectedValue === o.value;
|
||||||
const isActive = value === o.value;
|
const isActive = value === o.value;
|
||||||
|
const rightSlot = i === options.length - 1 ? inlineHelp : null;
|
||||||
if (o.icon == null) {
|
if (o.icon == null) {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
@@ -95,6 +101,7 @@ export function SegmentedControl<T extends string>({
|
|||||||
isActive && "text-text!",
|
isActive && "text-text!",
|
||||||
"focus:ring-1 focus:ring-border-focus",
|
"focus:ring-1 focus:ring-border-focus",
|
||||||
)}
|
)}
|
||||||
|
rightSlot={rightSlot}
|
||||||
onClick={() => onChange(o.value)}
|
onClick={() => onChange(o.value)}
|
||||||
>
|
>
|
||||||
{o.label}
|
{o.label}
|
||||||
@@ -117,6 +124,7 @@ export function SegmentedControl<T extends string>({
|
|||||||
)}
|
)}
|
||||||
title={o.label}
|
title={o.label}
|
||||||
icon={o.icon}
|
icon={o.icon}
|
||||||
|
rightSlot={rightSlot}
|
||||||
onClick={() => onChange(o.value)}
|
onClick={() => onChange(o.value)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -113,6 +113,10 @@ fn format_skipped(items: &[ImportPlanItem]) -> Option<String> {
|
|||||||
if keep_local > 0 {
|
if keep_local > 0 {
|
||||||
parts.push(format!("{keep_local} with local edits"));
|
parts.push(format!("{keep_local} with local edits"));
|
||||||
}
|
}
|
||||||
|
let not_imported = count(ImportPlanAction::NotImported);
|
||||||
|
if not_imported > 0 {
|
||||||
|
parts.push(format!("{not_imported} previously not imported"));
|
||||||
|
}
|
||||||
let unchanged = count(ImportPlanAction::Unchanged);
|
let unchanged = count(ImportPlanAction::Unchanged);
|
||||||
if unchanged > 0 {
|
if unchanged > 0 {
|
||||||
parts.push(format!("{unchanged} unchanged"));
|
parts.push(format!("{unchanged} unchanged"));
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use common::{cli_cmd, parse_created_id, query_manager, seed_request};
|
|||||||
use predicates::str::contains;
|
use predicates::str::contains;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
use yaak_models::util::UpdateSource;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn export_writes_yaak_workspace_file() {
|
fn export_writes_yaak_workspace_file() {
|
||||||
@@ -257,3 +258,60 @@ fn re_import_merges_into_linked_workspace() {
|
|||||||
assert!(requests.iter().any(|r| r.name == "Request B"), "removal must not auto-apply");
|
assert!(requests.iter().any(|r| r.name == "Request B"), "removal must not auto-apply");
|
||||||
assert!(requests.iter().any(|r| r.name == "Request C"));
|
assert!(requests.iter().any(|r| r.name == "Request C"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn re_import_leaves_deleted_resources_alone() {
|
||||||
|
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
||||||
|
let data_dir = temp_dir.path();
|
||||||
|
let import_path = temp_dir.path().join("linked.json");
|
||||||
|
|
||||||
|
write_linked_fixture(
|
||||||
|
&import_path,
|
||||||
|
&[
|
||||||
|
("req_a", "Request A", "https://example.com/a"),
|
||||||
|
("req_b", "Request B", "https://example.com/b"),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
cli_cmd(data_dir)
|
||||||
|
.args(["import", import_path.to_str().expect("import path is utf-8")])
|
||||||
|
.assert()
|
||||||
|
.success();
|
||||||
|
|
||||||
|
let workspace_id = {
|
||||||
|
let query_manager = query_manager(data_dir);
|
||||||
|
let db = query_manager.connect();
|
||||||
|
let workspace_id = db
|
||||||
|
.list_workspaces()
|
||||||
|
.expect("list workspaces")
|
||||||
|
.into_iter()
|
||||||
|
.find(|w| w.name == "Linked Workspace")
|
||||||
|
.expect("workspace imported")
|
||||||
|
.id;
|
||||||
|
let request_b = db
|
||||||
|
.list_http_requests(&workspace_id)
|
||||||
|
.expect("list requests")
|
||||||
|
.into_iter()
|
||||||
|
.find(|r| r.name == "Request B")
|
||||||
|
.expect("request B imported");
|
||||||
|
db.delete_http_request_by_id(&request_b.id, &UpdateSource::Sync)
|
||||||
|
.expect("delete request B");
|
||||||
|
workspace_id
|
||||||
|
};
|
||||||
|
|
||||||
|
cli_cmd(data_dir)
|
||||||
|
.args([
|
||||||
|
"import",
|
||||||
|
import_path.to_str().expect("import path is utf-8"),
|
||||||
|
"--workspace-id",
|
||||||
|
&workspace_id,
|
||||||
|
])
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(contains("Skipped 1 previously not imported"));
|
||||||
|
|
||||||
|
let query_manager = query_manager(data_dir);
|
||||||
|
let requests =
|
||||||
|
query_manager.connect().list_http_requests(&workspace_id).expect("list requests");
|
||||||
|
assert_eq!(requests.len(), 1, "a deleted request must not come back: {requests:?}");
|
||||||
|
assert_eq!(requests[0].name, "Request A");
|
||||||
|
}
|
||||||
|
|||||||
@@ -331,17 +331,6 @@ export type ImportSource = {
|
|||||||
lastImportedAt: string;
|
lastImportedAt: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ImportSourceResource = {
|
|
||||||
model: "import_source_resource";
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
importSourceId: string;
|
|
||||||
sourceKey: string;
|
|
||||||
modelType: string;
|
|
||||||
modelId: string;
|
|
||||||
snapshot: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type InheritedBoolSetting = { enabled?: boolean; value: boolean };
|
export type InheritedBoolSetting = { enabled?: boolean; value: boolean };
|
||||||
|
|
||||||
export type InheritedHttpVersionSetting = { enabled?: boolean; value: HttpVersion };
|
export type InheritedHttpVersionSetting = { enabled?: boolean; value: HttpVersion };
|
||||||
|
|||||||
+77
-23
@@ -1,7 +1,21 @@
|
|||||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
import type { Environment, Folder, GrpcRequest, HttpRequest, WebsocketRequest, Workspace } from "./gen_models";
|
import type {
|
||||||
|
Environment,
|
||||||
|
Folder,
|
||||||
|
GrpcRequest,
|
||||||
|
HttpRequest,
|
||||||
|
WebsocketRequest,
|
||||||
|
Workspace,
|
||||||
|
} from "./gen_models";
|
||||||
|
|
||||||
export type BatchUpsertResult = { workspaces: Array<Workspace>, environments: Array<Environment>, folders: Array<Folder>, httpRequests: Array<HttpRequest>, grpcRequests: Array<GrpcRequest>, websocketRequests: Array<WebsocketRequest>, };
|
export type BatchUpsertResult = {
|
||||||
|
workspaces: Array<Workspace>;
|
||||||
|
environments: Array<Environment>;
|
||||||
|
folders: Array<Folder>;
|
||||||
|
httpRequests: Array<HttpRequest>;
|
||||||
|
grpcRequests: Array<GrpcRequest>;
|
||||||
|
websocketRequests: Array<WebsocketRequest>;
|
||||||
|
};
|
||||||
|
|
||||||
export type ImportConflictResolution = "keep_mine" | "take_source";
|
export type ImportConflictResolution = "keep_mine" | "take_source";
|
||||||
|
|
||||||
@@ -11,38 +25,78 @@ export type ImportConflictResolution = "keep_mine" | "take_source";
|
|||||||
* The destination workspace and optional folder IDs are captured in the plan so the preview describes
|
* The destination workspace and optional folder IDs are captured in the plan so the preview describes
|
||||||
* the exact destination that confirmation will use.
|
* the exact destination that confirmation will use.
|
||||||
*/
|
*/
|
||||||
export type ImportDestination = { "type": "new_workspace" } | { "type": "existing_workspace", workspaceId: string, folderId?: string, };
|
export type ImportDestination =
|
||||||
|
| { type: "new_workspace" }
|
||||||
|
| { type: "existing_workspace"; workspaceId: string; folderId?: string };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Where an import's contents came from, used to link the committed workspace back to it.
|
* Where an import's contents came from, used to link the committed workspace back to it.
|
||||||
*/
|
*/
|
||||||
export type ImportOrigin = {
|
export type ImportOrigin = {
|
||||||
/**
|
/**
|
||||||
* The absolute file path or URL the contents were read from.
|
* The absolute file path or URL the contents were read from.
|
||||||
*/
|
*/
|
||||||
origin: string, label: string, };
|
origin: string;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type ImportPlan = { importer: string, destination: ImportDestination, resources: BatchUpsertResult, warnings: Array<ImportPlanWarning>,
|
export type ImportPlan = {
|
||||||
/**
|
importer: string;
|
||||||
* Stable source key for every model in `resources`, keyed by its planned ID.
|
destination: ImportDestination;
|
||||||
*/
|
resources: BatchUpsertResult;
|
||||||
sourceKeys: { [key in string]?: string },
|
warnings: Array<ImportPlanWarning>;
|
||||||
/**
|
/**
|
||||||
* One entry per plannable resource; commit applies only the selected ones.
|
* Stable source key for every model in `resources`, keyed by its planned ID.
|
||||||
*/
|
*/
|
||||||
items: Array<ImportPlanItem>, origin?: ImportOrigin, };
|
sourceKeys: { [key in string]?: string };
|
||||||
|
/**
|
||||||
|
* One entry per plannable resource; commit applies only the selected ones.
|
||||||
|
*/
|
||||||
|
items: Array<ImportPlanItem>;
|
||||||
|
origin?: ImportOrigin;
|
||||||
|
};
|
||||||
|
|
||||||
export type ImportPlanAction = "create" | "update" | "delete" | "unchanged" | "keep_local" | "conflict";
|
export type ImportPlanAction =
|
||||||
|
| "create"
|
||||||
|
| "update"
|
||||||
|
| "delete"
|
||||||
|
| "unchanged"
|
||||||
|
| "keep_local"
|
||||||
|
| "conflict"
|
||||||
|
| "not_imported";
|
||||||
|
|
||||||
|
export type ImportPlanItem = {
|
||||||
|
action: ImportPlanAction;
|
||||||
|
model: ImportResourceType;
|
||||||
|
modelId: string;
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
* Planned parent folder ID for incoming resources; current parent for deletions.
|
||||||
|
*/
|
||||||
|
parentId?: string;
|
||||||
|
selected: boolean;
|
||||||
|
resolution?: ImportConflictResolution;
|
||||||
|
reason?: ImportPlanReason;
|
||||||
|
/**
|
||||||
|
* Fields where the source and the local copy disagree, so the preview can say why
|
||||||
|
*/
|
||||||
|
changedFields: Array<string>;
|
||||||
|
};
|
||||||
|
|
||||||
export type ImportPlanItem = { action: ImportPlanAction, model: ImportResourceType, modelId: string, name: string,
|
|
||||||
/**
|
/**
|
||||||
* Planned parent folder ID for incoming resources; current parent for deletions.
|
* Extra context for an action that would otherwise be indistinguishable from its plain form.
|
||||||
*/
|
*/
|
||||||
parentId?: string, selected: boolean, resolution?: ImportConflictResolution, };
|
export type ImportPlanReason = "moved_into_not_imported_folder";
|
||||||
|
|
||||||
export type ImportPlanWarning = { title: string, detail: string, };
|
export type ImportPlanWarning = { title: string; detail: string };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The model types an import plan can contain.
|
* The model types an import plan can contain.
|
||||||
*/
|
*/
|
||||||
export type ImportResourceType = "environment" | "folder" | "grpc_request" | "http_request" | "websocket_request" | "workspace";
|
export type ImportResourceType =
|
||||||
|
| "environment"
|
||||||
|
| "folder"
|
||||||
|
| "grpc_request"
|
||||||
|
| "http_request"
|
||||||
|
| "websocket_request"
|
||||||
|
| "workspace";
|
||||||
|
|||||||
+8
-2
@@ -356,8 +356,14 @@ export type ImportSourceResource = {
|
|||||||
importSourceId: string;
|
importSourceId: string;
|
||||||
sourceKey: string;
|
sourceKey: string;
|
||||||
modelType: string;
|
modelType: string;
|
||||||
modelId: string;
|
/**
|
||||||
snapshot: string;
|
* `None` once the user has decided not to import this key
|
||||||
|
*/
|
||||||
|
modelId?: string;
|
||||||
|
/**
|
||||||
|
* Hash of the resource as last applied or decided from the source, if one was recorded
|
||||||
|
*/
|
||||||
|
contentHash?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type InheritedBoolSetting = { enabled?: boolean; value: boolean };
|
export type InheritedBoolSetting = { enabled?: boolean; value: boolean };
|
||||||
|
|||||||
Generated
+77
-23
@@ -1,7 +1,21 @@
|
|||||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
import type { Environment, Folder, GrpcRequest, HttpRequest, WebsocketRequest, Workspace } from "./gen_models";
|
import type {
|
||||||
|
Environment,
|
||||||
|
Folder,
|
||||||
|
GrpcRequest,
|
||||||
|
HttpRequest,
|
||||||
|
WebsocketRequest,
|
||||||
|
Workspace,
|
||||||
|
} from "./gen_models";
|
||||||
|
|
||||||
export type BatchUpsertResult = { workspaces: Array<Workspace>, environments: Array<Environment>, folders: Array<Folder>, httpRequests: Array<HttpRequest>, grpcRequests: Array<GrpcRequest>, websocketRequests: Array<WebsocketRequest>, };
|
export type BatchUpsertResult = {
|
||||||
|
workspaces: Array<Workspace>;
|
||||||
|
environments: Array<Environment>;
|
||||||
|
folders: Array<Folder>;
|
||||||
|
httpRequests: Array<HttpRequest>;
|
||||||
|
grpcRequests: Array<GrpcRequest>;
|
||||||
|
websocketRequests: Array<WebsocketRequest>;
|
||||||
|
};
|
||||||
|
|
||||||
export type ImportConflictResolution = "keep_mine" | "take_source";
|
export type ImportConflictResolution = "keep_mine" | "take_source";
|
||||||
|
|
||||||
@@ -11,38 +25,78 @@ export type ImportConflictResolution = "keep_mine" | "take_source";
|
|||||||
* The destination workspace and optional folder IDs are captured in the plan so the preview describes
|
* The destination workspace and optional folder IDs are captured in the plan so the preview describes
|
||||||
* the exact destination that confirmation will use.
|
* the exact destination that confirmation will use.
|
||||||
*/
|
*/
|
||||||
export type ImportDestination = { "type": "new_workspace" } | { "type": "existing_workspace", workspaceId: string, folderId?: string, };
|
export type ImportDestination =
|
||||||
|
| { type: "new_workspace" }
|
||||||
|
| { type: "existing_workspace"; workspaceId: string; folderId?: string };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Where an import's contents came from, used to link the committed workspace back to it.
|
* Where an import's contents came from, used to link the committed workspace back to it.
|
||||||
*/
|
*/
|
||||||
export type ImportOrigin = {
|
export type ImportOrigin = {
|
||||||
/**
|
/**
|
||||||
* The absolute file path or URL the contents were read from.
|
* The absolute file path or URL the contents were read from.
|
||||||
*/
|
*/
|
||||||
origin: string, label: string, };
|
origin: string;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type ImportPlan = { importer: string, destination: ImportDestination, resources: BatchUpsertResult, warnings: Array<ImportPlanWarning>,
|
export type ImportPlan = {
|
||||||
/**
|
importer: string;
|
||||||
* Stable source key for every model in `resources`, keyed by its planned ID.
|
destination: ImportDestination;
|
||||||
*/
|
resources: BatchUpsertResult;
|
||||||
sourceKeys: { [key in string]?: string },
|
warnings: Array<ImportPlanWarning>;
|
||||||
/**
|
/**
|
||||||
* One entry per plannable resource; commit applies only the selected ones.
|
* Stable source key for every model in `resources`, keyed by its planned ID.
|
||||||
*/
|
*/
|
||||||
items: Array<ImportPlanItem>, origin?: ImportOrigin, };
|
sourceKeys: { [key in string]?: string };
|
||||||
|
/**
|
||||||
|
* One entry per plannable resource; commit applies only the selected ones.
|
||||||
|
*/
|
||||||
|
items: Array<ImportPlanItem>;
|
||||||
|
origin?: ImportOrigin;
|
||||||
|
};
|
||||||
|
|
||||||
export type ImportPlanAction = "create" | "update" | "delete" | "unchanged" | "keep_local" | "conflict";
|
export type ImportPlanAction =
|
||||||
|
| "create"
|
||||||
|
| "update"
|
||||||
|
| "delete"
|
||||||
|
| "unchanged"
|
||||||
|
| "keep_local"
|
||||||
|
| "conflict"
|
||||||
|
| "not_imported";
|
||||||
|
|
||||||
|
export type ImportPlanItem = {
|
||||||
|
action: ImportPlanAction;
|
||||||
|
model: ImportResourceType;
|
||||||
|
modelId: string;
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
* Planned parent folder ID for incoming resources; current parent for deletions.
|
||||||
|
*/
|
||||||
|
parentId?: string;
|
||||||
|
selected: boolean;
|
||||||
|
resolution?: ImportConflictResolution;
|
||||||
|
reason?: ImportPlanReason;
|
||||||
|
/**
|
||||||
|
* Fields where the source and the local copy disagree, so the preview can say why
|
||||||
|
*/
|
||||||
|
changedFields: Array<string>;
|
||||||
|
};
|
||||||
|
|
||||||
export type ImportPlanItem = { action: ImportPlanAction, model: ImportResourceType, modelId: string, name: string,
|
|
||||||
/**
|
/**
|
||||||
* Planned parent folder ID for incoming resources; current parent for deletions.
|
* Extra context for an action that would otherwise be indistinguishable from its plain form.
|
||||||
*/
|
*/
|
||||||
parentId?: string, selected: boolean, resolution?: ImportConflictResolution, };
|
export type ImportPlanReason = "moved_into_not_imported_folder";
|
||||||
|
|
||||||
export type ImportPlanWarning = { title: string, detail: string, };
|
export type ImportPlanWarning = { title: string; detail: string };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The model types an import plan can contain.
|
* The model types an import plan can contain.
|
||||||
*/
|
*/
|
||||||
export type ImportResourceType = "environment" | "folder" | "grpc_request" | "http_request" | "websocket_request" | "workspace";
|
export type ImportResourceType =
|
||||||
|
| "environment"
|
||||||
|
| "folder"
|
||||||
|
| "grpc_request"
|
||||||
|
| "http_request"
|
||||||
|
| "websocket_request"
|
||||||
|
| "workspace";
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
-- Replace the per-resource snapshot with a content hash, and let a row exist without a model
|
||||||
|
-- so a resource the user chose not to import can be remembered.
|
||||||
|
CREATE TABLE import_source_resources_new
|
||||||
|
(
|
||||||
|
model TEXT DEFAULT 'import_source_resource' NOT NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
import_source_id TEXT NOT NULL,
|
||||||
|
source_key TEXT NOT NULL,
|
||||||
|
model_type TEXT NOT NULL,
|
||||||
|
model_id TEXT,
|
||||||
|
content_hash TEXT,
|
||||||
|
PRIMARY KEY (import_source_id, source_key)
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO import_source_resources_new (model, created_at, updated_at, import_source_id,
|
||||||
|
source_key, model_type, model_id, content_hash)
|
||||||
|
SELECT model, created_at, updated_at, import_source_id, source_key, model_type, model_id, NULL
|
||||||
|
FROM import_source_resources;
|
||||||
|
|
||||||
|
DROP TABLE import_source_resources;
|
||||||
|
|
||||||
|
ALTER TABLE import_source_resources_new
|
||||||
|
RENAME TO import_source_resources;
|
||||||
@@ -3118,8 +3118,12 @@ pub struct ImportSourceResource {
|
|||||||
pub import_source_id: String,
|
pub import_source_id: String,
|
||||||
pub source_key: String,
|
pub source_key: String,
|
||||||
pub model_type: String,
|
pub model_type: String,
|
||||||
pub model_id: String,
|
/// `None` once the user has decided not to import this key
|
||||||
pub snapshot: String,
|
#[ts(optional)]
|
||||||
|
pub model_id: Option<String>,
|
||||||
|
/// Hash of the resource as last applied or decided from the source, if one was recorded
|
||||||
|
#[ts(optional)]
|
||||||
|
pub content_hash: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'s> TryFrom<&Row<'s>> for ImportSourceResource {
|
impl<'s> TryFrom<&Row<'s>> for ImportSourceResource {
|
||||||
@@ -3134,7 +3138,7 @@ impl<'s> TryFrom<&Row<'s>> for ImportSourceResource {
|
|||||||
source_key: r.get("source_key")?,
|
source_key: r.get("source_key")?,
|
||||||
model_type: r.get("model_type")?,
|
model_type: r.get("model_type")?,
|
||||||
model_id: r.get("model_id")?,
|
model_id: r.get("model_id")?,
|
||||||
snapshot: r.get("snapshot")?,
|
content_hash: r.get("content_hash")?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ impl<'a> ClientDb<'a> {
|
|||||||
ImportSourceResourceIden::SourceKey,
|
ImportSourceResourceIden::SourceKey,
|
||||||
ImportSourceResourceIden::ModelType,
|
ImportSourceResourceIden::ModelType,
|
||||||
ImportSourceResourceIden::ModelId,
|
ImportSourceResourceIden::ModelId,
|
||||||
ImportSourceResourceIden::Snapshot,
|
ImportSourceResourceIden::ContentHash,
|
||||||
])
|
])
|
||||||
.values_panic([
|
.values_panic([
|
||||||
CurrentTimestamp.into(),
|
CurrentTimestamp.into(),
|
||||||
@@ -42,8 +42,8 @@ impl<'a> ClientDb<'a> {
|
|||||||
resource.import_source_id.as_str().into(),
|
resource.import_source_id.as_str().into(),
|
||||||
resource.source_key.as_str().into(),
|
resource.source_key.as_str().into(),
|
||||||
resource.model_type.as_str().into(),
|
resource.model_type.as_str().into(),
|
||||||
resource.model_id.as_str().into(),
|
resource.model_id.clone().into(),
|
||||||
resource.snapshot.as_str().into(),
|
resource.content_hash.clone().into(),
|
||||||
])
|
])
|
||||||
.on_conflict(
|
.on_conflict(
|
||||||
OnConflict::columns([
|
OnConflict::columns([
|
||||||
@@ -54,7 +54,7 @@ impl<'a> ClientDb<'a> {
|
|||||||
ImportSourceResourceIden::UpdatedAt,
|
ImportSourceResourceIden::UpdatedAt,
|
||||||
ImportSourceResourceIden::ModelType,
|
ImportSourceResourceIden::ModelType,
|
||||||
ImportSourceResourceIden::ModelId,
|
ImportSourceResourceIden::ModelId,
|
||||||
ImportSourceResourceIden::Snapshot,
|
ImportSourceResourceIden::ContentHash,
|
||||||
])
|
])
|
||||||
.to_owned(),
|
.to_owned(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -169,6 +169,16 @@ pub enum ImportPlanAction {
|
|||||||
Unchanged,
|
Unchanged,
|
||||||
KeepLocal,
|
KeepLocal,
|
||||||
Conflict,
|
Conflict,
|
||||||
|
/// Present in the source but previously turned down; selecting it imports it again
|
||||||
|
NotImported,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extra context for an action that would otherwise be indistinguishable from its plain form.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, TS)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
#[ts(export, export_to = "gen_util.ts")]
|
||||||
|
pub enum ImportPlanReason {
|
||||||
|
MovedIntoNotImportedFolder,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, TS)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, TS)]
|
||||||
@@ -193,6 +203,11 @@ pub struct ImportPlanItem {
|
|||||||
pub selected: bool,
|
pub selected: bool,
|
||||||
#[ts(optional)]
|
#[ts(optional)]
|
||||||
pub resolution: Option<ImportConflictResolution>,
|
pub resolution: Option<ImportConflictResolution>,
|
||||||
|
#[ts(optional)]
|
||||||
|
pub reason: Option<ImportPlanReason>,
|
||||||
|
/// Fields where the source and the local copy disagree, so the preview can say why
|
||||||
|
#[serde(default)]
|
||||||
|
pub changed_fields: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, TS)]
|
#[derive(Debug, Deserialize, Serialize, TS)]
|
||||||
|
|||||||
+13
@@ -11,6 +11,7 @@ export type AnyModel =
|
|||||||
| HttpRequest
|
| HttpRequest
|
||||||
| HttpResponse
|
| HttpResponse
|
||||||
| HttpResponseEvent
|
| HttpResponseEvent
|
||||||
|
| ImportSource
|
||||||
| KeyValue
|
| KeyValue
|
||||||
| Plugin
|
| Plugin
|
||||||
| Settings
|
| Settings
|
||||||
@@ -318,6 +319,18 @@ export type HttpUrlParameter = {
|
|||||||
|
|
||||||
export type HttpVersion = "auto" | "http1" | "http2";
|
export type HttpVersion = "auto" | "http1" | "http2";
|
||||||
|
|
||||||
|
export type ImportSource = {
|
||||||
|
model: "import_source";
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
workspaceId: string;
|
||||||
|
importer: string;
|
||||||
|
origin: string;
|
||||||
|
originLabel: string;
|
||||||
|
lastImportedAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type InheritedBoolSetting = { enabled?: boolean; value: boolean };
|
export type InheritedBoolSetting = { enabled?: boolean; value: boolean };
|
||||||
|
|
||||||
export type InheritedHttpVersionSetting = { enabled?: boolean; value: HttpVersion };
|
export type InheritedHttpVersionSetting = { enabled?: boolean; value: HttpVersion };
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ async-trait = "0.1"
|
|||||||
base64 = "0.22.1" # For carrying body chunks over a text-only plugin transport
|
base64 = "0.22.1" # For carrying body chunks over a text-only plugin transport
|
||||||
log = { workspace = true }
|
log = { workspace = true }
|
||||||
md5 = "0.8.0"
|
md5 = "0.8.0"
|
||||||
|
sha2 = { workspace = true }
|
||||||
chrono = { workspace = true }
|
chrono = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
|
|||||||
+695
-110
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user