mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
fix: Disable update button when the user is not allowed to update (#19223)
This commit is contained in:
@@ -3438,6 +3438,7 @@
|
|||||||
"insights.upgradeModal.title": "Upgrade to Enterprise",
|
"insights.upgradeModal.title": "Upgrade to Enterprise",
|
||||||
"whatsNew.versionsBehind": "{count} version behind | {count} versions behind",
|
"whatsNew.versionsBehind": "{count} version behind | {count} versions behind",
|
||||||
"whatsNew.update": "Update",
|
"whatsNew.update": "Update",
|
||||||
|
"whatsNew.updateNudgeTooltip": "Only owners can perform updates",
|
||||||
"whatsNew.updateAvailable": "You're currently on version {currentVersion}. Update to {latestVersion} to get {count} versions worth of new features, improvements, and fixes. See what changed",
|
"whatsNew.updateAvailable": "You're currently on version {currentVersion}. Update to {latestVersion} to get {count} versions worth of new features, improvements, and fixes. See what changed",
|
||||||
"whatsNew.updateAvailable.changelogLink": "in the full changelog",
|
"whatsNew.updateAvailable.changelogLink": "in the full changelog",
|
||||||
"workflowDiff.changes": "Changes",
|
"workflowDiff.changes": "Changes",
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ import MainSidebar from '@/components/MainSidebar.vue';
|
|||||||
import { useSettingsStore } from '@/stores/settings.store';
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||||
|
import { useVersionsStore } from '@/stores/versions.store';
|
||||||
|
import { useUsersStore } from '@/stores/users.store';
|
||||||
|
import type { Version } from '@n8n/rest-api-client/api/versions';
|
||||||
|
|
||||||
vi.mock('vue-router', () => ({
|
vi.mock('vue-router', () => ({
|
||||||
useRouter: () => ({}),
|
useRouter: () => ({}),
|
||||||
@@ -18,6 +21,20 @@ let renderComponent: ReturnType<typeof createComponentRenderer>;
|
|||||||
let settingsStore: MockedStore<typeof useSettingsStore>;
|
let settingsStore: MockedStore<typeof useSettingsStore>;
|
||||||
let uiStore: MockedStore<typeof useUIStore>;
|
let uiStore: MockedStore<typeof useUIStore>;
|
||||||
let sourceControlStore: MockedStore<typeof useSourceControlStore>;
|
let sourceControlStore: MockedStore<typeof useSourceControlStore>;
|
||||||
|
let versionsStore: MockedStore<typeof useVersionsStore>;
|
||||||
|
let usersStore: MockedStore<typeof useUsersStore>;
|
||||||
|
|
||||||
|
const mockVersion: Version = {
|
||||||
|
name: '1.2.0',
|
||||||
|
nodes: [],
|
||||||
|
createdAt: '2025-01-01T00:00:00Z',
|
||||||
|
description: 'Test version',
|
||||||
|
documentationUrl: 'https://docs.n8n.io',
|
||||||
|
hasBreakingChange: false,
|
||||||
|
hasSecurityFix: false,
|
||||||
|
hasSecurityIssue: false,
|
||||||
|
securityIssueFixVersion: '',
|
||||||
|
};
|
||||||
|
|
||||||
describe('MainSidebar', () => {
|
describe('MainSidebar', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -27,8 +44,15 @@ describe('MainSidebar', () => {
|
|||||||
settingsStore = mockedStore(useSettingsStore);
|
settingsStore = mockedStore(useSettingsStore);
|
||||||
uiStore = mockedStore(useUIStore);
|
uiStore = mockedStore(useUIStore);
|
||||||
sourceControlStore = mockedStore(useSourceControlStore);
|
sourceControlStore = mockedStore(useSourceControlStore);
|
||||||
|
versionsStore = mockedStore(useVersionsStore);
|
||||||
|
usersStore = mockedStore(useUsersStore);
|
||||||
|
|
||||||
settingsStore.settings = defaultSettings;
|
settingsStore.settings = defaultSettings;
|
||||||
|
|
||||||
|
// Default store values
|
||||||
|
versionsStore.hasVersionUpdates = false;
|
||||||
|
versionsStore.nextVersions = [];
|
||||||
|
usersStore.canUserUpdateVersion = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the sidebar without error', () => {
|
it('renders the sidebar without error', () => {
|
||||||
@@ -51,4 +75,39 @@ describe('MainSidebar', () => {
|
|||||||
expect(queryByTestId('read-only-env-icon') !== null).toBe(shouldRender);
|
expect(queryByTestId('read-only-env-icon') !== null).toBe(shouldRender);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
describe('Version Update CTA', () => {
|
||||||
|
it('should not render version update CTA when hasVersionUpdates is false', () => {
|
||||||
|
versionsStore.hasVersionUpdates = false;
|
||||||
|
usersStore.canUserUpdateVersion = true;
|
||||||
|
|
||||||
|
const { queryByTestId } = renderComponent();
|
||||||
|
|
||||||
|
expect(queryByTestId('version-update-cta-button')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render version update CTA disabled when canUserUpdateVersion is false', () => {
|
||||||
|
versionsStore.hasVersionUpdates = true;
|
||||||
|
versionsStore.nextVersions = [mockVersion];
|
||||||
|
usersStore.canUserUpdateVersion = false;
|
||||||
|
|
||||||
|
const { getByTestId } = renderComponent();
|
||||||
|
|
||||||
|
const updateButton = getByTestId('version-update-cta-button');
|
||||||
|
expect(updateButton).toBeInTheDocument();
|
||||||
|
expect(updateButton).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render version update CTA enabled when canUserUpdateVersion is true and hasVersionUpdates is true', () => {
|
||||||
|
versionsStore.hasVersionUpdates = true;
|
||||||
|
versionsStore.nextVersions = [mockVersion];
|
||||||
|
usersStore.canUserUpdateVersion = true;
|
||||||
|
|
||||||
|
const { getByTestId } = renderComponent();
|
||||||
|
|
||||||
|
const updateButton = getByTestId('version-update-cta-button');
|
||||||
|
expect(updateButton).toBeInTheDocument();
|
||||||
|
expect(updateButton).toBeEnabled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -264,7 +264,12 @@ const mainMenuItems = computed<IMenuItem[]>(() => [
|
|||||||
id: 'version-upgrade-cta',
|
id: 'version-upgrade-cta',
|
||||||
component: VersionUpdateCTA,
|
component: VersionUpdateCTA,
|
||||||
available: versionsStore.hasVersionUpdates,
|
available: versionsStore.hasVersionUpdates,
|
||||||
props: {},
|
props: {
|
||||||
|
disabled: !usersStore.canUserUpdateVersion,
|
||||||
|
tooltipText: !usersStore.canUserUpdateVersion
|
||||||
|
? i18n.baseText('whatsNew.updateNudgeTooltip')
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,12 +1,22 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useVersionsStore } from '@/stores/versions.store';
|
import { useVersionsStore } from '@/stores/versions.store';
|
||||||
import { N8nButton, N8nLink } from '@n8n/design-system';
|
import { N8nButton, N8nLink, N8nTooltip } from '@n8n/design-system';
|
||||||
import { useI18n } from '@n8n/i18n';
|
import { useI18n } from '@n8n/i18n';
|
||||||
import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
|
import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
import { useTelemetry } from '@/composables/useTelemetry';
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
import { VERSIONS_MODAL_KEY } from '@/constants';
|
import { VERSIONS_MODAL_KEY } from '@/constants';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
disabled?: boolean;
|
||||||
|
tooltipText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
disabled: false,
|
||||||
|
tooltipText: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const versionsStore = useVersionsStore();
|
const versionsStore = useVersionsStore();
|
||||||
const uiStore = useUIStore();
|
const uiStore = useUIStore();
|
||||||
@@ -44,13 +54,16 @@ const onUpdateClick = async () => {
|
|||||||
}}
|
}}
|
||||||
</N8nLink>
|
</N8nLink>
|
||||||
|
|
||||||
<N8nButton
|
<N8nTooltip :disabled="!props.tooltipText" :content="props.tooltipText">
|
||||||
:class="$style.button"
|
<N8nButton
|
||||||
:label="i18n.baseText('whatsNew.update')"
|
:class="$style.button"
|
||||||
data-test-id="version-update-cta-button"
|
:label="i18n.baseText('whatsNew.update')"
|
||||||
size="mini"
|
data-test-id="version-update-cta-button"
|
||||||
@click="onUpdateClick"
|
size="mini"
|
||||||
/>
|
:disabled="props.disabled"
|
||||||
|
@click="onUpdateClick"
|
||||||
|
/>
|
||||||
|
</N8nTooltip>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { mockedStore, type MockedStore } from '@/__tests__/utils';
|
|||||||
import { useUIStore } from '@/stores/ui.store';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
import { WHATS_NEW_MODAL_KEY, VERSIONS_MODAL_KEY } from '@/constants';
|
import { WHATS_NEW_MODAL_KEY, VERSIONS_MODAL_KEY } from '@/constants';
|
||||||
import { useVersionsStore } from '@/stores/versions.store';
|
import { useVersionsStore } from '@/stores/versions.store';
|
||||||
|
import { useUsersStore } from '@/stores/users.store';
|
||||||
import type { Version } from '@n8n/rest-api-client/api/versions';
|
import type { Version } from '@n8n/rest-api-client/api/versions';
|
||||||
|
|
||||||
import WhatsNewModal from './WhatsNewModal.vue';
|
import WhatsNewModal from './WhatsNewModal.vue';
|
||||||
@@ -40,6 +41,7 @@ const renderComponent = createComponentRenderer(WhatsNewModal, {
|
|||||||
|
|
||||||
let uiStore: MockedStore<typeof useUIStore>;
|
let uiStore: MockedStore<typeof useUIStore>;
|
||||||
let versionsStore: MockedStore<typeof useVersionsStore>;
|
let versionsStore: MockedStore<typeof useVersionsStore>;
|
||||||
|
let usersStore: MockedStore<typeof useUsersStore>;
|
||||||
|
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
const pageRedirectionHelper = usePageRedirectionHelper();
|
const pageRedirectionHelper = usePageRedirectionHelper();
|
||||||
@@ -67,7 +69,9 @@ describe('WhatsNewModal', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
versionsStore = mockedStore(useVersionsStore);
|
versionsStore = mockedStore(useVersionsStore);
|
||||||
|
usersStore = mockedStore(useUsersStore);
|
||||||
versionsStore.hasVersionUpdates = false;
|
versionsStore.hasVersionUpdates = false;
|
||||||
|
usersStore.canUserUpdateVersion = true;
|
||||||
versionsStore.currentVersion = currentVersion;
|
versionsStore.currentVersion = currentVersion;
|
||||||
versionsStore.latestVersion = currentVersion;
|
versionsStore.latestVersion = currentVersion;
|
||||||
versionsStore.nextVersions = [];
|
versionsStore.nextVersions = [];
|
||||||
@@ -129,7 +133,7 @@ describe('WhatsNewModal', () => {
|
|||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render with update button disabled', async () => {
|
it('should not render update button when no version updates available', async () => {
|
||||||
const { getByTestId, queryByTestId } = renderComponent({
|
const { getByTestId, queryByTestId } = renderComponent({
|
||||||
props: {
|
props: {
|
||||||
data: {
|
data: {
|
||||||
@@ -143,7 +147,7 @@ describe('WhatsNewModal', () => {
|
|||||||
|
|
||||||
expect(screen.getByText("What's New in n8n 1.100.0")).toBeInTheDocument();
|
expect(screen.getByText("What's New in n8n 1.100.0")).toBeInTheDocument();
|
||||||
expect(getByTestId('whats-new-item-1')).toMatchSnapshot();
|
expect(getByTestId('whats-new-item-1')).toMatchSnapshot();
|
||||||
expect(getByTestId('whats-new-modal-update-button')).toBeDisabled();
|
expect(queryByTestId('whats-new-modal-update-button')).not.toBeInTheDocument();
|
||||||
expect(queryByTestId('whats-new-modal-next-versions-link')).not.toBeInTheDocument();
|
expect(queryByTestId('whats-new-modal-next-versions-link')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -219,4 +223,59 @@ describe('WhatsNewModal', () => {
|
|||||||
|
|
||||||
expect(uiStore.openModal).toHaveBeenCalledWith(VERSIONS_MODAL_KEY);
|
expect(uiStore.openModal).toHaveBeenCalledWith(VERSIONS_MODAL_KEY);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Update button permission tests', () => {
|
||||||
|
it('should not render update button when hasVersionUpdates is false', async () => {
|
||||||
|
versionsStore.hasVersionUpdates = false;
|
||||||
|
usersStore.canUserUpdateVersion = true;
|
||||||
|
|
||||||
|
const { queryByTestId } = renderComponent({
|
||||||
|
props: {
|
||||||
|
data: {
|
||||||
|
articleId: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => expect(queryByTestId('whatsNew-modal')).toBeInTheDocument());
|
||||||
|
expect(queryByTestId('whats-new-modal-update-button')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render update button disabled when canUserUpdateVersion is false', async () => {
|
||||||
|
versionsStore.hasVersionUpdates = true;
|
||||||
|
usersStore.canUserUpdateVersion = false;
|
||||||
|
|
||||||
|
const { getByTestId } = renderComponent({
|
||||||
|
props: {
|
||||||
|
data: {
|
||||||
|
articleId: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => expect(getByTestId('whatsNew-modal')).toBeInTheDocument());
|
||||||
|
|
||||||
|
const updateButton = getByTestId('whats-new-modal-update-button');
|
||||||
|
expect(updateButton).toBeInTheDocument();
|
||||||
|
expect(updateButton).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render update button enabled when canUserUpdateVersion is true and hasVersionUpdates is true', async () => {
|
||||||
|
versionsStore.hasVersionUpdates = true;
|
||||||
|
usersStore.canUserUpdateVersion = true;
|
||||||
|
|
||||||
|
const { getByTestId } = renderComponent({
|
||||||
|
props: {
|
||||||
|
data: {
|
||||||
|
articleId: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => expect(getByTestId('whatsNew-modal')).toBeInTheDocument());
|
||||||
|
const updateButton = getByTestId('whats-new-modal-update-button');
|
||||||
|
expect(updateButton).toBeInTheDocument();
|
||||||
|
expect(updateButton).toBeEnabled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,13 +2,22 @@
|
|||||||
import dateformat from 'dateformat';
|
import dateformat from 'dateformat';
|
||||||
import { useI18n } from '@n8n/i18n';
|
import { useI18n } from '@n8n/i18n';
|
||||||
import { RELEASE_NOTES_URL, VERSIONS_MODAL_KEY, WHATS_NEW_MODAL_KEY } from '@/constants';
|
import { RELEASE_NOTES_URL, VERSIONS_MODAL_KEY, WHATS_NEW_MODAL_KEY } from '@/constants';
|
||||||
import { N8nCallout, N8nHeading, N8nIcon, N8nLink, N8nMarkdown, N8nText } from '@n8n/design-system';
|
import {
|
||||||
|
N8nCallout,
|
||||||
|
N8nHeading,
|
||||||
|
N8nIcon,
|
||||||
|
N8nLink,
|
||||||
|
N8nMarkdown,
|
||||||
|
N8nText,
|
||||||
|
N8nTooltip,
|
||||||
|
} from '@n8n/design-system';
|
||||||
import { createEventBus } from '@n8n/utils/event-bus';
|
import { createEventBus } from '@n8n/utils/event-bus';
|
||||||
import { useVersionsStore } from '@/stores/versions.store';
|
import { useVersionsStore } from '@/stores/versions.store';
|
||||||
import { computed, nextTick, ref } from 'vue';
|
import { computed, nextTick, ref } from 'vue';
|
||||||
import { useTelemetry } from '@/composables/useTelemetry';
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
|
import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
|
import { useUsersStore } from '@/stores/users.store';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modalName: string;
|
modalName: string;
|
||||||
@@ -24,6 +33,7 @@ const i18n = useI18n();
|
|||||||
const modalBus = createEventBus();
|
const modalBus = createEventBus();
|
||||||
const versionsStore = useVersionsStore();
|
const versionsStore = useVersionsStore();
|
||||||
const uiStore = useUIStore();
|
const uiStore = useUIStore();
|
||||||
|
const usersStore = useUsersStore();
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
|
|
||||||
const nextVersions = computed(() => versionsStore.nextVersions);
|
const nextVersions = computed(() => versionsStore.nextVersions);
|
||||||
@@ -112,13 +122,20 @@ modalBus.on('opened', () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<n8n-button
|
<N8nTooltip
|
||||||
:size="'large'"
|
v-if="versionsStore.hasVersionUpdates"
|
||||||
:label="i18n.baseText('whatsNew.update')"
|
:disabled="usersStore.canUserUpdateVersion"
|
||||||
:disabled="!versionsStore.hasVersionUpdates"
|
:content="i18n.baseText('whatsNew.updateNudgeTooltip')"
|
||||||
data-test-id="whats-new-modal-update-button"
|
placement="bottom"
|
||||||
@click="onUpdateClick"
|
>
|
||||||
/>
|
<n8n-button
|
||||||
|
:size="'large'"
|
||||||
|
:label="i18n.baseText('whatsNew.update')"
|
||||||
|
:disabled="!usersStore.canUserUpdateVersion"
|
||||||
|
data-test-id="whats-new-modal-update-button"
|
||||||
|
@click="onUpdateClick"
|
||||||
|
/>
|
||||||
|
</N8nTooltip>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #content>
|
<template #content>
|
||||||
|
|||||||
@@ -1,5 +1,381 @@
|
|||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
|
exports[`WhatsNewModal > should not render update button when no version updates available 1`] = `
|
||||||
|
<div
|
||||||
|
class="article"
|
||||||
|
data-test-id="whats-new-item-1"
|
||||||
|
>
|
||||||
|
<h2
|
||||||
|
class="n8n-heading size-xlarge bold"
|
||||||
|
>
|
||||||
|
|
||||||
|
Convert to sub-workflow
|
||||||
|
|
||||||
|
</h2>
|
||||||
|
<div
|
||||||
|
class="n8n-markdown markdown"
|
||||||
|
>
|
||||||
|
<!-- Needed to support YouTube player embeds. HTML rendered here is sanitized. -->
|
||||||
|
<!-- eslint-disable vue/no-v-html -->
|
||||||
|
<div
|
||||||
|
class="markdown"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Large, monolithic workflows can slow things down. They’re harder to maintain, tougher to debug, and more difficult to scale. With sub-workflows, you can take a more modular approach, breaking up big workflows into smaller, manageable parts that are easier to reuse, test, understand, and explain.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Until now, creating sub-workflows required copying and pasting nodes manually, setting up a new workflow from scratch, and reconnecting everything by hand.
|
||||||
|
<strong>
|
||||||
|
Convert to sub-workflow
|
||||||
|
</strong>
|
||||||
|
allows you to simplify this process into a single action, so you can spend more time building and less time restructuring.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
<h3>
|
||||||
|
How it works
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
|
||||||
|
<ol>
|
||||||
|
|
||||||
|
|
||||||
|
<li>
|
||||||
|
Highlight the nodes you want to convert to a sub-workflow. These must:
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
|
||||||
|
|
||||||
|
<li>
|
||||||
|
Be fully connected, meaning no missing steps in between them
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
<li>
|
||||||
|
Start from a single starting node
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
<li>
|
||||||
|
End with a single node
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
<li>
|
||||||
|
Right-click to open the context menu and select
|
||||||
|
<strong>
|
||||||
|
Convert to sub-workflow
|
||||||
|
</strong>
|
||||||
|
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
|
||||||
|
|
||||||
|
<li>
|
||||||
|
Or use the shortcut:
|
||||||
|
<code>
|
||||||
|
Alt + X
|
||||||
|
</code>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
<li>
|
||||||
|
n8n will:
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
|
||||||
|
|
||||||
|
<li>
|
||||||
|
Open a new tab containing the selected nodes
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
<li>
|
||||||
|
Preserve all node parameters as-is
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
<li>
|
||||||
|
Replace the selected nodes in the original workflow with a
|
||||||
|
<strong>
|
||||||
|
Call My Sub-workflow
|
||||||
|
</strong>
|
||||||
|
node
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<em>
|
||||||
|
Note:
|
||||||
|
</em>
|
||||||
|
You will need to manually adjust the field types in the Start and Return nodes in the new sub-workflow.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
<p>
|
||||||
|
This makes it easier to keep workflows modular, performant, and easier to maintain.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Learn more about
|
||||||
|
<a
|
||||||
|
href="https://docs.n8n.io/flow-logic/subworkflows/"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
sub-workflows
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
<p>
|
||||||
|
This release contains performance improvements and bug fixes.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<iframe
|
||||||
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||||
|
allowfullscreen=""
|
||||||
|
frameborder="0"
|
||||||
|
height="315"
|
||||||
|
referrerpolicy="strict-origin-when-cross-origin"
|
||||||
|
src="https://www.youtube-nocookie.com/embed/ZCuL2e4zC_4"
|
||||||
|
title="YouTube video player"
|
||||||
|
width="100%"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Fusce malesuada diam eget tincidunt ultrices. Mauris quis mauris mollis, venenatis risus ut.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
<h2>
|
||||||
|
Second level title
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
|
||||||
|
<h3>
|
||||||
|
Third level title
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
|
||||||
|
<p>
|
||||||
|
This
|
||||||
|
<strong>
|
||||||
|
is bold
|
||||||
|
</strong>
|
||||||
|
, this
|
||||||
|
<em>
|
||||||
|
in italics
|
||||||
|
</em>
|
||||||
|
.
|
||||||
|
<br />
|
||||||
|
|
||||||
|
|
||||||
|
<s>
|
||||||
|
Strikethrough is also something we support
|
||||||
|
</s>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Here’s a peace of code:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<code>
|
||||||
|
const props = defineProps<{
|
||||||
|
modalName: string;
|
||||||
|
data: {
|
||||||
|
articleId: number;
|
||||||
|
};
|
||||||
|
}>();
|
||||||
|
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Inline
|
||||||
|
<code>
|
||||||
|
code also works
|
||||||
|
</code>
|
||||||
|
withing text.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
<p>
|
||||||
|
This is a list:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
|
||||||
|
|
||||||
|
<li>
|
||||||
|
first
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
<li>
|
||||||
|
second
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
<li>
|
||||||
|
third
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
|
||||||
|
<p>
|
||||||
|
And this list is ordered
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
<ol>
|
||||||
|
|
||||||
|
|
||||||
|
<li>
|
||||||
|
foo
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
<li>
|
||||||
|
bar
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
<li>
|
||||||
|
qux
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Dividers:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Three or more…
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Hyphens
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Asterisks
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Underscores
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
|
||||||
|
<details>
|
||||||
|
|
||||||
|
|
||||||
|
<summary>
|
||||||
|
Fixes (4)
|
||||||
|
</summary>
|
||||||
|
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<strong>
|
||||||
|
Credential Storage Issue
|
||||||
|
</strong>
|
||||||
|
Resolved an issue where credentials would occasionally become inaccessible after server restarts
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<strong>
|
||||||
|
Webhook Timeout Handling
|
||||||
|
</strong>
|
||||||
|
Fixed timeout issues with long-running webhook requests
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<strong>
|
||||||
|
Node Connection Validation
|
||||||
|
</strong>
|
||||||
|
Improved validation for node connections to prevent invalid workflow configurations
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<strong>
|
||||||
|
Memory Leak in Execution Engine
|
||||||
|
</strong>
|
||||||
|
Fixed memory leak that could occur during long-running workflow executions
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`WhatsNewModal > should render with update button disabled 1`] = `
|
exports[`WhatsNewModal > should render with update button disabled 1`] = `
|
||||||
<div
|
<div
|
||||||
class="article"
|
class="article"
|
||||||
|
|||||||
@@ -79,6 +79,10 @@ export const useUsersStore = defineStore(STORES.USERS, () => {
|
|||||||
Boolean(currentUser.value?.settings?.easyAIWorkflowOnboarded),
|
Boolean(currentUser.value?.settings?.easyAIWorkflowOnboarded),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const canUserUpdateVersion = computed(() => {
|
||||||
|
return isInstanceOwner.value;
|
||||||
|
});
|
||||||
|
|
||||||
const setEasyAIWorkflowOnboardingDone = () => {
|
const setEasyAIWorkflowOnboardingDone = () => {
|
||||||
if (currentUser.value?.settings) {
|
if (currentUser.value?.settings) {
|
||||||
currentUser.value.settings.easyAIWorkflowOnboarded = true;
|
currentUser.value.settings.easyAIWorkflowOnboarded = true;
|
||||||
@@ -446,6 +450,7 @@ export const useUsersStore = defineStore(STORES.USERS, () => {
|
|||||||
personalizedNodeTypes,
|
personalizedNodeTypes,
|
||||||
userClaimedAiCredits,
|
userClaimedAiCredits,
|
||||||
isEasyAIWorkflowOnboardingDone,
|
isEasyAIWorkflowOnboardingDone,
|
||||||
|
canUserUpdateVersion,
|
||||||
usersLimitNotReached,
|
usersLimitNotReached,
|
||||||
addUsers,
|
addUsers,
|
||||||
loginWithCookie,
|
loginWithCookie,
|
||||||
|
|||||||
Reference in New Issue
Block a user