mirror of
https://github.com/n8n-io/n8n.git
synced 2026-06-01 17:27:14 +02:00
refactor(editor): Add 'Build an Agent' to empty state (#30078)
This commit is contained in:
parent
0d262fe638
commit
341abecbfb
|
|
@ -136,6 +136,27 @@ describe('N8nNavigationDropdown', () => {
|
|||
expect(getByText('first')).toBeVisible();
|
||||
});
|
||||
|
||||
it('should render agent immediately after workflow', async () => {
|
||||
const { getByTestId, getAllByTestId } = render(NavigationDropdown, {
|
||||
slots: { default: h('button', { 'data-test-id': 'test-trigger' }) },
|
||||
props: {
|
||||
menu: [
|
||||
{ id: 'workflow', title: 'New workflow' },
|
||||
{ id: 'credential', title: 'New credential' },
|
||||
{ id: 'agent', title: 'New agent' },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
await userEvent.click(getByTestId('test-trigger'));
|
||||
|
||||
expect(getAllByTestId('navigation-menu-item').map((item) => item.textContent?.trim())).toEqual([
|
||||
'New workflow',
|
||||
'New agent',
|
||||
'New credential',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should toggle nested level on mouseenter / mouseleave', async () => {
|
||||
const { getByTestId, getByText } = render(NavigationDropdown, {
|
||||
slots: { default: h('button', { 'data-test-id': 'test-trigger' }) },
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { ElMenu, ElSubMenu, ElMenuItem, type MenuItemRegistered } from 'element-plus';
|
||||
import { defineComponent, ref, useSlots } from 'vue';
|
||||
import { computed, defineComponent, ref, useSlots } from 'vue';
|
||||
import type { RouteLocationRaw } from 'vue-router';
|
||||
|
||||
import type { IconSize } from '@n8n/design-system/types';
|
||||
|
|
@ -31,7 +31,7 @@ defineOptions({
|
|||
name: 'N8nNavigationDropdown',
|
||||
});
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
menu: Array<Item | Divider>;
|
||||
disabled?: boolean;
|
||||
teleport?: boolean;
|
||||
|
|
@ -55,6 +55,22 @@ const emit = defineEmits<{
|
|||
const slots = useSlots();
|
||||
const hasAppendSlot = (id: string) => Boolean(slots[`item.append.${id}`]);
|
||||
|
||||
const orderedMenu = computed(() => {
|
||||
const workflowIndex = props.menu.findIndex((item) => !item.isDivider && item.id === 'workflow');
|
||||
const agentIndex = props.menu.findIndex((item) => !item.isDivider && item.id === 'agent');
|
||||
|
||||
if (workflowIndex === -1 || agentIndex === -1 || agentIndex === workflowIndex + 1) {
|
||||
return props.menu;
|
||||
}
|
||||
|
||||
const ordered = [...props.menu];
|
||||
const [agentItem] = ordered.splice(agentIndex, 1);
|
||||
const nextWorkflowIndex = ordered.findIndex((item) => !item.isDivider && item.id === 'workflow');
|
||||
ordered.splice(nextWorkflowIndex + 1, 0, agentItem);
|
||||
|
||||
return ordered;
|
||||
});
|
||||
|
||||
defineSlots<{
|
||||
default?: () => unknown;
|
||||
'item-icon'?: (props: { item: BaseItem }) => unknown;
|
||||
|
|
@ -111,7 +127,7 @@ defineExpose({
|
|||
<slot />
|
||||
</template>
|
||||
|
||||
<template v-for="item in menu" :key="item.id">
|
||||
<template v-for="item in orderedMenu" :key="item.id">
|
||||
<hr v-if="item.isDivider" />
|
||||
<template v-else-if="item.submenu">
|
||||
<ElSubMenu
|
||||
|
|
|
|||
|
|
@ -4090,11 +4090,11 @@
|
|||
"workflows.noResults.withSearch.switchToShared.link": "hidden",
|
||||
"workflows.empty.heading": "Welcome, {name}!",
|
||||
"workflows.empty.heading.userNotSetup": "Welcome!",
|
||||
"workflows.empty.headingWithIcon": "👋 Welcome, {name}!",
|
||||
"workflows.empty.headingWithIcon.userNotSetup": "👋 Welcome!",
|
||||
"workflows.empty.headingWithIcon": "What do you want to build, {name}",
|
||||
"workflows.empty.headingWithIcon.userNotSetup": "What do you want to build?",
|
||||
"workflows.empty.heading.builder": "Hi {name}, what do you want to automate?",
|
||||
"workflows.empty.heading.builder.userNotSetup": "Hi, what do you want to automate?",
|
||||
"workflows.empty.description": "Create your first workflow",
|
||||
"workflows.empty.description": "What do you want to build?",
|
||||
"workflows.empty.description.readOnlyEnv": "No workflows here yet",
|
||||
"workflows.empty.description.noPermission": "There are currently no workflows to view",
|
||||
"workflows.empty.startFromScratch": "Start from scratch",
|
||||
|
|
@ -4102,6 +4102,8 @@
|
|||
"workflows.empty.browseTemplates": "Explore workflow templates",
|
||||
"workflows.empty.learnN8n": "Learn n8n",
|
||||
"workflows.empty.button.disabled.tooltip": "Your current role in the project does not allow you to create workflows",
|
||||
"workflows.empty.buildAgent": "Build an agent",
|
||||
"workflows.empty.buildWorkflow": "Build a workflow",
|
||||
"workflows.empty.easyAI": "Test a simple AI Agent example",
|
||||
"workflows.empty.tryAiWorkflow": "Run live demo",
|
||||
"workflows.empty.shared-with-me": "No {resource} has been shared with you",
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { useSourceControlStore } from '@/features/integrations/sourceControl.ee/
|
|||
import { useRecommendedTemplatesStore } from '@/features/workflows/templates/recommendations/recommendedTemplates.store';
|
||||
import { useReadyToRunStore } from '@/features/workflows/readyToRun/stores/readyToRun.store';
|
||||
import { useBannersStore } from '@/features/shared/banners/banners.store';
|
||||
import { useSettingsStore } from '@/app/stores/settings.store';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import type { IUser } from '@n8n/rest-api-client/api/users';
|
||||
|
||||
|
|
@ -15,6 +16,7 @@ const surfaceMcpEmptyState = vi.hoisted(() => ({
|
|||
showTile: false,
|
||||
showReminder: false,
|
||||
}));
|
||||
const trackClickedNewAgent = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock('vue-router', () => ({
|
||||
useRouter: () => ({
|
||||
|
|
@ -39,6 +41,12 @@ vi.mock('@/experiments/surfaceMcpToNewCloudUsers/composables/useSurfaceMcpEmptyS
|
|||
};
|
||||
});
|
||||
|
||||
vi.mock('@/features/agents/composables/useAgentTelemetry', () => ({
|
||||
useAgentTelemetry: () => ({
|
||||
trackClickedNewAgent,
|
||||
}),
|
||||
}));
|
||||
|
||||
const renderComponent = createComponentRenderer(EmptyStateLayout, {
|
||||
pinia: createTestingPinia(),
|
||||
global: {
|
||||
|
|
@ -82,6 +90,7 @@ describe('EmptyStateLayout', () => {
|
|||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
email: 'john@example.com',
|
||||
globalScopes: ['agent:create'],
|
||||
isDefaultUser: false,
|
||||
isPendingUser: false,
|
||||
mfaEnabled: false,
|
||||
|
|
@ -101,6 +110,9 @@ describe('EmptyStateLayout', () => {
|
|||
|
||||
bannersStore.bannersHeight = 0;
|
||||
readyToRunStore.userCanClaimOpenAiCredits = false;
|
||||
vi.spyOn(useSettingsStore(), 'isModuleActive').mockImplementation((moduleName) => {
|
||||
return moduleName === 'agents';
|
||||
});
|
||||
surfaceMcpEmptyState.showTile = false;
|
||||
surfaceMcpEmptyState.showReminder = false;
|
||||
|
||||
|
|
@ -229,6 +241,14 @@ describe('EmptyStateLayout', () => {
|
|||
|
||||
expect(emitted('click:add')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should track New Agent card clicks', async () => {
|
||||
const { getByTestId } = renderComponent();
|
||||
|
||||
await userEvent.click(getByTestId('build-agent-card'));
|
||||
|
||||
expect(trackClickedNewAgent).toHaveBeenCalledWith('card');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when in read-only environment', () => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { N8nButton, N8nCard, N8nHeading, N8nIcon, N8nText } from '@n8n/design-system';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { useBannersStore } from '@/features/shared/banners/banners.store';
|
||||
|
|
@ -15,6 +15,10 @@ import RecommendedTemplatesSection from '@/features/workflows/templates/recommen
|
|||
import ReadyToRunButton from '@/features/workflows/readyToRun/components/ReadyToRunButton.vue';
|
||||
import EmptyStateBuilderPrompt from '@/experiments/emptyStateBuilderPrompt/components/EmptyStateBuilderPrompt.vue';
|
||||
import AppSelectionPage from '@/experiments/credentialsAppSelection/components/AppSelectionPage.vue';
|
||||
import { useSettingsStore } from '@/app/stores/settings.store';
|
||||
import { NEW_AGENT_VIEW } from '@/features/agents/constants';
|
||||
import { useAgentTelemetry } from '@/features/agents/composables/useAgentTelemetry';
|
||||
import { useAgentPermissions } from '@/features/agents/composables/useAgentPermissions';
|
||||
import SurfaceMcpEmptyStateReminder from '@/experiments/surfaceMcpToNewCloudUsers/components/SurfaceMcpEmptyStateReminder.vue';
|
||||
import SurfaceMcpEmptyStateTile from '@/experiments/surfaceMcpToNewCloudUsers/components/SurfaceMcpEmptyStateTile.vue';
|
||||
|
||||
|
|
@ -24,12 +28,15 @@ const emit = defineEmits<{
|
|||
|
||||
const i18n = useI18n();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const bannersStore = useBannersStore();
|
||||
const projectsStore = useProjectsStore();
|
||||
const projectPages = useProjectPages();
|
||||
const emptyStateBuilderPromptStore = useEmptyStateBuilderPromptStore();
|
||||
const credentialsAppSelectionStore = useCredentialsAppSelectionStore();
|
||||
const readyToRunStore = useReadyToRunStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
const agentTelemetry = useAgentTelemetry();
|
||||
|
||||
const {
|
||||
showAppSelection,
|
||||
|
|
@ -57,6 +64,17 @@ const showReadyToRunCard = computed(() => {
|
|||
return readyToRunStore.userCanClaimOpenAiCredits && canCreateWorkflow.value && !showMcpTile.value;
|
||||
});
|
||||
|
||||
const builderProjectId = computed(() =>
|
||||
projectPages.isOverviewSubPage
|
||||
? projectsStore.personalProject?.id
|
||||
: (route.params.projectId as string),
|
||||
);
|
||||
const { canCreate } = useAgentPermissions(builderProjectId);
|
||||
|
||||
const showBuildAgentCard = computed(() => {
|
||||
return settingsStore.isModuleActive('agents') && canCreate.value;
|
||||
});
|
||||
|
||||
const handleReadyToRunClick = async () => {
|
||||
try {
|
||||
await readyToRunStore.claimCreditsAndOpenWorkflow(
|
||||
|
|
@ -69,16 +87,20 @@ const handleReadyToRunClick = async () => {
|
|||
}
|
||||
};
|
||||
|
||||
const handleBuildAgentClick = () => {
|
||||
agentTelemetry.trackClickedNewAgent('card');
|
||||
void router.push({
|
||||
name: NEW_AGENT_VIEW,
|
||||
query: {
|
||||
projectId: builderProjectId.value,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const containerStyle = computed(() => ({
|
||||
minHeight: `calc(100vh - ${bannersStore.bannersHeight}px)`,
|
||||
}));
|
||||
|
||||
const builderProjectId = computed(() =>
|
||||
projectPages.isOverviewSubPage
|
||||
? projectsStore.personalProject?.id
|
||||
: (route.params.projectId as string),
|
||||
);
|
||||
|
||||
const builderParentFolderId = computed(() => route.params.folderId as string | undefined);
|
||||
|
||||
const handleBuilderPromptSubmit = async (prompt: string) => {
|
||||
|
|
@ -123,8 +145,10 @@ const handleAppSelectionContinue = () => {
|
|||
data-test-id="empty-state-builder-prompt"
|
||||
:project-id="builderProjectId"
|
||||
:parent-folder-id="builderParentFolderId"
|
||||
:show-build-agent-button="showBuildAgentCard"
|
||||
@submit="handleBuilderPromptSubmit"
|
||||
@start-from-scratch="addWorkflow"
|
||||
@build-agent="handleBuildAgentClick"
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
|
@ -146,13 +170,23 @@ const handleAppSelectionContinue = () => {
|
|||
<div :class="$style.actionButtons">
|
||||
<ReadyToRunButton type="secondary" size="large" />
|
||||
<N8nButton
|
||||
v-if="showBuildAgentCard"
|
||||
variant="subtle"
|
||||
icon="file"
|
||||
icon="robot"
|
||||
size="large"
|
||||
data-test-id="build-agent-button"
|
||||
@click="handleBuildAgentClick"
|
||||
>
|
||||
{{ i18n.baseText('workflows.empty.buildAgent') }}
|
||||
</N8nButton>
|
||||
<N8nButton
|
||||
variant="subtle"
|
||||
icon="workflow"
|
||||
size="large"
|
||||
data-test-id="start-from-scratch-button"
|
||||
@click="addWorkflow"
|
||||
>
|
||||
{{ i18n.baseText('workflows.empty.startFromScratch') }}
|
||||
{{ i18n.baseText('workflows.empty.buildWorkflow') }}
|
||||
</N8nButton>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -160,22 +194,33 @@ const handleAppSelectionContinue = () => {
|
|||
|
||||
<!-- State 3: Fallback (Baseline) -->
|
||||
<template v-else>
|
||||
<N8nHeading tag="h1" size="2xlarge" bold :class="$style.welcomeTitle">
|
||||
<N8nHeading
|
||||
tag="h1"
|
||||
size="2xlarge"
|
||||
bold
|
||||
:class="[$style.welcomeTitle, $style.fallbackHeading]"
|
||||
>
|
||||
{{ emptyStateHeading }}
|
||||
</N8nHeading>
|
||||
<div :class="$style.fallbackContent">
|
||||
<N8nText tag="p" size="large" color="text-base">
|
||||
<N8nText
|
||||
v-if="!canCreateWorkflow"
|
||||
tag="p"
|
||||
size="large"
|
||||
color="text-base"
|
||||
:class="$style.fallbackDescription"
|
||||
>
|
||||
{{ emptyStateDescription }}
|
||||
</N8nText>
|
||||
<SurfaceMcpEmptyStateReminder v-if="showMcpReminder" />
|
||||
|
||||
<!-- Two cards or single card depending on ready-to-run availability -->
|
||||
<!-- Cards vary based on enabled modules and ready-to-run availability -->
|
||||
<div
|
||||
v-if="canCreateWorkflow"
|
||||
:class="[
|
||||
$style.actionCardsContainer,
|
||||
{
|
||||
[$style.singleCard]: !showReadyToRunCard && !showMcpTile,
|
||||
[$style.singleCard]: !showReadyToRunCard && !showMcpTile && !showBuildAgentCard,
|
||||
},
|
||||
]"
|
||||
>
|
||||
|
|
@ -202,7 +247,28 @@ const handleAppSelectionContinue = () => {
|
|||
</div>
|
||||
</N8nCard>
|
||||
|
||||
<!-- Card 2: Start from scratch (always shown) -->
|
||||
<!-- Card 2: Build an agent (conditional) -->
|
||||
<N8nCard
|
||||
v-if="showBuildAgentCard"
|
||||
:class="$style.actionCard"
|
||||
hoverable
|
||||
data-test-id="build-agent-card"
|
||||
@click="handleBuildAgentClick"
|
||||
>
|
||||
<div :class="$style.cardContent">
|
||||
<N8nIcon
|
||||
:class="$style.cardIcon"
|
||||
icon="robot"
|
||||
color="foreground-dark"
|
||||
:stroke-width="1.5"
|
||||
/>
|
||||
<N8nText size="large" class="mt-xs">
|
||||
{{ i18n.baseText('workflows.empty.buildAgent') }}
|
||||
</N8nText>
|
||||
</div>
|
||||
</N8nCard>
|
||||
|
||||
<!-- Card 3: Start from scratch (always shown) -->
|
||||
<N8nCard
|
||||
:class="$style.actionCard"
|
||||
hoverable
|
||||
|
|
@ -212,12 +278,12 @@ const handleAppSelectionContinue = () => {
|
|||
<div :class="$style.cardContent">
|
||||
<N8nIcon
|
||||
:class="$style.cardIcon"
|
||||
icon="file"
|
||||
icon="workflow"
|
||||
color="foreground-dark"
|
||||
:stroke-width="1.5"
|
||||
/>
|
||||
<N8nText size="large" class="mt-xs">
|
||||
{{ i18n.baseText('workflows.empty.startFromScratch') }}
|
||||
{{ i18n.baseText('workflows.empty.buildWorkflow') }}
|
||||
</N8nText>
|
||||
</div>
|
||||
</N8nCard>
|
||||
|
|
@ -234,8 +300,12 @@ const handleAppSelectionContinue = () => {
|
|||
.emptyStateLayout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
height: 100%;
|
||||
padding: var(--spacing--4xl) var(--spacing--2xl) 0;
|
||||
max-width: var(--content-container--width);
|
||||
width: 100%;
|
||||
|
||||
@media (max-width: vars.$breakpoint-lg) {
|
||||
padding: var(--spacing--xl) var(--spacing--xs) 0;
|
||||
|
|
@ -271,7 +341,7 @@ const handleAppSelectionContinue = () => {
|
|||
}
|
||||
|
||||
.welcomeTitle {
|
||||
margin-bottom: var(--spacing--2xl);
|
||||
margin-bottom: var(--spacing--sm);
|
||||
}
|
||||
|
||||
.templatesSection {
|
||||
|
|
@ -283,37 +353,46 @@ const handleAppSelectionContinue = () => {
|
|||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.fallbackDescription {
|
||||
animation: contentDropIn var(--duration--base) var(--easing--ease-out)
|
||||
calc(var(--duration--base) / 4) both;
|
||||
}
|
||||
|
||||
.fallbackHeading {
|
||||
animation: headingLift var(--duration--base) var(--easing--ease-out) both;
|
||||
}
|
||||
|
||||
.actionCardsContainer {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 192px);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing--lg);
|
||||
margin-top: var(--spacing--2xl);
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
|
||||
&.singleCard {
|
||||
grid-template-columns: 192px;
|
||||
max-width: 192px;
|
||||
}
|
||||
|
||||
@media (max-width: vars.$breakpoint-xs) {
|
||||
grid-template-columns: 1fr;
|
||||
gap: var(--spacing--md);
|
||||
margin-top: var(--spacing--lg);
|
||||
}
|
||||
}
|
||||
|
||||
.actionCard {
|
||||
position: relative;
|
||||
width: 192px;
|
||||
height: 230px;
|
||||
--action-card-index: 0;
|
||||
|
||||
max-width: 192px;
|
||||
aspect-ratio: 4/3;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition:
|
||||
transform 0.2s ease,
|
||||
box-shadow 0.2s ease;
|
||||
animation: actionCardIn var(--duration--base) var(--easing--ease-out)
|
||||
calc(160ms + var(--action-card-index) * 80ms) both;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
|
|
@ -323,6 +402,62 @@ const handleAppSelectionContinue = () => {
|
|||
color: var(--color--primary);
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
--action-card-index: 1;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
--action-card-index: 2;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes headingLift {
|
||||
from {
|
||||
opacity: 0;
|
||||
filter: blur(3px);
|
||||
transform: translateY(var(--spacing--xs));
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
filter: blur(0);
|
||||
transform: translateY(calc(-1 * var(--spacing--xs)));
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes contentDropIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
filter: blur(3px);
|
||||
transform: translateY(calc(-1 * var(--spacing--xs)));
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
filter: blur(0);
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes actionCardIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(var(--spacing--2xs));
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.fallbackHeading,
|
||||
.fallbackDescription,
|
||||
.actionCard {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
.cardContent {
|
||||
|
|
@ -330,7 +465,7 @@ const handleAppSelectionContinue = () => {
|
|||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--spacing--md);
|
||||
padding: var(--spacing--sm);
|
||||
}
|
||||
|
||||
.cardIcon {
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ describe('WorkflowsView', () => {
|
|||
it('for non setup user', async () => {
|
||||
const { getByText } = renderComponent({ pinia });
|
||||
await waitAllPromises();
|
||||
expect(getByText('👋 Welcome!')).toBeVisible();
|
||||
expect(getByText('What do you want to build?')).toBeVisible();
|
||||
});
|
||||
|
||||
it('for currentUser user', async () => {
|
||||
|
|
@ -135,7 +135,7 @@ describe('WorkflowsView', () => {
|
|||
const { getByText } = renderComponent({ pinia });
|
||||
await waitAllPromises();
|
||||
|
||||
expect(getByText('👋 Welcome, John!')).toBeVisible();
|
||||
expect(getByText('What do you want to build, John')).toBeVisible();
|
||||
});
|
||||
|
||||
describe('when onboardingExperiment -> False', () => {
|
||||
|
|
@ -160,9 +160,9 @@ describe('WorkflowsView', () => {
|
|||
const projectsStore = mockedStore(useProjectsStore);
|
||||
projectsStore.currentProject = { scopes: ['workflow:create'] } as Project;
|
||||
|
||||
const { getByText } = renderComponent({ pinia });
|
||||
const { getByRole } = renderComponent({ pinia });
|
||||
await waitAllPromises();
|
||||
expect(getByText('Create your first workflow')).toBeInTheDocument();
|
||||
expect(getByRole('heading', { name: 'What do you want to build?' })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { useEmptyStateBuilderPromptStore } from '../stores/emptyStateBuilderProm
|
|||
const props = defineProps<{
|
||||
projectId?: string;
|
||||
parentFolderId?: string;
|
||||
showBuildAgentButton?: boolean;
|
||||
}>();
|
||||
|
||||
const router = useRouter();
|
||||
|
|
@ -26,6 +27,7 @@ const emptyStateBuilderPromptStore = useEmptyStateBuilderPromptStore();
|
|||
const emit = defineEmits<{
|
||||
submit: [prompt: string];
|
||||
startFromScratch: [];
|
||||
buildAgent: [];
|
||||
}>();
|
||||
|
||||
const textInputValue = ref<string>('');
|
||||
|
|
@ -59,6 +61,10 @@ function onImportFromFile() {
|
|||
importFileRef.value?.click();
|
||||
}
|
||||
|
||||
function onBuildAgent() {
|
||||
emit('buildAgent');
|
||||
}
|
||||
|
||||
function handleFileImport() {
|
||||
const input = importFileRef.value;
|
||||
if (!input?.files?.length) return;
|
||||
|
|
@ -136,6 +142,16 @@ function handleFileImport() {
|
|||
{{ i18n.baseText('emptyStateBuilderPrompt.fromScratch') }}
|
||||
</N8nButton>
|
||||
</N8nTooltip>
|
||||
<N8nButton
|
||||
v-if="props.showBuildAgentButton"
|
||||
variant="subtle"
|
||||
size="small"
|
||||
icon="robot"
|
||||
data-test-id="build-agent-button"
|
||||
@click="onBuildAgent"
|
||||
>
|
||||
{{ i18n.baseText('workflows.empty.buildAgent') }}
|
||||
</N8nButton>
|
||||
<N8nTooltip :content="i18n.baseText('emptyStateBuilderPrompt.templateTooltip')">
|
||||
<N8nButton variant="subtle" size="small" icon="layout-template" @click="onTemplate">
|
||||
{{ i18n.baseText('emptyStateBuilderPrompt.template') }}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,14 @@ describe('useAgentTelemetry', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('trackClickedNewAgent tracks card source', () => {
|
||||
useAgentTelemetry().trackClickedNewAgent('card');
|
||||
expect(trackMock).toHaveBeenCalledWith('User clicked new agent', {
|
||||
source: 'card',
|
||||
session_id: 'session-xyz',
|
||||
});
|
||||
});
|
||||
|
||||
it('trackSubmittedMessage includes mode, status, agent_config (no raw message)', () => {
|
||||
const fingerprint = {
|
||||
instructions: 'hello',
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useRootStore } from '@n8n/stores/useRootStore';
|
|||
import type { AgentConfigFingerprint, AgentTelemetryStatus } from './agentTelemetry.utils';
|
||||
|
||||
export type AgentChatMode = 'build' | 'test';
|
||||
export type AgentCreateSource = 'button' | 'dropdown' | 'card';
|
||||
export type AgentConfigPart =
|
||||
| 'instructions'
|
||||
| 'model'
|
||||
|
|
@ -31,7 +32,7 @@ export function useAgentTelemetry() {
|
|||
}
|
||||
}
|
||||
|
||||
function trackClickedNewAgent(source: 'button' | 'dropdown') {
|
||||
function trackClickedNewAgent(source: AgentCreateSource) {
|
||||
safeTrack('User clicked new agent', { source, ...common() });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { useProjectPages } from '@/features/collaboration/projects/composables/u
|
|||
import { useDocumentTitle } from '@/app/composables/useDocumentTitle';
|
||||
import { listAgents } from '../composables/useAgentApi';
|
||||
import { useAgentPermissions } from '../composables/useAgentPermissions';
|
||||
import { useAgentTelemetry } from '../composables/useAgentTelemetry';
|
||||
import type { AgentResource } from '../types';
|
||||
import { AGENT_BUILDER_VIEW, NEW_AGENT_VIEW } from '../constants';
|
||||
import AgentCard from '../components/AgentCard.vue';
|
||||
|
|
@ -26,6 +27,7 @@ const rootStore = useRootStore();
|
|||
const projectsStore = useProjectsStore();
|
||||
const insightsStore = useInsightsStore();
|
||||
const projectPages = useProjectPages();
|
||||
const agentTelemetry = useAgentTelemetry();
|
||||
|
||||
const homeProject = computed(() => projectsStore.currentProject ?? projectsStore.personalProject);
|
||||
|
||||
|
|
@ -76,6 +78,7 @@ function onAgentDeleted(agentId: string) {
|
|||
}
|
||||
|
||||
function onCreateAgentClick() {
|
||||
agentTelemetry.trackClickedNewAgent('button');
|
||||
void router.push({ name: NEW_AGENT_VIEW, query: { projectId: projectId.value } });
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user