mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-14 08:02:58 +00:00
feat: show secondary detail actions as buttons while the bar has room for them
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
66195ce9dc
commit
5b94c07849
@@ -78,6 +78,7 @@ The row's height matches the sidebar's own header row, so the two read as one ba
|
||||
actions:
|
||||
actions.length === 0 ? [0] : actionsRigid ? actionWidths : [GAP + FLEXIBLE_ACTIONS_RESERVE],
|
||||
folds: actions.length > 0 && actionsRigid ? (content?.actionFolds ?? 0) : 0,
|
||||
earlyFolds: content?.earlyActionFolds,
|
||||
natural: {
|
||||
family: trailMeasures.family,
|
||||
fork: trailMeasures.fork,
|
||||
|
||||
@@ -19,6 +19,7 @@ the header, so nesting is decided by the caller rather than guessed from context
|
||||
actionsFlexible,
|
||||
actionsFill,
|
||||
actionFolds,
|
||||
earlyActionFolds,
|
||||
belowBar,
|
||||
belowBarFold,
|
||||
separator,
|
||||
@@ -37,6 +38,7 @@ the header, so nesting is decided by the caller rather than guessed from context
|
||||
actionsFlexible,
|
||||
actionsFill,
|
||||
actionFolds,
|
||||
earlyActionFolds,
|
||||
belowBar,
|
||||
belowBarFold,
|
||||
separator,
|
||||
|
||||
@@ -20,10 +20,13 @@
|
||||
/** Shown under the label once the button is a row of a menu. */
|
||||
description?: string
|
||||
buttonProps: ButtonProps
|
||||
/** A secondary action, kept out of the bar at every width: in the ellipsis menu, or in the
|
||||
* dropdown of the enabled bar button labelled `dropdownOf` (the menu when there is none).
|
||||
* Unset makes it a bar button, which folds into the menu only when the bar is short. */
|
||||
/** A secondary action: a bar button while the bar has room for every one, then, from the
|
||||
* first fold, in the ellipsis menu or in the dropdown of the enabled bar button labelled
|
||||
* `dropdownOf` (the menu when there is none). Unset makes it a main button, which goes to
|
||||
* the menu only on the folds after that. */
|
||||
placement?: 'menu' | { dropdownOf: string }
|
||||
/** In its `placement` at every width, never a bar button of its own. */
|
||||
alwaysPlaced?: boolean
|
||||
}
|
||||
|
||||
type ButtonProps = any
|
||||
@@ -101,18 +104,27 @@
|
||||
// no error handler and says so itself.
|
||||
const renameKind = $derived(itemKind ?? errorHandlerKind)
|
||||
|
||||
// The band folds the bar buttons once its breadcrumb has given all it can (`actionFolds` below):
|
||||
// first every one but the last, which is what the page is for (Edit), then that one too. The
|
||||
// host knows when its own box is narrow while the window is not — a session's preview panel —
|
||||
// and says so through `wide`, which only sends the error handler's toggle to the menu.
|
||||
// The band folds the buttons once its breadcrumb has given all it can (`actionFolds` below):
|
||||
// first the secondary ones go to their `placement`, then every bar button but the last, which
|
||||
// is what the page is for (Edit), then that one too. The host knows when its own box is narrow
|
||||
// while the window is not — a session's preview panel — and says so through `wide`, which only
|
||||
// sends the error handler's toggle to the menu.
|
||||
const fold = $derived(ownsPageHeader ? pageHeader.actionFold : 0)
|
||||
const wideRow = $derived(wide && fold === 0)
|
||||
const wideRow = $derived(wide && fold <= 1)
|
||||
|
||||
const rowButtons = $derived(mainButtons.filter((b) => !b.placement))
|
||||
const barButtons = $derived(fold >= 2 ? [] : fold === 1 ? rowButtons.slice(-1) : rowButtons)
|
||||
const barButtons = $derived(
|
||||
fold === 0
|
||||
? mainButtons.filter((b) => !b.alwaysPlaced)
|
||||
: fold === 1
|
||||
? rowButtons
|
||||
: fold === 2
|
||||
? rowButtons.slice(-1)
|
||||
: []
|
||||
)
|
||||
|
||||
function dropdownHost(btn: MainButton): MainButton | undefined {
|
||||
if (typeof btn.placement !== 'object') return undefined
|
||||
if (typeof btn.placement !== 'object' || barButtons.includes(btn)) return undefined
|
||||
const label = btn.placement.dropdownOf
|
||||
return barButtons.find((b) => b.label === label && !b.buttonProps.disabled)
|
||||
}
|
||||
@@ -121,7 +133,9 @@
|
||||
* dropdown would have held, then the secondary actions with no bar button to hang from. */
|
||||
const menuButtons = $derived.by(() => {
|
||||
const folded = rowButtons.filter((b) => !barButtons.includes(b))
|
||||
const homeless = mainButtons.filter((b) => b.placement && !dropdownHost(b))
|
||||
const homeless = mainButtons.filter(
|
||||
(b) => b.placement && !barButtons.includes(b) && !dropdownHost(b)
|
||||
)
|
||||
const childOf = (host: MainButton) =>
|
||||
homeless.filter(
|
||||
(b) => typeof b.placement === 'object' && b.placement.dropdownOf === host.label
|
||||
@@ -295,7 +309,8 @@
|
||||
pathTrigger: onSaved ? pathTrigger : undefined
|
||||
}}
|
||||
{actions}
|
||||
actionFolds={2}
|
||||
actionFolds={3}
|
||||
earlyActionFolds={1}
|
||||
contexts={headerContexts}
|
||||
separator="always"
|
||||
/>
|
||||
|
||||
@@ -436,6 +436,7 @@
|
||||
buttons.push({
|
||||
label: 'Build app',
|
||||
placement: 'menu',
|
||||
alwaysPlaced: true,
|
||||
buttonProps: {
|
||||
onClick: async () => {
|
||||
const app = createRawAppFromFlow(flow.path, flow.summary, flow.schema)
|
||||
|
||||
@@ -603,6 +603,7 @@
|
||||
buttons.push({
|
||||
label: 'Build app',
|
||||
placement: 'menu',
|
||||
alwaysPlaced: true,
|
||||
buttonProps: {
|
||||
onClick: async () => {
|
||||
const app = createRawAppFromScript(script.path, script.summary, script.schema)
|
||||
|
||||
@@ -57,6 +57,9 @@ export type PageHeaderContent = {
|
||||
* all it can, and the page reads it back as `pageHeader.actionFold`. Without it the actions
|
||||
* never fold and the breadcrumb gives up width around them. */
|
||||
actionFolds?: number
|
||||
/** How many of the `actionFolds` levels come before the breadcrumb drops anything: buttons
|
||||
* that are there only while the bar has room for everything. */
|
||||
earlyActionFolds?: number
|
||||
/** A second row of the band, under the breadcrumb and the actions: where a control too wide
|
||||
* for one row goes at a fold that has no room for it beside them, rather than being squeezed
|
||||
* until it is useless. The band is taller while it is there. */
|
||||
@@ -167,6 +170,7 @@ export const pageHeader = {
|
||||
if (c.actionsFlexible !== undefined) merged.actionsFlexible = c.actionsFlexible
|
||||
if (c.actionsFill !== undefined) merged.actionsFill = c.actionsFill
|
||||
if (c.actionFolds !== undefined) merged.actionFolds = c.actionFolds
|
||||
if (c.earlyActionFolds !== undefined) merged.earlyActionFolds = c.earlyActionFolds
|
||||
if (c.belowBar !== undefined) merged.belowBar = c.belowBar
|
||||
if (c.belowBarFold !== undefined) merged.belowBarFold = c.belowBarFold
|
||||
if (c.separator !== undefined) merged.separator = c.separator
|
||||
|
||||
@@ -26,6 +26,14 @@ describe('fitBar', () => {
|
||||
expect(fitBar({ ...base, barWidth: 400 })).toMatchObject({ stage: 5, fold: 2 })
|
||||
})
|
||||
|
||||
it('spends early folds before the breadcrumb drops anything', () => {
|
||||
// 600 + 200 fits an 800px bar at the first fold; without early folds stage 2 would have
|
||||
// dropped parts beside the full row instead.
|
||||
expect(fitBar({ ...base, barWidth: 800, earlyFolds: 1 })).toMatchObject({ stage: 0, fold: 1 })
|
||||
// Past that, the breadcrumb gives way at the early fold before the later folds start.
|
||||
expect(fitBar({ ...base, barWidth: 700, earlyFolds: 1 })).toMatchObject({ stage: 2, fold: 1 })
|
||||
})
|
||||
|
||||
it('tries a level it has not seen at the width of the next one folded further', () => {
|
||||
// Only the second fold has been measured: the levels before it are assumed no wider, so the
|
||||
// bar tries the full row first and goes on to the first fold once it has seen it overflow.
|
||||
|
||||
@@ -122,6 +122,9 @@ export type BarMeasure = {
|
||||
actions: (number | undefined)[]
|
||||
/** How many levels past the full row the page's actions can fold through. */
|
||||
folds: number
|
||||
/** How many of those levels fold before the breadcrumb drops anything: buttons that are there
|
||||
* only while there is room for everything. */
|
||||
earlyFolds?: number
|
||||
natural: Record<Part, number>
|
||||
}
|
||||
|
||||
@@ -151,19 +154,27 @@ export function fitBar(m: BarMeasure): BarFit {
|
||||
const floorWidth = (s: number, f: number) =>
|
||||
m.chrome + (m.trail[s] ?? 0) + (m.summary[s] ?? 0) + actionsAt(f)
|
||||
|
||||
const early = Math.min(m.earlyFolds ?? 0, m.folds)
|
||||
let stage = last
|
||||
let fold = m.folds
|
||||
choose: {
|
||||
for (let s = 0; s <= last; s++) {
|
||||
if (floorWidth(s, 0) <= m.barWidth) {
|
||||
stage = s
|
||||
fold = 0
|
||||
for (let f = 0; f < early; f++) {
|
||||
if (floorWidth(0, f) <= m.barWidth) {
|
||||
stage = 0
|
||||
fold = f
|
||||
break choose
|
||||
}
|
||||
}
|
||||
// The actions fold only once the breadcrumb has given all it can, so a narrowing bar only
|
||||
// ever loses things: the parts it dropped do not come back when the buttons fold.
|
||||
for (let f = 1; f <= m.folds; f++) {
|
||||
for (let s = 0; s <= last; s++) {
|
||||
if (floorWidth(s, early) <= m.barWidth) {
|
||||
stage = s
|
||||
fold = early
|
||||
break choose
|
||||
}
|
||||
}
|
||||
// The other levels fold only once the breadcrumb has given all it can, so a narrowing bar
|
||||
// only ever loses things: the parts it dropped do not come back when the buttons fold.
|
||||
for (let f = early + 1; f <= m.folds; f++) {
|
||||
if (floorWidth(last, f) <= m.barWidth) {
|
||||
fold = f
|
||||
break choose
|
||||
|
||||
Reference in New Issue
Block a user