From 98c57eede34710c7d8f4342831ff650dbc0665f1 Mon Sep 17 00:00:00 2001 From: Guilhem Lemouel Date: Mon, 17 Aug 2026 19:28:24 +0200 Subject: [PATCH] fix: hide the header create button while the empty state offers it Co-Authored-By: Claude Opus 5 (1M context) --- .../common/emptyState/EmptyState.svelte | 6 ++++ .../(logged)/amqp_triggers/+page.svelte | 19 ++++++----- .../(logged)/azure_triggers/+page.svelte | 19 ++++++----- .../(logged)/email_triggers/+page.svelte | 21 +++++++----- .../(root)/(logged)/folders/+page.svelte | 8 ++++- .../(root)/(logged)/gcp_triggers/+page.svelte | 19 ++++++----- .../(logged)/kafka_triggers/+page.svelte | 19 ++++++----- .../(logged)/mqtt_triggers/+page.svelte | 19 ++++++----- .../[service_name]/+page.svelte | 23 ++++++++----- .../(logged)/nats_triggers/+page.svelte | 19 ++++++----- .../(logged)/postgres_triggers/+page.svelte | 19 ++++++----- .../(root)/(logged)/resources/+page.svelte | 33 ++++++++++++------- .../(root)/(logged)/routes/+page.svelte | 19 ++++++----- .../(root)/(logged)/schedules/+page.svelte | 23 +++++++------ .../(root)/(logged)/sqs_triggers/+page.svelte | 19 ++++++----- .../(root)/(logged)/variables/+page.svelte | 18 +++++++++- .../(logged)/websocket_triggers/+page.svelte | 19 ++++++----- 17 files changed, 201 insertions(+), 121 deletions(-) diff --git a/frontend/src/lib/components/common/emptyState/EmptyState.svelte b/frontend/src/lib/components/common/emptyState/EmptyState.svelte index 2bed452f4a..8510b2b096 100644 --- a/frontend/src/lib/components/common/emptyState/EmptyState.svelte +++ b/frontend/src/lib/components/common/emptyState/EmptyState.svelte @@ -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 diff --git a/frontend/src/routes/(root)/(logged)/amqp_triggers/+page.svelte b/frontend/src/routes/(root)/(logged)/amqp_triggers/+page.svelte index 8d08defd12..662395541b 100644 --- a/frontend/src/routes/(root)/(logged)/amqp_triggers/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/amqp_triggers/+page.svelte @@ -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." > - + {#if !emptyCtaShown} + + {/if} {#if isCloudHosted()} diff --git a/frontend/src/routes/(root)/(logged)/azure_triggers/+page.svelte b/frontend/src/routes/(root)/(logged)/azure_triggers/+page.svelte index 69963ed803..e2ec500532 100644 --- a/frontend/src/routes/(root)/(logged)/azure_triggers/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/azure_triggers/+page.svelte @@ -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 @@ - + {#if !emptyCtaShown} + + {/if} {#if isCloudHosted()} diff --git a/frontend/src/routes/(root)/(logged)/email_triggers/+page.svelte b/frontend/src/routes/(root)/(logged)/email_triggers/+page.svelte index 87c9558dbb..e72a6e38cd 100644 --- a/frontend/src/routes/(root)/(logged)/email_triggers/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/email_triggers/+page.svelte @@ -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} - + {#if !emptyCtaShown} + + {/if} {/if}
diff --git a/frontend/src/routes/(root)/(logged)/folders/+page.svelte b/frontend/src/routes/(root)/(logged)/folders/+page.svelte index 38038e9656..97e42c1964 100644 --- a/frontend/src/routes/(root)/(logged)/folders/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/folders/+page.svelte @@ -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 - {:else} + {:else if !emptyCtaShown} {@render newFolderPopover('New folder', 'bottom-end')} {/if}
diff --git a/frontend/src/routes/(root)/(logged)/gcp_triggers/+page.svelte b/frontend/src/routes/(root)/(logged)/gcp_triggers/+page.svelte index 22234e0f04..2fe1d9027f 100644 --- a/frontend/src/routes/(root)/(logged)/gcp_triggers/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/gcp_triggers/+page.svelte @@ -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 @@ - + {#if !emptyCtaShown} + + {/if} {#if isCloudHosted()} diff --git a/frontend/src/routes/(root)/(logged)/kafka_triggers/+page.svelte b/frontend/src/routes/(root)/(logged)/kafka_triggers/+page.svelte index 6ee790818a..e0f5e3c215 100644 --- a/frontend/src/routes/(root)/(logged)/kafka_triggers/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/kafka_triggers/+page.svelte @@ -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." > - + {#if !emptyCtaShown} + + {/if} {#if isCloudHosted()} diff --git a/frontend/src/routes/(root)/(logged)/mqtt_triggers/+page.svelte b/frontend/src/routes/(root)/(logged)/mqtt_triggers/+page.svelte index 8a6d84fe90..b63bdaa623 100644 --- a/frontend/src/routes/(root)/(logged)/mqtt_triggers/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/mqtt_triggers/+page.svelte @@ -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." > - + {#if !emptyCtaShown} + + {/if} {#if isCloudHosted()} diff --git a/frontend/src/routes/(root)/(logged)/native_triggers/[service_name]/+page.svelte b/frontend/src/routes/(root)/(logged)/native_triggers/[service_name]/+page.svelte index f5e9a30dc5..34755f6f80 100644 --- a/frontend/src/routes/(root)/(logged)/native_triggers/[service_name]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/native_triggers/[service_name]/+page.svelte @@ -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." > - + {#if !emptyCtaShown} + + {/if} {#if serviceAvailable === false} diff --git a/frontend/src/routes/(root)/(logged)/nats_triggers/+page.svelte b/frontend/src/routes/(root)/(logged)/nats_triggers/+page.svelte index 834869891a..3405743a1d 100644 --- a/frontend/src/routes/(root)/(logged)/nats_triggers/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/nats_triggers/+page.svelte @@ -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." > - + {#if !emptyCtaShown} + + {/if} {#if isCloudHosted()} diff --git a/frontend/src/routes/(root)/(logged)/postgres_triggers/+page.svelte b/frontend/src/routes/(root)/(logged)/postgres_triggers/+page.svelte index d1c07a7e75..90221b9d9b 100644 --- a/frontend/src/routes/(root)/(logged)/postgres_triggers/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/postgres_triggers/+page.svelte @@ -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." > - + {#if !emptyCtaShown} + + {/if} {#if isCloudHosted()} diff --git a/frontend/src/routes/(root)/(logged)/resources/+page.svelte b/frontend/src/routes/(root)/(logged)/resources/+page.svelte index e3c47085c3..adcf7b636e 100644 --- a/frontend/src/routes/(root)/(logged)/resources/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/resources/+page.svelte @@ -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 + ) Add resource type - + {#if !emptyCtaShown} + + {/if} {/if} diff --git a/frontend/src/routes/(root)/(logged)/routes/+page.svelte b/frontend/src/routes/(root)/(logged)/routes/+page.svelte index 53de5457d1..39f6b2ac02 100644 --- a/frontend/src/routes/(root)/(logged)/routes/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/routes/+page.svelte @@ -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 - + {#if !emptyCtaShown} + + {/if}
diff --git a/frontend/src/routes/(root)/(logged)/schedules/+page.svelte b/frontend/src/routes/(root)/(logged)/schedules/+page.svelte index f92490ebb8..63e6160436 100644 --- a/frontend/src/routes/(root)/(logged)/schedules/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/schedules/+page.svelte @@ -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" > - + {#if !emptyCtaShown} + + {/if}
diff --git a/frontend/src/routes/(root)/(logged)/sqs_triggers/+page.svelte b/frontend/src/routes/(root)/(logged)/sqs_triggers/+page.svelte index 1226fc2c0f..e7b7ec2c09 100644 --- a/frontend/src/routes/(root)/(logged)/sqs_triggers/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/sqs_triggers/+page.svelte @@ -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 @@ - + {#if !emptyCtaShown} + + {/if} {#if isCloudHosted()} diff --git a/frontend/src/routes/(root)/(logged)/variables/+page.svelte b/frontend/src/routes/(root)/(logged)/variables/+page.svelte index f2c330da84..1fe505e84d 100644 --- a/frontend/src/routes/(root)/(logged)/variables/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/variables/+page.svelte @@ -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}
{#if tab == 'contextual' && ($userStore?.is_admin || $userStore?.is_super_admin)} + {#if !emptyCtaShown} + + {/if} {#if isCloudHosted()}