Files
windmill/frontend/src/lib/components/raw_apps/RawAppDataTableList.svelte
T
GuilhemandClaude Opus 4.7 b0ed27096d feat(editors): responsive top-bars + collapsible raw-app sidebar (#9237)
* feat(editors): responsive top-bars + script test-pane pixel-min + flow graph overlay

Editor top bars now collapse on narrow widths (measured via container
clientWidth, not viewport — they live inside drawers / session panes
where the viewport stays wide):

- FlowBuilder: Diff + Save draft fold into the ellipsis menu when
  the top bar narrows below 720px (Save draft keeps its ⌘S / Ctrl+S
  shortcut indicator). Test-flow button moves out of the top bar
  and into a graph-pane overlay matching the dev page; the overlay
  position flips from top-2 right-2 to top-14 left-1/2 when the
  graph pane itself is narrower than 800px. FlowEditor exposes a
  graphOverlay snippet prop for that.
- ScriptBuilder: Settings + Draft labels collapse to icon-only;
  a new DropdownV2 ellipsis surfaces Tag / Settings / Save draft
  when even icons don't fit. The ellipsis itself uses variant=subtle.
- AppEditorHeader / RawAppEditorHeader: fullscreen / dark-mode /
  breakpoint toggle group + Debug-runs / Jobs buttons hide; Save
  draft moves into the Deploy dropdown.
- EditorBar: a "Helpers" DropdownV2 collapse for Context var /
  Variable / S3 / Resource / Git repo / Resource type / Database /
  Ducklake / Data table / Reset when the bar narrows below 800px
  (EDITOR_BAR_HELPERS_COMPACT_THRESHOLD). Above that, the existing
  icon-only mode (1420px threshold) still applies.
- ScriptEditor's test pane gets a pixel-based minimum width (400px)
  derived from the splitpane's clientWidth. The Pane uses Svelte 5
  function-binding so the splitter writes to a raw $state while the
  splitpane reads the clamped derived value — no $effect, no
  release-time bounce, drag stops at the boundary. Cap raised to
  80% so the test pane can take most of the editor on very narrow
  layouts while leaving a sliver of code visible.
- VS Code button on ScriptEditor: collapses to icon-only below the
  EDITOR_BAR_WIDTH_THRESHOLD (1420px) instead of being hidden
  entirely by viewport `lg:` breakpoint; hidden completely when the
  editor is rendered inside a session pane.
- AI wand button on ScriptEditor + RawAppEditorHeader: hidden inside
  a session pane (detected via `getContext('aiChatManager')`) — the
  session owns its own AI chat.
- DeployButton: drops the unused `newFlow` gate (callers updated).
- FlowDiffViewer / FlowGraphDiffViewer: inlineDiff prop forwarding
  + onHeight callback on FlowGraphV2 so diff viewers can equalize
  side-by-side graph heights.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ui: unify Debug / wand / test-toggle button sizes + HideButton defaults

Two small consistency passes on shared button components:

- ScriptEditor's Debug, AI wand and Test-panel-toggle buttons all
  use unifiedSize="sm" so they line up in the toolbar; Test toggle
  switches from custom marine btnClasses to variant="accent-secondary".
  HideButton gains a passthrough unifiedSize prop so the wand and
  test toggle can match Debug without overriding btnClasses.
- HideButton's own defaults shift to variant="subtle" + sm
  unifiedSize, dropping the legacy color="light" / variant="contained"
  + tailwind-merge background overlay; the selected (hidden) state
  is now a tinted wrapper div instead of overriding btnClasses.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ui(script): editor toolbar polish

Small consistency tweaks on the script editor's top-right overlay:

- Lowercase "test" / "Exit debug" panel labels.
- `bg-surface` on the overlay container so the absolute-positioned
  buttons read as a single panel over the graph rather than disjoint
  pills.
- Debug button picks up `destructive={debugMode}` so the active
  state reads as "you're in debug mode" instead of accent.
- Console and "Delegating to git repo" buttons drop the custom
  `btnClasses` border-on-surface treatment and switch from
  `size="xs"` to `unifiedSize="sm"` so they match the other buttons
  in the cluster.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(raw-app): collapsible file sidebar, default-collapsed in session preview

The raw-app editor's left sidebar (file tree, runnables, history) ate a
lot of horizontal space — fine in the standalone editor, painful in the
session preview pane where the chat is already taking half the screen.

Add a small collapse / expand toggle. Persist the user's preference in
localStorage so it sticks across opens.

Two independent localStorage keys via the new `sidebarStorageKey` prop:
- standalone editor: `raw-app-sidebar-collapsed` (default expanded)
- session preview:   `raw-app-sidebar-collapsed-preview` (default collapsed)

Otherwise the two contexts would race for the same key — whichever
opens first would dictate the other's default. Splitting the keys lets
each have its own remembered preference.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fixup(editors): use untyped getContext for AI-chat-manager session detection

The cherry-picks landed `getContext<AIChatManager>('aiChatManager')` to
hide per-editor AI/VSCode buttons when rendered inside a session pane.
The `AIChatManager` class is exported only on the sessions branch (used
for typing session-provided manager overrides). On `main` the manager
file exports only the singleton instance, so importing the class fails
the type-check.

The session-pane detection just needs a truthy/falsy probe — drop the
type parameter and the class import. `inSessionPane` ends up as
`getContext('aiChatManager')` (returns `unknown`, coerced to boolean
via `!!`). Same runtime behaviour, no class-export dependency.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* ui(editors): flow test in top bar; ellipsis folds draft/jobs/tutorials

* refactor(app-editor): drop dead AppEditorTutorial button path

* ui(editors): wire compactHelpers in flow-step + raw-app inline editors

* ui(raw-app): sidebar Cmd/Ctrl+B toggle + uppercase section titles

* ui(editors): keep Diff/Settings inline as icon-only when narrow

* fix(editors): address review nits on test-pane/Helpers/thresholds

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 12:55:20 +00:00

209 lines
6.4 KiB
Svelte

<script lang="ts">
import { Plus, Database, Trash2, Table2, Star } from 'lucide-svelte'
import PanelSection, {
SUBTLE_PANEL_TITLE
} from '../apps/editor/settingsPanel/common/PanelSection.svelte'
import type { DataTableRef } from './dataTableRefUtils'
import DefaultDatabaseSelector from './DefaultDatabaseSelector.svelte'
import { Button } from '../common'
// Re-export for backwards compatibility
export type { DataTableRef } from './dataTableRefUtils'
interface Props {
dataTableRefs: DataTableRef[]
/** Default datatable for new tables */
defaultDatatable?: string | undefined
/** Default schema for new tables */
defaultSchema?: string | undefined
onAdd?: () => void
onRemove?: (index: number) => void
onSelect?: (ref: DataTableRef, index: number) => void
onDefaultChange?: (datatable: string | undefined, schema: string | undefined) => void
selectedIndex?: number | undefined
/** When true, renders without PanelSection wrapper (for use in modals) */
standalone?: boolean
/** Hide the default database selector */
hideDefaultSelector?: boolean
}
let {
dataTableRefs = [],
defaultDatatable = undefined,
defaultSchema = undefined,
onAdd,
onRemove,
onSelect,
onDefaultChange,
selectedIndex = undefined,
standalone = false,
hideDefaultSelector = false
}: Props = $props()
// Group refs by datatable, then by schema
type GroupedRefs = Map<string, Map<string, { ref: DataTableRef; index: number }[]>>
const groupedRefs = $derived.by(() => {
const groups: GroupedRefs = new Map()
dataTableRefs.forEach((ref, index) => {
const datatableKey = ref.datatable
const schemaKey = ref.schema ?? ''
if (!groups.has(datatableKey)) {
groups.set(datatableKey, new Map())
}
const datatableGroup = groups.get(datatableKey)!
if (!datatableGroup.has(schemaKey)) {
datatableGroup.set(schemaKey, [])
}
datatableGroup.get(schemaKey)!.push({ ref, index })
})
return groups
})
// Sort entries with default datatable first
const sortedDatatableEntries = $derived.by(() => {
const entries = [...groupedRefs.entries()]
return entries.sort(([a], [b]) => {
if (a === defaultDatatable) return -1
if (b === defaultDatatable) return 1
return a.localeCompare(b)
})
})
// Helper to sort schema entries with default schema first
function sortSchemaEntries(
entries: [string, { ref: DataTableRef; index: number }[]][],
datatableName: string
) {
return entries.sort(([a], [b]) => {
const aIsDefault =
datatableName === defaultDatatable && (a === defaultSchema || (a === '' && !defaultSchema))
const bIsDefault =
datatableName === defaultDatatable && (b === defaultSchema || (b === '' && !defaultSchema))
if (aIsDefault) return -1
if (bIsDefault) return 1
return a.localeCompare(b)
})
}
</script>
{#snippet actionButtons()}
<div class="flex items-center gap-1">
<!-- Settings popover for default database/schema -->
{#if !hideDefaultSelector}
<DefaultDatabaseSelector
datatable={defaultDatatable}
schema={defaultSchema}
onChange={onDefaultChange}
/>
{/if}
<!-- Add datatable button -->
<Button
onClick={() => onAdd?.()}
title="Add datatable reference"
unifiedSize="xs"
variant="subtle"
btnClasses="px-1 gap-0.5"
>
<Plus size={12} />
<Database size={12} />
</Button>
</div>
{/snippet}
{#snippet tableList()}
{#if dataTableRefs.length === 0}
<span class="text-2xs text-tertiary">No tables referenced yet</span>
{:else}
<div class="flex flex-col w-full">
{#each sortedDatatableEntries as [datatableName, schemaGroups] (datatableName)}
{@const isDefaultDatatable = datatableName === defaultDatatable}
<!-- Datatable header -->
<div class="flex items-center gap-1.5 px-1 py-1 text-2xs text-tertiary">
<Database size={12} class="shrink-0" />
<span class="font-medium truncate">{datatableName}</span>
{#if isDefaultDatatable}
<span title="Default datatable">
<Star size={10} class="shrink-0 text-primary" />
</span>
{/if}
</div>
{#each sortSchemaEntries([...schemaGroups.entries()], datatableName) as [schemaName, items] (schemaName)}
{@const isDefaultSchema =
isDefaultDatatable &&
(schemaName === defaultSchema || (schemaName === '' && !defaultSchema))}
<!-- Schema header (only if schema exists) -->
{#if schemaName}
<div class="flex items-center gap-1.5 pl-4 py-0.5 text-2xs text-tertiary">
<span class="truncate">{schemaName}</span>
{#if isDefaultSchema}
<span title="Default schema">
<Star size={8} class="shrink-0 text-primary" />
</span>
{/if}
</div>
{/if}
<!-- Tables -->
{#each items as { ref, index } (index)}
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="group flex items-center gap-2 py-1 rounded text-left w-full hover:bg-surface-hover transition-colors cursor-pointer {selectedIndex ===
index
? 'bg-surface-selected'
: ''}"
class:pl-7={schemaName}
class:pl-4={!schemaName}
onclick={() => onSelect?.(ref, index)}
>
<Table2 size={12} class="text-tertiary shrink-0" />
<span class="text-2xs text-secondary truncate flex-1">
{ref.table ?? '(all tables)'}
</span>
<button
class="p-1 hover:bg-red-500/10 rounded transition-colors opacity-0 group-hover:opacity-100 mr-1"
onclick={(e) => {
e.stopPropagation()
onRemove?.(index)
}}
title="Remove table reference"
>
<Trash2 size={10} class="text-red-500" />
</button>
</div>
{/each}
{/each}
{/each}
</div>
{/if}
{/snippet}
{#if standalone}
<div class="flex flex-col gap-2">
<div class="flex items-center justify-between">
<span class="text-xs text-emphasis font-semibold">Existing tables to use</span>
{@render actionButtons()}
</div>
{@render tableList()}
</div>
{:else}
<PanelSection
fullHeight={false}
size="sm"
title="data"
titleClass={SUBTLE_PANEL_TITLE}
id="app-editor-data-panel"
tooltip="Data tables to use in the app. Adding some here does not change the behavior of the app, since they can be used in code directly regardless. But it allows AI to always keep their schema in context as well as allowing quick access and clear view of the app's data layer."
>
{#snippet action()}
{@render actionButtons()}
{/snippet}
{@render tableList()}
</PanelSection>
{/if}