feat: Table in confirm modal to see all workflows using nodes before updating / uninstalling (#17488)

This commit is contained in:
Michael Kret
2025-08-01 09:41:04 +03:00
committed by GitHub
parent d924d82ee2
commit 76230d2640
12 changed files with 564 additions and 30 deletions

View File

@@ -19,6 +19,7 @@ import type {
FolderWithWorkflowAndSubFolderCount,
ListQuery,
} from '../entities/types-db';
import { buildWorkflowsByNodesQuery } from '../utils/build-workflows-by-nodes-query';
import { isStringArray } from '../utils/is-string-array';
import { TimedQuery } from '../utils/timed-query';
@@ -712,4 +713,22 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
{ parentFolder: toFolderId === PROJECT_ROOT ? null : { id: toFolderId } },
);
}
async findWorkflowsWithNodeType(nodeTypes: string[]) {
if (!nodeTypes?.length) return [];
const qb = this.createQueryBuilder('workflow');
const { whereClause, parameters } = buildWorkflowsByNodesQuery(
nodeTypes,
this.globalConfig.database.type,
);
const workflows: Array<{ id: string; name: string; active: boolean }> = await qb
.select(['workflow.id', 'workflow.name', 'workflow.active'])
.where(whereClause, parameters)
.getMany();
return workflows;
}
}

View File

@@ -0,0 +1,48 @@
import { buildWorkflowsByNodesQuery } from '../build-workflows-by-nodes-query';
describe('WorkflowRepository', () => {
describe('filterWorkflowsByNodesConstructWhereClause', () => {
it('should return the correct WHERE clause and parameters for sqlite', () => {
const nodeTypes = ['HTTP Request', 'Set'];
const expectedInQuery =
"FROM json_each(workflow.nodes) WHERE json_extract(json_each.value, '$.type')";
const expectedParameters = {
nodeType0: 'HTTP Request',
nodeType1: 'Set',
nodeTypes,
};
const { whereClause, parameters } = buildWorkflowsByNodesQuery(nodeTypes, 'sqlite');
expect(whereClause).toContain(expectedInQuery);
expect(parameters).toEqual(expectedParameters);
});
it('should return the correct WHERE clause and parameters for postgresdb', () => {
const nodeTypes = ['HTTP Request', 'Set'];
const expectedInQuery = 'FROM jsonb_array_elements(workflow.nodes::jsonb) AS node';
const expectedParameters = { nodeTypes };
const { whereClause, parameters } = buildWorkflowsByNodesQuery(nodeTypes, 'postgresdb');
expect(whereClause).toContain(expectedInQuery);
expect(parameters).toEqual(expectedParameters);
});
it('should return the correct WHERE clause and parameters for mysqldb', () => {
const nodeTypes = ['HTTP Request', 'Set'];
const expectedWhereClause =
"(JSON_SEARCH(JSON_EXTRACT(workflow.nodes, '$[*].type'), 'one', :nodeType0) IS NOT NULL OR JSON_SEARCH(JSON_EXTRACT(workflow.nodes, '$[*].type'), 'one', :nodeType1) IS NOT NULL)";
const expectedParameters = {
nodeType0: 'HTTP Request',
nodeType1: 'Set',
nodeTypes,
};
const { whereClause, parameters } = buildWorkflowsByNodesQuery(nodeTypes, 'mysqldb');
expect(whereClause).toEqual(expectedWhereClause);
expect(parameters).toEqual(expectedParameters);
});
});
});

View File

@@ -0,0 +1,56 @@
/**
* Builds the WHERE clause and parameters for a query to find workflows by node types
*/
export function buildWorkflowsByNodesQuery(
nodeTypes: string[],
dbType: 'postgresdb' | 'mysqldb' | 'mariadb' | 'sqlite',
) {
let whereClause: string;
const parameters: Record<string, string | string[]> = { nodeTypes };
switch (dbType) {
case 'postgresdb':
whereClause = `EXISTS (
SELECT 1
FROM jsonb_array_elements(workflow.nodes::jsonb) AS node
WHERE node->>'type' = ANY(:nodeTypes)
)`;
break;
case 'mysqldb':
case 'mariadb': {
const conditions = nodeTypes
.map(
(_, i) =>
`JSON_SEARCH(JSON_EXTRACT(workflow.nodes, '$[*].type'), 'one', :nodeType${i}) IS NOT NULL`,
)
.join(' OR ');
whereClause = `(${conditions})`;
nodeTypes.forEach((nodeType, index) => {
parameters[`nodeType${index}`] = nodeType;
});
break;
}
case 'sqlite': {
const conditions = nodeTypes
.map(
(_, i) =>
`EXISTS (SELECT 1 FROM json_each(workflow.nodes) WHERE json_extract(json_each.value, '$.type') = :nodeType${i})`,
)
.join(' OR ');
whereClause = `(${conditions})`;
nodeTypes.forEach((nodeType, index) => {
parameters[`nodeType${index}`] = nodeType;
});
break;
}
default:
throw new Error('Unsupported database type');
}
return { whereClause, parameters };
}

