mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
chore: Remove template credential experiment's feature flag. (no-changelog) (#14429)
This commit is contained in:
@@ -56,7 +56,6 @@ 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.enableTemplateCredentialSetupFeatureFlag();
|
|
||||||
clickUseWorkflowButtonByTitle('Promote new Shopify products');
|
clickUseWorkflowButtonByTitle('Promote new Shopify products');
|
||||||
|
|
||||||
templateCredentialsSetupPage.getters
|
templateCredentialsSetupPage.getters
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { CredentialsModal, MessageBox } from './modals';
|
import { CredentialsModal, MessageBox } from './modals';
|
||||||
import { overrideFeatureFlag } from '../composables/featureFlags';
|
|
||||||
import * as formStep from '../composables/setup-template-form-step';
|
import * as formStep from '../composables/setup-template-form-step';
|
||||||
|
|
||||||
const credentialsModal = new CredentialsModal();
|
const credentialsModal = new CredentialsModal();
|
||||||
@@ -12,13 +11,8 @@ export const getters = {
|
|||||||
infoCallout: () => cy.getByTestId('info-callout'),
|
infoCallout: () => cy.getByTestId('info-callout'),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const enableTemplateCredentialSetupFeatureFlag = () => {
|
|
||||||
overrideFeatureFlag('017_template_credential_setup_v2', true);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const visitTemplateCredentialSetupPage = (templateId: number) => {
|
export const visitTemplateCredentialSetupPage = (templateId: number) => {
|
||||||
cy.visit(`templates/${templateId}/setup`);
|
cy.visit(`templates/${templateId}/setup`);
|
||||||
enableTemplateCredentialSetupFeatureFlag();
|
|
||||||
|
|
||||||
formStep.getFormStep().eq(0).should('be.visible');
|
formStep.getFormStep().eq(0).should('be.visible');
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, onBeforeUnmount, watch } from 'vue';
|
import { computed, onBeforeUnmount, watch } from 'vue';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import { SETUP_CREDENTIALS_MODAL_KEY, TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT } from '@/constants';
|
import { SETUP_CREDENTIALS_MODAL_KEY } from '@/constants';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
import { usePostHog } from '@/stores/posthog.store';
|
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { doesNodeHaveAllCredentialsFilled } from '@/utils/nodes/nodeTransforms';
|
import { doesNodeHaveAllCredentialsFilled } from '@/utils/nodes/nodeTransforms';
|
||||||
@@ -11,7 +10,6 @@ import { doesNodeHaveAllCredentialsFilled } from '@/utils/nodes/nodeTransforms';
|
|||||||
const workflowsStore = useWorkflowsStore();
|
const workflowsStore = useWorkflowsStore();
|
||||||
const nodeTypesStore = useNodeTypesStore();
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
const uiStore = useUIStore();
|
const uiStore = useUIStore();
|
||||||
const posthogStore = usePostHog();
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
|
||||||
const isTemplateSetupCompleted = computed(() => {
|
const isTemplateSetupCompleted = computed(() => {
|
||||||
@@ -32,9 +30,8 @@ const allCredentialsFilled = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const showButton = computed(() => {
|
const showButton = computed(() => {
|
||||||
const isFeatureEnabled = posthogStore.isFeatureEnabled(TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT);
|
|
||||||
const isCreatedFromTemplate = !!workflowsStore.workflow?.meta?.templateId;
|
const isCreatedFromTemplate = !!workflowsStore.workflow?.meta?.templateId;
|
||||||
if (!isFeatureEnabled || !isCreatedFromTemplate || isTemplateSetupCompleted.value) {
|
if (!isCreatedFromTemplate || isTemplateSetupCompleted.value) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -723,8 +723,6 @@ export const KEEP_AUTH_IN_NDV_FOR_NODES = [
|
|||||||
export const MAIN_AUTH_FIELD_NAME = 'authentication';
|
export const MAIN_AUTH_FIELD_NAME = 'authentication';
|
||||||
export const NODE_RESOURCE_FIELD_NAME = 'resource';
|
export const NODE_RESOURCE_FIELD_NAME = 'resource';
|
||||||
|
|
||||||
export const TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT = '017_template_credential_setup_v2';
|
|
||||||
|
|
||||||
export const CANVAS_AUTO_ADD_MANUAL_TRIGGER_EXPERIMENT = {
|
export const CANVAS_AUTO_ADD_MANUAL_TRIGGER_EXPERIMENT = {
|
||||||
name: '20_canvas_auto_add_manual_trigger',
|
name: '20_canvas_auto_add_manual_trigger',
|
||||||
control: 'control',
|
control: 'control',
|
||||||
@@ -762,7 +760,6 @@ export const SCHEMA_PREVIEW_EXPERIMENT = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const EXPERIMENTS_TO_TRACK = [
|
export const EXPERIMENTS_TO_TRACK = [
|
||||||
TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT,
|
|
||||||
CANVAS_AUTO_ADD_MANUAL_TRIGGER_EXPERIMENT.name,
|
CANVAS_AUTO_ADD_MANUAL_TRIGGER_EXPERIMENT.name,
|
||||||
AI_ASSISTANT_EXPERIMENT.name,
|
AI_ASSISTANT_EXPERIMENT.name,
|
||||||
CREDENTIAL_DOCS_EXPERIMENT.name,
|
CREDENTIAL_DOCS_EXPERIMENT.name,
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ import type { ITemplatesWorkflowFull } from '@/Interface';
|
|||||||
import { Telemetry } from '@/plugins/telemetry';
|
import { Telemetry } from '@/plugins/telemetry';
|
||||||
import type { NodeTypesStore } from '@/stores/nodeTypes.store';
|
import type { NodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
import type { PosthogStore } from '@/stores/posthog.store';
|
|
||||||
import { usePostHog } from '@/stores/posthog.store';
|
|
||||||
import type { TemplatesStore } from '@/stores/templates.store';
|
import type { TemplatesStore } from '@/stores/templates.store';
|
||||||
import { useTemplatesStore } from '@/stores/templates.store';
|
import { useTemplatesStore } from '@/stores/templates.store';
|
||||||
import { useTemplateWorkflow } from '@/utils/templates/templateActions';
|
import { useTemplateWorkflow } from '@/utils/templates/templateActions';
|
||||||
@@ -53,7 +51,6 @@ describe('templateActions', () => {
|
|||||||
resolve: vi.fn(),
|
resolve: vi.fn(),
|
||||||
} as unknown as Router;
|
} as unknown as Router;
|
||||||
let nodeTypesStore: NodeTypesStore;
|
let nodeTypesStore: NodeTypesStore;
|
||||||
let posthogStore: PosthogStore;
|
|
||||||
let templatesStore: TemplatesStore;
|
let templatesStore: TemplatesStore;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -66,48 +63,19 @@ describe('templateActions', () => {
|
|||||||
|
|
||||||
vi.spyOn(telemetry, 'track').mockImplementation(() => {});
|
vi.spyOn(telemetry, 'track').mockImplementation(() => {});
|
||||||
nodeTypesStore = useNodeTypesStore();
|
nodeTypesStore = useNodeTypesStore();
|
||||||
posthogStore = usePostHog();
|
|
||||||
templatesStore = useTemplatesStore();
|
templatesStore = useTemplatesStore();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('When feature flag is disabled', () => {
|
describe('When template has nodes requiring credentials', () => {
|
||||||
const templateId = '1';
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
posthogStore.isFeatureEnabled = vi.fn().mockReturnValue(false);
|
|
||||||
|
|
||||||
await useTemplateWorkflow({
|
|
||||||
externalHooks,
|
|
||||||
posthogStore,
|
|
||||||
nodeTypesStore,
|
|
||||||
telemetry,
|
|
||||||
templateId,
|
|
||||||
templatesStore,
|
|
||||||
router,
|
|
||||||
source: 'workflow',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should navigate to correct url', async () => {
|
|
||||||
expect(router.push).toHaveBeenCalledWith({
|
|
||||||
name: VIEWS.TEMPLATE_IMPORT,
|
|
||||||
params: { id: templateId },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('When feature flag is enabled and template has nodes requiring credentials', () => {
|
|
||||||
const templateId = testTemplate2.id.toString();
|
const templateId = testTemplate2.id.toString();
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
posthogStore.isFeatureEnabled = vi.fn().mockReturnValue(true);
|
|
||||||
templatesStore.addWorkflows([testTemplate2]);
|
templatesStore.addWorkflows([testTemplate2]);
|
||||||
nodeTypesStore.setNodeTypes([nodeTypeTelegram]);
|
nodeTypesStore.setNodeTypes([nodeTypeTelegram]);
|
||||||
vi.spyOn(nodeTypesStore, 'loadNodeTypesIfNotLoaded').mockResolvedValue();
|
vi.spyOn(nodeTypesStore, 'loadNodeTypesIfNotLoaded').mockResolvedValue();
|
||||||
|
|
||||||
await useTemplateWorkflow({
|
await useTemplateWorkflow({
|
||||||
externalHooks,
|
externalHooks,
|
||||||
posthogStore,
|
|
||||||
nodeTypesStore,
|
nodeTypesStore,
|
||||||
telemetry,
|
telemetry,
|
||||||
templateId,
|
templateId,
|
||||||
@@ -125,17 +93,15 @@ describe('templateActions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("When feature flag is enabled and template doesn't have nodes requiring credentials", () => {
|
describe("When template doesn't have nodes requiring credentials", () => {
|
||||||
const templateId = testTemplate1.id.toString();
|
const templateId = testTemplate1.id.toString();
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
posthogStore.isFeatureEnabled = vi.fn().mockReturnValue(true);
|
|
||||||
templatesStore.addWorkflows([testTemplate1]);
|
templatesStore.addWorkflows([testTemplate1]);
|
||||||
vi.spyOn(nodeTypesStore, 'loadNodeTypesIfNotLoaded').mockResolvedValue();
|
vi.spyOn(nodeTypesStore, 'loadNodeTypesIfNotLoaded').mockResolvedValue();
|
||||||
|
|
||||||
await useTemplateWorkflow({
|
await useTemplateWorkflow({
|
||||||
externalHooks,
|
externalHooks,
|
||||||
posthogStore,
|
|
||||||
nodeTypesStore,
|
nodeTypesStore,
|
||||||
telemetry,
|
telemetry,
|
||||||
templateId,
|
templateId,
|
||||||
|
|||||||
@@ -5,9 +5,8 @@ import type {
|
|||||||
IWorkflowTemplate,
|
IWorkflowTemplate,
|
||||||
} from '@/Interface';
|
} from '@/Interface';
|
||||||
import { getNewWorkflow } from '@/api/workflows';
|
import { getNewWorkflow } from '@/api/workflows';
|
||||||
import { TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT, VIEWS } from '@/constants';
|
import { VIEWS } from '@/constants';
|
||||||
import type { useRootStore } from '@/stores/root.store';
|
import type { useRootStore } from '@/stores/root.store';
|
||||||
import type { PosthogStore } from '@/stores/posthog.store';
|
|
||||||
import type { useWorkflowsStore } from '@/stores/workflows.store';
|
import type { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { getNodesWithNormalizedPosition } from '@/utils/nodeViewUtils';
|
import { getNodesWithNormalizedPosition } from '@/utils/nodeViewUtils';
|
||||||
import type { NodeTypeProvider } from '@/utils/nodeTypes/nodeTypeTransforms';
|
import type { NodeTypeProvider } from '@/utils/nodeTypes/nodeTypeTransforms';
|
||||||
@@ -147,7 +146,6 @@ async function getFullTemplate(templatesStore: TemplatesStore, templateId: strin
|
|||||||
export async function useTemplateWorkflow(opts: {
|
export async function useTemplateWorkflow(opts: {
|
||||||
externalHooks: ExternalHooks;
|
externalHooks: ExternalHooks;
|
||||||
nodeTypesStore: NodeTypesStore;
|
nodeTypesStore: NodeTypesStore;
|
||||||
posthogStore: PosthogStore;
|
|
||||||
templateId: string;
|
templateId: string;
|
||||||
templatesStore: TemplatesStore;
|
templatesStore: TemplatesStore;
|
||||||
router: Router;
|
router: Router;
|
||||||
@@ -155,13 +153,7 @@ export async function useTemplateWorkflow(opts: {
|
|||||||
telemetry: Telemetry;
|
telemetry: Telemetry;
|
||||||
source: string;
|
source: string;
|
||||||
}) {
|
}) {
|
||||||
const { nodeTypesStore, posthogStore, templateId, templatesStore } = opts;
|
const { nodeTypesStore, templateId, templatesStore } = opts;
|
||||||
|
|
||||||
const openCredentialSetup = posthogStore.isFeatureEnabled(TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT);
|
|
||||||
if (!openCredentialSetup) {
|
|
||||||
await openTemplateWorkflowOnNodeView(opts);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [template] = await Promise.all([
|
const [template] = await Promise.all([
|
||||||
getFullTemplate(templatesStore, templateId),
|
getFullTemplate(templatesStore, templateId),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onBeforeMount, onMounted, watch } from 'vue';
|
import { computed, onMounted, watch } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { useSetupTemplateStore } from './setupTemplate.store';
|
import { useSetupTemplateStore } from './setupTemplate.store';
|
||||||
import N8nHeading from '@n8n/design-system/components/N8nHeading';
|
import N8nHeading from '@n8n/design-system/components/N8nHeading';
|
||||||
@@ -7,14 +7,12 @@ import N8nLink from '@n8n/design-system/components/N8nLink';
|
|||||||
import AppsRequiringCredsNotice from './AppsRequiringCredsNotice.vue';
|
import AppsRequiringCredsNotice from './AppsRequiringCredsNotice.vue';
|
||||||
import SetupTemplateFormStep from './SetupTemplateFormStep.vue';
|
import SetupTemplateFormStep from './SetupTemplateFormStep.vue';
|
||||||
import TemplatesView from '../TemplatesView.vue';
|
import TemplatesView from '../TemplatesView.vue';
|
||||||
import { TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT, VIEWS } from '@/constants';
|
import { VIEWS } from '@/constants';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import { usePostHog } from '@/stores/posthog.store';
|
|
||||||
|
|
||||||
// Store
|
// Store
|
||||||
const setupTemplateStore = useSetupTemplateStore();
|
const setupTemplateStore = useSetupTemplateStore();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const posthogStore = usePostHog();
|
|
||||||
|
|
||||||
// Router
|
// Router
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -79,15 +77,6 @@ const skipIfTemplateHasNoCreds = async () => {
|
|||||||
|
|
||||||
setupTemplateStore.setTemplateId(templateId.value);
|
setupTemplateStore.setTemplateId(templateId.value);
|
||||||
|
|
||||||
onBeforeMount(async () => {
|
|
||||||
if (!posthogStore.isFeatureEnabled(TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT)) {
|
|
||||||
void router.replace({
|
|
||||||
name: VIEWS.TEMPLATE_IMPORT,
|
|
||||||
params: { id: templateId.value },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await setupTemplateStore.init();
|
await setupTemplateStore.init();
|
||||||
await skipIfTemplateHasNoCreds();
|
await skipIfTemplateHasNoCreds();
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import TemplatesView from './TemplatesView.vue';
|
|||||||
import type { ITemplatesWorkflow } from '@/Interface';
|
import type { ITemplatesWorkflow } from '@/Interface';
|
||||||
import { VIEWS } from '@/constants';
|
import { VIEWS } from '@/constants';
|
||||||
import { useTemplatesStore } from '@/stores/templates.store';
|
import { useTemplatesStore } from '@/stores/templates.store';
|
||||||
import { usePostHog } from '@/stores/posthog.store';
|
|
||||||
import { useTemplateWorkflow } from '@/utils/templates/templateActions';
|
import { useTemplateWorkflow } from '@/utils/templates/templateActions';
|
||||||
import { useExternalHooks } from '@/composables/useExternalHooks';
|
import { useExternalHooks } from '@/composables/useExternalHooks';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
@@ -18,7 +17,6 @@ import { useI18n } from '@/composables/useI18n';
|
|||||||
|
|
||||||
const externalHooks = useExternalHooks();
|
const externalHooks = useExternalHooks();
|
||||||
const templatesStore = useTemplatesStore();
|
const templatesStore = useTemplatesStore();
|
||||||
const posthogStore = usePostHog();
|
|
||||||
const nodeTypesStore = useNodeTypesStore();
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -64,7 +62,6 @@ const onOpenTemplate = ({ event, id }: { event: MouseEvent; id: number }) => {
|
|||||||
|
|
||||||
const onUseWorkflow = async ({ event, id }: { event: MouseEvent; id: number }) => {
|
const onUseWorkflow = async ({ event, id }: { event: MouseEvent; id: number }) => {
|
||||||
await useTemplateWorkflow({
|
await useTemplateWorkflow({
|
||||||
posthogStore,
|
|
||||||
router,
|
router,
|
||||||
templateId: `${id}`,
|
templateId: `${id}`,
|
||||||
inNewBrowserTab: event.metaKey || event.ctrlKey,
|
inNewBrowserTab: event.metaKey || event.ctrlKey,
|
||||||
@@ -78,8 +75,8 @@ const onUseWorkflow = async ({ event, id }: { event: MouseEvent; id: number }) =
|
|||||||
|
|
||||||
const navigateTo = (e: MouseEvent, page: string, id: string) => {
|
const navigateTo = (e: MouseEvent, page: string, id: string) => {
|
||||||
if (e.metaKey || e.ctrlKey) {
|
if (e.metaKey || e.ctrlKey) {
|
||||||
const route = router.resolve({ name: page, params: { id } });
|
const { href } = router.resolve({ name: page, params: { id } });
|
||||||
window.open(route.href, '_blank');
|
window.open(href, '_blank');
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
void router.push({ name: page, params: { id } });
|
void router.push({ name: page, params: { id } });
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref, watch } from 'vue';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
import { useTemplatesStore } from '@/stores/templates.store';
|
import { useTemplatesStore } from '@/stores/templates.store';
|
||||||
import { usePostHog } from '@/stores/posthog.store';
|
|
||||||
import { useTemplateWorkflow } from '@/utils/templates/templateActions';
|
import { useTemplateWorkflow } from '@/utils/templates/templateActions';
|
||||||
import { useExternalHooks } from '@/composables/useExternalHooks';
|
import { useExternalHooks } from '@/composables/useExternalHooks';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
@@ -13,7 +12,6 @@ import TemplatesView from './TemplatesView.vue';
|
|||||||
|
|
||||||
const externalHooks = useExternalHooks();
|
const externalHooks = useExternalHooks();
|
||||||
const templatesStore = useTemplatesStore();
|
const templatesStore = useTemplatesStore();
|
||||||
const posthogStore = usePostHog();
|
|
||||||
const nodeTypesStore = useNodeTypesStore();
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -34,7 +32,6 @@ const template = computed(() => templatesStore.getFullTemplateById(templateId.va
|
|||||||
|
|
||||||
const openTemplateSetup = async (id: string, e: PointerEvent) => {
|
const openTemplateSetup = async (id: string, e: PointerEvent) => {
|
||||||
await useTemplateWorkflow({
|
await useTemplateWorkflow({
|
||||||
posthogStore,
|
|
||||||
router,
|
router,
|
||||||
templateId: id,
|
templateId: id,
|
||||||
inNewBrowserTab: e.metaKey || e.ctrlKey,
|
inNewBrowserTab: e.metaKey || e.ctrlKey,
|
||||||
|
|||||||
Reference in New Issue
Block a user