Skip to content
Merged
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 isAdmin = computed(() => usersStore.isAdmin);

// 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 || !isAdmin.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="!isAdmin" />
</div>
</template>

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

describe('Owner permissions', () => {
it('should show install button when user is owner', async () => {
mockUseUsersStore.isInstanceOwner = true;
mockUseUsersStore.isAdmin = true;
const node = mockNode({ name: 'Test Node', type: 'n8n-nodes-test.testNode' });
const { getByTestId } = renderComponent(NodeSettingsInvalidNodeWarning, {
props: {
Expand All @@ -69,7 +69,7 @@ describe('NodeSettingsInvalidNodeWarning', () => {
});

it('should show ContactAdministratorToInstall when user is not owner', async () => {
mockUseUsersStore.isInstanceOwner = false;
mockUseUsersStore.isAdmin = false;
const node = mockNode({ name: 'Test Node', type: 'n8n-nodes-test.testNode' });
const { getByText } = renderComponent(NodeSettingsInvalidNodeWarning, {
props: {
Expand All @@ -86,7 +86,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 +109,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 +135,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 +165,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 +195,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 +228,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 +260,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 +282,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 isAdmin = computed(() => usersStore.isAdmin);
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="isAdmin" :class="$style.communityNodeActionsContainer">
<N8nButton
variant="solid"
v-if="isOwner"
v-if="isAdmin"
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,
isAdmin: 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.isAdmin = false;

const wrapper = renderComponent({ pinia });

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ vi.mock('@/app/stores/nodeTypes.store', () => ({

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

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

const nodeTypesStore = useNodeTypesStore();

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

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

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ beforeEach(() => {

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

Object.defineProperty(usersStore, 'isInstanceOwner', {
Object.defineProperty(usersStore, 'isAdmin', {
value: true,
writable: true,
});
Expand Down Expand Up @@ -133,7 +133,7 @@ beforeEach(() => {
describe('useInstallNode', () => {
describe('installNode', () => {
it('should return error when user is not an owner', async () => {
Object.defineProperty(usersStore, 'isInstanceOwner', {
Object.defineProperty(usersStore, 'isAdmin', {
value: false,
writable: true,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export function useInstallNode() {
const nodeTypesStore = useNodeTypesStore();
const credentialsStore = useCredentialsStore();
const workflowsStore = useWorkflowsStore();
const isOwner = computed(() => useUsersStore().isInstanceOwner);
const isAdmin = computed(() => useUsersStore().isAdmin);
const loading = ref(false);
const toast = useToast();
const canvasOperations = useCanvasOperations();
Expand All @@ -56,7 +56,7 @@ export function useInstallNode() {
};

const installNode = async (props: InstallNodeProps): Promise<InstallNodeResult> => {
if (!isOwner.value) {
if (!isAdmin.value) {
const error = new Error('User is not an owner');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should update the error message to be in line with new behavior:

Suggested change
const error = new Error('User is not an owner');
const error = new Error('User is not an owner or an admin');

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RomanDavydchuk I did search through the code base and found/updated more places with owner checks related to node installation/management. Now the admin will also see the buttons to manage and update nodes - they already had the rights to do so (in the backend/role/scope), it was only not displayed.

toast.showError(error, i18n.baseText('settings.communityNodes.messages.install.error'));
return { success: false, error };
Expand Down
Loading