View File

@@ -557,4 +557,25 @@ export class WorkflowService {
})),
);
}
async getWorkflowsWithNodesIncluded(user: User, nodeTypes: string[]) {
const foundWorkflows = await this.workflowRepository.findWorkflowsWithNodeType(nodeTypes);
let { workflows } = await this.workflowRepository.getManyAndCount(
foundWorkflows.map((w) => w.id),
);
if (hasSharing(workflows)) {
workflows = await this.processSharedWorkflows(workflows);
}
workflows = await this.addUserScopes(workflows, user);
this.cleanupSharedField(workflows);
return workflows.map((workflow) => ({
resourceType: 'workflow',
...workflow,
}));
}
}

View File

@@ -1,6 +1,7 @@
import {
ImportWorkflowFromUrlDto,
ManualRunQueryDto,
ROLE,
TransferWorkflowBodyDto,
} from '@n8n/api-types';
import { Logger } from '@n8n/backend-common';
@@ -559,4 +560,31 @@ export class WorkflowsController {
body.destinationParentFolderId,
);
}
@Post('/with-node-types')
async getWorkflowsWithNodesIncluded(req: AuthenticatedRequest, res: express.Response) {
try {
const hasPermission = req.user.role === ROLE.Owner || req.user.role === ROLE.Admin;
if (!hasPermission) {
res.json({ data: [], count: 0 });
return;
}
const { nodeTypes } = req.body as { nodeTypes: string[] };
const workflows = await this.workflowService.getWorkflowsWithNodesIncluded(
req.user,
nodeTypes,
);
res.json({
data: workflows,
count: workflows.length,
});
} catch (maybeError) {
const error = utils.toError(maybeError);
ResponseHelper.reportError(error);
ResponseHelper.sendErrorResponse(res, error);
}
}
}

View File

@@ -1949,16 +1949,20 @@
"settings.communityNodes.messages.update.success.title": "Package updated",
"settings.communityNodes.messages.update.success.message": "{packageName} updated to version {version}",
"settings.communityNodes.messages.update.error.title": "Problem updating package",
"settings.communityNodes.confirmModal.uninstall.title": "Uninstall package?",
"settings.communityNodes.confirmModal.uninstall.title": "Uninstall node package",
"settings.communityNodes.confirmModal.uninstall.message": "Any workflows that use nodes from the {packageName} package won't be able to run. Are you sure?",
"settings.communityNodes.confirmModal.uninstall.buttonLabel": "Uninstall package",
"settings.communityNodes.confirmModal.uninstall.description": "Uninstalling the package will remove every instance of nodes included in this package. The following workflows will be effected:",
"settings.communityNodes.confirmModal.noWorkflowsUsingNodes": "Nodes from this package are not used in any workflows",
"settings.communityNodes.confirmModal.uninstall.buttonLabel": "Confirm uninstall",
"settings.communityNodes.confirmModal.uninstall.buttonLoadingLabel": "Uninstalling",
"settings.communityNodes.confirmModal.update.title": "Update community node package?",
"settings.communityNodes.confirmModal.update.title": "Update node package",
"settings.communityNodes.confirmModal.update.message": "You are about to update {packageName} to version {version}",
"settings.communityNodes.confirmModal.includedNodes": "Package includes: {nodes}",
"settings.communityNodes.confirmModal.update.warning": "This version has not been verified by n8n and may contain breaking changes or bugs.",
"settings.communityNodes.confirmModal.update.description": "We recommend you deactivate workflows that use any of the package's nodes and reactivate them once the update is completed",
"settings.communityNodes.confirmModal.update.buttonLabel": "Update package",
"settings.communityNodes.confirmModal.update.description": "Updating to the latest version will update every instance of these nodes. The following workflows will be effected:",
"settings.communityNodes.confirmModal.update.buttonLabel": "Confirm update",
"settings.communityNodes.confirmModal.update.buttonLoadingLabel": "Updating...",
"settings.communityNodes.confirmModal.cancel": "Cancel",
"settings.goBack": "Go back",
"settings.personal": "Personal",
"settings.personal.basicInformation": "Basic Information",

