Skip to content

Commit

Permalink
Export and Import Workflow Tags
Browse files Browse the repository at this point in the history
Support exporting and importing tags of workflows via frontend and cli.

On export, all tag data is included in the json.
- id
- name
- updatedAt
- createdAt

When importing a workflow json to n8n we:
- first check if a tag with the same id and createdAt date exists in the
  database, then we can assume the tag is identical. Changes on the name
  of the tag are now preserved.
- check if a tag with the same name exists on the database.
- create a new tag with the given name.
  • Loading branch information
Lucaber committed Mar 21, 2022
1 parent fa7b12c commit 7c1e65c
Show file tree
Hide file tree
Showing 7 changed files with 110 additions and 12 deletions.
5 changes: 4 additions & 1 deletion packages/cli/commands/export/workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,10 @@ export class ExportWorkflowsCommand extends Command {
}

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const workflows = await Db.collections.Workflow!.find(findQuery);
const workflows = await Db.collections.Workflow!.find({
where: findQuery,
relations: ['tags'],
});

if (workflows.length === 0) {
throw new Error('No workflows found with specified filters.');
Expand Down
33 changes: 23 additions & 10 deletions packages/cli/commands/import/workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,21 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable no-console */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { Command, flags } from '@oclif/command';
import {Command, flags} from '@oclif/command';

import { INode, INodeCredentialsDetails, LoggerProxy } from 'n8n-workflow';
import {INode, INodeCredentialsDetails, ITag, LoggerProxy, setTagsForImport} from 'n8n-workflow';

import * as fs from 'fs';
import * as glob from 'fast-glob';
import { UserSettings } from 'n8n-core';
import { EntityManager, getConnection } from 'typeorm';
import { getLogger } from '../../src/Logger';
import { Db, ICredentialsDb } from '../../src';
import { SharedWorkflow } from '../../src/databases/entities/SharedWorkflow';
import { WorkflowEntity } from '../../src/databases/entities/WorkflowEntity';
import { Role } from '../../src/databases/entities/Role';
import { User } from '../../src/databases/entities/User';
import {UserSettings} from 'n8n-core';
import {EntityManager, getConnection} from 'typeorm';
import {getLogger} from '../../src/Logger';
import {Db, ICredentialsDb} from '../../src';
import {SharedWorkflow} from '../../src/databases/entities/SharedWorkflow';
import {WorkflowEntity} from '../../src/databases/entities/WorkflowEntity';
import {Role} from '../../src/databases/entities/Role';
import {User} from '../../src/databases/entities/User';
import {TagEntity} from '../../src/databases/entities/TagEntity';

const FIX_INSTRUCTION =
'Please fix the database by running ./packages/cli/bin/n8n user-management:reset';
Expand Down Expand Up @@ -83,6 +84,7 @@ export class ImportWorkflowsCommand extends Command {
// Make sure the settings exist
await UserSettings.prepareUserSettings();
const credentials = (await Db.collections.Credentials?.find()) ?? [];
const tags = (await Db.collections.Tag?.find()) ?? [];

let totalImported = 0;

Expand Down Expand Up @@ -111,6 +113,17 @@ export class ImportWorkflowsCommand extends Command {
});
}

// Import tags
await setTagsForImport(
workflow as { tags: ITag[] },
tags as ITag[],
async (name: string): Promise<ITag> => {
const tag = new TagEntity();
tag.name = name;
return this.transactionManager.save<TagEntity>(tag);
},
);

await this.storeWorkflow(workflow, user);
}
});
Expand Down
2 changes: 2 additions & 0 deletions packages/editor-ui/src/Interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,8 @@ export interface ITag {
id: string;
name: string;
usageCount?: number;
createdAt?: string;
updatedAt?: string;
}

export interface ITagRow {
Expand Down
15 changes: 14 additions & 1 deletion packages/editor-ui/src/components/MainSidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -501,10 +501,23 @@ export default mixins(
if (data.id && typeof data.id === 'string') {
data.id = parseInt(data.id, 10);
}
const blob = new Blob([JSON.stringify(data, null, 2)], {
// Add tag info
const exportData: IWorkflowDataUpdate = {
...data,
tags: (tags||[]).map(tagId => {
return {
...this.$store.getters["tags/getTagById"](tagId),
usageCount: undefined,
};
}),
};
const blob = new Blob([JSON.stringify(exportData, null, 2)], {
type: 'application/json;charset=utf-8',
});
let workflowName = this.$store.getters.workflowName || 'unsaved_workflow';
workflowName = workflowName.replace(/[^a-z0-9]/gi, '_');
Expand Down
13 changes: 13 additions & 0 deletions packages/editor-ui/src/views/NodeView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,8 @@ import {
import '../plugins/N8nCustomConnectorType';
import '../plugins/PlusEndpointType';
import { setTagsForImport } from "n8n-workflow/dist/src/WorkflowHelpers";
import {createTag, getTags} from "@/api/tags";
export default mixins(
copyPaste,
Expand Down Expand Up @@ -1213,6 +1215,17 @@ export default mixins(
this.nodeSelectedByName(node.name);
});
});
if (workflowData.tags) {
await setTagsForImport(workflowData as { tags: ITag[] }, await getTags(this.$store.getters.getRestApiContext), (name: string) => {
return createTag(this.$store.getters.getRestApiContext, {
name,
});
});
const tagIds = workflowData.tags.map((tag) => typeof tag === "string" ? tag : tag.id);
this.$store.commit('setWorkflowTagIds', tagIds || []);
}
} catch (error) {
this.$showError(
error,
Expand Down
53 changes: 53 additions & 0 deletions packages/workflow/src/WorkflowHelpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Supports both database entries and frontend api responses
export interface ITag {
id: string | number;
name: string;
createdAt?: string | Date;
updatedAt?: string | Date;
}

// Set tag ids to use existing tags, creates a new tag if no matching tag could be found
export async function setTagsForImport(
workflow: { tags: ITag[] },
tagsEntities: ITag[],
createTag: (name: string) => Promise<ITag>,
): Promise<void> {
const findOrCreateTag = async (importTag: ITag) => {
// Assume tag is identical if createdAt date is the same to preserve a changed tag name
const identicalMatch = tagsEntities.find(
(existingTag) =>
existingTag.id.toString() === importTag.id.toString() &&
existingTag.createdAt &&
importTag.createdAt &&
new Date(existingTag.createdAt) === new Date(importTag.createdAt),
);
if (identicalMatch) {
return identicalMatch;
}

// Find tag with identical name
const nameMatch = tagsEntities.find((existingTag) => existingTag.name === importTag.name);
if (nameMatch) {
return nameMatch;
}

// Create new Tag
const createdTag = await createTag(importTag.name);
// add new tag to available tags
tagsEntities.push(createdTag);
return createdTag;
};
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const workflowTags = workflow.tags;
if (!workflowTags || !Array.isArray(workflowTags) || workflowTags.length === 0) {
return;
}
for (let i = 0; i < workflowTags.length; i++) {
// eslint-disable-next-line no-await-in-loop
const tag = await findOrCreateTag(workflowTags[i]);
workflowTags[i] = {
id: tag.id,
name: tag.name,
};
}
}
1 change: 1 addition & 0 deletions packages/workflow/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ export * from './Workflow';
export * from './WorkflowDataProxy';
export * from './WorkflowErrors';
export * from './WorkflowHooks';
export * from './WorkflowHelpers';
export { LoggerProxy, NodeHelpers, ObservableObject };

0 comments on commit 7c1e65c

Please sign in to comment.