mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-08 16:03:27 +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>
275 lines
10 KiB
TypeScript
275 lines
10 KiB
TypeScript
import type { UserDraftItemKind } from '$lib/gen'
|
|
import type { DraftItem } from '$lib/workspaceDrafts.svelte'
|
|
import type { Kind } from '$lib/utils_deployable'
|
|
import { maskKey } from './modifiedItemsMask'
|
|
|
|
// Unified item model for the session Review & Deploy surface. This is the pure,
|
|
// UI-free core: every item the session touched is either a pending **draft** or
|
|
// **deployed** in the session workspace (terminal). Deploying always lands in
|
|
// the session's own workspace — promoting a fork's changes to its parent is the
|
|
// compare page's concern, not modeled here.
|
|
//
|
|
// Kept free of Svelte runes and async so it unit-tests in the node project. The
|
|
// reactive wrapper (data loading, deploy execution) lives in the sibling
|
|
// `.svelte.ts` and consumes these functions.
|
|
|
|
// ── Item taxonomy bridge ────────────────────────────────────────────────────
|
|
// The draft list speaks UserDraftItemKind (`trigger_schedule`, `raw_app`); the
|
|
// deploy utils speak the layout `Kind` (`schedule`, `raw_app`, `http_trigger`).
|
|
// This maps draft→deploy to key deploy calls and row icons. Only non-identity
|
|
// cases listed; everything else is identity.
|
|
const DEPLOY_KIND_BY_DRAFT_KIND: Partial<Record<UserDraftItemKind, Kind>> = {
|
|
trigger_schedule: 'schedule',
|
|
trigger_http: 'http_trigger',
|
|
trigger_websocket: 'websocket_trigger',
|
|
trigger_kafka: 'kafka_trigger',
|
|
trigger_nats: 'nats_trigger',
|
|
trigger_postgres: 'postgres_trigger',
|
|
trigger_mqtt: 'mqtt_trigger',
|
|
trigger_sqs: 'sqs_trigger',
|
|
trigger_gcp: 'gcp_trigger',
|
|
trigger_azure: 'azure_trigger',
|
|
// Both email draft kinds deploy as the one email trigger kind: the editor
|
|
// saves per-path email-trigger drafts as `trigger_email`, the fork diff
|
|
// speaks `trigger_default_email`.
|
|
trigger_default_email: 'email_trigger',
|
|
trigger_email: 'email_trigger',
|
|
raw_app: 'raw_app'
|
|
}
|
|
|
|
function deployKindOf(draftKind: UserDraftItemKind, rawApp: boolean): Kind {
|
|
if (rawApp) return 'raw_app'
|
|
return DEPLOY_KIND_BY_DRAFT_KIND[draftKind] ?? (draftKind as unknown as Kind)
|
|
}
|
|
|
|
// ── Public types ────────────────────────────────────────────────────────────
|
|
|
|
export interface DeployItem {
|
|
/** Canonical identity `${UserDraftItemKind}:${path}`. */
|
|
key: string
|
|
/** Layout/deploy kind (script, flow, raw_app, http_trigger, schedule…). */
|
|
deployKind: Kind
|
|
/** UserDraftItemKind for draft ops. */
|
|
draftKind: UserDraftItemKind
|
|
path: string
|
|
/** Friendly path for display; storage `path` stays the op/edit key. */
|
|
displayPath: string
|
|
summary?: string
|
|
/** Deployed with no pending draft — the terminal state. */
|
|
done: boolean
|
|
hasDraft: boolean
|
|
draftOnly: boolean
|
|
draftUsers?: { username?: string | null }[]
|
|
/** Own draft (deployable/discardable). */
|
|
mine: boolean
|
|
canWrite: boolean
|
|
/** Legacy workspace-level draft (drives discard's `legacy` flag). */
|
|
legacy: boolean
|
|
rawApp: boolean
|
|
}
|
|
|
|
export interface BuildInput {
|
|
draftItems: DraftItem[]
|
|
/** Chat-modified-items mask (`UserDraftItemKind:path`). When set, only masked
|
|
* items are included, and mask-only entries (no draft) surface as terminal
|
|
* deployed rows — but only when confirmed to still exist (see `existingKeys`). */
|
|
mask?: Set<string>
|
|
/** Mask keys confirmed to still exist in the workspace (deployed). A mask-only
|
|
* key that isn't here is treated as gone (a discarded draft or a deleted item)
|
|
* and dropped, so it doesn't masquerade as deployed. Undefined → no mask-only
|
|
* rows are emitted yet (existence still resolving). */
|
|
existingKeys?: Set<string>
|
|
}
|
|
|
|
// ── Build: draft list + mask-only deployed rows into unified items ──────────
|
|
|
|
export function buildDeployItems(input: BuildInput): DeployItem[] {
|
|
const { draftItems, mask, existingKeys } = input
|
|
const scopedDrafts = mask
|
|
? draftItems.filter((it) => mask.has(maskKey(it.kind, it.path)))
|
|
: draftItems
|
|
|
|
const out: DeployItem[] = []
|
|
const seen = new Set<string>()
|
|
|
|
// 1. Pending drafts.
|
|
for (const it of scopedDrafts) {
|
|
const canonical = maskKey(it.kind, it.path)
|
|
if (seen.has(canonical)) continue
|
|
seen.add(canonical)
|
|
out.push({
|
|
key: canonical,
|
|
deployKind: deployKindOf(it.kind, it.raw_app),
|
|
draftKind: it.kind,
|
|
path: it.path,
|
|
displayPath: it.draft_path ?? it.path,
|
|
summary: it.summary,
|
|
done: false,
|
|
hasDraft: true,
|
|
draftOnly: it.draft_only,
|
|
draftUsers: it.draft_users,
|
|
mine: it.mine,
|
|
canWrite: it.can_write,
|
|
legacy: it.legacy_draft,
|
|
rawApp: it.raw_app
|
|
})
|
|
}
|
|
|
|
// 2. Mask-only items (touched by the chat, no pending draft) → terminal
|
|
// deployed rows, but ONLY when confirmed to still exist. A discarded draft
|
|
// or a deleted item leaves a mask entry with no item; without this guard it
|
|
// would masquerade as deployed.
|
|
if (mask) {
|
|
for (const k of mask) {
|
|
if (seen.has(k)) continue
|
|
if (!existingKeys?.has(k)) continue
|
|
const sep = k.indexOf(':')
|
|
if (sep < 0) continue
|
|
const udk = k.slice(0, sep) as UserDraftItemKind
|
|
const path = k.slice(sep + 1)
|
|
seen.add(k)
|
|
out.push({
|
|
key: k,
|
|
deployKind: deployKindOf(udk, udk === 'raw_app'),
|
|
draftKind: udk,
|
|
path,
|
|
displayPath: path,
|
|
summary: undefined,
|
|
done: true,
|
|
hasDraft: false,
|
|
draftOnly: false,
|
|
draftUsers: undefined,
|
|
mine: true,
|
|
canWrite: true,
|
|
legacy: false,
|
|
rawApp: udk === 'raw_app'
|
|
})
|
|
}
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
/** Mask keys not covered by a current draft — the candidates for a terminal
|
|
* deployed row. The reactive layer existence-checks these (in the session
|
|
* workspace) to tell a deployed item from a discarded/deleted one, then feeds
|
|
* the survivors back as `existingKeys`. */
|
|
export function maskOnlyCandidates(
|
|
input: Omit<BuildInput, 'existingKeys'>
|
|
): { key: string; deployKind: Kind; draftKind: UserDraftItemKind; path: string }[] {
|
|
const { draftItems, mask } = input
|
|
if (!mask) return []
|
|
const covered = new Set<string>()
|
|
for (const it of draftItems) {
|
|
const k = maskKey(it.kind, it.path)
|
|
if (mask.has(k)) covered.add(k)
|
|
}
|
|
const out: { key: string; deployKind: Kind; draftKind: UserDraftItemKind; path: string }[] = []
|
|
for (const k of mask) {
|
|
if (covered.has(k)) continue
|
|
const sep = k.indexOf(':')
|
|
if (sep < 0) continue
|
|
const udk = k.slice(0, sep) as UserDraftItemKind
|
|
const path = k.slice(sep + 1)
|
|
out.push({ key: k, draftKind: udk, deployKind: deployKindOf(udk, udk === 'raw_app'), path })
|
|
}
|
|
return out
|
|
}
|
|
|
|
// ── Status badge ─────────────────────────────────────────────────────────────
|
|
|
|
export type BadgeKind = 'draft' | 'deployed'
|
|
|
|
/** The single status badge a row shows: a pending draft, or deployed (green,
|
|
* terminal). Every built row is one of the two. */
|
|
export function badgeOf(item: DeployItem): BadgeKind {
|
|
return item.done ? 'deployed' : 'draft'
|
|
}
|
|
|
|
/** Per-status tallies over `badgeOf` — the bar's at-a-glance readout, so its
|
|
* counts always agree with the badges the drawer rows show. */
|
|
export function badgeCounts(items: DeployItem[]): Record<BadgeKind, number> {
|
|
const out: Record<BadgeKind, number> = { draft: 0, deployed: 0 }
|
|
for (const it of items) out[badgeOf(it)]++
|
|
return out
|
|
}
|
|
|
|
// ── Action ──────────────────────────────────────────────────────────────────
|
|
|
|
export type DeployOp = 'deploy_draft' | 'discard' | 'none'
|
|
|
|
export interface DeployAction {
|
|
op: DeployOp
|
|
label: string
|
|
secondary?: DeployAction[]
|
|
}
|
|
|
|
/** The primary (and secondary) action for a row: a draft deploys into the
|
|
* session workspace, a deployed row has nothing left to do. */
|
|
export function actionFor(item: DeployItem): DeployAction {
|
|
if (item.done) return { op: 'none', label: 'Done' }
|
|
return {
|
|
op: 'deploy_draft',
|
|
label: 'Deploy',
|
|
secondary: [{ op: 'discard', label: item.draftOnly ? 'Discard draft' : 'Discard' }]
|
|
}
|
|
}
|
|
|
|
// ── Diff base ───────────────────────────────────────────────────────────────
|
|
|
|
export type DiffBase =
|
|
| { kind: 'draft'; draftKind: UserDraftItemKind; workspaceId: string; draftOnly: boolean }
|
|
| { kind: 'self'; deployKind: Kind; path: string; workspaceId: string }
|
|
|
|
/** Where a row's before/after diff values come from. A draft row diffs
|
|
* deployed↔draft; a deployed row has nothing to diff but its value is still
|
|
* loadable (`self`) — the sidebar unwraps a deployed raw app into its files.
|
|
* The executor maps this to getDraftDiffValues / getItemValue. */
|
|
export function diffBaseFor(item: DeployItem, workspaceId: string): DiffBase {
|
|
if (item.hasDraft) {
|
|
return { kind: 'draft', draftKind: item.draftKind, workspaceId, draftOnly: item.draftOnly }
|
|
}
|
|
return { kind: 'self', deployKind: item.deployKind, path: item.path, workspaceId }
|
|
}
|
|
|
|
// ── Deploy plan (executed by the reactive layer) ─────────────────────────────
|
|
|
|
export interface DeployPlanEntry {
|
|
key: string
|
|
op: Exclude<DeployOp, 'none'>
|
|
draftKind: UserDraftItemKind
|
|
path: string
|
|
draftOnly: boolean
|
|
rawApp: boolean
|
|
legacy: boolean
|
|
}
|
|
|
|
/** Concrete util arguments for deploying one item's draft into the session
|
|
* workspace. Returns undefined for deployed (terminal) rows. Mirrors actionFor. */
|
|
export function deployPlanFor(item: DeployItem): DeployPlanEntry | undefined {
|
|
if (!item.hasDraft) return undefined
|
|
return {
|
|
key: item.key,
|
|
op: 'deploy_draft',
|
|
draftKind: item.draftKind,
|
|
path: item.path,
|
|
draftOnly: item.draftOnly,
|
|
rawApp: item.rawApp,
|
|
legacy: item.legacy
|
|
}
|
|
}
|
|
|
|
/** Discard plan for a draft row (secondary action). */
|
|
export function discardPlanFor(item: DeployItem): DeployPlanEntry | undefined {
|
|
if (!item.hasDraft) return undefined
|
|
return {
|
|
key: item.key,
|
|
op: 'discard',
|
|
draftKind: item.draftKind,
|
|
path: item.path,
|
|
draftOnly: item.draftOnly,
|
|
rawApp: item.rawApp,
|
|
legacy: item.legacy
|
|
}
|
|
}
|