View File

@@ -8,6 +8,7 @@ import type {
IWorkflowDb,
NewWorkflowResponse,
WorkflowListResource,
WorkflowResource,
} from '@/Interface';
import type { IRestApiContext } from '@n8n/rest-api-client';
import type {
@@ -43,6 +44,17 @@ export async function getWorkflows(context: IRestApiContext, filter?: object, op
});
}
export async function getWorkflowsWithNodesIncluded(context: IRestApiContext, nodeTypes: string[]) {
return await getFullApiResponse<WorkflowResource[]>(
context,
'POST',
'/workflows/with-node-types',
{
nodeTypes,
},
);
}
export async function getWorkflowsAndFolders(
context: IRestApiContext,
filter?: object,

View File

@@ -9,6 +9,13 @@ import { createTestingPinia } from '@pinia/testing';
import { STORES } from '@n8n/stores';
import { COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY } from '@/constants';
const fetchWorkflowsWithNodesIncluded = vi.fn();
vi.mock('@/stores/workflows.store', () => ({
useWorkflowsStore: vi.fn(() => ({
fetchWorkflowsWithNodesIncluded,
})),
}));
const renderComponent = createComponentRenderer(CommunityPackageManageConfirmModal, {
data() {
return {
@@ -28,6 +35,7 @@ const renderComponent = createComponentRenderer(CommunityPackageManageConfirmMod
packageName: 'n8n-nodes-test',
installedVersion: '1.0.0',
updateAvailable: '2.0.0',
installedNodes: [{ name: 'TestNode' }],
},
},
},
@@ -103,4 +111,93 @@ describe('CommunityPackageManageConfirmModal', () => {
const testId = getByTestId('communityPackageManageConfirmModal-warning');
expect(testId).toBeInTheDocument();
});
it('should include table with affected workflows', async () => {
useSettingsStore().setSettings({ ...defaultSettings, communityNodesEnabled: true });
nodeTypesStore.loadNodeTypesIfNotLoaded = vi.fn().mockResolvedValue(undefined);
nodeTypesStore.getCommunityNodeAttributes = vi.fn().mockResolvedValue({ npmVersion: '1.5.0' });
fetchWorkflowsWithNodesIncluded.mockResolvedValue({
data: [
{
id: 'workflow-1',
name: 'Test Workflow 1',
resourceType: 'workflow',
active: true,
createdAt: '2023-01-01T00:00:00.000Z',
updatedAt: '2023-01-01T00:00:00.000Z',
homeProject: {
id: 'project-1',
name: 'Test Project 1',
icon: { type: 'emoji', value: 'test' },
type: 'personal',
createdAt: '2023-01-01T00:00:00.000Z',
updatedAt: '2023-01-01T00:00:00.000Z',
},
isArchived: false,
readOnly: false,
scopes: [],
tags: [],
},
],
});
const screen = renderComponent({
props: {
modalName: 'test-modal',
activePackageName: 'n8n-nodes-test',
mode: 'update',
},
global: {
stubs: {
'router-link': {
template: '<a><slot /></a>',
},
},
plugins: [createTestingPinia()],
},
});
await flushPromises();
const testId = screen.getByTestId('communityPackageManageConfirmModal-warning');
expect(testId).toBeInTheDocument();
expect(screen.getByText('Test Workflow 1')).toBeInTheDocument();
expect(screen.getByText('Test Project 1')).toBeInTheDocument();
expect(screen.getByText('Active')).toBeInTheDocument();
expect(screen.getByText('Confirm update')).toBeInTheDocument();
expect(screen.getByText('Cancel')).toBeInTheDocument();
expect(screen.getByText('Package includes: TestNode')).toBeInTheDocument();
});
it('should notinclude table with affected workflows', async () => {
useSettingsStore().setSettings({ ...defaultSettings, communityNodesEnabled: true });
nodeTypesStore.loadNodeTypesIfNotLoaded = vi.fn().mockResolvedValue(undefined);
nodeTypesStore.getCommunityNodeAttributes = vi.fn().mockResolvedValue({ npmVersion: '1.5.0' });
fetchWorkflowsWithNodesIncluded.mockResolvedValue({
data: [],
});
const screen = renderComponent({
props: {
modalName: 'test-modal',
activePackageName: 'n8n-nodes-test',
mode: 'update',
},
});
await flushPromises();
const testId = screen.getByTestId('communityPackageManageConfirmModal-warning');
expect(testId).toBeInTheDocument();
expect(screen.getByText('Package includes: TestNode')).toBeInTheDocument();
expect(
screen.getByText('Nodes from this package are not used in any workflows'),
).toBeInTheDocument();
});
});

View File

@@ -11,6 +11,10 @@ import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import type { CommunityNodeType } from '@n8n/api-types';
import { useSettingsStore } from '@/stores/settings.store';
import semver from 'semver';
import { N8nText } from '@n8n/design-system';
import { useUIStore } from '@/stores/ui.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
import type { WorkflowResource } from '@/Interface';
export type CommunityPackageManageMode = 'uninstall' | 'update' | 'view-documentation';
@@ -25,6 +29,7 @@ const props = defineProps<Props>();
const communityNodesStore = useCommunityNodesStore();
const nodeTypesStore = useNodeTypesStore();
const settingsStore = useSettingsStore();
const workflowsStore = useWorkflowsStore();
const modalBus = createEventBus();
@@ -34,6 +39,8 @@ const telemetry = useTelemetry();
const loading = ref(false);
const workflowsWithPackageNodes = ref<WorkflowResource[]>([]);
const isUsingVerifiedAndUnverifiedPackages =
settingsStore.isCommunityNodesFeatureEnabled && settingsStore.isUnverifiedPackagesEnabled;
const isUsingVerifiedPackagesOnly =
@@ -53,15 +60,22 @@ const isLatestPackageVerified = ref<boolean>(true);
const packageVersion = ref<string>(communityStorePackage.value.updateAvailable ?? '');
const includedNodes = computed(() => {
return communityStorePackage.value.installedNodes.map((node) => node.name).join(', ');
});
const getModalContent = computed(() => {
if (props.mode === COMMUNITY_PACKAGE_MANAGE_ACTIONS.UNINSTALL) {
return {
title: i18n.baseText('settings.communityNodes.confirmModal.uninstall.title'),
message: i18n.baseText('settings.communityNodes.confirmModal.uninstall.message', {
message: i18n.baseText('settings.communityNodes.confirmModal.includedNodes', {
interpolate: {
packageName: props.activePackageName,
nodes: includedNodes.value,
},
}),
description: workflowsWithPackageNodes.value.length
? i18n.baseText('settings.communityNodes.confirmModal.uninstall.description')
: i18n.baseText('settings.communityNodes.confirmModal.noWorkflowsUsingNodes'),
buttonLabel: i18n.baseText('settings.communityNodes.confirmModal.uninstall.buttonLabel'),
buttonLoadingLabel: i18n.baseText(
'settings.communityNodes.confirmModal.uninstall.buttonLoadingLabel',
@@ -69,19 +83,16 @@ const getModalContent = computed(() => {
};
}
return {
title: i18n.baseText('settings.communityNodes.confirmModal.update.title', {
title: i18n.baseText('settings.communityNodes.confirmModal.update.title'),
message: i18n.baseText('settings.communityNodes.confirmModal.includedNodes', {
interpolate: {
packageName: props.activePackageName,
nodes: includedNodes.value,
},
}),
description: i18n.baseText('settings.communityNodes.confirmModal.update.description'),
description: workflowsWithPackageNodes.value.length
? i18n.baseText('settings.communityNodes.confirmModal.update.description')
: i18n.baseText('settings.communityNodes.confirmModal.noWorkflowsUsingNodes'),
warning: i18n.baseText('settings.communityNodes.confirmModal.update.warning'),
message: i18n.baseText('settings.communityNodes.confirmModal.update.message', {
interpolate: {
packageName: props.activePackageName,
version: packageVersion.value,
},
}),
buttonLabel: i18n.baseText('settings.communityNodes.confirmModal.update.buttonLabel'),
buttonLoadingLabel: i18n.baseText(
'settings.communityNodes.confirmModal.update.buttonLoadingLabel',
@@ -200,11 +211,21 @@ function setPackageVersion() {
}
}
const onClick = async () => {
useUIStore().closeModal(COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY);
};
onMounted(async () => {
if (props.activePackageName) {
await fetchPackageInfo(props.activePackageName);
}
if (communityStorePackage.value?.installedNodes.length) {
const nodeTypes = communityStorePackage.value.installedNodes.map((node) => node.type);
const response = await workflowsStore.fetchWorkflowsWithNodesIncluded(nodeTypes);
workflowsWithPackageNodes.value = response?.data ?? [];
}
setIsVerifiedLatestPackage();
setPackageVersion();
});
@@ -212,7 +233,7 @@ onMounted(async () => {
<template>
<Modal
width="540px"
width="640px"
:name="COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY"
:title="getModalContent.title"
:event-bus="modalBus"
@@ -221,22 +242,32 @@ onMounted(async () => {
:before-close="onModalClose"
>
<template #content>
<n8n-text>{{ getModalContent.message }}</n8n-text>
<div
v-if="mode === COMMUNITY_PACKAGE_MANAGE_ACTIONS.UPDATE"
:class="$style.descriptionContainer"
>
<n8n-info-tip theme="info" type="note" :bold="false">
<span v-text="getModalContent.description"></span>
</n8n-info-tip>
<n8n-notice
v-if="!isLatestPackageVerified"
data-test-id="communityPackageManageConfirmModal-warning"
:content="getModalContent.warning"
/>
<N8nText color="text-dark" :bold="true">{{ getModalContent.message }}</N8nText>
<n8n-notice
v-if="!isLatestPackageVerified"
data-test-id="communityPackageManageConfirmModal-warning"
:content="getModalContent.warning"
/>
<div :class="$style.descriptionContainer">
<N8nText size="medium" color="text-base">
{{ getModalContent.description }}
</N8nText>
</div>
<NodesInWorkflowTable
v-if="workflowsWithPackageNodes?.length"
:data="workflowsWithPackageNodes"
/>
</template>
<template #footer>
<n8n-button
:label="i18n.baseText('settings.communityNodes.confirmModal.cancel')"
size="large"
float="left"
type="secondary"
data-test-id="close-button"
@click="onClick"
/>
<n8n-button
:loading="loading"
:disabled="loading"

View File

@@ -0,0 +1,76 @@
import { createComponentRenderer } from '@/__tests__/render';
import NodesInWorkflowTable from '@/components/NodesInWorkflowTable.vue';
import { describe, it, expect } from 'vitest';
import { screen } from '@testing-library/vue';
import { createTestingPinia } from '@pinia/testing';
import type { WorkflowResource } from '@/Interface';
const mockWorkflows: WorkflowResource[] = [
{
id: 'workflow-1',
name: 'Test Workflow 1',
resourceType: 'workflow',
active: true,
createdAt: '2023-01-01T00:00:00.000Z',
updatedAt: '2023-01-01T00:00:00.000Z',
homeProject: {
id: 'project-1',
name: 'Test Project 1',
icon: { type: 'emoji', value: 'test' },
type: 'personal',
createdAt: '2023-01-01T00:00:00.000Z',
updatedAt: '2023-01-01T00:00:00.000Z',
},
isArchived: false,
readOnly: false,
scopes: [],
tags: [],
},
{
id: 'workflow-2',
name: 'Test Workflow 2',
resourceType: 'workflow',
active: false,
createdAt: '2023-01-01T00:00:00.000Z',
updatedAt: '2023-01-01T00:00:00.000Z',
homeProject: {
id: 'project-2',
name: 'Test Project 2',
icon: { type: 'emoji', value: 'test' },
type: 'personal',
createdAt: '2023-01-01T00:00:00.000Z',
updatedAt: '2023-01-01T00:00:00.000Z',
},
isArchived: false,
readOnly: false,
scopes: [],
tags: [],
},
];
describe('NodesInWorkflowTable', () => {
it('should render workflow data in table rows', () => {
const renderComponent = createComponentRenderer(NodesInWorkflowTable, {
props: {
data: mockWorkflows,
},
global: {
stubs: {
'router-link': {
template: '<a><slot /></a>',
},
},
plugins: [createTestingPinia()],
},
});
renderComponent();
expect(screen.getByText('Test Workflow 1')).toBeInTheDocument();
expect(screen.getByText('Test Workflow 2')).toBeInTheDocument();
expect(screen.getByText('Test Project 1')).toBeInTheDocument();
expect(screen.getByText('Test Project 2')).toBeInTheDocument();
expect(screen.getByText('Active')).toBeInTheDocument();
expect(screen.getByText('Inactive')).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,137 @@
<script lang="ts" setup>
import { ref, computed } from 'vue';
import type { TableHeader } from '@n8n/design-system/components/N8nDataTableServer';
import type { RouteLocationRaw } from 'vue-router';
import { VIEWS } from '@/constants';
import type { WorkflowResource } from '@/Interface';
import ProjectCardBadge from '@/components/Projects/ProjectCardBadge.vue';
import { useI18n } from '@n8n/i18n';
import { ResourceType } from '@/utils/projects.utils';
import { useProjectsStore } from '@/stores/projects.store';
type WorkflowData = WorkflowResource[];
type WorkflowDataItem = WorkflowData[number];
type WorkflowDataKeys = keyof WorkflowDataItem;
const props = defineProps<{
data: WorkflowData;
}>();
const sortBy = defineModel<Array<{ id: WorkflowDataKeys; desc: boolean }>>('sortBy');
const i18n = useI18n();
const projectsStore = useProjectsStore();
const headers = ref<Array<TableHeader<WorkflowDataItem>>>([
{
title: 'Workflow',
key: 'name',
width: 250,
},
{
title: 'Owner',
key: 'homeProject.name',
width: 100,
},
{
title: 'Status',
key: 'active',
width: 30,
},
]);
const sortedItems = computed(() => {
if (!sortBy?.value?.length) return props.data;
const [{ id, desc }] = sortBy.value;
return [...props.data].sort((a, b) => {
if (!a[id] || !b[id]) return 0;
if (a[id] < b[id]) return desc ? 1 : -1;
if (a[id] > b[id]) return desc ? -1 : 1;
return 0;
});
});
const getWorkflowLink = (workflowId: string): RouteLocationRaw => ({
name: VIEWS.WORKFLOW,
params: {
name: workflowId,
},
});
</script>
<template>
<div>
<N8nDataTableServer
v-if="sortedItems?.length"
v-model:sort-by="sortBy"
:headers="headers"
:items="sortedItems"
:items-length="sortedItems.length"
:page-sizes="[sortedItems.length + 1]"
>
<template #[`item.name`]="{ item }">
<router-link :to="getWorkflowLink(item.id)" target="_blank">
<N8nText class="ellipsis" style="color: var(--color-text-base)">{{ item.name }}</N8nText>
</router-link>
</template>
<template #[`item.homeProject.name`]="{ item }">
<div>
<ProjectCardBadge
class="cardBadge"
:resource="item"
:resource-type="ResourceType.Workflow"
:resource-type-label="i18n.baseText('generic.workflow').toLowerCase()"
:personal-project="projectsStore.personalProject"
:show-badge-border="false"
/>
</div>
</template>
<template #[`item.active`]="{ item }">
<span v-if="item.active" class="status active">Active</span>
<span v-else class="status inactive">Inactive</span>
</template>
</N8nDataTableServer>
</div>
</template>
<style scoped>
/* Use deep selector to access internal scroll container in N8nDataTableServer */
:deep(.n8n-data-table-server-wrapper .table-scroll) {
max-height: 275px;
overflow-y: auto;
overflow-x: hidden;
}
.status {
border-style: solid;
border-width: var(--border-width-base);
padding: var(--spacing-4xs);
border-radius: var(--border-radius-base);
}
.active {
color: var(--color-primary);
}
.inactive {
color: var(--color-text-light);
}
.ellipsis {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.2;
width: fit-content;
max-width: 100%;
}
.cardBadge {
margin-right: auto;
width: fit-content;
}
</style>

View File

@@ -648,6 +648,10 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
return workflowData;
}
async function fetchWorkflowsWithNodesIncluded(nodeTypes: string[]) {
return await workflowsApi.getWorkflowsWithNodesIncluded(rootStore.restApiContext, nodeTypes);
}
async function getNewWorkflowData(
name?: string,
projectId?: string,
@@ -2013,6 +2017,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
fetchAllWorkflows,
fetchWorkflowsPage,
fetchWorkflow,
fetchWorkflowsWithNodesIncluded,
getNewWorkflowData,
makeNewWorkflowShareable,
resetWorkflow,