mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-31 08:46:58 +02:00
fix(editor): Automatically close credential modal after claiming free OpenAI credits (#30610)
This commit is contained in:
parent
f5cc969c7c
commit
05c25da010
|
|
@ -11,6 +11,7 @@ import { useToast } from '@/app/composables/useToast';
|
|||
import { renderComponent } from '@/__tests__/render';
|
||||
import { mockedStore } from '@/__tests__/utils';
|
||||
import { useTelemetry } from '@/app/composables/useTelemetry';
|
||||
import { useFreeAiCredits } from '@/app/composables/useFreeAiCredits';
|
||||
|
||||
vi.mock('@/app/composables/useToast', () => ({
|
||||
useToast: vi.fn(),
|
||||
|
|
@ -111,6 +112,8 @@ describe('FreeAiCreditsCallout', () => {
|
|||
(useTelemetry as any).mockReturnValue({
|
||||
track: vi.fn(),
|
||||
});
|
||||
|
||||
useFreeAiCredits().showSuccessCallout.value = false;
|
||||
});
|
||||
|
||||
it('should shows the claim callout when the user can claim credits', () => {
|
||||
|
|
@ -191,6 +194,44 @@ describe('FreeAiCreditsCallout', () => {
|
|||
assertUserCannotClaimCredits();
|
||||
});
|
||||
|
||||
it('should emit "claimed" after a successful claim', async () => {
|
||||
const { emitted } = renderComponent(FreeAiCreditsCallout);
|
||||
|
||||
await fireEvent.click(screen.getByRole('button', { name: 'Claim credits' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(emitted()).toHaveProperty('claimed');
|
||||
});
|
||||
});
|
||||
|
||||
it('should not emit "claimed" when the claim fails', async () => {
|
||||
const credentialsStore = mockedStore(useCredentialsStore);
|
||||
credentialsStore.claimFreeAiCredits.mockRejectedValueOnce(new Error('boom'));
|
||||
|
||||
const { emitted } = renderComponent(FreeAiCreditsCallout);
|
||||
|
||||
await fireEvent.click(screen.getByRole('button', { name: 'Claim credits' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(credentialsStore.claimFreeAiCredits).toHaveBeenCalled();
|
||||
});
|
||||
expect(emitted()).not.toHaveProperty('claimed');
|
||||
});
|
||||
|
||||
it('should use the telemetrySource prop value when tracking the claim', async () => {
|
||||
renderComponent(FreeAiCreditsCallout, {
|
||||
props: { telemetrySource: 'instanceAiWorkflowSetup' },
|
||||
});
|
||||
|
||||
await fireEvent.click(screen.getByRole('button', { name: 'Claim credits' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(useTelemetry().track).toHaveBeenCalledWith('User claimed OpenAI credits', {
|
||||
source: 'instanceAiWorkflowSetup',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should not be able to claim credits if active node it is not a valid node', async () => {
|
||||
(useNDVStore as any).mockReturnValue({
|
||||
activeNode: { type: '@n8n/n8n-nodes.jira' },
|
||||
|
|
|
|||
|
|
@ -2,15 +2,20 @@
|
|||
import { useI18n } from '@n8n/i18n';
|
||||
import { useNDVStore } from '@/features/ndv/shared/ndv.store';
|
||||
import { useFreeAiCredits } from '@/app/composables/useFreeAiCredits';
|
||||
import { computed, ref } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
import { OPEN_AI_API_CREDENTIAL_TYPE } from 'n8n-workflow';
|
||||
import { N8nButton, N8nCallout, N8nText } from '@n8n/design-system';
|
||||
|
||||
type Props = {
|
||||
credentialTypeName?: string;
|
||||
telemetrySource?: 'freeAiCreditsCallout' | 'instanceAiWorkflowSetup';
|
||||
};
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
telemetrySource: 'freeAiCreditsCallout',
|
||||
});
|
||||
|
||||
const emit = defineEmits<{ claimed: [] }>();
|
||||
|
||||
const LANGCHAIN_NODES_PREFIX = '@n8n/n8n-nodes-langchain.';
|
||||
const N8N_NODES_PREFIX = '@n8n/n8n-nodes.';
|
||||
|
|
@ -22,13 +27,16 @@ const NODES_WITH_OPEN_AI_API_CREDENTIAL = [
|
|||
`${N8N_NODES_PREFIX}openAi`,
|
||||
];
|
||||
|
||||
const showSuccessCallout = ref(false);
|
||||
|
||||
const ndvStore = useNDVStore();
|
||||
const i18n = useI18n();
|
||||
|
||||
const { aiCreditsQuota, userCanClaimOpenAiCredits, claimingCredits, claimCredits } =
|
||||
useFreeAiCredits();
|
||||
const {
|
||||
aiCreditsQuota,
|
||||
userCanClaimOpenAiCredits,
|
||||
claimingCredits,
|
||||
showSuccessCallout,
|
||||
claimCredits,
|
||||
} = useFreeAiCredits();
|
||||
|
||||
const isEditingOpenAiCredential = computed(
|
||||
() => props.credentialTypeName && props.credentialTypeName === OPEN_AI_API_CREDENTIAL_TYPE,
|
||||
|
|
@ -40,23 +48,24 @@ const activeNodeHasOpenAiApiCredential = computed(
|
|||
NODES_WITH_OPEN_AI_API_CREDENTIAL.includes(ndvStore.activeNode.type),
|
||||
);
|
||||
|
||||
const showCallout = computed(() => {
|
||||
return (
|
||||
userCanClaimOpenAiCredits.value &&
|
||||
(activeNodeHasOpenAiApiCredential.value || isEditingOpenAiCredential.value)
|
||||
);
|
||||
});
|
||||
const isRelevantContext = computed(
|
||||
() => activeNodeHasOpenAiApiCredential.value || isEditingOpenAiCredential.value,
|
||||
);
|
||||
|
||||
const showCallout = computed(() => userCanClaimOpenAiCredits.value && isRelevantContext.value);
|
||||
|
||||
const showSuccess = computed(() => showSuccessCallout.value && isRelevantContext.value);
|
||||
|
||||
const onClaimCreditsClicked = async () => {
|
||||
const success = await claimCredits('freeAiCreditsCallout');
|
||||
const success = await claimCredits(props.telemetrySource);
|
||||
if (success) {
|
||||
showSuccessCallout.value = true;
|
||||
emit('claimed');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<N8nCallout
|
||||
v-if="showCallout && !showSuccessCallout"
|
||||
v-if="showCallout && !showSuccess"
|
||||
theme="secondary"
|
||||
icon="circle-alert"
|
||||
class="mt-xs"
|
||||
|
|
@ -76,7 +85,7 @@ const onClaimCreditsClicked = async () => {
|
|||
/>
|
||||
</template>
|
||||
</N8nCallout>
|
||||
<N8nCallout v-else-if="showSuccessCallout" theme="success" icon="circle-check" class="mt-xs">
|
||||
<N8nCallout v-else-if="showSuccess" theme="success" icon="circle-check" class="mt-xs">
|
||||
<N8nText size="small">
|
||||
{{
|
||||
i18n.baseText('freeAi.credits.callout.success.title.part1', {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import { useTelemetry } from '@/app/composables/useTelemetry';
|
|||
import { useToast } from '@/app/composables/useToast';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
|
||||
const showSuccessCallout = ref(false);
|
||||
|
||||
export function useFreeAiCredits() {
|
||||
const credentialsStore = useCredentialsStore();
|
||||
const projectsStore = useProjectsStore();
|
||||
|
|
@ -42,7 +44,7 @@ export function useFreeAiCredits() {
|
|||
);
|
||||
|
||||
async function claimCredits(
|
||||
source: 'chatHubAutoClaim' | 'freeAiCreditsCallout',
|
||||
source: 'chatHubAutoClaim' | 'freeAiCreditsCallout' | 'instanceAiWorkflowSetup',
|
||||
): Promise<boolean> {
|
||||
if (!userCanClaimOpenAiCredits.value) {
|
||||
return false;
|
||||
|
|
@ -57,6 +59,7 @@ export function useFreeAiCredits() {
|
|||
usersStore.currentUser.settings.userClaimedAiCredits = true;
|
||||
}
|
||||
|
||||
showSuccessCallout.value = true;
|
||||
telemetry.track('User claimed OpenAI credits', { source });
|
||||
|
||||
return true;
|
||||
|
|
@ -75,6 +78,7 @@ export function useFreeAiCredits() {
|
|||
aiCreditsQuota,
|
||||
userCanClaimOpenAiCredits,
|
||||
claimingCredits,
|
||||
showSuccessCallout,
|
||||
claimCredits,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,10 @@ vi.mock('@/features/ndv/parameters/components/ParameterInputList.vue', () => ({
|
|||
default: { template: '<div />' },
|
||||
}));
|
||||
|
||||
vi.mock('@/app/components/FreeAiCreditsCallout.vue', () => ({
|
||||
default: { template: '<div />' },
|
||||
}));
|
||||
|
||||
vi.mock('@n8n/i18n', async (importOriginal) => ({
|
||||
...(await importOriginal()),
|
||||
useI18n: () => ({
|
||||
|
|
|
|||
|
|
@ -47,6 +47,10 @@ vi.mock('@/features/credentials/components/NodeCredentials.vue', () => ({
|
|||
},
|
||||
}));
|
||||
|
||||
vi.mock('@/app/components/FreeAiCreditsCallout.vue', () => ({
|
||||
default: { template: '<div />' },
|
||||
}));
|
||||
|
||||
vi.mock('@/features/ndv/parameters/components/ParameterInputList.vue', async () => {
|
||||
const { defineComponent, h } = await import('vue');
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { computed, provide, ref, watch } from 'vue';
|
|||
import { N8nText, N8nTooltip } from '@n8n/design-system';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import NodeCredentials from '@/features/credentials/components/NodeCredentials.vue';
|
||||
import FreeAiCreditsCallout from '@/app/components/FreeAiCreditsCallout.vue';
|
||||
import ParameterInputList from '@/features/ndv/parameters/components/ParameterInputList.vue';
|
||||
import { useCredentialsStore } from '@/features/credentials/credentials.store';
|
||||
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
|
||||
|
|
@ -136,6 +137,12 @@ function onParameterValueChanged(update: IUpdateInformation) {
|
|||
|
||||
<template>
|
||||
<div :class="$style.body">
|
||||
<FreeAiCreditsCallout
|
||||
v-if="credentialType"
|
||||
:credential-type-name="credentialType"
|
||||
telemetry-source="instanceAiWorkflowSetup"
|
||||
/>
|
||||
|
||||
<NodeCredentials
|
||||
v-if="credentialType"
|
||||
:node="displayNode"
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ const emit = defineEmits<{
|
|||
retest: [];
|
||||
oauth: [];
|
||||
quickConnect: [];
|
||||
claimed: [];
|
||||
'update:isResolvable': [value: boolean];
|
||||
}>();
|
||||
|
||||
|
|
@ -281,7 +282,10 @@ watch(showOAuthSuccessBanner, (newValue, oldValue) => {
|
|||
</N8nCallout>
|
||||
<div v-else>
|
||||
<div :class="$style.config" data-test-id="node-credentials-config-container">
|
||||
<FreeAiCreditsCallout :credential-type-name="credentialType?.name" />
|
||||
<FreeAiCreditsCallout
|
||||
:credential-type-name="credentialType?.name"
|
||||
@claimed="$emit('claimed')"
|
||||
/>
|
||||
|
||||
<CredentialModeSelector
|
||||
v-if="canWrite"
|
||||
|
|
|
|||
|
|
@ -1508,6 +1508,7 @@ const { width } = useElementSize(credNameRef);
|
|||
@retest="retestCredential"
|
||||
@scroll-to-top="scrollToTop"
|
||||
@auth-type-changed="onAuthTypeChanged"
|
||||
@claimed="closeDialog"
|
||||
@update:is-resolvable="onResolvableChange"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user