mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-07 08:02:40 +00:00
* feat(apps): show raw-app fork diffs as per-file tree items Raw-app diffs previously rendered as one big YAML diff of the whole serialized app. This explodes a raw app into separate, independently collapsible diff items — one per file, one per runnable, and an app.yaml metadata item — that flow through the existing fork-diff list, sidebar tree, search and count via composite paths (<appPath>/<file>). Runnables render as script/flow rows (code shown in a Content tab), and files get extension-specific icons reused from the raw-app editor. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: remove raw-app tree-diff plan doc from the branch The implementation plan was an authoring aid, not product documentation; drop it so it doesn't ship in the PR. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: present raw app as an app-headed folder in the diff tree Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: narrow RawAppFileItem in diff viewer branch (fixes svelte-check) DiffRow.kind is a plain string so the kind check didn't narrow the union; assert the synthetic item. Also size-guard on the larger side's line count instead of the doubled total, and document normalizeRawApp's per-field value-wrapper precedence. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style: single-line, lighter diff-tree rows for all item kinds Add a singleLine mode to WorkspaceItemRow (summary ?? path on one line; DRY'd via a shared body snippet) and use it for every diff-tree leaf, so scripts/flows/triggers/resources/etc. match the raw-app header. Bump rows to py-1.5, force font-normal, and split colours: items in text-primary, folders in text-secondary. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor: extract pure diffTree model from WorkspaceDiffDrawer Move tree construction + keyboard-nav traversal + the folder-keying convention out of the 775-line component into a pure, generic, tested module (buildDiffTree → root/order/parentKeyOf/firstChildKeyOf). Parent and first-child come from a child→parent map built during construction, not from re-splitting a path at the call site, so a node's tree position and its nav parent can't drift — the class of bug behind the ArrowLeft regression. Deletes the forkDiffNav half-seam (its bug lived in the untested caller). 12 new unit tests cover order/parent/first-child incl. the storage-key-vs-friendly-path case. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(apps): keep raw-app metadata flag + dedup runnables across path collisions Addresses two P2 review nits (Codex/claude): (1) rawAppDiffToItems marked metadata by matching path==='app.yaml', so when a real file is named app.yaml the reserved app.yaml~2 metadata item lost its flag/full-YAML toggle — now parseRawAppDiff tags the entry with isMetadata and the items read the flag; (2) runnable composite leaves weren't deduped against real files, so a real file at runnables/<name> could produce a duplicate leaf — now reserved (slash-normalized) like parseRawAppDiff. +2 tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(apps): dedup /app.yaml metadata collision + disambiguate synthetic row keys Two follow-up P2s from Pi/Codex re-review of the prior fix: (1) parseRawAppDiff's collision set used raw file keys, so a real file /app.yaml (leading slash, which joinAppPath strips) still collided with the synthetic app.yaml leaf — now slash-normalized via a shared stripLeadingSlash, +test. (2) synthetic raw-app items (runnables rendered as script/flow) could share kind+path identity with a real workspace script/flow at <appPath>/runnables/<name>, causing duplicate {#each} keys and broken nav — itemKey now prefixes synthetic items (rawapp:). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(apps): canonicalize raw-app file keys to dedup leading-slash collisions Codex P2: a file keyed /App.tsx on one side and App.tsx on the other became two entries that joinAppPath collapsed to one composite path → duplicate row key. asFileMap now strips the leading slash so both sides resolve to one file. +test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf(apps): lazy-mount per-file diff editors as they scroll into view Exploding a raw app into N per-file rows mounted N Monaco DiffEditors at once (3 reviews flagged it). Each block's editor now mounts only when it scrolls within ~200px of the viewport (IntersectionObserver rooted on the scroll container), showing a light placeholder until then; mountedRows latches so it never unmounts on scroll-away. Verified: ~6 of 13 mount initially, the rest on scroll. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
181 lines
6.4 KiB
Svelte
181 lines
6.4 KiB
Svelte
<!--
|
|
@component
|
|
Inline diff renderer for a single workspace item. Mirrors the per-kind
|
|
rendering that DiffDrawer does in its body (`DiffDrawer.svelte:181-271`):
|
|
|
|
- `flow` → `<FlowDiffViewer>` (its own Graph / YAML toggle inside)
|
|
- `raw_app_file` → `<RawAppFileDiff>` (one synthesized raw-app file item: a
|
|
single diff with a per-file size guard; the metadata item adds a full-app
|
|
YAML expand). Raw apps are exploded into these items by `rawAppDiffToItems`.
|
|
- has `content` (scripts) → Tabs(Content | Metadata) with two Monaco diffs
|
|
- everything else (apps, resources, variables, schedules, triggers…) →
|
|
a single Monaco YAML diff over the metadata
|
|
|
|
`inlineDiff` flips Monaco's `renderSideBySide` to false (unified view).
|
|
The component is content-sized — each Monaco block is sized to fit its
|
|
diff text (no internal scroll) using `lines * 19 + 24`; for the
|
|
Content+Metadata case we use the max of the two so switching tabs
|
|
doesn't reflow the parent.
|
|
-->
|
|
<script lang="ts">
|
|
import Tabs from './common/tabs/Tabs.svelte'
|
|
import Tab from './common/tabs/Tab.svelte'
|
|
import FlowDiffViewer from './FlowDiffViewer.svelte'
|
|
import RawAppFileDiff from './raw_apps/RawAppFileDiff.svelte'
|
|
import type { RawAppFileItem } from './raw_apps/rawAppDiffUtils'
|
|
import { Loader2 } from 'lucide-svelte'
|
|
import { cleanValueProperties, orderedYamlStringify, replaceFalseWithUndefined } from '$lib/utils'
|
|
import { scriptLangToEditorLang } from '$lib/scripts'
|
|
|
|
interface Props {
|
|
/** Any WorkspaceItemDiff['kind'], plus the synthetic `raw_app_file`.
|
|
* `flow` and `raw_app_file` are special-cased. */
|
|
kind: string
|
|
/** Raw value from `getItemValue(kind, path, parentWorkspace)`. Undefined
|
|
* for "added" items (don't exist in the parent). */
|
|
originalRaw?: unknown
|
|
/** Raw value from `getItemValue(kind, path, forkWorkspace)`. Undefined
|
|
* for "removed" items (don't exist in the fork). */
|
|
currentRaw?: unknown
|
|
/** Force unified diff (Monaco renderSideBySide=false). Default false. */
|
|
inlineDiff?: boolean
|
|
/** For `raw_app_file`: the synthesized per-file diff item to render. */
|
|
rawFile?: RawAppFileItem
|
|
}
|
|
|
|
let { kind, originalRaw, currentRaw, inlineDiff = false, rawFile }: Props = $props()
|
|
|
|
type Prepared = { lang?: string; content?: string; metadata: string }
|
|
|
|
function prepareValue(raw: unknown): Prepared {
|
|
if (!raw || typeof raw !== 'object') {
|
|
return { metadata: raw == null ? '' : String(raw) }
|
|
}
|
|
const cleaned = structuredClone(
|
|
cleanValueProperties(replaceFalseWithUndefined(raw as Record<string, unknown>))
|
|
)
|
|
const content = (cleaned as Record<string, unknown>)['content']
|
|
if (content !== undefined) {
|
|
delete (cleaned as Record<string, unknown>)['content']
|
|
}
|
|
const language = (raw as Record<string, unknown>).language
|
|
return {
|
|
lang:
|
|
typeof language === 'string'
|
|
? scriptLangToEditorLang(language as Parameters<typeof scriptLangToEditorLang>[0])
|
|
: undefined,
|
|
content: typeof content === 'string' ? content : undefined,
|
|
metadata: orderedYamlStringify(cleaned)
|
|
}
|
|
}
|
|
|
|
const original = $derived(prepareValue(originalRaw))
|
|
const current = $derived(prepareValue(currentRaw))
|
|
const hasContent = $derived(original.content !== undefined || current.content !== undefined)
|
|
|
|
// For added / removed flows, the missing side feeds an empty YAML so
|
|
// the YAML-mode editor shows the whole new (or removed) flow as a
|
|
// single-sided diff. FlowGraphDiffViewer uses the *Missing flag to
|
|
// swap in its own OpenFlow stub for parsing and to draw a placeholder
|
|
// pane in side-by-side mode.
|
|
const beforeFlowYaml = $derived(originalRaw == null ? '' : original.metadata)
|
|
const afterFlowYaml = $derived(currentRaw == null ? '' : current.metadata)
|
|
|
|
let contentTab: 'content' | 'metadata' = $state('content')
|
|
|
|
// Per-tab height: each Monaco block sizes to its own content. Switching
|
|
// tabs reflows the row, which is the expected tab behavior; we don't
|
|
// over-allocate to the larger tab the way the previous max() did.
|
|
const LINE_HEIGHT = 19
|
|
const EDITOR_CHROME = 24
|
|
function linesIn(s?: string): number {
|
|
return Math.max((s ?? '').split('\n').length, 1)
|
|
}
|
|
const contentHeight = $derived(
|
|
`${Math.max(linesIn(original.content), linesIn(current.content)) * LINE_HEIGHT + EDITOR_CHROME}px`
|
|
)
|
|
const metadataHeight = $derived(
|
|
`${Math.max(linesIn(original.metadata), linesIn(current.metadata)) * LINE_HEIGHT + EDITOR_CHROME}px`
|
|
)
|
|
const activeTabHeight = $derived(contentTab === 'content' ? contentHeight : metadataHeight)
|
|
</script>
|
|
|
|
{#if kind === 'flow'}
|
|
<div class="h-[600px]">
|
|
<FlowDiffViewer
|
|
beforeYaml={beforeFlowYaml}
|
|
afterYaml={afterFlowYaml}
|
|
beforeMissing={originalRaw == null}
|
|
afterMissing={currentRaw == null}
|
|
{inlineDiff}
|
|
/>
|
|
</div>
|
|
{:else if kind === 'raw_app_file' && rawFile}
|
|
<RawAppFileDiff
|
|
original={rawFile.original}
|
|
current={rawFile.current}
|
|
lang={rawFile.lang}
|
|
isMetadata={rawFile.isMetadata}
|
|
fullYamlOriginal={rawFile.fullYamlOriginal}
|
|
fullYamlCurrent={rawFile.fullYamlCurrent}
|
|
{inlineDiff}
|
|
/>
|
|
{:else if hasContent}
|
|
<div class="flex flex-col">
|
|
<Tabs bind:selected={contentTab}>
|
|
<Tab value="content" label="Content" />
|
|
<Tab value="metadata" label="Metadata" />
|
|
</Tabs>
|
|
<div style="height: {activeTabHeight}">
|
|
{#if contentTab === 'content'}
|
|
{#await import('$lib/components/DiffEditor.svelte')}
|
|
<div class="p-3"><Loader2 class="w-3.5 h-3.5 animate-spin" /></div>
|
|
{:then Module}
|
|
<Module.default
|
|
open={true}
|
|
automaticLayout
|
|
className="h-full"
|
|
defaultLang={original.lang ?? current.lang}
|
|
defaultOriginal={original.content ?? ''}
|
|
defaultModified={current.content ?? ''}
|
|
{inlineDiff}
|
|
readOnly
|
|
/>
|
|
{/await}
|
|
{:else}
|
|
{#await import('$lib/components/DiffEditor.svelte')}
|
|
<div class="p-3"><Loader2 class="w-3.5 h-3.5 animate-spin" /></div>
|
|
{:then Module}
|
|
<Module.default
|
|
open={true}
|
|
automaticLayout
|
|
className="h-full"
|
|
defaultLang="yaml"
|
|
defaultOriginal={original.metadata}
|
|
defaultModified={current.metadata}
|
|
{inlineDiff}
|
|
readOnly
|
|
/>
|
|
{/await}
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
{:else}
|
|
{#await import('$lib/components/DiffEditor.svelte')}
|
|
<div class="p-3"><Loader2 class="w-3.5 h-3.5 animate-spin" /></div>
|
|
{:then Module}
|
|
<div style="height: {metadataHeight}">
|
|
<Module.default
|
|
open={true}
|
|
automaticLayout
|
|
className="h-full"
|
|
defaultLang="yaml"
|
|
defaultOriginal={original.metadata}
|
|
defaultModified={current.metadata}
|
|
{inlineDiff}
|
|
readOnly
|
|
/>
|
|
</div>
|
|
{/await}
|
|
{/if}
|