-
Notifications
You must be signed in to change notification settings - Fork 8.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Nudge users to become template creators if eligible (#8357)
- Loading branch information
Showing
14 changed files
with
385 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
//#region Getters | ||
|
||
export const getBecomeTemplateCreatorCta = () => cy.getByTestId('become-template-creator-cta'); | ||
|
||
export const getCloseBecomeTemplateCreatorCtaButton = () => | ||
cy.getByTestId('close-become-template-creator-cta'); | ||
|
||
//#endregion | ||
|
||
//#region Actions | ||
|
||
export const interceptCtaRequestWithResponse = (becomeCreator: boolean) => { | ||
return cy.intercept('GET', `/rest/cta/become-creator`, { | ||
body: becomeCreator, | ||
}); | ||
}; | ||
|
||
//#endregion |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import { | ||
getBecomeTemplateCreatorCta, | ||
getCloseBecomeTemplateCreatorCtaButton, | ||
interceptCtaRequestWithResponse, | ||
} from '../composables/becomeTemplateCreatorCta'; | ||
import { WorkflowsPage as WorkflowsPageClass } from '../pages/workflows'; | ||
|
||
const WorkflowsPage = new WorkflowsPageClass(); | ||
|
||
describe('Become creator CTA', () => { | ||
it('should not show the CTA if user is not eligible', () => { | ||
interceptCtaRequestWithResponse(false).as('cta'); | ||
cy.visit(WorkflowsPage.url); | ||
|
||
cy.wait('@cta'); | ||
|
||
getBecomeTemplateCreatorCta().should('not.exist'); | ||
}); | ||
|
||
it('should show the CTA if the user is eligible', () => { | ||
interceptCtaRequestWithResponse(true).as('cta'); | ||
cy.visit(WorkflowsPage.url); | ||
|
||
cy.wait('@cta'); | ||
|
||
getBecomeTemplateCreatorCta().should('be.visible'); | ||
|
||
getCloseBecomeTemplateCreatorCtaButton().click(); | ||
|
||
getBecomeTemplateCreatorCta().should('not.exist'); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import express from 'express'; | ||
import { Authorized, Get, RestController } from '@/decorators'; | ||
import { AuthenticatedRequest } from '@/requests'; | ||
import { CtaService } from '@/services/cta.service'; | ||
|
||
/** | ||
* Controller for Call to Action (CTA) endpoints. CTAs are certain | ||
* messages that are shown to users in the UI. | ||
*/ | ||
@Authorized() | ||
@RestController('/cta') | ||
export class CtaController { | ||
constructor(private readonly ctaService: CtaService) {} | ||
|
||
@Get('/become-creator') | ||
async getCta(req: AuthenticatedRequest, res: express.Response) { | ||
const becomeCreator = await this.ctaService.getBecomeCreatorCta(req.user.id); | ||
|
||
res.json(becomeCreator); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { Service } from 'typedi'; | ||
import { WorkflowStatisticsRepository } from '@/databases/repositories/workflowStatistics.repository'; | ||
import type { User } from '@/databases/entities/User'; | ||
|
||
@Service() | ||
export class CtaService { | ||
constructor(private readonly workflowStatisticsRepository: WorkflowStatisticsRepository) {} | ||
|
||
async getBecomeCreatorCta(userId: User['id']) { | ||
// There need to be at least 3 workflows with at least 5 executions | ||
const numWfsWithOver5ProdExecutions = | ||
await this.workflowStatisticsRepository.queryNumWorkflowsUserHasWithFiveOrMoreProdExecs( | ||
userId, | ||
); | ||
|
||
return numWfsWithOver5ProdExecutions >= 3; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import Container from 'typedi'; | ||
import * as testDb from './shared/testDb'; | ||
import { CtaService } from '@/services/cta.service'; | ||
import { createUser } from './shared/db/users'; | ||
import { createManyWorkflows } from './shared/db/workflows'; | ||
import type { User } from '@/databases/entities/User'; | ||
import { createWorkflowStatisticsItem } from './shared/db/workflowStatistics'; | ||
import { StatisticsNames } from '@/databases/entities/WorkflowStatistics'; | ||
|
||
describe('CtaService', () => { | ||
let ctaService: CtaService; | ||
let user: User; | ||
|
||
beforeAll(async () => { | ||
await testDb.init(); | ||
|
||
ctaService = Container.get(CtaService); | ||
user = await createUser(); | ||
}); | ||
|
||
afterAll(async () => { | ||
await testDb.terminate(); | ||
}); | ||
|
||
describe('getBecomeCreatorCta()', () => { | ||
afterEach(async () => { | ||
await testDb.truncate(['Workflow', 'SharedWorkflow']); | ||
}); | ||
|
||
test.each([ | ||
[false, 0, 0], | ||
[false, 2, 5], | ||
[false, 3, 4], | ||
[true, 3, 5], | ||
])( | ||
'should return %p if user has %d active workflows with %d successful production executions', | ||
async (expected, numWorkflows, numExecutions) => { | ||
const workflows = await createManyWorkflows(numWorkflows, { active: true }, user); | ||
|
||
await Promise.all( | ||
workflows.map( | ||
async (workflow) => | ||
await createWorkflowStatisticsItem(workflow.id, { | ||
count: numExecutions, | ||
name: StatisticsNames.productionSuccess, | ||
}), | ||
), | ||
); | ||
|
||
expect(await ctaService.getBecomeCreatorCta(user.id)).toBe(expected); | ||
}, | ||
); | ||
}); | ||
}); |
21 changes: 21 additions & 0 deletions
21
packages/cli/test/integration/shared/db/workflowStatistics.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import Container from 'typedi'; | ||
import { StatisticsNames, type WorkflowStatistics } from '@/databases/entities/WorkflowStatistics'; | ||
import type { Workflow } from 'n8n-workflow'; | ||
import { WorkflowStatisticsRepository } from '@/databases/repositories/workflowStatistics.repository'; | ||
|
||
export async function createWorkflowStatisticsItem( | ||
workflowId: Workflow['id'], | ||
data?: Partial<WorkflowStatistics>, | ||
) { | ||
const entity = Container.get(WorkflowStatisticsRepository).create({ | ||
count: 0, | ||
latestEvent: new Date().toISOString(), | ||
name: StatisticsNames.manualSuccess, | ||
...(data ?? {}), | ||
workflowId, | ||
}); | ||
|
||
await Container.get(WorkflowStatisticsRepository).insert(entity); | ||
|
||
return entity; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import type { IRestApiContext } from '@/Interface'; | ||
import { get } from '@/utils/apiUtils'; | ||
|
||
export async function getBecomeCreatorCta(context: IRestApiContext): Promise<boolean> { | ||
const response = await get(context.baseUrl, '/cta/become-creator'); | ||
|
||
return response; | ||
} |
78 changes: 78 additions & 0 deletions
78
packages/editor-ui/src/components/BecomeTemplateCreatorCta/BecomeTemplateCreatorCta.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
<script setup lang="ts"> | ||
import { useBecomeTemplateCreatorStore } from './becomeTemplateCreatorStore'; | ||
import { useI18n } from '@/composables/useI18n'; | ||
const i18n = useI18n(); | ||
const store = useBecomeTemplateCreatorStore(); | ||
</script> | ||
|
||
<template> | ||
<div | ||
v-if="store.showBecomeCreatorCta" | ||
:class="$style.container" | ||
data-test-id="become-template-creator-cta" | ||
> | ||
<div :class="$style.textAndCloseButton"> | ||
<p :class="$style.text"> | ||
{{ i18n.baseText('becomeCreator.text') }} | ||
</p> | ||
|
||
<button | ||
:class="$style.closeButton" | ||
data-test-id="close-become-template-creator-cta" | ||
@click="store.dismissCta()" | ||
> | ||
<n8n-icon icon="times" size="xsmall" :title="i18n.baseText('generic.close')" /> | ||
</button> | ||
</div> | ||
|
||
<n8n-button | ||
:class="$style.becomeCreatorButton" | ||
:label="i18n.baseText('becomeCreator.buttonText')" | ||
size="xmini" | ||
type="secondary" | ||
element="a" | ||
href="https://creators.n8n.io/hub" | ||
target="_blank" | ||
/> | ||
</div> | ||
</template> | ||
|
||
<style module lang="scss"> | ||
.container { | ||
display: flex; | ||
flex-direction: column; | ||
background-color: var(--color-background-light); | ||
border: var(--border-base); | ||
border-right: 0; | ||
} | ||
.textAndCloseButton { | ||
display: flex; | ||
margin-top: var(--spacing-xs); | ||
margin-left: var(--spacing-s); | ||
margin-right: var(--spacing-2xs); | ||
} | ||
.text { | ||
flex: 1; | ||
font-size: var(--font-size-3xs); | ||
line-height: var(--font-line-height-compact); | ||
} | ||
.closeButton { | ||
flex: 0; | ||
display: flex; | ||
align-items: center; | ||
justify-content: center; | ||
width: var(--spacing-2xs); | ||
height: var(--spacing-2xs); | ||
border: none; | ||
color: var(--color-text-light); | ||
background-color: transparent; | ||
} | ||
.becomeCreatorButton { | ||
margin: var(--spacing-s); | ||
} | ||
</style> |
Oops, something went wrong.