@myko/ui-svelte 4.4.0 → 4.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/{src/lib → dist}/components/ConnectionStats.svelte +8 -22
  2. package/dist/components/ConnectionStats.svelte.d.ts +10 -0
  3. package/dist/components/EntityDiffBadge.svelte +10 -0
  4. package/dist/components/EntityDiffBadge.svelte.d.ts +7 -0
  5. package/{src/lib → dist}/components/EntityDiffFields.svelte +27 -49
  6. package/dist/components/EntityDiffFields.svelte.d.ts +11 -0
  7. package/{src/lib → dist}/components/EntityDiffNode.svelte +25 -43
  8. package/dist/components/EntityDiffNode.svelte.d.ts +14 -0
  9. package/{src/lib → dist}/components/EntityDiffTree.svelte +150 -200
  10. package/dist/components/EntityDiffTree.svelte.d.ts +21 -0
  11. package/dist/components/Logs.svelte +30 -0
  12. package/dist/components/Logs.svelte.d.ts +7 -0
  13. package/dist/components/Query.svelte +21 -0
  14. package/dist/components/Query.svelte.d.ts +35 -0
  15. package/dist/components/Report.svelte +13 -0
  16. package/dist/components/Report.svelte.d.ts +32 -0
  17. package/dist/components/Search.svelte +46 -0
  18. package/dist/components/Search.svelte.d.ts +56 -0
  19. package/dist/components/ServerView.svelte +76 -0
  20. package/dist/components/ServerView.svelte.d.ts +8 -0
  21. package/dist/components/state/resolutions.d.ts +11 -0
  22. package/dist/components/state/resolutions.js +129 -0
  23. package/dist/components/state/viewstate.svelte.d.ts +60 -0
  24. package/dist/components/state/viewstate.svelte.js +259 -0
  25. package/dist/components/state/windback.svelte.d.ts +16 -0
  26. package/dist/components/state/windback.svelte.js +65 -0
  27. package/dist/components/transactions/EntityHistory.svelte +135 -0
  28. package/dist/components/transactions/EntityHistory.svelte.d.ts +10 -0
  29. package/{src/lib → dist}/components/transactions/TimeStrip.svelte +18 -25
  30. package/dist/components/transactions/TimeStrip.svelte.d.ts +3 -0
  31. package/dist/components/transactions/TransactionDetails.svelte +24 -0
  32. package/dist/components/transactions/TransactionDetails.svelte.d.ts +7 -0
  33. package/{src/lib → dist}/components/transactions/TransactionEvent.svelte +20 -32
  34. package/dist/components/transactions/TransactionEvent.svelte.d.ts +7 -0
  35. package/{src/lib → dist}/components/transactions/TransactionEventGroup.svelte +8 -13
  36. package/dist/components/transactions/TransactionEventGroup.svelte.d.ts +8 -0
  37. package/dist/components/transactions/Transactions.svelte +94 -0
  38. package/dist/components/transactions/Transactions.svelte.d.ts +8 -0
  39. package/{src/lib → dist}/components/transactions/TransactonView.svelte +3 -7
  40. package/dist/components/transactions/TransactonView.svelte.d.ts +7 -0
  41. package/{src/lib → dist}/components/windback/WindbackFrame.svelte +3 -6
  42. package/dist/components/windback/WindbackFrame.svelte.d.ts +7 -0
  43. package/dist/components/windback/index.js +1 -0
  44. package/dist/index.d.ts +18 -0
  45. package/{src/lib/index.ts → dist/index.js} +2 -15
  46. package/dist/services/svelte-client.svelte.d.ts +278 -0
  47. package/dist/services/svelte-client.svelte.js +678 -0
  48. package/dist/utils/entity-apply.d.ts +17 -0
  49. package/dist/utils/entity-apply.js +35 -0
  50. package/dist/utils/entity-diff.d.ts +40 -0
  51. package/dist/utils/entity-diff.js +57 -0
  52. package/dist/utils/entity-tree.d.ts +24 -0
  53. package/dist/utils/entity-tree.js +111 -0
  54. package/package.json +13 -9
  55. package/.prettierignore +0 -4
  56. package/.prettierrc +0 -15
  57. package/src/app.d.ts +0 -13
  58. package/src/app.html +0 -12
  59. package/src/lib/components/EntityDiffBadge.svelte +0 -18
  60. package/src/lib/components/Logs.svelte +0 -37
  61. package/src/lib/components/Query.svelte +0 -34
  62. package/src/lib/components/Report.svelte +0 -25
  63. package/src/lib/components/Search.svelte +0 -85
  64. package/src/lib/components/ServerView.svelte +0 -95
  65. package/src/lib/components/state/resolutions.ts +0 -137
  66. package/src/lib/components/state/viewstate.svelte.ts +0 -375
  67. package/src/lib/components/state/windback.svelte.ts +0 -88
  68. package/src/lib/components/transactions/EntityHistory.svelte +0 -173
  69. package/src/lib/components/transactions/TransactionDetails.svelte +0 -26
  70. package/src/lib/components/transactions/Transactions.svelte +0 -111
  71. package/src/lib/services/svelte-client.svelte.ts +0 -863
  72. package/src/lib/utils/entity-apply.ts +0 -47
  73. package/src/lib/utils/entity-diff.ts +0 -105
  74. package/src/lib/utils/entity-tree.ts +0 -130
  75. package/src/routes/+page.svelte +0 -3
  76. package/static/favicon.png +0 -0
  77. package/svelte.config.js +0 -18
  78. package/tsconfig.json +0 -13
  79. package/vite.config.ts +0 -6
  80. /package/{src/lib/components/windback/index.ts → dist/components/windback/index.d.ts} +0 -0
