mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-12 13:27:31 -08:00
feat(editor): Show template credential setup based on feature flag (#7989)
Replace the local storage based feature flag with posthog feature flag. Also: - Fix bunch of eslint warnings in posthog store
This commit is contained in:
parent
c378f60a25
commit
08ee307209
|
@ -4,12 +4,11 @@ import {
|
||||||
visitTemplateCollectionPage,
|
visitTemplateCollectionPage,
|
||||||
testData,
|
testData,
|
||||||
} from '../pages/template-collection';
|
} from '../pages/template-collection';
|
||||||
import { TemplateCredentialSetupPage } from '../pages/template-credential-setup';
|
import * as templateCredentialsSetupPage from '../pages/template-credential-setup';
|
||||||
import { TemplateWorkflowPage } from '../pages/template-workflow';
|
import { TemplateWorkflowPage } from '../pages/template-workflow';
|
||||||
import { WorkflowPage } from '../pages/workflow';
|
import { WorkflowPage } from '../pages/workflow';
|
||||||
|
|
||||||
const templateWorkflowPage = new TemplateWorkflowPage();
|
const templateWorkflowPage = new TemplateWorkflowPage();
|
||||||
const templateCredentialsSetupPage = new TemplateCredentialSetupPage();
|
|
||||||
const credentialsModal = new CredentialsModal();
|
const credentialsModal = new CredentialsModal();
|
||||||
const messageBox = new MessageBox();
|
const messageBox = new MessageBox();
|
||||||
const workflowPage = new WorkflowPage();
|
const workflowPage = new WorkflowPage();
|
||||||
|
@ -25,7 +24,8 @@ describe('Template credentials setup', () => {
|
||||||
|
|
||||||
it('can be opened from template workflow page', () => {
|
it('can be opened from template workflow page', () => {
|
||||||
templateWorkflowPage.actions.visit(testTemplate.id);
|
templateWorkflowPage.actions.visit(testTemplate.id);
|
||||||
templateCredentialsSetupPage.actions.enableFeatureFlag();
|
templateWorkflowPage.getters.useTemplateButton().should('be.visible');
|
||||||
|
templateCredentialsSetupPage.enableTemplateCredentialSetupFeatureFlag();
|
||||||
templateWorkflowPage.actions.clickUseThisWorkflowButton();
|
templateWorkflowPage.actions.clickUseThisWorkflowButton();
|
||||||
|
|
||||||
templateCredentialsSetupPage.getters
|
templateCredentialsSetupPage.getters
|
||||||
|
@ -35,7 +35,7 @@ describe('Template credentials setup', () => {
|
||||||
|
|
||||||
it('can be opened from template collection page', () => {
|
it('can be opened from template collection page', () => {
|
||||||
visitTemplateCollectionPage(testData.ecommerceStarterPack);
|
visitTemplateCollectionPage(testData.ecommerceStarterPack);
|
||||||
templateCredentialsSetupPage.actions.enableFeatureFlag();
|
templateCredentialsSetupPage.enableTemplateCredentialSetupFeatureFlag();
|
||||||
clickUseWorkflowButtonByTitle('Promote new Shopify products on Twitter and Telegram');
|
clickUseWorkflowButtonByTitle('Promote new Shopify products on Twitter and Telegram');
|
||||||
|
|
||||||
templateCredentialsSetupPage.getters
|
templateCredentialsSetupPage.getters
|
||||||
|
@ -44,7 +44,7 @@ describe('Template credentials setup', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can be opened with a direct url', () => {
|
it('can be opened with a direct url', () => {
|
||||||
templateCredentialsSetupPage.actions.visit(testTemplate.id);
|
templateCredentialsSetupPage.visitTemplateCredentialSetupPage(testTemplate.id);
|
||||||
|
|
||||||
templateCredentialsSetupPage.getters
|
templateCredentialsSetupPage.getters
|
||||||
.title(`Setup 'Promote new Shopify products on Twitter and Telegram' template`)
|
.title(`Setup 'Promote new Shopify products on Twitter and Telegram' template`)
|
||||||
|
@ -52,7 +52,7 @@ describe('Template credentials setup', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has all the elements on page', () => {
|
it('has all the elements on page', () => {
|
||||||
templateCredentialsSetupPage.actions.visit(testTemplate.id);
|
templateCredentialsSetupPage.visitTemplateCredentialSetupPage(testTemplate.id);
|
||||||
|
|
||||||
templateCredentialsSetupPage.getters
|
templateCredentialsSetupPage.getters
|
||||||
.title(`Setup 'Promote new Shopify products on Twitter and Telegram' template`)
|
.title(`Setup 'Promote new Shopify products on Twitter and Telegram' template`)
|
||||||
|
@ -83,14 +83,14 @@ describe('Template credentials setup', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can skip template creation', () => {
|
it('can skip template creation', () => {
|
||||||
templateCredentialsSetupPage.actions.visit(testTemplate.id);
|
templateCredentialsSetupPage.visitTemplateCredentialSetupPage(testTemplate.id);
|
||||||
|
|
||||||
templateCredentialsSetupPage.getters.skipLink().click();
|
templateCredentialsSetupPage.getters.skipLink().click();
|
||||||
workflowPage.getters.canvasNodes().should('have.length', 3);
|
workflowPage.getters.canvasNodes().should('have.length', 3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can create credentials and workflow from the template', () => {
|
it('can create credentials and workflow from the template', () => {
|
||||||
templateCredentialsSetupPage.actions.visit(testTemplate.id);
|
templateCredentialsSetupPage.visitTemplateCredentialSetupPage(testTemplate.id);
|
||||||
|
|
||||||
// Continue button should be disabled if no credentials are created
|
// Continue button should be disabled if no credentials are created
|
||||||
templateCredentialsSetupPage.getters.continueButton().should('be.disabled');
|
templateCredentialsSetupPage.getters.continueButton().should('be.disabled');
|
||||||
|
|
|
@ -1,40 +1,35 @@
|
||||||
import { BasePage } from './base';
|
|
||||||
|
|
||||||
export type TemplateTestData = {
|
export type TemplateTestData = {
|
||||||
id: number;
|
id: number;
|
||||||
fixture: string;
|
fixture: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class TemplateCredentialSetupPage extends BasePage {
|
export const testData = {
|
||||||
testData = {
|
simpleTemplate: {
|
||||||
simpleTemplate: {
|
id: 1205,
|
||||||
id: 1205,
|
fixture: 'Test_Template_1.json',
|
||||||
fixture: 'Test_Template_1.json',
|
},
|
||||||
},
|
};
|
||||||
};
|
|
||||||
|
|
||||||
getters = {
|
export const getters = {
|
||||||
continueButton: () => cy.getByTestId('continue-button'),
|
continueButton: () => cy.getByTestId('continue-button'),
|
||||||
skipLink: () => cy.get('a:contains("Skip")'),
|
skipLink: () => cy.get('a:contains("Skip")'),
|
||||||
title: (title: string) => cy.get(`h1:contains(${title})`),
|
title: (title: string) => cy.get(`h1:contains(${title})`),
|
||||||
infoCallout: () => cy.getByTestId('info-callout'),
|
infoCallout: () => cy.getByTestId('info-callout'),
|
||||||
createAppCredentialsButton: (appName: string) =>
|
createAppCredentialsButton: (appName: string) =>
|
||||||
cy.get(`button:contains("Create new ${appName} credential")`),
|
cy.get(`button:contains("Create new ${appName} credential")`),
|
||||||
appCredentialSteps: () => cy.getByTestId('setup-credentials-form-step'),
|
appCredentialSteps: () => cy.getByTestId('setup-credentials-form-step'),
|
||||||
stepHeading: ($el: JQuery<HTMLElement>) =>
|
stepHeading: ($el: JQuery<HTMLElement>) =>
|
||||||
cy.wrap($el).findChildByTestId('credential-step-heading'),
|
cy.wrap($el).findChildByTestId('credential-step-heading'),
|
||||||
stepDescription: ($el: JQuery<HTMLElement>) =>
|
stepDescription: ($el: JQuery<HTMLElement>) =>
|
||||||
cy.wrap($el).findChildByTestId('credential-step-description'),
|
cy.wrap($el).findChildByTestId('credential-step-description'),
|
||||||
};
|
};
|
||||||
|
|
||||||
actions = {
|
export const visitTemplateCredentialSetupPage = (templateId: number) => {
|
||||||
visit: (templateId: number) => {
|
cy.visit(`/templates/${templateId}/setup`);
|
||||||
cy.visit(`/templates/${templateId}/setup`);
|
};
|
||||||
},
|
|
||||||
enableFeatureFlag: () => {
|
export const enableTemplateCredentialSetupFeatureFlag = () => {
|
||||||
cy.window().then((window) => {
|
cy.window().then((win) => {
|
||||||
window.localStorage.setItem('template-credentials-setup', 'true');
|
win.featureFlags.override('016_template_credential_setup', true);
|
||||||
});
|
});
|
||||||
},
|
};
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
|
@ -41,6 +41,13 @@ declare global {
|
||||||
draganddrop(draggableSelector: string, droppableSelector: string): void;
|
draganddrop(draggableSelector: string, droppableSelector: string): void;
|
||||||
push(type: string, data: unknown): void;
|
push(type: string, data: unknown): void;
|
||||||
shouldNotHaveConsoleErrors(): void;
|
shouldNotHaveConsoleErrors(): void;
|
||||||
|
window(): Chainable<
|
||||||
|
AUTWindow & {
|
||||||
|
featureFlags: {
|
||||||
|
override: (feature: string, value: any) => void;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -617,7 +617,9 @@ export const ASK_AI_EXPERIMENT = {
|
||||||
gpt4: 'gpt4',
|
gpt4: 'gpt4',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EXPERIMENTS_TO_TRACK = [ASK_AI_EXPERIMENT.name];
|
export const TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT = '016_template_credential_setup';
|
||||||
|
|
||||||
|
export const EXPERIMENTS_TO_TRACK = [ASK_AI_EXPERIMENT.name, TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT];
|
||||||
|
|
||||||
export const MFA_AUTHENTICATION_REQUIRED_ERROR_CODE = 998;
|
export const MFA_AUTHENTICATION_REQUIRED_ERROR_CODE = 998;
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,8 @@ const EVENTS = {
|
||||||
IS_PART_OF_EXPERIMENT: 'User is part of experiment',
|
IS_PART_OF_EXPERIMENT: 'User is part of experiment',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type PosthogStore = ReturnType<typeof usePostHog>;
|
||||||
|
|
||||||
export const usePostHog = defineStore('posthog', () => {
|
export const usePostHog = defineStore('posthog', () => {
|
||||||
const usersStore = useUsersStore();
|
const usersStore = useUsersStore();
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
|
@ -39,6 +41,13 @@ export const usePostHog = defineStore('posthog', () => {
|
||||||
return getVariant(experiment) === variant;
|
return getVariant(experiment) === variant;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the given feature flag is enabled. Should only be used for boolean flags
|
||||||
|
*/
|
||||||
|
const isFeatureEnabled = (experiment: keyof FeatureFlags) => {
|
||||||
|
return featureFlags.value?.[experiment] === true;
|
||||||
|
};
|
||||||
|
|
||||||
if (!window.featureFlags) {
|
if (!window.featureFlags) {
|
||||||
// for testing
|
// for testing
|
||||||
const cachedOverrides = useStorage(LOCAL_STORAGE_EXPERIMENT_OVERRIDES).value;
|
const cachedOverrides = useStorage(LOCAL_STORAGE_EXPERIMENT_OVERRIDES).value;
|
||||||
|
@ -65,7 +74,7 @@ export const usePostHog = defineStore('posthog', () => {
|
||||||
},
|
},
|
||||||
|
|
||||||
getVariant,
|
getVariant,
|
||||||
getAll: () => featureFlags.value || {},
|
getAll: () => featureFlags.value ?? {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,6 +99,25 @@ export const usePostHog = defineStore('posthog', () => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const trackExperiment = (featFlags: FeatureFlags, name: string) => {
|
||||||
|
const variant = featFlags[name];
|
||||||
|
if (!variant || trackedDemoExp.value[name] === variant) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
telemetryStore.track(EVENTS.IS_PART_OF_EXPERIMENT, {
|
||||||
|
name,
|
||||||
|
variant,
|
||||||
|
});
|
||||||
|
|
||||||
|
trackedDemoExp.value[name] = variant;
|
||||||
|
};
|
||||||
|
|
||||||
|
const trackExperiments = (featFlags: FeatureFlags) => {
|
||||||
|
EXPERIMENTS_TO_TRACK.forEach((name) => trackExperiment(featFlags, name));
|
||||||
|
};
|
||||||
|
const trackExperimentsDebounced = debounce(trackExperiments, 2000);
|
||||||
|
|
||||||
const init = (evaluatedFeatureFlags?: FeatureFlags) => {
|
const init = (evaluatedFeatureFlags?: FeatureFlags) => {
|
||||||
if (!window.posthog) {
|
if (!window.posthog) {
|
||||||
return;
|
return;
|
||||||
|
@ -143,25 +171,6 @@ export const usePostHog = defineStore('posthog', () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const trackExperiments = (featureFlags: FeatureFlags) => {
|
|
||||||
EXPERIMENTS_TO_TRACK.forEach((name) => trackExperiment(featureFlags, name));
|
|
||||||
};
|
|
||||||
const trackExperimentsDebounced = debounce(trackExperiments, 2000);
|
|
||||||
|
|
||||||
const trackExperiment = (featureFlags: FeatureFlags, name: string) => {
|
|
||||||
const variant = featureFlags[name];
|
|
||||||
if (!variant || trackedDemoExp.value[name] === variant) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
telemetryStore.track(EVENTS.IS_PART_OF_EXPERIMENT, {
|
|
||||||
name,
|
|
||||||
variant,
|
|
||||||
});
|
|
||||||
|
|
||||||
trackedDemoExp.value[name] = variant;
|
|
||||||
};
|
|
||||||
|
|
||||||
const capture = (event: string, properties: IDataObject) => {
|
const capture = (event: string, properties: IDataObject) => {
|
||||||
if (typeof window.posthog?.capture === 'function') {
|
if (typeof window.posthog?.capture === 'function') {
|
||||||
window.posthog.capture(event, properties);
|
window.posthog.capture(event, properties);
|
||||||
|
@ -181,6 +190,7 @@ export const usePostHog = defineStore('posthog', () => {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
init,
|
init,
|
||||||
|
isFeatureEnabled,
|
||||||
isVariantEnabled,
|
isVariantEnabled,
|
||||||
getVariant,
|
getVariant,
|
||||||
reset,
|
reset,
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
// Feature flags
|
|
||||||
export const enum FeatureFlag {
|
|
||||||
templateCredentialsSetup = 'template-credentials-setup',
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasLocaleStorageKey = (key: string): boolean => {
|
|
||||||
try {
|
|
||||||
// Local storage might not be available in all envs e.g. when user has
|
|
||||||
// disabled it in their browser
|
|
||||||
return !!localStorage.getItem(key);
|
|
||||||
} catch (e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isFeatureFlagEnabled = (flag: FeatureFlag): boolean => hasLocaleStorageKey(flag);
|
|
|
@ -1,9 +1,9 @@
|
||||||
import type { INodeUi, IWorkflowData, IWorkflowTemplate } from '@/Interface';
|
import type { INodeUi, IWorkflowData, IWorkflowTemplate } from '@/Interface';
|
||||||
import { getNewWorkflow } from '@/api/workflows';
|
import { getNewWorkflow } from '@/api/workflows';
|
||||||
import { VIEWS } from '@/constants';
|
import { TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT, VIEWS } from '@/constants';
|
||||||
import type { useRootStore } from '@/stores/n8nRoot.store';
|
import type { useRootStore } from '@/stores/n8nRoot.store';
|
||||||
|
import type { PosthogStore } from '@/stores/posthog.store';
|
||||||
import type { useWorkflowsStore } from '@/stores/workflows.store';
|
import type { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { FeatureFlag, isFeatureFlagEnabled } from '@/utils/featureFlag';
|
|
||||||
import { getFixedNodesList } from '@/utils/nodeViewUtils';
|
import { getFixedNodesList } from '@/utils/nodeViewUtils';
|
||||||
import type { TemplateCredentialKey } from '@/utils/templates/templateTransforms';
|
import type { TemplateCredentialKey } from '@/utils/templates/templateTransforms';
|
||||||
import { replaceAllTemplateNodeCredentials } from '@/utils/templates/templateTransforms';
|
import { replaceAllTemplateNodeCredentials } from '@/utils/templates/templateTransforms';
|
||||||
|
@ -45,13 +45,16 @@ export async function createWorkflowFromTemplate(
|
||||||
* if the feature flag is disabled)
|
* if the feature flag is disabled)
|
||||||
*/
|
*/
|
||||||
export async function openTemplateCredentialSetup(opts: {
|
export async function openTemplateCredentialSetup(opts: {
|
||||||
|
posthogStore: PosthogStore;
|
||||||
templateId: string;
|
templateId: string;
|
||||||
router: Router;
|
router: Router;
|
||||||
inNewBrowserTab?: boolean;
|
inNewBrowserTab?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { router, templateId, inNewBrowserTab = false } = opts;
|
const { router, templateId, inNewBrowserTab = false, posthogStore } = opts;
|
||||||
|
|
||||||
const routeLocation: RouteLocationRaw = isFeatureFlagEnabled(FeatureFlag.templateCredentialsSetup)
|
const routeLocation: RouteLocationRaw = posthogStore.isFeatureEnabled(
|
||||||
|
TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT,
|
||||||
|
)
|
||||||
? {
|
? {
|
||||||
name: VIEWS.TEMPLATE_SETUP,
|
name: VIEWS.TEMPLATE_SETUP,
|
||||||
params: { id: templateId },
|
params: { id: templateId },
|
||||||
|
|
|
@ -66,10 +66,9 @@ import type {
|
||||||
} from '@/Interface';
|
} from '@/Interface';
|
||||||
|
|
||||||
import { setPageTitle } from '@/utils/htmlUtils';
|
import { setPageTitle } from '@/utils/htmlUtils';
|
||||||
import { VIEWS } from '@/constants';
|
import { TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT, VIEWS } from '@/constants';
|
||||||
import { useTemplatesStore } from '@/stores/templates.store';
|
import { useTemplatesStore } from '@/stores/templates.store';
|
||||||
import { usePostHog } from '@/stores/posthog.store';
|
import { usePostHog } from '@/stores/posthog.store';
|
||||||
import { FeatureFlag, isFeatureFlagEnabled } from '@/utils/featureFlag';
|
|
||||||
import { openTemplateCredentialSetup } from '@/utils/templates/templateActions';
|
import { openTemplateCredentialSetup } from '@/utils/templates/templateActions';
|
||||||
import { useExternalHooks } from '@/composables/useExternalHooks';
|
import { useExternalHooks } from '@/composables/useExternalHooks';
|
||||||
|
|
||||||
|
@ -129,7 +128,7 @@ export default defineComponent({
|
||||||
this.navigateTo(event, VIEWS.TEMPLATE, id);
|
this.navigateTo(event, VIEWS.TEMPLATE, id);
|
||||||
},
|
},
|
||||||
async onUseWorkflow({ event, id }: { event: MouseEvent; id: string }) {
|
async onUseWorkflow({ event, id }: { event: MouseEvent; id: string }) {
|
||||||
if (!isFeatureFlagEnabled(FeatureFlag.templateCredentialsSetup)) {
|
if (this.posthogStore.isFeatureEnabled(TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT)) {
|
||||||
const telemetryPayload = {
|
const telemetryPayload = {
|
||||||
template_id: id,
|
template_id: id,
|
||||||
wf_template_repo_session_id: this.templatesStore.currentSessionId,
|
wf_template_repo_session_id: this.templatesStore.currentSessionId,
|
||||||
|
@ -142,6 +141,7 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
await openTemplateCredentialSetup({
|
await openTemplateCredentialSetup({
|
||||||
|
posthogStore: this.posthogStore,
|
||||||
router: this.$router,
|
router: this.$router,
|
||||||
templateId: id,
|
templateId: id,
|
||||||
inNewBrowserTab: event.metaKey || event.ctrlKey,
|
inNewBrowserTab: event.metaKey || event.ctrlKey,
|
||||||
|
|
|
@ -69,8 +69,8 @@ import { setPageTitle } from '@/utils/htmlUtils';
|
||||||
import { useTemplatesStore } from '@/stores/templates.store';
|
import { useTemplatesStore } from '@/stores/templates.store';
|
||||||
import { usePostHog } from '@/stores/posthog.store';
|
import { usePostHog } from '@/stores/posthog.store';
|
||||||
import { openTemplateCredentialSetup } from '@/utils/templates/templateActions';
|
import { openTemplateCredentialSetup } from '@/utils/templates/templateActions';
|
||||||
import { FeatureFlag, isFeatureFlagEnabled } from '@/utils/featureFlag';
|
|
||||||
import { useExternalHooks } from '@/composables/useExternalHooks';
|
import { useExternalHooks } from '@/composables/useExternalHooks';
|
||||||
|
import { TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT } from '@/constants';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'TemplatesWorkflowView',
|
name: 'TemplatesWorkflowView',
|
||||||
|
@ -107,7 +107,7 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async openTemplateSetup(id: string, e: PointerEvent) {
|
async openTemplateSetup(id: string, e: PointerEvent) {
|
||||||
if (!isFeatureFlagEnabled(FeatureFlag.templateCredentialsSetup)) {
|
if (!this.posthogStore.isFeatureEnabled(TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT)) {
|
||||||
const telemetryPayload = {
|
const telemetryPayload = {
|
||||||
source: 'workflow',
|
source: 'workflow',
|
||||||
template_id: id,
|
template_id: id,
|
||||||
|
@ -121,6 +121,7 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
await openTemplateCredentialSetup({
|
await openTemplateCredentialSetup({
|
||||||
|
posthogStore: this.posthogStore,
|
||||||
router: this.$router,
|
router: this.$router,
|
||||||
templateId: id,
|
templateId: id,
|
||||||
inNewBrowserTab: e.metaKey || e.ctrlKey,
|
inNewBrowserTab: e.metaKey || e.ctrlKey,
|
||||||
|
|
Loading…
Reference in a new issue