Files
windmill/frontend/src/lib/components/raw_apps/FileIcon.svelte
T
GuilhemandClaude Opus 4.8 e98df38ac4 feat(apps): show raw-app fork diffs as per-file tree items (#9491)
* 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>
2026-06-24 09:23:14 +02:00

89 lines
2.7 KiB
Svelte

<!--
@component
Icon for a file (or folder) chosen from its extension. Single source of truth
for raw-app file icons — used by the file tree (FileTreeNode) and the fork-diff
rows (via RowIcon). Pass a bare name or a full path; the extension is taken from
the last path segment. Falls back to a generic file icon.
-->
<script lang="ts">
import { File, Folder, ImageIcon } from 'lucide-svelte'
import { twMerge } from 'tailwind-merge'
import TypeScript from '../common/languageIcons/TypeScript.svelte'
import JavaScriptIcon from '../icons/JavaScriptIcon.svelte'
import JsonIcon from '../icons/JsonIcon.svelte'
import ReactIcon from '../icons/ReactIcon.svelte'
import SvelteIcon from '../icons/SvelteIcon.svelte'
import VueIcon from '../icons/VueIcon.svelte'
import CssIcon from '../icons/CssIcon.svelte'
import SassIcon from '../icons/SassIcon.svelte'
import LessIcon from '../icons/LessIcon.svelte'
import HtmlIcon from '../icons/HtmlIcon.svelte'
import MarkdownIcon from '../icons/MarkdownIcon.svelte'
import YamlIcon from '../icons/YamlIcon.svelte'
interface Props {
/** File name or path; extension drives the icon. */
name: string
isFolder?: boolean
size?: number
class?: string
}
let { name, isFolder = false, size = 14, class: className = '' }: Props = $props()
function extOf(n: string): string {
const base = n.split('/').pop() ?? n
return base.includes('.') ? (base.split('.').pop() ?? '').toLowerCase() : ''
}
const spec = $derived.by(() => {
if (isFolder) return { icon: Folder, className: 'text-secondary' }
switch (extOf(name)) {
case 'json':
return { icon: JsonIcon, className: '' }
case 'tsx':
case 'jsx':
return { icon: ReactIcon, className: '' }
case 'ts':
return { icon: TypeScript, className: '' }
case 'js':
return { icon: JavaScriptIcon, className: '' }
case 'svelte':
return { icon: SvelteIcon, className: '' }
case 'vue':
return { icon: VueIcon, className: '' }
case 'css':
return { icon: CssIcon, className: '' }
case 'scss':
case 'sass':
return { icon: SassIcon, className: '' }
case 'less':
return { icon: LessIcon, className: '' }
case 'png':
case 'jpg':
case 'jpeg':
case 'gif':
case 'svg':
case 'webp':
case 'ico':
return { icon: ImageIcon, className: 'text-purple-500' }
case 'html':
case 'htm':
return { icon: HtmlIcon, className: '' }
case 'md':
case 'markdown':
return { icon: MarkdownIcon, className: '' }
case 'yaml':
case 'yml':
return { icon: YamlIcon, className: '' }
default:
return { icon: File, className: 'text-tertiary' }
}
})
</script>
{#if spec}
{@const Icon = spec.icon}
<Icon {size} class={twMerge('flex-shrink-0', spec.className, className)} />
{/if}