@@ -1,25 +1,11 @@
1
- <script lang="ts">
2
- import { GetConnectedServer } from '@myko/core';
3
- import { getMykoClient, type SvelteMykoClient } from '../services/svelte-client.svelte.js';
4
-
5
- interface Props {
6
- /** Optional client instance (defaults to global singleton) */
7
- client?: SvelteMykoClient;
8
- /** CSS class for the container */
9
- class?: string;
10
- }
11
-
12
- let { client, class: className = '' }: Props = $props();
13
-
14
- const resolvedClient = client ?? getMykoClient();
15
-
16
- const stats = $derived(resolvedClient.stats);
17
- const isConnected = $derived(resolvedClient.isConnected);
18
-
19
- // Query for connected server
20
- const serverQuery = resolvedClient.liveQuery(() => new GetConnectedServer({}));
21
-
22
- const server = $derived([...serverQuery.items.values()][0]);
1
+ <script lang="ts">import { GetConnectedServer } from "@myko/core";
2
+ import { getMykoClient } from "../services/svelte-client.svelte.js";
3
+ let { client, class: className = "" } = $props();
4
+ const resolvedClient = client ?? getMykoClient();
5
+ const stats = $derived(resolvedClient.stats);
6
+ const isConnected = $derived(resolvedClient.isConnected);
7
+ const serverQuery = resolvedClient.liveQuery(() => new GetConnectedServer({}));
8
+ const server = $derived([...serverQuery.items.values()][0]);
23
9
  </script>
24
10
 
