Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const usersStore = useUsersStore();
const nodeTypesStore = useNodeTypesStore();
const { installNode, loading } = useInstallNode();

const isOwner = computed(() => usersStore.isInstanceOwner);
const isAdminOrOwner = computed(() => usersStore.isAdminOrOwner);

// Fetched data from API (like CommunityNodeInfo does)
const publisherName = ref<string | undefined>(undefined);
Expand Down Expand Up @@ -134,7 +134,7 @@ const nodeTypeForIcon = computed((): SimplifiedNodeType | null => {
});

const handleInstall = async () => {
if (!props.appEntry?.packageName || !isOwner.value) return;
if (!props.appEntry?.packageName || !isAdminOrOwner.value) return;

const result = await installNode({
type: 'verified',
Expand Down Expand Up @@ -263,14 +263,14 @@ watch(
</a>
</div>

<ContactAdministratorToInstall v-if="!isOwner" />
<ContactAdministratorToInstall v-if="!isAdminOrOwner" />
</div>
</template>

<template #footer>
<div :class="$style.footer">
<N8nButton
v-if="isOwner"
v-if="isAdminOrOwner"
:label="i18n.baseText('communityNodeDetails.install')"
icon="download"
:loading="loading"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,12 @@ describe('NodeSettingsInvalidNodeWarning', () => {
});

describe('Owner permissions', () => {
it('should show install button when user is owner', async () => {
mockUseUsersStore.isInstanceOwner = true;
it.each([
{ isAdmin: true, isInstanceOwner: false, label: 'admin' },
{ isAdmin: false, isInstanceOwner: true, label: 'instance owner' },
])('should show install button when user is $label', ({ isAdmin, isInstanceOwner }) => {
mockUseUsersStore.isAdmin = isAdmin;
mockUseUsersStore.isInstanceOwner = isInstanceOwner;
const node = mockNode({ name: 'Test Node', type: 'n8n-nodes-test.testNode' });
const { getByTestId } = renderComponent(NodeSettingsInvalidNodeWarning, {
props: {
Expand All @@ -68,7 +72,8 @@ describe('NodeSettingsInvalidNodeWarning', () => {
expect(getByTestId('install-community-node-button')).toBeInTheDocument();
});

it('should show ContactAdministratorToInstall when user is not owner', async () => {
it('should show ContactAdministratorToInstall when user is not owner or admin', async () => {
mockUseUsersStore.isAdmin = false;
mockUseUsersStore.isInstanceOwner = false;
const node = mockNode({ name: 'Test Node', type: 'n8n-nodes-test.testNode' });
const { getByText } = renderComponent(NodeSettingsInvalidNodeWarning, {
Expand All @@ -86,7 +91,7 @@ describe('NodeSettingsInvalidNodeWarning', () => {

describe('View Details button', () => {
it('should open node creator when node is verified community node', async () => {
mockUseUsersStore.isInstanceOwner = true;
mockUseUsersStore.isAdmin = true;
mockUseNodeTypesStore.communityNodeType = () =>
({
isOfficialNode: true,
Expand All @@ -109,7 +114,7 @@ describe('NodeSettingsInvalidNodeWarning', () => {
});

it('should open NPM page when node is not verified community node', async () => {
mockUseUsersStore.isInstanceOwner = true;
mockUseUsersStore.isAdmin = true;
mockUseNodeTypesStore.communityNodeType = () =>
({
isOfficialNode: false,
Expand All @@ -135,7 +140,7 @@ describe('NodeSettingsInvalidNodeWarning', () => {

describe('Install button logic', () => {
it('should call installNode directly for verified community node', async () => {
mockUseUsersStore.isInstanceOwner = true;
mockUseUsersStore.isAdmin = true;
mockUseNodeTypesStore.communityNodeType = () =>
({
isOfficialNode: true,
Expand Down Expand Up @@ -165,7 +170,7 @@ describe('NodeSettingsInvalidNodeWarning', () => {
});

it('should call installNode without preview token directly for verified community node', async () => {
mockUseUsersStore.isInstanceOwner = true;
mockUseUsersStore.isAdmin = true;
mockUseNodeTypesStore.communityNodeType = () =>
({
isOfficialNode: true,
Expand Down Expand Up @@ -195,7 +200,7 @@ describe('NodeSettingsInvalidNodeWarning', () => {
});

it('should open modal for non-verified community node', async () => {
mockUseUsersStore.isInstanceOwner = true;
mockUseUsersStore.isAdmin = true;
mockUseNodeTypesStore.communityNodeType = () =>
({
isOfficialNode: false,
Expand Down Expand Up @@ -228,7 +233,7 @@ describe('NodeSettingsInvalidNodeWarning', () => {

describe('Node installation watcher', () => {
it('should call unsetActiveNodeName when node is defined', async () => {
mockUseUsersStore.isInstanceOwner = true;
mockUseUsersStore.isAdmin = true;
mockUseNodeTypesStore.communityNodeType = () =>
({
isOfficialNode: true,
Expand Down Expand Up @@ -260,7 +265,7 @@ describe('NodeSettingsInvalidNodeWarning', () => {
});

it('should not call unsetActiveNodeName when node is not defined', () => {
mockUseUsersStore.isInstanceOwner = true;
mockUseUsersStore.isAdmin = true;
mockUseNodeTypesStore.communityNodeType = () =>
({
isOfficialNode: true,
Expand All @@ -282,7 +287,7 @@ describe('NodeSettingsInvalidNodeWarning', () => {

describe('Non-community nodes', () => {
it('should show custom node documentation link for non-community nodes', () => {
mockUseUsersStore.isInstanceOwner = true;
mockUseUsersStore.isAdmin = true;
const node = mockNode({ name: 'Custom Node', type: 'custom-node' });

const { getByText } = renderComponent(NodeSettingsInvalidNodeWarning, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const isVerifiedCommunityNode = computed(
nodeTypesStore.communityNodeType(node.type)?.isOfficialNode,
);
const npmPackage = computed(() => removePreviewToken(node.type.split('.')[0]));
const isOwner = computed(() => usersStore.isInstanceOwner);
const isAdminOrOwner = computed(() => usersStore.isAdminOrOwner);
const { getQuickConnectOptionByPackageName } = useQuickConnect();
const quickConnect = computed(() => getQuickConnectOptionByPackageName(npmPackage.value));

Expand Down Expand Up @@ -114,10 +114,10 @@ watch(isNodeDefined, () => {
<N8nText size="medium" bold>{{ npmPackage }}</N8nText>
</template>
</I18nT>
<div v-if="isOwner" :class="$style.communityNodeActionsContainer">
<div v-if="isAdminOrOwner" :class="$style.communityNodeActionsContainer">
<N8nButton
variant="solid"
v-if="isOwner"
v-if="isAdminOrOwner"
icon="hard-drive-download"
data-test-id="install-community-node-button"
:loading="loading"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const showError = vi.fn();
const removeNodeFromMergedNodes = vi.fn();

const usersStore = {
isInstanceOwner: true,
isAdminOrOwner: true,
};

vi.mock('@/features/credentials/credentials.store', () => ({
Expand Down Expand Up @@ -211,7 +211,7 @@ describe('CommunityNodeDetails', () => {
});

it('should not render install button if not instance owner', async () => {
usersStore.isInstanceOwner = false;
usersStore.isAdminOrOwner = false;

const wrapper = renderComponent({ pinia });

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const quickConnect = computed(() => {
const nodeCreatorStore = useNodeCreatorStore();
const { installNode, loading } = useInstallNode();

const isOwner = computed(() => useUsersStore().isInstanceOwner);
const isAdminOrOwner = computed(() => useUsersStore().isAdminOrOwner);

const updateViewStack = (key: string) => {
const installedNodeKey = removePreviewToken(key);
Expand Down Expand Up @@ -71,7 +71,11 @@ const updateStoresAndViewStack = (key: string) => {
};

const onInstall = async () => {
if (isOwner.value && activeViewStack.communityNodeDetails && !communityNodeDetails?.installed) {
if (
isAdminOrOwner.value &&
activeViewStack.communityNodeDetails &&
!communityNodeDetails?.installed
) {
const { key, packageName } = activeViewStack.communityNodeDetails;
const result = await installNode({
type: 'verified',
Expand Down Expand Up @@ -123,7 +127,7 @@ const onInstall = async () => {
</div>

<N8nButton
v-if="isOwner && !communityNodeDetails.installed"
v-if="isAdminOrOwner && !communityNodeDetails.installed"
:loading="loading"
:disabled="loading"
:label="i18n.baseText('communityNodeDetails.install')"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ vi.mock('@/app/stores/nodeTypes.store', () => ({

vi.mock('@/features/settings/users/users.store', () => ({
useUsersStore: vi.fn(() => ({
isInstanceOwner: true,
isAdmin: true,
isAdminOrOwner: true,
isInstanceOwner: false,
})),
}));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ const quickConnect = computed(() => {

const nodeTypesStore = useNodeTypesStore();

const isOwner = computed(() => useUsersStore().isInstanceOwner);
const usersStore = useUsersStore();
const isAdminOrOwner = computed(() => usersStore.isAdminOrOwner);

const formatNumber = (number: number) => {
if (!number) return null;
Expand Down Expand Up @@ -175,7 +176,7 @@ onMounted(async () => {
</div>

<QuickConnectBanner v-if="quickConnect" :text="quickConnect?.text" />
<ContactAdministratorToInstall v-if="!isOwner && !communityNodeDetails?.installed" />
<ContactAdministratorToInstall v-if="!isAdminOrOwner && !communityNodeDetails?.installed" />
</div>
</template>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ export interface Props {
hint: string;
}

const isOwner = computed(() => useUsersStore().isInstanceOwner);
const isAdminOrOwner = computed(() => useUsersStore().isAdminOrOwner);

defineProps<Props>();
</script>

<template>
<div v-if="isOwner" :class="$style.container">
<div v-if="isAdminOrOwner" :class="$style.container">
<N8nIcon color="text-light" icon="info" size="large" />
<N8nText color="text-base" size="medium"> {{ hint }} </N8nText>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,15 @@ beforeEach(() => {

vi.mocked(useToast).mockReturnValue(toast);

Object.defineProperty(usersStore, 'isAdmin', {
value: true,
writable: true,
});
Object.defineProperty(usersStore, 'isInstanceOwner', {
value: false,
writable: true,
});
Object.defineProperty(usersStore, 'isAdminOrOwner', {
value: true,
writable: true,
});
Expand Down Expand Up @@ -132,11 +140,19 @@ beforeEach(() => {

describe('useInstallNode', () => {
describe('installNode', () => {
it('should return error when user is not an owner', async () => {
it('should return error when user is not an owner or admin', async () => {
Object.defineProperty(usersStore, 'isAdmin', {
value: false,
writable: true,
});
Object.defineProperty(usersStore, 'isInstanceOwner', {
value: false,
writable: true,
});
Object.defineProperty(usersStore, 'isAdminOrOwner', {
value: false,
writable: true,
});
const { installNode } = useInstallNode();

const result = await installNode({
Expand All @@ -147,13 +163,45 @@ describe('useInstallNode', () => {

expect(result.success).toBe(false);
expect(result.error).toBeInstanceOf(Error);
expect(result.error?.message).toBe('User is not an owner');
expect(result.error?.message).toBe('User is not an owner or admin');
expect(showError).toHaveBeenCalledWith(
expect.any(Error),
'settings.communityNodes.messages.install.error',
);
});

it.each([
{ isAdmin: true, isInstanceOwner: false, label: 'admin' },
{ isAdmin: false, isInstanceOwner: true, label: 'instance owner' },
])('should allow installing when user is $label', async ({ isAdmin, isInstanceOwner }) => {
Object.defineProperty(usersStore, 'isAdmin', {
value: isAdmin,
writable: true,
});
Object.defineProperty(usersStore, 'isInstanceOwner', {
value: isInstanceOwner,
writable: true,
});
Object.defineProperty(usersStore, 'isAdminOrOwner', {
value: isAdmin || isInstanceOwner,
writable: true,
});
const { installNode } = useInstallNode();

const result = await installNode({
type: 'verified',
packageName: 'test-package',
nodeType: 'test-node',
});

expect(result.success).toBe(true);
expect(communityNodesStore.installPackage).toHaveBeenCalledWith(
'test-package',
true,
'1.0.0',
);
});

it('should install verified node with npm version', async () => {
const { installNode } = useInstallNode();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useCommunityNodesStore } from '../communityNodes.store';
import { useCredentialsStore } from '@/features/credentials/credentials.store';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import { useUsersStore } from '@/features/settings/users/users.store';
import { computed, nextTick, ref } from 'vue';
import { nextTick, ref } from 'vue';
import { i18n } from '@n8n/i18n';
import { useToast } from '@/app/composables/useToast';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
Expand Down Expand Up @@ -39,7 +39,7 @@ export function useInstallNode() {
const nodeTypesStore = useNodeTypesStore();
const credentialsStore = useCredentialsStore();
const workflowsStore = useWorkflowsStore();
const isOwner = computed(() => useUsersStore().isInstanceOwner);
const userStore = useUsersStore();
const loading = ref(false);
const toast = useToast();
const canvasOperations = useCanvasOperations();
Expand All @@ -56,8 +56,8 @@ export function useInstallNode() {
};

const installNode = async (props: InstallNodeProps): Promise<InstallNodeResult> => {
if (!isOwner.value) {
const error = new Error('User is not an owner');
if (!userStore.isAdminOrOwner) {
const error = new Error('User is not an owner or admin');
toast.showError(error, i18n.baseText('settings.communityNodes.messages.install.error'));
return { success: false, error };
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ describe('useInstalledCommunityPackage', () => {

it('should compute isUpdateCheckAvailable correctly when user is instance owner and node is community', () => {
const usersStore = mockedStore(useUsersStore);
usersStore.isInstanceOwner = true;
usersStore.isAdminOrOwner = true;
mockIsCommunityPackageName.mockReturnValue(true);

const { isUpdateCheckAvailable } = useInstalledCommunityPackage(
Expand All @@ -86,7 +86,7 @@ describe('useInstalledCommunityPackage', () => {

it('should compute isUpdateCheckAvailable as false when user is not instance owner', () => {
const usersStore = mockedStore(useUsersStore);
usersStore.isInstanceOwner = false;
usersStore.isAdminOrOwner = false;
mockIsCommunityPackageName.mockReturnValue(true);

const { isUpdateCheckAvailable } = useInstalledCommunityPackage(
Expand All @@ -98,7 +98,7 @@ describe('useInstalledCommunityPackage', () => {

it('should compute isUpdateCheckAvailable as false when node is not community', () => {
const usersStore = mockedStore(useUsersStore);
usersStore.isInstanceOwner = true;
usersStore.isAdminOrOwner = true;
mockIsCommunityPackageName.mockReturnValue(false);

const { isUpdateCheckAvailable } = useInstalledCommunityPackage('n8n-nodes-base.HttpRequest');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export function useInstalledCommunityPackage(nodeTypeName?: MaybeRefOrGetter<str
const isUpdateCheckAvailable = computed(() => {
return (
isCommunityNode.value &&
usersStore.isInstanceOwner &&
usersStore.isAdminOrOwner &&
!installedPackage.value?.unverifiedUpdate
);
});
Expand Down
Loading