mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-21 08:03:33 +00:00
* feat(frontend): chat-scoped unified session changes bar Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(frontend): drop diff-baseline toggle, show natural per-row diffs Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): carry Draft marker to expanded raw-app file rows Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): show raw-app Draft badge once at tree root, not per file Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(frontend): reuse shared DraftBadge in session diff drawer Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(frontend): show draft-author avatars in session diff badge, icon-only in sidebar Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): keep badge pill around avatar in icon-only DraftBadge Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): small draft marker = indigo pen + avatar; correct itemKind label Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(frontend): drop package-lock churn from merge (match origin/main) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): dedup diff-button count for legacy fork sessions; test mask helper Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(frontend): collapsible session diff panel + per-row open-diff action Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(frontend): shared sessionDeployModel for review & deploy (S1) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(frontend): model-driven session review drawer, deploy inert (S2) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(frontend): wire session deploy + on-behalf/conflict gating (S3) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(frontend): behind banner + Update fork + deployment request (S4) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(frontend): session changes dock opens drawer by filter (S5) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): session review UI polish (badge, dock, In parent, tree width) - draft rows show only the avatar DraftBadge, not a duplicate state pill - drop redundant dock Review button (same as "N to review") - rename Done -> In parent with a "deployed in parent workspace" tooltip - widen the file tree Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): existence-gate In parent rows; Badge filters; badge hover - drop discarded mask-only items from the In-parent segment (existence check) - use the Badge component for the drawer filter segments and the changes dock - soften the blue Badge hover (blue-50 base was jumping to blue-200) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(frontend): polish session diff drawer (layout, badges, actions) - remove empty fork-banner gap; uniform sidebar tree padding - full-bleed diff list: drop card borders/side padding, separators between items - clamp tree x-overflow; right-align tree badges (min-w-0 on the row button) - brand-compliant selected filter badges; smaller draft badge - hide per-row open-diff button when the panel is open - rename "Delete draft" to "Discard draft" (destructive); remove header Review button - larger sm deploy/discard action buttons; remove per-item diff-content collapse Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(frontend): session Edits dock — deploy gating + change-op tracking Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(frontend): session bar per-status badges; drop change-op tracking Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(frontend): unwrap raw apps into per-file tree in session diff sidebar Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(frontend): dot-parcours pipeline (badge-derived, melt tooltip) + discard confirm Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(frontend): behind-only session item reads as deployed, not bare Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * style(frontend): even sidebar tree margins; gutter-aware right padding Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(frontend): pass chat id as from_session; wire deploying flag Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(frontend): session diff drawer scroll-to-flush, ordering, spacer, deploy gating Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): preserve chat mask on compact; guard stale existence checks; clear poll timers Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(frontend): refresh bar after drawer deploys; conflict hint over chip; plain conflict badge Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * style(frontend): diff drawer card layout with flash ring and aligned insets Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(frontend): hide stale deployed chip once row status badge reads deployed Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(frontend): session dock to two states; drop parent deploy Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(frontend): staged deploy animation in session edits drawer Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(frontend): stale-draft warning in session edits drawer Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(frontend): keep chat mask honest on deploy and discard Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(frontend): map trigger_email deploy kind; serialize mask persists Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(frontend): reset mask on new chat; close review-flagged races Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * style(frontend): rename session drawer title to Edited during session Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(frontend): keep mask persist queue alive after a failed save Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(frontend): sync session chatId on chat rotation; gate deploy on canWrite Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(frontend): keep compare handoff for deletion-only session edits Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(frontend): hold deploy success beat across re-keyed rows Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
174 lines
5.9 KiB
TypeScript
174 lines
5.9 KiB
TypeScript
// Pure file-tree model for the diff drawer (WorkspaceDiffDrawer).
|
|
//
|
|
// Turns a flat list of diff items into a navigable folder tree and answers the
|
|
// keyboard-navigation queries the drawer needs (visible order, parent, first
|
|
// child). It is generic over the leaf payload `T` on purpose: the drawer's
|
|
// `DiffRow` type lives in WorkspaceDiffDrawer, which its two adapters import —
|
|
// importing it here would make a cycle.
|
|
//
|
|
// Items are grouped by "scope" (the first two path segments, e.g. `f/foo` or
|
|
// `u/alice`); deeper segments become nested folders. Folder keys are
|
|
// `folder:<fullPath>`. The crucial invariant: every structural answer
|
|
// (parent/first-child/order) comes from the tree as it was *built*, never from
|
|
// re-splitting a path at the call site — so a node's tree position and its
|
|
// navigation parent can never drift apart.
|
|
//
|
|
// Two path spaces meet here. `structurePath` decides tree placement (it's the
|
|
// friendly path, so a never-deployed draft nests under `u/me/my_app` rather
|
|
// than `u/me/draft_<uuid>`); `key` is the caller's stable identity (the storage
|
|
// key — used for the load cache, the row id and `data-nav-key`). Structure code
|
|
// only ever reads `structurePath`; it carries `key` through opaquely.
|
|
|
|
export type DiffTreeItem<T> = {
|
|
/** Stable identity (storage key). Becomes the file node's key + nav key. */
|
|
key: string
|
|
/** Friendly path that decides where the item sits in the tree. */
|
|
structurePath: string
|
|
data: T
|
|
}
|
|
|
|
/** Marks a folder as a raw app's root so the drawer renders its header as the
|
|
* raw-app row instead of a plain folder. Keyed by the app's friendly path. */
|
|
export type AppRootMeta = {
|
|
summaryKey: string
|
|
summary?: string
|
|
hasDraft?: boolean
|
|
draftOnly?: boolean
|
|
draftUsers?: { username?: string | null }[]
|
|
draftItemKind?: import('$lib/gen').UserDraftItemKind
|
|
}
|
|
|
|
export type FolderNode<T> = {
|
|
type: 'folder'
|
|
name: string
|
|
fullPath: string
|
|
isScope: boolean
|
|
key: string
|
|
children: TreeNode<T>[]
|
|
app?: AppRootMeta
|
|
}
|
|
export type FileNode<T> = { type: 'file'; name: string; key: string; data: T }
|
|
export type TreeNode<T> = FolderNode<T> | FileNode<T>
|
|
|
|
export type NavEntry<T> =
|
|
| { type: 'folder'; key: string; node: FolderNode<T> }
|
|
| { type: 'file'; key: string; data: T }
|
|
|
|
export type DiffTreeModel<T> = {
|
|
root: FolderNode<T>
|
|
/** Visible entries top-to-bottom, honoring the caller's fold state. */
|
|
order(isOpen: (key: string) => boolean): NavEntry<T>[]
|
|
/** Folder key containing `key`, or undefined at the top (no parent folder). */
|
|
parentKeyOf(key: string): string | undefined
|
|
/** Key of a folder's first child (ArrowRight target), or undefined if empty. */
|
|
firstChildKeyOf(folderKey: string): string | undefined
|
|
}
|
|
|
|
export function folderKeyFor(fullPath: string): string {
|
|
return `folder:${fullPath}`
|
|
}
|
|
|
|
export function buildDiffTree<T>(
|
|
items: DiffTreeItem<T>[],
|
|
appMeta: Map<string, AppRootMeta>
|
|
): DiffTreeModel<T> {
|
|
const root: FolderNode<T> = {
|
|
type: 'folder',
|
|
name: '',
|
|
fullPath: '',
|
|
isScope: false,
|
|
key: folderKeyFor(''),
|
|
children: []
|
|
}
|
|
const folderCache = new Map<string, FolderNode<T>>()
|
|
const nodeByKey = new Map<string, FolderNode<T>>()
|
|
// child key → parent folder key. Direct children of `root` are intentionally
|
|
// absent (they have no navigable parent), so parentKeyOf returns undefined
|
|
// for scope folders and root-level leaves — matching ArrowLeft's old behavior.
|
|
const parentByKey = new Map<string, string>()
|
|
|
|
function getFolder(parent: FolderNode<T>, fullPath: string, name: string, isScope: boolean) {
|
|
let f = folderCache.get(fullPath)
|
|
if (!f) {
|
|
f = { type: 'folder', name, fullPath, isScope, key: folderKeyFor(fullPath), children: [] }
|
|
folderCache.set(fullPath, f)
|
|
nodeByKey.set(f.key, f)
|
|
parent.children.push(f)
|
|
if (parent !== root) parentByKey.set(f.key, parent.key)
|
|
}
|
|
return f
|
|
}
|
|
|
|
function addFile(parent: FolderNode<T>, name: string, it: DiffTreeItem<T>) {
|
|
parent.children.push({ type: 'file', name, key: it.key, data: it.data })
|
|
if (parent !== root) parentByKey.set(it.key, parent.key)
|
|
}
|
|
|
|
for (const it of items) {
|
|
const parts = it.structurePath.split('/')
|
|
if (parts.length < 2) {
|
|
addFile(root, it.structurePath, it)
|
|
continue
|
|
}
|
|
const scopeKey = parts.slice(0, 2).join('/')
|
|
const scope = getFolder(root, scopeKey, scopeKey, true)
|
|
if (parts.length === 2) {
|
|
addFile(scope, parts[1], it)
|
|
continue
|
|
}
|
|
const rest = parts.slice(2)
|
|
let parent = scope
|
|
let fkey = scopeKey
|
|
for (let i = 0; i < rest.length - 1; i++) {
|
|
fkey = `${fkey}/${rest[i]}`
|
|
parent = getFolder(parent, fkey, rest[i], false)
|
|
}
|
|
addFile(parent, rest[rest.length - 1], it)
|
|
}
|
|
|
|
// Tag the folder matching each raw app's friendly path as its app root.
|
|
// Must happen before sorting: app roots sort as items, not as folders.
|
|
for (const [fp, meta] of appMeta) {
|
|
const f = folderCache.get(fp)
|
|
if (f) f.app = meta
|
|
}
|
|
|
|
// Path folders group first; everything item-like — leaves AND app roots —
|
|
// sorts together by name, so a raw app keeps its position when expanding
|
|
// turns its leaf row into an app-root folder.
|
|
const isGrouping = (n: TreeNode<T>) => n.type === 'folder' && !n.app
|
|
const sortRec = (n: FolderNode<T>) => {
|
|
n.children.sort((a, b) => {
|
|
const ga = isGrouping(a) ? 0 : 1
|
|
const gb = isGrouping(b) ? 0 : 1
|
|
if (ga !== gb) return ga - gb
|
|
return a.name.localeCompare(b.name)
|
|
})
|
|
for (const c of n.children) if (c.type === 'folder') sortRec(c)
|
|
}
|
|
sortRec(root)
|
|
|
|
return {
|
|
root,
|
|
order(isOpen) {
|
|
const out: NavEntry<T>[] = []
|
|
const walk = (n: TreeNode<T>) => {
|
|
if (n.type === 'file') {
|
|
out.push({ type: 'file', key: n.key, data: n.data })
|
|
return
|
|
}
|
|
out.push({ type: 'folder', key: n.key, node: n })
|
|
if (isOpen(n.key)) for (const c of n.children) walk(c)
|
|
}
|
|
for (const c of root.children) walk(c)
|
|
return out
|
|
},
|
|
parentKeyOf(key) {
|
|
return parentByKey.get(key)
|
|
},
|
|
firstChildKeyOf(folderKey) {
|
|
return nodeByKey.get(folderKey)?.children[0]?.key
|
|
}
|
|
}
|
|
}
|