25
11
  {#if isConnected && stats}
@@ -0,0 +1,10 @@
1
+ import { type SvelteMykoClient } from '../services/svelte-client.svelte.js';
2
+ interface Props {
3
+ /** Optional client instance (defaults to global singleton) */
4
+ client?: SvelteMykoClient;
5
+ /** CSS class for the container */
6
+ class?: string;
7
+ }
8
+ declare const ConnectionStats: import("svelte").Component<Props, {}, "">;
9
+ type ConnectionStats = ReturnType<typeof ConnectionStats>;
10
+ export default ConnectionStats;
@@ -0,0 +1,10 @@
1
+ <script lang="ts">let { status } = $props();
2
+ const config = {
3
+ added: { label: "Added", class: "badge-success" },
4
+ removed: { label: "Removed", class: "badge-error" },
5
+ modified: { label: "Modified", class: "badge-warning" },
6
+ unchanged: { label: "Unchanged", class: "badge-ghost" }
7
+ };
8
+ </script>
9
+
10
+ <span class="badge badge-sm {config[status].class}">{config[status].label}</span>
@@ -0,0 +1,7 @@
1
+ import type { DiffStatus } from '../utils/entity-diff.js';
2
+ interface Props {
3
+ status: DiffStatus;
4
+ }
5
+ declare const EntityDiffBadge: import("svelte").Component<Props, {}, "">;
6
+ type EntityDiffBadge = ReturnType<typeof EntityDiffBadge>;
7
+ export default EntityDiffBadge;
@@ -1,54 +1,32 @@
1
- <script lang="ts">
2
- import type { FieldDiff } from '../utils/entity-diff.js';
3
-
4
- interface Props {
5
- fields: FieldDiff[];
6
- /** Full current entity data (for side-by-side view). */
7
- currentData?: Record<string, unknown>;
8
- /** Full incoming entity data (for side-by-side view). */
9
- incomingData?: Record<string, unknown>;
10
- }
11
-
12
- let { fields, currentData, incomingData }: Props = $props();
13
-
14
- const hasSideBySide = $derived(!!currentData && !!incomingData);
15
-
16
- function formatValue(v: unknown): string {
17
- if (v === undefined || v === null) return '';
18
- if (typeof v === 'object') return JSON.stringify(v, null, 2);
19
- return String(v);
20
- }
21
-
22
- function isObject(v: unknown): v is Record<string, unknown> {
23
- return typeof v === 'object' && v !== null && !Array.isArray(v);
24
- }
25
-
26
- interface DiffLine {
27
- key: string;
28
- status: 'added' | 'removed' | 'changed' | 'unchanged';
29
- oldVal?: string;
30
- newVal?: string;
31
- }
32
-
33
- function computeJsonDiff(oldVal: unknown, newVal: unknown): DiffLine[] | null {
34
- if (!isObject(oldVal) || !isObject(newVal)) return null;
35
- const allKeys = new Set([...Object.keys(oldVal), ...Object.keys(newVal)]);
36
- const lines: DiffLine[] = [];
37
- for (const key of allKeys) {
38
- const o = oldVal[key];
39
- const n = newVal[key];
40
- if (!(key in newVal)) {
41
- lines.push({ key, status: 'removed', oldVal: JSON.stringify(o) });
42
- } else if (!(key in oldVal)) {
43
- lines.push({ key, status: 'added', newVal: JSON.stringify(n) });
44
- } else if (JSON.stringify(o) !== JSON.stringify(n)) {
45
- lines.push({ key, status: 'changed', oldVal: JSON.stringify(o), newVal: JSON.stringify(n) });
46
- } else {
47
- lines.push({ key, status: 'unchanged', oldVal: JSON.stringify(o), newVal: JSON.stringify(n) });
48
- }
1
+ <script lang="ts">let { fields, currentData, incomingData } = $props();
2
+ const hasSideBySide = $derived(!!currentData && !!incomingData);
3
+ function formatValue(v) {
4
+ if (v === void 0 || v === null) return "";
5
+ if (typeof v === "object") return JSON.stringify(v, null, 2);
6
+ return String(v);
7
+ }
8
+ function isObject(v) {
9
+ return typeof v === "object" && v !== null && !Array.isArray(v);
10
+ }
11
+ function computeJsonDiff(oldVal, newVal) {
12
+ if (!isObject(oldVal) || !isObject(newVal)) return null;
13
+ const allKeys = /* @__PURE__ */ new Set([...Object.keys(oldVal), ...Object.keys(newVal)]);
14
+ const lines = [];
15
+ for (const key of allKeys) {
16
+ const o = oldVal[key];
17
+ const n = newVal[key];
18
+ if (!(key in newVal)) {
19
+ lines.push({ key, status: "removed", oldVal: JSON.stringify(o) });
20
+ } else if (!(key in oldVal)) {
21
+ lines.push({ key, status: "added", newVal: JSON.stringify(n) });
22
+ } else if (JSON.stringify(o) !== JSON.stringify(n)) {
23
+ lines.push({ key, status: "changed", oldVal: JSON.stringify(o), newVal: JSON.stringify(n) });
24
+ } else {
25
+ lines.push({ key, status: "unchanged", oldVal: JSON.stringify(o), newVal: JSON.stringify(n) });
49
26
  }
50
- return lines;
51
27
  }
28
+ return lines;
29
+ }
52
30
  </script>
53
31
 
54
32
  {#if hasSideBySide}
@@ -0,0 +1,11 @@
1
+ import type { FieldDiff } from '../utils/entity-diff.js';
2
+ interface Props {
3
+ fields: FieldDiff[];
4
+ /** Full current entity data (for side-by-side view). */
5
+ currentData?: Record<string, unknown>;
6
+ /** Full incoming entity data (for side-by-side view). */
7
+ incomingData?: Record<string, unknown>;
8
+ }
9
+ declare const EntityDiffFields: import("svelte").Component<Props, {}, "">;
10
+ type EntityDiffFields = ReturnType<typeof EntityDiffFields>;
11
+ export default EntityDiffFields;
@@ -1,47 +1,29 @@
1
- <script lang="ts">
2
- import type { EntityDiff } from '../utils/entity-diff.js';
3
- import EntityDiffBadge from './EntityDiffBadge.svelte';
4
- import EntityDiffFields from './EntityDiffFields.svelte';
5
-
6
- interface Props {
7
- node: EntityDiff;
8
- showUnchanged?: boolean;
9
- /** Set of "Type:id" keys excluded from import. */
10
- excluded?: Set<string>;
11
- /** Callback to toggle an entity's excluded state. */
12
- onToggleExclude?: (key: string) => void;
13
- /** Whether this is the root node (always visible). */
14
- isRoot?: boolean;
15
- }
16
-
17
- let { node, showUnchanged = false, excluded, onToggleExclude, isRoot = false }: Props = $props();
18
- let expanded = $state(node.status !== 'unchanged' || isRoot);
19
-
20
- const key = $derived(`${node.type}:${node.id}`);
21
- const isExcluded = $derived(excluded?.has(key) ?? false);
22
- const canExclude = $derived(
23
- !isRoot && (node.status === 'modified' || node.status === 'added' || node.status === 'removed'),
24
- );
25
-
26
- const toggleExclude = (e: Event) => {
27
- e.stopPropagation();
28
- onToggleExclude?.(key);
1
+ <script lang="ts">import EntityDiffBadge from "./EntityDiffBadge.svelte";
2
+ import EntityDiffFields from "./EntityDiffFields.svelte";
3
+ let { node, showUnchanged = false, excluded, onToggleExclude, isRoot = false } = $props();
4
+ let expanded = $state(node.status !== "unchanged" || isRoot);
5
+ const key = $derived(`${node.type}:${node.id}`);
6
+ const isExcluded = $derived(excluded?.has(key) ?? false);
7
+ const canExclude = $derived(
8
+ !isRoot && (node.status === "modified" || node.status === "added" || node.status === "removed")
9
+ );
10
+ const toggleExclude = (e) => {
11
+ e.stopPropagation();
12
+ onToggleExclude?.(key);
13
+ };
14
+ const hasChangedDescendant = $derived.by(() => {
15
+ const check = (n) => {
16
+ if (n.status !== "unchanged") return true;
17
+ return n.children.some(check);
29
18
  };
30
-
31
- const hasChangedDescendant = $derived.by((): boolean => {
32
- const check = (n: EntityDiff): boolean => {
33
- if (n.status !== 'unchanged') return true;
34
- return n.children.some(check);
35
- };
36
- return node.children.some(check);
37
- });
38
-
39
- const visible = $derived(
40
- isRoot || showUnchanged || node.status !== 'unchanged' || hasChangedDescendant,
41
- );
42
- const hasDetails = $derived(
43
- (node.fields && node.fields.length > 0) || node.children.length > 0,
44
- );
19
+ return node.children.some(check);
20
+ });
21
+ const visible = $derived(
22
+ isRoot || showUnchanged || node.status !== "unchanged" || hasChangedDescendant
23
+ );
24
+ const hasDetails = $derived(
25
+ node.fields && node.fields.length > 0 || node.children.length > 0
26
+ );
45
27
  </script>
46
28
 
47
29
  {#if visible}
@@ -0,0 +1,14 @@
1
+ import type { EntityDiff } from '../utils/entity-diff.js';
2
+ interface Props {
3
+ node: EntityDiff;
4
+ showUnchanged?: boolean;
5
+ /** Set of "Type:id" keys excluded from import. */
6
+ excluded?: Set<string>;
7
+ /** Callback to toggle an entity's excluded state. */
8
+ onToggleExclude?: (key: string) => void;
9
+ /** Whether this is the root node (always visible). */
10
+ isRoot?: boolean;
11
+ }
12
+ declare const EntityDiffNode: import("svelte").Component<Props, {}, "">;
13
+ type EntityDiffNode = ReturnType<typeof EntityDiffNode>;
14
+ export default EntityDiffNode;
@@ -1,211 +1,161 @@
1
- <script lang="ts">
2
- import type { ExportedEntity, EntityDiff, DiffStatus } from '../utils/entity-diff.js';
3
- import { diffEntityLists } from '../utils/entity-diff.js';
4
- import type { ParentFkFields } from '../utils/entity-tree.js';
5
- import { buildDiffTree } from '../utils/entity-tree.js';
6
- import EntityDiffBadge from './EntityDiffBadge.svelte';
7
-
8
- interface Props {
9
- /** Root entity type (e.g., "Project") */
10
- rootType: string;
11
- /** Root entity ID */
12
- rootId: string;
13
- /** Entities currently on the server */
14
- current: ExportedEntity[];
15
- /** Entities from the incoming source (file or snapshot) */
16
- incoming: ExportedEntity[];
17
- /** Maps child entity type to parent FK field names */
18
- parentFkFields: ParentFkFields;
19
- /** Set of "Type:id" keys the user chose to exclude from import */
20
- excluded?: Set<string>;
21
- /** Whether to show unchanged entities */
22
- showUnchanged?: boolean;
23
- }
24
-
25
- let { rootType, rootId, current, incoming, parentFkFields, excluded = $bindable(new Set()), showUnchanged = false }: Props = $props();
26
-
27
- const diffs = $derived(diffEntityLists(current, incoming));
28
- const tree = $derived(buildDiffTree(rootType, rootId, diffs, parentFkFields));
29
-
30
- const toggleExclude = (key: string) => {
31
- const next = new Set(excluded);
32
- if (next.has(key)) {
33
- next.delete(key);
34
- } else {
35
- next.add(key);
1
+ <script lang="ts">import { diffEntityLists } from "../utils/entity-diff.js";
2
+ import { buildDiffTree } from "../utils/entity-tree.js";
3
+ import EntityDiffBadge from "./EntityDiffBadge.svelte";
4
+ let { rootType, rootId, current, incoming, parentFkFields, excluded = $bindable(/* @__PURE__ */ new Set()), showUnchanged = false } = $props();
5
+ const diffs = $derived(diffEntityLists(current, incoming));
6
+ const tree = $derived(buildDiffTree(rootType, rootId, diffs, parentFkFields));
7
+ const toggleExclude = (key) => {
8
+ const next = new Set(excluded);
9
+ if (next.has(key)) {
10
+ next.delete(key);
11
+ } else {
12
+ next.add(key);
13
+ }
14
+ excluded = next;
15
+ };
16
+ function actionLabels(status) {
17
+ switch (status) {
18
+ case "removed":
19
+ return ["delete", "keep"];
20
+ case "added":
21
+ return ["add", "skip"];
22
+ case "modified":
23
+ return ["update", "keep"];
24
+ default:
25
+ return ["apply", "skip"];
26
+ }
27
+ }
28
+ const stats = $derived.by(() => {
29
+ let toDelete = 0, toUpdate = 0, toAdd = 0, toSkip = 0, unchanged = 0;
30
+ for (const [key, d] of diffs) {
31
+ if (d.status === "unchanged") {
32
+ unchanged++;
33
+ continue;
36
34
  }
37
- excluded = next;
38
- };
39
-
40
- /** Action labels per status. First = apply action, second = skip action. */
41
- function actionLabels(status: DiffStatus): [string, string] {
42
- switch (status) {
43
- case 'removed': return ['delete', 'keep'];
44
- case 'added': return ['add', 'skip'];
45
- case 'modified': return ['update', 'keep'];
46
- default: return ['apply', 'skip'];
47
- }
48
- }
49
-
50
-
51
- const stats = $derived.by(() => {
52
- let toDelete = 0, toUpdate = 0, toAdd = 0, toSkip = 0, unchanged = 0;
53
- for (const [key, d] of diffs) {
54
- if (d.status === 'unchanged') { unchanged++; continue; }
55
- if (excluded.has(key)) { toSkip++; continue; }
56
- if (d.status === 'removed') toDelete++;
57
- else if (d.status === 'modified') toUpdate++;
58
- else if (d.status === 'added') toAdd++;
35
+ if (excluded.has(key)) {
36
+ toSkip++;
37
+ continue;
59
38
  }
60
- return { toDelete, toUpdate, toAdd, toSkip, unchanged };
61
- });
62
-
63
- // --- Batch operations ---
64
-
65
- /** Keys grouped by change type (excludes unchanged). */
66
- const keysByStatus = $derived.by(() => {
67
- const groups: Record<string, string[]> = { added: [], removed: [], modified: [] };
68
- for (const [key, d] of diffs) {
69
- if (d.status !== 'unchanged') groups[d.status]?.push(key);
70
- }
71
- return groups;
72
- });
73
-
74
- /** Keys grouped by entity type (excludes unchanged). */
75
- const keysByEntityType = $derived.by(() => {
76
- const groups: Record<string, string[]> = {};
77
- for (const [key, d] of diffs) {
78
- if (d.status === 'unchanged') continue;
79
- const [type] = key.split(':');
80
- (groups[type] ??= []).push(key);
81
- }
82
- return groups;
83
- });
84
-
85
- function batchApply(keys: string[]) {
86
- const next = new Set(excluded);
87
- for (const k of keys) next.delete(k);
88
- excluded = next;
89
- saveExpanded(expandedSet);
90
- }
91
-
92
- function batchSkip(keys: string[]) {
93
- const next = new Set(excluded);
94
- for (const k of keys) next.add(k);
95
- excluded = next;
96
- saveExpanded(expandedSet);
97
- }
98
-
99
- // --- Flatten tree into grid rows ---
100
-
101
- type FlatRow =
102
- | { kind: 'entity'; node: EntityDiff; depth: number; isRoot: boolean }
103
- | { kind: 'field'; fieldName: string; existing: string; pending: string; result: string; status: 'added' | 'removed' | 'changed'; depth: number; parentKey: string };
104
-
105
- const STORAGE_KEY = `rship:diff-expanded:${rootType}:${rootId}`;
106
-
107
- let expandedSet = $state(loadExpanded());
108
-
109
- function loadExpanded(): Set<string> {
110
- try {
111
- const stored = sessionStorage.getItem(STORAGE_KEY);
112
- if (stored) return new Set(JSON.parse(stored));
113
- } catch { /* ignore */ }
114
- // Default: only root expanded
115
- return new Set([`${rootType}:${rootId}`]);
116
- }
117
-
118
- function saveExpanded(set: Set<string>) {
119
- try {
120
- sessionStorage.setItem(STORAGE_KEY, JSON.stringify([...set]));
121
- } catch { /* ignore */ }
122
- }
123
-
124
- function hasChanges(node: EntityDiff): boolean {
125
- if (node.status !== 'unchanged') return true;
126
- return node.children.some(hasChanges);
127
- }
128
-
129
- function toggleExpand(key: string) {
130
- const next = new Set(expandedSet);
131
- if (next.has(key)) next.delete(key);
132
- else next.add(key);
133
- expandedSet = next;
134
- saveExpanded(next);
135
- }
136
-
137
- function formatVal(v: unknown): string {
138
- if (v === undefined || v === null) return '';
139
- if (typeof v === 'object') return JSON.stringify(v, null, 2);
140
- return String(v);
141
- }
142
-
143
- const rows = $derived.by((): FlatRow[] => {
144
- const result: FlatRow[] = [];
145
-
146
- const walk = (node: EntityDiff, depth: number, isRoot: boolean) => {
147
- const key = `${node.type}:${node.id}`;
148
- const visible = isRoot || showUnchanged || node.status !== 'unchanged' || hasChanges(node);
149
- if (!visible) return;
150
-
151
- result.push({ kind: 'entity', node, depth, isRoot });
152
-
153
- const isExpanded = expandedSet.has(key);
154
- const isExcluded = excluded.has(key);
155
-
156
- if (isExpanded && !isExcluded) {
157
- // Emit field rows for modified entities
158
- if (node.fields && node.fields.length > 0) {
159
- for (const f of node.fields) {
160
- const oldStr = formatVal(f.oldValue);
161
- const newStr = formatVal(f.newValue);
162
- const status: 'added' | 'removed' | 'changed' =
163
- f.oldValue === undefined ? 'added' :
164
- f.newValue === undefined ? 'removed' : 'changed';
39
+ if (d.status === "removed") toDelete++;
40
+ else if (d.status === "modified") toUpdate++;
41
+ else if (d.status === "added") toAdd++;
42
+ }
43
+ return { toDelete, toUpdate, toAdd, toSkip, unchanged };
44
+ });
45
+ const keysByStatus = $derived.by(() => {
46
+ const groups = { added: [], removed: [], modified: [] };
47
+ for (const [key, d] of diffs) {
48
+ if (d.status !== "unchanged") groups[d.status]?.push(key);
49
+ }
50
+ return groups;
51
+ });
52
+ const keysByEntityType = $derived.by(() => {
53
+ const groups = {};
54
+ for (const [key, d] of diffs) {
55
+ if (d.status === "unchanged") continue;
56
+ const [type] = key.split(":");
57
+ (groups[type] ??= []).push(key);
58
+ }
59
+ return groups;
60
+ });
61
+ function batchApply(keys) {
62
+ const next = new Set(excluded);
63
+ for (const k of keys) next.delete(k);
64
+ excluded = next;
65
+ saveExpanded(expandedSet);
66
+ }
67
+ function batchSkip(keys) {
68
+ const next = new Set(excluded);
69
+ for (const k of keys) next.add(k);
70
+ excluded = next;
71
+ saveExpanded(expandedSet);
72
+ }
73
+ const STORAGE_KEY = `rship:diff-expanded:${rootType}:${rootId}`;
74
+ let expandedSet = $state(loadExpanded());
75
+ function loadExpanded() {
76
+ try {
77
+ const stored = sessionStorage.getItem(STORAGE_KEY);
78
+ if (stored) return new Set(JSON.parse(stored));
79
+ } catch {
80
+ }
81
+ return /* @__PURE__ */ new Set([`${rootType}:${rootId}`]);
82
+ }
83
+ function saveExpanded(set) {
84
+ try {
85
+ sessionStorage.setItem(STORAGE_KEY, JSON.stringify([...set]));
86
+ } catch {
87
+ }
88
+ }
89
+ function hasChanges(node) {
90
+ if (node.status !== "unchanged") return true;
91
+ return node.children.some(hasChanges);
92
+ }
93
+ function toggleExpand(key) {
94
+ const next = new Set(expandedSet);
95
+ if (next.has(key)) next.delete(key);
96
+ else next.add(key);
97
+ expandedSet = next;
98
+ saveExpanded(next);
99
+ }
100
+ function formatVal(v) {
101
+ if (v === void 0 || v === null) return "";
102
+ if (typeof v === "object") return JSON.stringify(v, null, 2);
103
+ return String(v);
104
+ }
105
+ const rows = $derived.by(() => {
106
+ const result = [];
107
+ const walk = (node, depth, isRoot) => {
108
+ const key = `${node.type}:${node.id}`;
109
+ const visible = isRoot || showUnchanged || node.status !== "unchanged" || hasChanges(node);
110
+ if (!visible) return;
111
+ result.push({ kind: "entity", node, depth, isRoot });
112
+ const isExpanded = expandedSet.has(key);
113
+ const isExcluded = excluded.has(key);
114
+ if (isExpanded && !isExcluded) {
115
+ if (node.fields && node.fields.length > 0) {
116
+ for (const f of node.fields) {
117
+ const oldStr = formatVal(f.oldValue);
118
+ const newStr = formatVal(f.newValue);
119
+ const status = f.oldValue === void 0 ? "added" : f.newValue === void 0 ? "removed" : "changed";
120
+ result.push({
121
+ kind: "field",
122
+ fieldName: f.field,
123
+ existing: oldStr,
124
+ pending: newStr,
125
+ result: status === "removed" ? "" : newStr,
126
+ status,
127
+ depth: depth + 1,
128
+ parentKey: key
129
+ });
130
+ }
131
+ }
132
+ if (!node.fields || node.fields.length === 0) {
133
+ const data = node.status === "removed" ? node.currentData : node.status === "added" ? node.incomingData : null;
134
+ if (data) {
135
+ for (const [k, v] of Object.entries(data)) {
136
+ if (k === "hash") continue;
137
+ const valStr = formatVal(v);
165
138
  result.push({
166
- kind: 'field',
167
- fieldName: f.field,
168
- existing: oldStr,
169
- pending: newStr,
170
- result: status === 'removed' ? '' : newStr,
171
- status,
139
+ kind: "field",
140
+ fieldName: k,
141
+ existing: node.status === "removed" ? valStr : "",
142
+ pending: node.status === "added" ? valStr : "",
143
+ result: node.status === "added" ? valStr : "",
144
+ status: node.status === "removed" ? "removed" : "added",
172
145
  depth: depth + 1,
173
- parentKey: key,
146
+ parentKey: key
174
147
  });
175
148
  }
176
149
  }
177
-
178
- // Emit fields for added/removed entities (all fields as a block)
179
- if (!node.fields || node.fields.length === 0) {
180
- const data = node.status === 'removed' ? node.currentData : node.status === 'added' ? node.incomingData : null;
181
- if (data) {
182
- for (const [k, v] of Object.entries(data)) {
183
- if (k === 'hash') continue;
184
- const valStr = formatVal(v);
185
- result.push({
186
- kind: 'field',
187
- fieldName: k,
188
- existing: node.status === 'removed' ? valStr : '',
189
- pending: node.status === 'added' ? valStr : '',
190
- result: node.status === 'added' ? valStr : '',
191
- status: node.status === 'removed' ? 'removed' : 'added',
192
- depth: depth + 1,
193
- parentKey: key,
194
- });
195
- }
196
- }
197
- }
198
-
199
- // Recurse into children
200
- for (const child of node.children) {
201
- walk(child, depth + 1, false);
202
- }
203
150
  }
204
- };
205
-
206
- walk(tree, 0, true);
207
- return result;
208
- });
151
+ for (const child of node.children) {
152
+ walk(child, depth + 1, false);
153
+ }
154
+ }
155
+ };
156
+ walk(tree, 0, true);
157
+ return result;
158
+ });
209
159
  </script>
210
160
 
211
161
  <div class="diff-grid-wrapper">
@@ -0,0 +1,21 @@
1
+ import type { ExportedEntity } from '../utils/entity-diff.js';
2
+ import type { ParentFkFields } from '../utils/entity-tree.js';
3
+ interface Props {
4
+ /** Root entity type (e.g., "Project") */
5
+ rootType: string;
6
+ /** Root entity ID */
7
+ rootId: string;
8
+ /** Entities currently on the server */
9
+ current: ExportedEntity[];
10
+ /** Entities from the incoming source (file or snapshot) */
11
+ incoming: ExportedEntity[];
12
+ /** Maps child entity type to parent FK field names */
13
+ parentFkFields: ParentFkFields;
14
+ /** Set of "Type:id" keys the user chose to exclude from import */
15
+ excluded?: Set<string>;
16
+ /** Whether to show unchanged entities */
17
+ showUnchanged?: boolean;
18
+ }
19
+ declare const EntityDiffTree: import("svelte").Component<Props, {}, "excluded">;
20
+ type EntityDiffTree = ReturnType<typeof EntityDiffTree>;
21
+ export default EntityDiffTree;