diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt index eaaec7f38c..e6198b836b 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1 @@ -e1851154b61494928ba698d58ba7a50a8f4e991f +1b569b727364e8f3db8513fb41e99f1fff1d1406 diff --git a/frontend/src/lib/components/ImportSetupStep.svelte b/frontend/src/lib/components/ImportSetupStep.svelte index fbb7a1bc31..b8c309b1b6 100644 --- a/frontend/src/lib/components/ImportSetupStep.svelte +++ b/frontend/src/lib/components/ImportSetupStep.svelte @@ -43,7 +43,8 @@ * own slug and `installProject` retargets them, so reading the raw paths here would * look for stubs that are not where they landed. */ folder?: string - onSkip: () => void + /** Left with `outstanding` rows still unfilled, which the caller may want to count. */ + onSkip: (outstanding: number) => void /** Off where the surface already names the step, e.g. a dialog whose title is it. */ showHeading?: boolean /** Fill the height given, actions pinned to the bottom. See ImportProjectStep. */ @@ -492,7 +493,7 @@ }) if (!confirmed) return } - onSkip() + onSkip(outstanding) } /** @@ -564,7 +565,7 @@ {#if row.status === 'done'} {:else if row.status === 'running'} - + {:else if row.status === 'failed'} {:else if row.status === 'unknown'} diff --git a/frontend/src/lib/components/InstanceSettings.svelte b/frontend/src/lib/components/InstanceSettings.svelte index cdcef14452..f02b5ca35c 100644 --- a/frontend/src/lib/components/InstanceSettings.svelte +++ b/frontend/src/lib/components/InstanceSettings.svelte @@ -1072,8 +1072,8 @@ model identifiers, the names of public hub scripts used, the languages debug sessions are started for, whether AI chat skills are turned on or off and how often one is loaded, the plan tier and quota shown when the execution meter is opened, and which - home-page entry point a new item is created from and the name of any public hub - project imported from it, last 30 days)
  • feature adoption (counts of which flow, script, trigger and worker features your @@ -1127,8 +1127,8 @@ model identifiers, the names of public hub scripts used, the languages debug sessions are started for, whether AI chat skills are turned on or off and how often one is loaded, the plan tier and quota shown when the execution meter is opened, and which - home-page entry point a new item is created from and the name of any public hub - project imported from it, last 30 days)
  • feature adoption (counts of which flow, script, trigger and worker features your diff --git a/frontend/src/lib/components/home/CreateActionsMenu.svelte b/frontend/src/lib/components/home/CreateActionsMenu.svelte index e37f48ac12..2cf1f28d54 100644 --- a/frontend/src/lib/components/home/CreateActionsMenu.svelte +++ b/frontend/src/lib/components/home/CreateActionsMenu.svelte @@ -15,6 +15,7 @@ Loader2, Workflow, Import, + Store, PanelLeftClose } from 'lucide-svelte' import BarsStaggered from '$lib/components/icons/BarsStaggered.svelte' @@ -36,9 +37,15 @@ triggerElement?: HTMLElement /** Which entry point this menu hangs off, for telemetry. */ source?: 'toolbar' | 'empty_state' + /** + * Opens the hub project picker. The menu only offers the entry; the picker and the + * import dialog belong to the host, which is the one place a single import modal can + * serve both this menu and the empty state's own link. + */ + onImportHubProject?: () => void } - let { trigger, triggerElement, source = 'toolbar' }: Props = $props() + let { trigger, triggerElement, source = 'toolbar', onImportHubProject }: Props = $props() type Variant = { label: string @@ -241,8 +248,15 @@ } let activeKey = $state(allOptions[0]?.key) - // every option's import action, surfaced together under the bottom "Import" submenu - const importActions: Extra[] = allOptions.flatMap((o) => o.extras ?? []) + // every option's import action, surfaced together under the bottom "Import" submenu. + // The hub project leads and is separated below: the others each paste one artifact the + // user already holds, while this one brings a whole project in from somewhere else. + const importActions: Extra[] = $derived([ + ...(onImportHubProject + ? [{ label: 'Import a hub project', onSelect: onImportHubProject }] + : []), + ...allOptions.flatMap((o) => o.extras ?? []) + ]) // melt dropdown menu: arrow-key nav, typeahead, focus management and outside/escape // close all come for free; we only drive the doc panel off the highlighted item. @@ -554,17 +568,24 @@ use:hugViewportRight class="z-[6001] flex flex-col gap-0.5 p-1 w-52 rounded-lg border border-gray-200 dark:border-gray-700 bg-surface shadow-xl focus:outline-none" > - {#each importActions as action (action.label)} + {#each importActions as action, i (action.label)} + {#if onImportHubProject && i === 0} +
    + {/if} {/each} {/if} diff --git a/frontend/src/lib/components/home/HubProjectPickerModal.svelte b/frontend/src/lib/components/home/HubProjectPickerModal.svelte new file mode 100644 index 0000000000..6a353ea79d --- /dev/null +++ b/frontend/src/lib/components/home/HubProjectPickerModal.svelte @@ -0,0 +1,48 @@ + + + + + +
    + +
    +
    diff --git a/frontend/src/lib/components/home/HubTemplatePicker.svelte b/frontend/src/lib/components/home/HubTemplatePicker.svelte index 3843e0cd35..5642d668a6 100644 --- a/frontend/src/lib/components/home/HubTemplatePicker.svelte +++ b/frontend/src/lib/components/home/HubTemplatePicker.svelte @@ -11,9 +11,11 @@ interface Props { onPick: (project: HubProjectPick) => void + /** Take the width given instead of the popover's own, for a host that sets one. */ + fullWidth?: boolean } - let { onPick }: Props = $props() + let { onPick, fullWidth = false }: Props = $props() let list: InfiniteList | undefined = $state(undefined) @@ -47,7 +49,7 @@ -
    +

    diff --git a/frontend/src/lib/components/home/ImportProjectModal.svelte b/frontend/src/lib/components/home/ImportProjectModal.svelte index 15993a48e2..7d3f0a1406 100644 --- a/frontend/src/lib/components/home/ImportProjectModal.svelte +++ b/frontend/src/lib/components/home/ImportProjectModal.svelte @@ -55,6 +55,13 @@ * there rather than importing twice. */ function dismiss() { + // Where they left, which is the half of the funnel Finish cannot report: `running` + // walked out on an import in progress, `setup` on the credentials it asked for, + // `idle` opened the dialog and picked nothing up. + if (!finishing) { + const stage = execution?.running ? 'running' : onSetupStep ? 'setup' : 'idle' + logFeatureUsage('home', 'template_abandon', { key: stage }) + } if (execution?.running) { execution.abandon() // Not on the way out through Finish: `done` survives a retry, so Finish is clickable @@ -129,10 +136,17 @@ // by the same falling edge as the X. let finishing = false - function finish() { + /** + * How the credentials step ended, counted alongside the import itself: `filled` only when + * nothing was left outstanding — the step disables Finish until then — `skipped` carrying + * how many rows were walked away from, and `none` where the project asked for nothing. + * Skipping with one credential left and skipping with eight are different problems. + */ + function finish(setupOutcome: 'filled' | 'skipped' | 'none', outstanding = 1) { // On the way out rather than on the pick: what is worth counting is an import that // landed, not a dialog that was opened and abandoned. if (slug) logFeatureUsage('home', 'template_import', { key: slug }) + logFeatureUsage('home', 'template_setup', { key: setupOutcome, value: outstanding }) finishing = true onImported?.() onClose() @@ -181,7 +195,7 @@ setupPending={setup.needed} setupUndecided={setup.undecided} onFolderChange={(f) => (folder = f)} - onFinish={() => (setup.needed ? (onSetupStep = true) : finish())} + onFinish={() => (setup.needed ? (onSetupStep = true) : finish('none'))} onBack={onClose} onExecution={(e) => (execution = e)} resume={execution} @@ -205,8 +219,8 @@ slug={slug ?? ''} {folder} showHeading={false} - onSkip={finish} - onFinish={finish} + onSkip={(outstanding) => finish('skipped', outstanding)} + onFinish={() => finish('filled')} onBack={execution ? () => (onSetupStep = false) : undefined} />

    diff --git a/frontend/src/lib/components/home/ItemsList.svelte b/frontend/src/lib/components/home/ItemsList.svelte index 1dba252eb0..8b47c19ef7 100644 --- a/frontend/src/lib/components/home/ItemsList.svelte +++ b/frontend/src/lib/components/home/ItemsList.svelte @@ -40,6 +40,9 @@ } from '$lib/components/FilterSearchbar.svelte' import NoItemFound from './NoItemFound.svelte' import WorkspaceEmptyState from './WorkspaceEmptyState.svelte' + import HubProjectPickerModal from './HubProjectPickerModal.svelte' + import ImportProjectModal from './ImportProjectModal.svelte' + import type { HubProjectPick } from '$lib/hubProject' import ToggleButtonGroup from '../common/toggleButton-v2/ToggleButtonGroup.svelte' import ToggleButton from '../common/toggleButton-v2/ToggleButton.svelte' import FlowIcon from './FlowIcon.svelte' @@ -975,6 +978,12 @@ // An import just landed, so the rows about to replace the empty state are all new: they // fade in one after another rather than appearing as a finished list. Cleared on a timer // because nothing else marks the end — the reload resolves before the rows animate. + // The hub import, owned here rather than by either entry point: the empty state's link and + // the create menu's Import section open the same dialog, and mounting one per entry point + // would put two of them on the page at once while the workspace is still empty. + let hubPick = $state(undefined) + let hubPickerOpen = $state(false) + let justImported = $state(false) let justImportedTimer: ReturnType | undefined function onImported() { @@ -1774,7 +1783,7 @@ whose direct-deploy protection cleared showEditButtons (NoDirectDeployAlert), since the menu itself does no permission check. --> {#if !$userStore?.operator && showEditButtons} - + (hubPickerOpen = true)} /> {/if}
    @@ -1810,7 +1819,7 @@ workspace whose direct-deploy protection cleared `showEditButtons`, gets the plain message instead of two actions it may not take. --> {#if workspaceEmpty && !$userStore?.operator && showEditButtons} - + (hubPick = project)} /> {:else} {/if} @@ -1930,6 +1939,16 @@ /> {/if} + (hubPickerOpen = false)} + onPick={(project) => { + hubPickerOpen = false + hubPick = project + }} +/> + (hubPick = undefined)} {onImported} /> +