fix: hide the header create button while the empty state offers it

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Guilhem Lemouel
2026-08-17 19:28:24 +02:00
parent 7cebeb3014
commit 98c57eede3
17 changed files with 201 additions and 121 deletions
@@ -6,6 +6,12 @@
icon: any
title: string
description?: string
/**
* Accent create button. A page that renders one here should hide the page-header
* button for the same action while it shows, so the two accent CTAs don't sit on
* screen together — and must derive that from the very conditions gating this
* `action`, or a state where the card has no button leaves no way to create at all.
*/
action?: {
label: string
icon?: any
@@ -53,6 +53,7 @@
let triggers: TriggerM[] = $state([])
let shareModal: ShareModal | undefined = $state()
let loading = $state(true)
let emptyCtaShown = $derived(!loading && !triggers?.length)
let deploymentDrawer: DeployWorkspaceDrawer | undefined = $state()
let deployUiSettings: WorkspaceDeployUISettings | undefined = $state(undefined)
@@ -286,14 +287,16 @@
title="AMQP triggers"
tooltip="Windmill can connect to an AMQP broker, subscribe to specific topics, and trigger scripts or flows based on those topics."
>
<Button
unifiedSize="md"
variant="accent"
startIcon={{ icon: Plus }}
on:click={() => amqpTriggerEditor?.openNew(false)}
>
New&nbsp;AMQP trigger
</Button>
{#if !emptyCtaShown}
<Button
unifiedSize="md"
variant="accent"
startIcon={{ icon: Plus }}
on:click={() => amqpTriggerEditor?.openNew(false)}
>
New&nbsp;AMQP trigger
</Button>
{/if}
</PageHeader>
{#if isCloudHosted()}
@@ -66,6 +66,7 @@
let triggers: TriggerD[] = $state([])
let shareModal: ShareModal | undefined = $state()
let loading = $state(true)
let emptyCtaShown = $derived(!loading && !triggers?.length)
let deploymentDrawer: DeployWorkspaceDrawer | undefined = $state()
let deployUiSettings: WorkspaceDeployUISettings | undefined = $state(undefined)
let isDeleting = $state(false)
@@ -353,14 +354,16 @@
<CenteredPage>
<PageHeader title="Azure Event Grid triggers" tooltip="Azure Event Grid trigger">
<Button
unifiedSize="md"
variant="accent"
startIcon={{ icon: Plus }}
on:click={() => azureTriggerEditor?.openNew(false)}
>
New&nbsp;Azure Event Grid trigger
</Button>
{#if !emptyCtaShown}
<Button
unifiedSize="md"
variant="accent"
startIcon={{ icon: Plus }}
on:click={() => azureTriggerEditor?.openNew(false)}
>
New&nbsp;Azure Event Grid trigger
</Button>
{/if}
</PageHeader>
{#if isCloudHosted()}
@@ -67,6 +67,9 @@
let triggers: TriggerW[] = $state([])
let shareModal: ShareModal | undefined = $state()
let loading = $state(true)
let emptyCtaShown = $derived(
!loading && !triggers?.length && Boolean($userStore?.is_admin || $userStore?.is_super_admin)
)
let deploymentDrawer: DeployWorkspaceDrawer | undefined = $state()
let deployUiSettings: WorkspaceDeployUISettings | undefined = $state(undefined)
let emailDomain: string | null = $state(null)
@@ -281,14 +284,16 @@
documentationLink="https://www.windmill.dev/docs/advanced/email_triggers"
>
{#if $userStore?.is_admin || $userStore?.is_super_admin}
<Button
unifiedSize="md"
variant="accent"
startIcon={{ icon: Plus }}
on:click={() => emailTriggerEditor?.openNew(false)}
>
New&nbsp;email trigger
</Button>
{#if !emptyCtaShown}
<Button
unifiedSize="md"
variant="accent"
startIcon={{ icon: Plus }}
on:click={() => emailTriggerEditor?.openNew(false)}
>
New&nbsp;email trigger
</Button>
{/if}
{/if}
</PageHeader>
<div class="w-full h-full flex flex-col">
@@ -30,6 +30,12 @@
let newFolderName: string = $state('')
let folders: FolderW[] | undefined = $state(undefined)
// Read through a typed parameter: a $derived reading a same-scope `$state(undefined)`
// infers `never` and loses `.length`.
function hasNoFolders(list: FolderW[] | undefined): boolean {
return list?.length === 0
}
let emptyCtaShown = $derived(hasNoFolders(folders) && !restricted)
let folderDrawer: Drawer | undefined = $state()
let hubDrawer: Drawer | undefined = $state()
let publishFolderName: string = $state('')
@@ -165,7 +171,7 @@
>
New folder
</Button>
{:else}
{:else if !emptyCtaShown}
{@render newFolderPopover('New folder', 'bottom-end')}
{/if}
</div>
@@ -66,6 +66,7 @@
let triggers: TriggerD[] = $state([])
let shareModal: ShareModal | undefined = $state()
let loading = $state(true)
let emptyCtaShown = $derived(!loading && !triggers?.length)
let deploymentDrawer: DeployWorkspaceDrawer | undefined = $state()
let deployUiSettings: WorkspaceDeployUISettings | undefined = $state(undefined)
let isDeleting = $state(false)
@@ -332,14 +333,16 @@
<CenteredPage>
<PageHeader title="GCP Pub/Sub triggers" tooltip="GCP Pub/Sub trigger">
<Button
unifiedSize="md"
variant="accent"
startIcon={{ icon: Plus }}
on:click={() => gcpTriggerEditor?.openNew(false)}
>
New&nbsp;GCP Pub/Sub trigger
</Button>
{#if !emptyCtaShown}
<Button
unifiedSize="md"
variant="accent"
startIcon={{ icon: Plus }}
on:click={() => gcpTriggerEditor?.openNew(false)}
>
New&nbsp;GCP Pub/Sub trigger
</Button>
{/if}
</PageHeader>
{#if isCloudHosted()}
@@ -58,6 +58,7 @@
let triggers: TriggerW[] = $state([])
let shareModal: ShareModal | undefined = $state()
let loading = $state(true)
let emptyCtaShown = $derived(!loading && !triggers?.length)
let deploymentDrawer: DeployWorkspaceDrawer | undefined = $state()
let deployUiSettings: WorkspaceDeployUISettings | undefined = $state(undefined)
@@ -296,14 +297,16 @@
title="Kafka triggers"
tooltip="Windmill can consume kafka events and trigger scripts or flows based on them."
>
<Button
unifiedSize="md"
variant="accent"
startIcon={{ icon: Plus }}
on:click={() => kafkaTriggerEditor?.openNew(false)}
>
New&nbsp;Kafka trigger
</Button>
{#if !emptyCtaShown}
<Button
unifiedSize="md"
variant="accent"
startIcon={{ icon: Plus }}
on:click={() => kafkaTriggerEditor?.openNew(false)}
>
New&nbsp;Kafka trigger
</Button>
{/if}
</PageHeader>
{#if isCloudHosted()}
@@ -53,6 +53,7 @@
let triggers: TriggerM[] = $state([])
let shareModal: ShareModal | undefined = $state()
let loading = $state(true)
let emptyCtaShown = $derived(!loading && !triggers?.length)
let deploymentDrawer: DeployWorkspaceDrawer | undefined = $state()
let deployUiSettings: WorkspaceDeployUISettings | undefined = $state(undefined)
@@ -286,14 +287,16 @@
title="MQTT triggers"
tooltip="Windmill can connect to an MQTT broker, subscribe to specific topics, and trigger scripts or flows based on those topics."
>
<Button
unifiedSize="md"
variant="accent"
startIcon={{ icon: Plus }}
on:click={() => mqttTriggerEditor?.openNew(false)}
>
New&nbsp;MQTT trigger
</Button>
{#if !emptyCtaShown}
<Button
unifiedSize="md"
variant="accent"
startIcon={{ icon: Plus }}
on:click={() => mqttTriggerEditor?.openNew(false)}
>
New&nbsp;MQTT trigger
</Button>
{/if}
</PageHeader>
{#if isCloudHosted()}
@@ -41,6 +41,9 @@
let loading = $state(true)
let serviceAvailable: boolean | undefined = $state(undefined)
let serviceSupported = $state(true)
// The card only renders inside the serviceAvailable branch, so the header button
// stays put when the service is not connected — that state shows an alert, not a card.
let emptyCtaShown = $derived(Boolean(serviceAvailable) && !loading && !triggers?.length)
let editor: NativeTriggerEditor
let filteredItems: TriggerW[] = $state([])
@@ -215,15 +218,17 @@
tooltip="Native triggers managed externally by {serviceConfig?.serviceDisplayName ||
serviceName}. These are more efficient than regular triggers as they're handled directly by the service provider."
>
<Button
unifiedSize="md"
variant="accent"
startIcon={{ icon: Plus }}
on:click={() => editor?.openNew()}
disabled={!serviceAvailable}
>
New&nbsp;{serviceConfig?.serviceDisplayName || serviceName} trigger
</Button>
{#if !emptyCtaShown}
<Button
unifiedSize="md"
variant="accent"
startIcon={{ icon: Plus }}
on:click={() => editor?.openNew()}
disabled={!serviceAvailable}
>
New&nbsp;{serviceConfig?.serviceDisplayName || serviceName} trigger
</Button>
{/if}
</PageHeader>
{#if serviceAvailable === false}
@@ -58,6 +58,7 @@
let triggers: TriggerW[] = $state([])
let shareModal: ShareModal | undefined = $state()
let loading = $state(true)
let emptyCtaShown = $derived(!loading && !triggers?.length)
let deploymentDrawer: DeployWorkspaceDrawer | undefined = $state()
let deployUiSettings: WorkspaceDeployUISettings | undefined = $state(undefined)
@@ -295,14 +296,16 @@
title="NATS triggers"
tooltip="Windmill can consume NATS events and trigger scripts or flows based on them."
>
<Button
unifiedSize="md"
variant="accent"
startIcon={{ icon: Plus }}
on:click={() => natsTriggerEditor?.openNew(false)}
>
New&nbsp;NATS trigger
</Button>
{#if !emptyCtaShown}
<Button
unifiedSize="md"
variant="accent"
startIcon={{ icon: Plus }}
on:click={() => natsTriggerEditor?.openNew(false)}
>
New&nbsp;NATS trigger
</Button>
{/if}
</PageHeader>
{#if isCloudHosted()}
@@ -65,6 +65,7 @@
let triggers: TriggerD[] = $state([])
let shareModal: ShareModal | undefined = $state()
let loading = $state(true)
let emptyCtaShown = $derived(!loading && !triggers?.length)
let deploymentDrawer: DeployWorkspaceDrawer | undefined = $state()
let deployUiSettings: WorkspaceDeployUISettings | undefined = $state(undefined)
@@ -361,14 +362,16 @@
title="Postgres triggers"
tooltip="Windmill enables real-time responsiveness by listening to specific database transactions—such as inserts, updates, and deletes—and automatically triggering scripts or workflows in response."
>
<Button
unifiedSize="md"
variant="accent"
startIcon={{ icon: Plus }}
on:click={() => postgresTriggerEditor?.openNew(false)}
>
New&nbsp;Postgres trigger
</Button>
{#if !emptyCtaShown}
<Button
unifiedSize="md"
variant="accent"
startIcon={{ icon: Plus }}
on:click={() => postgresTriggerEditor?.openNew(false)}
>
New&nbsp;Postgres trigger
</Button>
{/if}
</PageHeader>
{#if isCloudHosted()}
@@ -3,12 +3,11 @@
import { page } from '$app/state'
import AppConnect from '$lib/components/AppConnectDrawer.svelte'
import CenteredPage from '$lib/components/CenteredPage.svelte'
import { Alert, Badge, Button, EmptyState, Skeleton, Tab } from '$lib/components/common'
import { Alert, Badge, Button, EmptyState, Skeleton, Tab, TabFade } from '$lib/components/common'
import ConfirmationModal from '$lib/components/common/confirmationModal/ConfirmationModal.svelte'
import Drawer from '$lib/components/common/drawer/Drawer.svelte'
import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte'
import Tabs from '$lib/components/common/tabs/Tabs.svelte'
import TabFade from '$lib/components/common/tabs/TabFade.svelte'
import DeployWorkspaceDrawer from '$lib/components/DeployWorkspaceDrawer.svelte'
import Dropdown from '$lib/components/DropdownV2.svelte'
import IconedResourceType from '$lib/components/IconedResourceType.svelte'
@@ -668,6 +667,14 @@
}
}
let emptyState = $derived(emptyStates[tab] ?? emptyStates.workspace)
let emptyCtaShown = $derived(
tab === 'workspace' &&
showCreateButtons &&
!loading.resources &&
filteredItems?.length === 0 &&
!hasActiveFilters
)
</script>
<ConfirmationModal
@@ -933,16 +940,18 @@
>
Add resource type
</Button>
<Button
unifiedSize="md"
variant="accent"
startIcon={{ icon: Boxes }}
on:click={() => appConnect?.open?.()}
aiId="resources-add-resource"
aiDescription="Add resource"
>
Add resource
</Button>
{#if !emptyCtaShown}
<Button
unifiedSize="md"
variant="accent"
startIcon={{ icon: Boxes }}
on:click={() => appConnect?.open?.()}
aiId="resources-add-resource"
aiDescription="Add resource"
>
Add resource
</Button>
{/if}
</div>
{/if}
</PageHeader>
@@ -71,6 +71,7 @@
let triggers: TriggerW[] = $state([])
let shareModal: ShareModal | undefined = $state()
let loading = $state(true)
let emptyCtaShown = $derived(!loading && !triggers?.length)
let openAPISpecGenerator: OpenApiSpecGenerator | undefined = $state()
let routesGenerator: RoutesGenerator | undefined = $state()
let deploymentDrawer: DeployWorkspaceDrawer | undefined = $state()
@@ -320,14 +321,16 @@
>
To OpenAPI spec
</Button>
<Button
unifiedSize="md"
variant="accent"
startIcon={{ icon: Plus }}
on:click={() => routeEditor?.openNew(false)}
>
New&nbsp;route
</Button>
{#if !emptyCtaShown}
<Button
unifiedSize="md"
variant="accent"
startIcon={{ icon: Plus }}
on:click={() => routeEditor?.openNew(false)}
>
New&nbsp;route
</Button>
{/if}
</div>
</PageHeader>
<div class="w-full h-full flex flex-col">
@@ -255,6 +255,7 @@
let hasActiveFilters = $derived(
Object.values(filters.val).some((v) => v !== undefined && v !== null && v !== '' && v !== false)
)
let emptyCtaShown = $derived(!loading && !schedules?.length && !hasActiveFilters)
let allFolders = $derived(
Array.from(
new Set(
@@ -351,16 +352,18 @@
tooltip="Trigger Scripts and Flows according to a cron schedule"
documentationLink="https://www.windmill.dev/docs/core_concepts/scheduling"
>
<Button
size="lg"
variant="accent"
startIcon={{ icon: Plus }}
on:click={() => scheduleEditor?.openNew(false)}
aiId="schedules-add-schedule"
aiDescription="Add schedule"
>
New schedule
</Button>
{#if !emptyCtaShown}
<Button
size="lg"
variant="accent"
startIcon={{ icon: Plus }}
on:click={() => scheduleEditor?.openNew(false)}
aiId="schedules-add-schedule"
aiDescription="Add schedule"
>
New schedule
</Button>
{/if}
</PageHeader>
<div class="w-full h-full flex flex-col">
<div class="flex flex-row items-center justify-end gap-4 pb-4">
@@ -52,6 +52,7 @@
let triggers: TriggerD[] = $state([])
let shareModal: ShareModal | undefined = $state()
let loading = $state(true)
let emptyCtaShown = $derived(!loading && !triggers?.length)
let deploymentDrawer: DeployWorkspaceDrawer | undefined = $state()
let deployUiSettings: WorkspaceDeployUISettings | undefined = $state(undefined)
@@ -280,14 +281,16 @@
<CenteredPage>
<PageHeader title="SQS triggers" tooltip="SQS trigger">
<Button
unifiedSize="md"
variant="accent"
startIcon={{ icon: Plus }}
on:click={() => sqsTriggerEditor?.openNew(false)}
>
New&nbsp;SQS trigger
</Button>
{#if !emptyCtaShown}
<Button
unifiedSize="md"
variant="accent"
startIcon={{ icon: Plus }}
on:click={() => sqsTriggerEditor?.openNew(false)}
>
New&nbsp;SQS trigger
</Button>
{/if}
</PageHeader>
{#if isCloudHosted()}
@@ -242,6 +242,22 @@
let hasActiveFilters = $derived(
Object.values(filters.val).some((v) => v !== undefined && v !== null && v !== '' && v !== false)
)
// `tab` goes through a typed parameter: a $derived reading a same-scope $state
// narrows it to its initial literal, so comparing the other tab fails to compile.
function isEmptyCtaShown(currentTab: 'workspace' | 'contextual'): boolean {
if (!showCreateButtons) {
return false
}
if (currentTab === 'contextual') {
return (
!loading.contextual &&
contextualVariables.filter((x) => x.is_custom).length === 0 &&
Boolean($userStore?.is_admin || $userStore?.is_super_admin)
)
}
return filteredItems?.length === 0 && !hasActiveFilters
}
let emptyCtaShown = $derived(isEmptyCtaShown(tab))
let deploymentDrawer: DeployWorkspaceDrawer | undefined = $state()
@@ -295,7 +311,7 @@
tooltip="Save and permission strings to be reused in Scripts and Flows."
documentationLink="https://www.windmill.dev/docs/core_concepts/variables_and_secrets"
>
{#if showCreateButtons}
{#if showCreateButtons && !emptyCtaShown}
<div class="flex flex-row justify-end">
{#if tab == 'contextual' && ($userStore?.is_admin || $userStore?.is_super_admin)}
<Button
@@ -52,6 +52,7 @@
let triggers: TriggerW[] = $state([])
let shareModal: ShareModal | undefined = $state()
let loading = $state(true)
let emptyCtaShown = $derived(!loading && !triggers?.length)
let deploymentDrawer: DeployWorkspaceDrawer | undefined = $state()
let deployUiSettings: WorkspaceDeployUISettings | undefined = $state(undefined)
@@ -286,14 +287,16 @@
title="WebSocket triggers"
tooltip="Windmill can listen to WebSocket events and trigger scripts or flows based on them."
>
<Button
unifiedSize="md"
variant="accent"
startIcon={{ icon: Plus }}
on:click={() => websocketTriggerEditor?.openNew(false)}
>
New&nbsp;WebSocket trigger
</Button>
{#if !emptyCtaShown}
<Button
unifiedSize="md"
variant="accent"
startIcon={{ icon: Plus }}
on:click={() => websocketTriggerEditor?.openNew(false)}
>
New&nbsp;WebSocket trigger
</Button>
{/if}
</PageHeader>
{#if isCloudHosted()}