mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
add project settings
This commit is contained in:
parent
1a915239c6
commit
57f1a3fc59
|
@ -13,9 +13,11 @@ describe('CreateProjectDto', () => {
|
||||||
name: 'with name and emoji icon',
|
name: 'with name and emoji icon',
|
||||||
request: {
|
request: {
|
||||||
name: 'My Awesome Project',
|
name: 'My Awesome Project',
|
||||||
icon: {
|
settings: {
|
||||||
type: 'emoji',
|
icon: {
|
||||||
value: '🚀',
|
type: 'emoji',
|
||||||
|
value: '🚀',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -23,9 +25,11 @@ describe('CreateProjectDto', () => {
|
||||||
name: 'with name and regular icon',
|
name: 'with name and regular icon',
|
||||||
request: {
|
request: {
|
||||||
name: 'My Awesome Project',
|
name: 'My Awesome Project',
|
||||||
icon: {
|
settings: {
|
||||||
type: 'icon',
|
icon: {
|
||||||
value: 'blah',
|
type: 'icon',
|
||||||
|
value: 'blah',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -39,28 +43,31 @@ describe('CreateProjectDto', () => {
|
||||||
test.each([
|
test.each([
|
||||||
{
|
{
|
||||||
name: 'missing name',
|
name: 'missing name',
|
||||||
request: { icon: { type: 'emoji', value: '🚀' } },
|
request: { settings: { icon: { type: 'emoji', value: '🚀' } } },
|
||||||
expectedErrorPath: ['name'],
|
expectedErrorPath: ['name'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'empty name',
|
name: 'empty name',
|
||||||
request: { name: '', icon: { type: 'emoji', value: '🚀' } },
|
request: { name: '', settings: { icon: { type: 'emoji', value: '🚀' } } },
|
||||||
expectedErrorPath: ['name'],
|
expectedErrorPath: ['name'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'name too long',
|
name: 'name too long',
|
||||||
request: { name: 'a'.repeat(256), icon: { type: 'emoji', value: '🚀' } },
|
request: { name: 'a'.repeat(256), settings: { icon: { type: 'emoji', value: '🚀' } } },
|
||||||
expectedErrorPath: ['name'],
|
expectedErrorPath: ['name'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'invalid icon type',
|
name: 'invalid icon type',
|
||||||
request: { name: 'My Awesome Project', icon: { type: 'invalid', value: '🚀' } },
|
request: {
|
||||||
expectedErrorPath: ['icon', 'type'],
|
name: 'My Awesome Project',
|
||||||
|
settings: { icon: { type: 'invalid', value: '🚀' } },
|
||||||
|
},
|
||||||
|
expectedErrorPath: ['settings', 'icon', 'type'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'invalid icon value',
|
name: 'invalid icon value',
|
||||||
request: { name: 'My Awesome Project', icon: { type: 'emoji', value: '' } },
|
request: { name: 'My Awesome Project', settings: { icon: { type: 'emoji', value: '' } } },
|
||||||
expectedErrorPath: ['icon', 'value'],
|
expectedErrorPath: ['settings', 'icon', 'value'],
|
||||||
},
|
},
|
||||||
])('should fail validation for $name', ({ request, expectedErrorPath }) => {
|
])('should fail validation for $name', ({ request, expectedErrorPath }) => {
|
||||||
const result = CreateProjectDto.safeParse(request);
|
const result = CreateProjectDto.safeParse(request);
|
||||||
|
|
|
@ -13,9 +13,11 @@ describe('UpdateProjectDto', () => {
|
||||||
name: 'with name and emoji icon',
|
name: 'with name and emoji icon',
|
||||||
request: {
|
request: {
|
||||||
name: 'My Updated Project',
|
name: 'My Updated Project',
|
||||||
icon: {
|
settings: {
|
||||||
type: 'emoji',
|
icon: {
|
||||||
value: '🚀',
|
type: 'emoji',
|
||||||
|
value: '🚀',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -23,9 +25,11 @@ describe('UpdateProjectDto', () => {
|
||||||
name: 'with name and regular icon',
|
name: 'with name and regular icon',
|
||||||
request: {
|
request: {
|
||||||
name: 'My Updated Project',
|
name: 'My Updated Project',
|
||||||
icon: {
|
settings: {
|
||||||
type: 'icon',
|
icon: {
|
||||||
value: 'blah',
|
type: 'icon',
|
||||||
|
value: 'blah',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -44,9 +48,11 @@ describe('UpdateProjectDto', () => {
|
||||||
name: 'with all fields',
|
name: 'with all fields',
|
||||||
request: {
|
request: {
|
||||||
name: 'My Updated Project',
|
name: 'My Updated Project',
|
||||||
icon: {
|
settings: {
|
||||||
type: 'emoji',
|
icon: {
|
||||||
value: '🚀',
|
type: 'emoji',
|
||||||
|
value: '🚀',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
relations: [
|
relations: [
|
||||||
{
|
{
|
||||||
|
@ -76,13 +82,13 @@ describe('UpdateProjectDto', () => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'invalid icon type',
|
name: 'invalid icon type',
|
||||||
request: { icon: { type: 'invalid', value: '🚀' } },
|
request: { settings: { icon: { type: 'invalid', value: '🚀' } } },
|
||||||
expectedErrorPath: ['icon', 'type'],
|
expectedErrorPath: ['settings', 'icon', 'type'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'invalid icon value',
|
name: 'invalid icon value',
|
||||||
request: { icon: { type: 'emoji', value: '' } },
|
request: { settings: { icon: { type: 'emoji', value: '' } } },
|
||||||
expectedErrorPath: ['icon', 'value'],
|
expectedErrorPath: ['settings', 'icon', 'value'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'invalid relations userId',
|
name: 'invalid relations userId',
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { Z } from 'zod-class';
|
import { Z } from 'zod-class';
|
||||||
|
|
||||||
import { projectIconSchema, projectNameSchema } from '../../schemas/project.schema';
|
import { projectNameSchema, projectSettingsSchema } from '../../schemas/project.schema';
|
||||||
|
|
||||||
export class CreateProjectDto extends Z.class({
|
export class CreateProjectDto extends Z.class({
|
||||||
name: projectNameSchema,
|
name: projectNameSchema,
|
||||||
icon: projectIconSchema.optional(),
|
settings: projectSettingsSchema.optional(),
|
||||||
}) {}
|
}) {}
|
||||||
|
|
|
@ -2,13 +2,13 @@ import { z } from 'zod';
|
||||||
import { Z } from 'zod-class';
|
import { Z } from 'zod-class';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
projectIconSchema,
|
|
||||||
projectNameSchema,
|
projectNameSchema,
|
||||||
|
projectSettingsSchema,
|
||||||
projectRelationSchema,
|
projectRelationSchema,
|
||||||
} from '../../schemas/project.schema';
|
} from '../../schemas/project.schema';
|
||||||
|
|
||||||
export class UpdateProjectDto extends Z.class({
|
export class UpdateProjectDto extends Z.class({
|
||||||
name: projectNameSchema.optional(),
|
name: projectNameSchema.optional(),
|
||||||
icon: projectIconSchema.optional(),
|
settings: projectSettingsSchema.optional(),
|
||||||
relations: z.array(projectRelationSchema).optional(),
|
relations: z.array(projectRelationSchema).optional(),
|
||||||
}) {}
|
}) {}
|
||||||
|
|
|
@ -15,6 +15,7 @@ export { passwordSchema } from './schemas/password.schema';
|
||||||
export type {
|
export type {
|
||||||
ProjectType,
|
ProjectType,
|
||||||
ProjectIcon,
|
ProjectIcon,
|
||||||
|
ProjectSettings,
|
||||||
ProjectRole,
|
ProjectRole,
|
||||||
ProjectRelation,
|
ProjectRelation,
|
||||||
} from './schemas/project.schema';
|
} from './schemas/project.schema';
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import {
|
import {
|
||||||
projectNameSchema,
|
projectNameSchema,
|
||||||
projectTypeSchema,
|
projectTypeSchema,
|
||||||
projectIconSchema,
|
|
||||||
projectRoleSchema,
|
projectRoleSchema,
|
||||||
projectRelationSchema,
|
projectRelationSchema,
|
||||||
|
projectSettingsSchema,
|
||||||
} from '../project.schema';
|
} from '../project.schema';
|
||||||
|
|
||||||
describe('project.schema', () => {
|
describe('project.schema', () => {
|
||||||
|
@ -29,31 +29,33 @@ describe('project.schema', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('projectIconSchema', () => {
|
describe('projectSettingsSchema', () => {
|
||||||
test.each([
|
describe('.icon', () => {
|
||||||
{
|
test.each([
|
||||||
name: 'valid emoji icon',
|
{
|
||||||
value: { type: 'emoji', value: '🚀' },
|
name: 'valid emoji icon',
|
||||||
expected: true,
|
value: { type: 'emoji', value: '🚀' },
|
||||||
},
|
expected: true,
|
||||||
{
|
},
|
||||||
name: 'valid icon',
|
{
|
||||||
value: { type: 'icon', value: 'blah' },
|
name: 'valid icon',
|
||||||
expected: true,
|
value: { type: 'icon', value: 'blah' },
|
||||||
},
|
expected: true,
|
||||||
{
|
},
|
||||||
name: 'invalid icon type',
|
{
|
||||||
value: { type: 'invalid', value: '🚀' },
|
name: 'invalid icon type',
|
||||||
expected: false,
|
value: { type: 'invalid', value: '🚀' },
|
||||||
},
|
expected: false,
|
||||||
{
|
},
|
||||||
name: 'empty icon value',
|
{
|
||||||
value: { type: 'emoji', value: '' },
|
name: 'empty icon value',
|
||||||
expected: false,
|
value: { type: 'emoji', value: '' },
|
||||||
},
|
expected: false,
|
||||||
])('should validate $name', ({ value, expected }) => {
|
},
|
||||||
const result = projectIconSchema.safeParse(value);
|
])('should validate $name', ({ value, expected }) => {
|
||||||
expect(result.success).toBe(expected);
|
const result = projectSettingsSchema.safeParse({ icon: value });
|
||||||
|
expect(result.success).toBe(expected);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -5,12 +5,20 @@ export const projectNameSchema = z.string().min(1).max(255);
|
||||||
export const projectTypeSchema = z.enum(['personal', 'team']);
|
export const projectTypeSchema = z.enum(['personal', 'team']);
|
||||||
export type ProjectType = z.infer<typeof projectTypeSchema>;
|
export type ProjectType = z.infer<typeof projectTypeSchema>;
|
||||||
|
|
||||||
export const projectIconSchema = z.object({
|
const projectIconSchema = z.object({
|
||||||
type: z.enum(['emoji', 'icon']),
|
type: z.enum(['emoji', 'icon']),
|
||||||
value: z.string().min(1),
|
value: z.string().min(1),
|
||||||
});
|
});
|
||||||
export type ProjectIcon = z.infer<typeof projectIconSchema>;
|
export type ProjectIcon = z.infer<typeof projectIconSchema>;
|
||||||
|
|
||||||
|
export const projectSettingsSchema = z.object({
|
||||||
|
icon: projectIconSchema.optional(),
|
||||||
|
dedicatedQueue: z.boolean().default(false).optional(),
|
||||||
|
// TODO: add timezone, callerPolicy, saving options, etc
|
||||||
|
// TODO: add 'workflowsFromSameProject` to `CallerPolicy`
|
||||||
|
});
|
||||||
|
export type ProjectSettings = z.infer<typeof projectSettingsSchema>;
|
||||||
|
|
||||||
export const projectRoleSchema = z.enum([
|
export const projectRoleSchema = z.enum([
|
||||||
'project:personalOwner', // personalOwner is only used for personal projects
|
'project:personalOwner', // personalOwner is only used for personal projects
|
||||||
'project:admin',
|
'project:admin',
|
||||||
|
|
|
@ -171,7 +171,7 @@ export class ProjectController {
|
||||||
_res: Response,
|
_res: Response,
|
||||||
@Param('projectId') projectId: string,
|
@Param('projectId') projectId: string,
|
||||||
): Promise<ProjectRequest.ProjectWithRelations> {
|
): Promise<ProjectRequest.ProjectWithRelations> {
|
||||||
const [{ id, name, icon, type }, relations] = await Promise.all([
|
const [{ id, name, type, settings }, relations] = await Promise.all([
|
||||||
this.projectsService.getProject(projectId),
|
this.projectsService.getProject(projectId),
|
||||||
this.projectsService.getProjectRelations(projectId),
|
this.projectsService.getProjectRelations(projectId),
|
||||||
]);
|
]);
|
||||||
|
@ -180,8 +180,8 @@ export class ProjectController {
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
icon,
|
|
||||||
type,
|
type,
|
||||||
|
settings,
|
||||||
relations: relations.map((r) => ({
|
relations: relations.map((r) => ({
|
||||||
id: r.user.id,
|
id: r.user.id,
|
||||||
email: r.user.email,
|
email: r.user.email,
|
||||||
|
@ -206,9 +206,9 @@ export class ProjectController {
|
||||||
@Body payload: UpdateProjectDto,
|
@Body payload: UpdateProjectDto,
|
||||||
@Param('projectId') projectId: string,
|
@Param('projectId') projectId: string,
|
||||||
) {
|
) {
|
||||||
const { name, icon, relations } = payload;
|
const { name, settings, relations } = payload;
|
||||||
if (name || icon) {
|
if (name || settings) {
|
||||||
await this.projectsService.updateProject(projectId, { name, icon });
|
await this.projectsService.updateProject(projectId, { name, settings });
|
||||||
}
|
}
|
||||||
if (relations) {
|
if (relations) {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -129,6 +129,12 @@ export class Column {
|
||||||
options.default = isSqlite
|
options.default = isSqlite
|
||||||
? "STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')"
|
? "STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')"
|
||||||
: 'CURRENT_TIMESTAMP(3)';
|
: 'CURRENT_TIMESTAMP(3)';
|
||||||
|
}
|
||||||
|
if (type === 'json' && isSqlite) {
|
||||||
|
if (typeof this.defaultValue !== 'string') {
|
||||||
|
this.defaultValue = JSON.stringify(this.defaultValue);
|
||||||
|
}
|
||||||
|
options.default = `'${this.defaultValue as string}'`;
|
||||||
} else {
|
} else {
|
||||||
options.default = this.defaultValue;
|
options.default = this.defaultValue;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { ProjectIcon, ProjectType } from '@n8n/api-types';
|
import { ProjectSettings, ProjectType } from '@n8n/api-types';
|
||||||
import { Column, Entity, OneToMany } from '@n8n/typeorm';
|
import { Column, Entity, OneToMany } from '@n8n/typeorm';
|
||||||
|
|
||||||
import { WithTimestampsAndStringId } from './abstract-entity';
|
import { jsonColumnType, WithTimestampsAndStringId } from './abstract-entity';
|
||||||
import type { ProjectRelation } from './project-relation';
|
import type { ProjectRelation } from './project-relation';
|
||||||
import type { SharedCredentials } from './shared-credentials';
|
import type { SharedCredentials } from './shared-credentials';
|
||||||
import type { SharedWorkflow } from './shared-workflow';
|
import type { SharedWorkflow } from './shared-workflow';
|
||||||
|
@ -14,8 +14,8 @@ export class Project extends WithTimestampsAndStringId {
|
||||||
@Column({ type: 'varchar', length: 36 })
|
@Column({ type: 'varchar', length: 36 })
|
||||||
type: ProjectType;
|
type: ProjectType;
|
||||||
|
|
||||||
@Column({ type: 'json', nullable: true })
|
@Column({ type: jsonColumnType })
|
||||||
icon: ProjectIcon;
|
settings: ProjectSettings = {};
|
||||||
|
|
||||||
@OneToMany('ProjectRelation', 'project')
|
@OneToMany('ProjectRelation', 'project')
|
||||||
projectRelations: ProjectRelation[];
|
projectRelations: ProjectRelation[];
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
import type { MigrationContext, ReversibleMigration } from '@/databases/types';
|
||||||
|
|
||||||
|
export class AddProjectSettings1738496517619 implements ReversibleMigration {
|
||||||
|
async up({ schemaBuilder: { addColumns, dropColumns, column } }: MigrationContext) {
|
||||||
|
// TODO: copy over the icons data before dropping that column
|
||||||
|
await addColumns('project', [column('settings').json.notNull.default({})]);
|
||||||
|
await dropColumns('project', ['icon']);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down({ schemaBuilder: { addColumns, dropColumns, column } }: MigrationContext) {
|
||||||
|
await addColumns('project', [column('icon').json]);
|
||||||
|
await dropColumns('project', ['settings']);
|
||||||
|
}
|
||||||
|
}
|
|
@ -78,6 +78,7 @@ import { AddMockedNodesColumnToTestDefinition1733133775640 } from '../common/173
|
||||||
import { AddManagedColumnToCredentialsTable1734479635324 } from '../common/1734479635324-AddManagedColumnToCredentialsTable';
|
import { AddManagedColumnToCredentialsTable1734479635324 } from '../common/1734479635324-AddManagedColumnToCredentialsTable';
|
||||||
import { AddStatsColumnsToTestRun1736172058779 } from '../common/1736172058779-AddStatsColumnsToTestRun';
|
import { AddStatsColumnsToTestRun1736172058779 } from '../common/1736172058779-AddStatsColumnsToTestRun';
|
||||||
import { CreateTestCaseExecutionTable1736947513045 } from '../common/1736947513045-CreateTestCaseExecutionTable';
|
import { CreateTestCaseExecutionTable1736947513045 } from '../common/1736947513045-CreateTestCaseExecutionTable';
|
||||||
|
import { AddProjectSettings1738496517619 } from '../common/1738496517619-AddProjectSettings';
|
||||||
|
|
||||||
export const mysqlMigrations: Migration[] = [
|
export const mysqlMigrations: Migration[] = [
|
||||||
InitialMigration1588157391238,
|
InitialMigration1588157391238,
|
||||||
|
@ -158,4 +159,5 @@ export const mysqlMigrations: Migration[] = [
|
||||||
AddProjectIcons1729607673469,
|
AddProjectIcons1729607673469,
|
||||||
AddStatsColumnsToTestRun1736172058779,
|
AddStatsColumnsToTestRun1736172058779,
|
||||||
CreateTestCaseExecutionTable1736947513045,
|
CreateTestCaseExecutionTable1736947513045,
|
||||||
|
AddProjectSettings1738496517619,
|
||||||
];
|
];
|
||||||
|
|
|
@ -78,6 +78,7 @@ import { AddMockedNodesColumnToTestDefinition1733133775640 } from '../common/173
|
||||||
import { AddManagedColumnToCredentialsTable1734479635324 } from '../common/1734479635324-AddManagedColumnToCredentialsTable';
|
import { AddManagedColumnToCredentialsTable1734479635324 } from '../common/1734479635324-AddManagedColumnToCredentialsTable';
|
||||||
import { AddStatsColumnsToTestRun1736172058779 } from '../common/1736172058779-AddStatsColumnsToTestRun';
|
import { AddStatsColumnsToTestRun1736172058779 } from '../common/1736172058779-AddStatsColumnsToTestRun';
|
||||||
import { CreateTestCaseExecutionTable1736947513045 } from '../common/1736947513045-CreateTestCaseExecutionTable';
|
import { CreateTestCaseExecutionTable1736947513045 } from '../common/1736947513045-CreateTestCaseExecutionTable';
|
||||||
|
import { AddProjectSettings1738496517619 } from '../common/1738496517619-AddProjectSettings';
|
||||||
|
|
||||||
export const postgresMigrations: Migration[] = [
|
export const postgresMigrations: Migration[] = [
|
||||||
InitialMigration1587669153312,
|
InitialMigration1587669153312,
|
||||||
|
@ -158,4 +159,5 @@ export const postgresMigrations: Migration[] = [
|
||||||
AddProjectIcons1729607673469,
|
AddProjectIcons1729607673469,
|
||||||
AddStatsColumnsToTestRun1736172058779,
|
AddStatsColumnsToTestRun1736172058779,
|
||||||
CreateTestCaseExecutionTable1736947513045,
|
CreateTestCaseExecutionTable1736947513045,
|
||||||
|
AddProjectSettings1738496517619,
|
||||||
];
|
];
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { AddProjectSettings1738496517619 as BaseMigration } from '../common/1738496517619-AddProjectSettings';
|
||||||
|
|
||||||
|
export class AddProjectSettings1738496517619 extends BaseMigration {
|
||||||
|
transaction = false as const;
|
||||||
|
}
|
|
@ -42,6 +42,7 @@ import { AddMissingPrimaryKeyOnAnnotationTagMapping1728659839644 } from './17286
|
||||||
import { AddProjectIcons1729607673469 } from './1729607673469-AddProjectIcons';
|
import { AddProjectIcons1729607673469 } from './1729607673469-AddProjectIcons';
|
||||||
import { AddDescriptionToTestDefinition1731404028106 } from './1731404028106-AddDescriptionToTestDefinition';
|
import { AddDescriptionToTestDefinition1731404028106 } from './1731404028106-AddDescriptionToTestDefinition';
|
||||||
import { MigrateTestDefinitionKeyToString1731582748663 } from './1731582748663-MigrateTestDefinitionKeyToString';
|
import { MigrateTestDefinitionKeyToString1731582748663 } from './1731582748663-MigrateTestDefinitionKeyToString';
|
||||||
|
import { AddProjectSettings1738496517619 } from './1738496517619-AddProjectSettings';
|
||||||
import { UniqueWorkflowNames1620821879465 } from '../common/1620821879465-UniqueWorkflowNames';
|
import { UniqueWorkflowNames1620821879465 } from '../common/1620821879465-UniqueWorkflowNames';
|
||||||
import { UpdateWorkflowCredentials1630330987096 } from '../common/1630330987096-UpdateWorkflowCredentials';
|
import { UpdateWorkflowCredentials1630330987096 } from '../common/1630330987096-UpdateWorkflowCredentials';
|
||||||
import { AddNodeIds1658930531669 } from '../common/1658930531669-AddNodeIds';
|
import { AddNodeIds1658930531669 } from '../common/1658930531669-AddNodeIds';
|
||||||
|
@ -152,6 +153,7 @@ const sqliteMigrations: Migration[] = [
|
||||||
AddProjectIcons1729607673469,
|
AddProjectIcons1729607673469,
|
||||||
AddStatsColumnsToTestRun1736172058779,
|
AddStatsColumnsToTestRun1736172058779,
|
||||||
CreateTestCaseExecutionTable1736947513045,
|
CreateTestCaseExecutionTable1736947513045,
|
||||||
|
AddProjectSettings1738496517619,
|
||||||
];
|
];
|
||||||
|
|
||||||
export { sqliteMigrations };
|
export { sqliteMigrations };
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { ProjectIcon, ProjectRole, ProjectType } from '@n8n/api-types';
|
import type { ProjectRole, ProjectType, ProjectSettings } from '@n8n/api-types';
|
||||||
import type { Scope } from '@n8n/permissions';
|
import type { Scope } from '@n8n/permissions';
|
||||||
import type express from 'express';
|
import type express from 'express';
|
||||||
import type {
|
import type {
|
||||||
|
@ -119,7 +119,7 @@ export namespace ListQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
type SlimUser = Pick<IUser, 'id' | 'email' | 'firstName' | 'lastName'>;
|
type SlimUser = Pick<IUser, 'id' | 'email' | 'firstName' | 'lastName'>;
|
||||||
export type SlimProject = Pick<Project, 'id' | 'type' | 'name' | 'icon'>;
|
export type SlimProject = Pick<Project, 'id' | 'type' | 'name' | 'settings'>;
|
||||||
|
|
||||||
export function hasSharing(
|
export function hasSharing(
|
||||||
workflows: ListQuery.Workflow.Plain[] | ListQuery.Workflow.WithSharing[],
|
workflows: ListQuery.Workflow.Plain[] | ListQuery.Workflow.WithSharing[],
|
||||||
|
@ -383,8 +383,8 @@ export declare namespace ProjectRequest {
|
||||||
type ProjectWithRelations = {
|
type ProjectWithRelations = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string | undefined;
|
name: string | undefined;
|
||||||
icon: ProjectIcon;
|
|
||||||
type: ProjectType;
|
type: ProjectType;
|
||||||
|
settings: ProjectSettings;
|
||||||
relations: ProjectRelationResponse[];
|
relations: ProjectRelationResponse[];
|
||||||
scopes: Scope[];
|
scopes: Scope[];
|
||||||
};
|
};
|
||||||
|
|
|
@ -37,7 +37,9 @@ import type {
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class ScalingService {
|
export class ScalingService {
|
||||||
private queue: JobQueue;
|
private sharedQueue: JobQueue;
|
||||||
|
|
||||||
|
private readonly dedicatedQueues = new Map<string, JobQueue>();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly logger: Logger,
|
private readonly logger: Logger,
|
||||||
|
@ -63,7 +65,7 @@ export class ScalingService {
|
||||||
const bullPrefix = this.globalConfig.queue.bull.prefix;
|
const bullPrefix = this.globalConfig.queue.bull.prefix;
|
||||||
const prefix = service.toValidPrefix(bullPrefix);
|
const prefix = service.toValidPrefix(bullPrefix);
|
||||||
|
|
||||||
this.queue = new BullQueue(QUEUE_NAME, {
|
this.sharedQueue = new BullQueue(QUEUE_NAME, {
|
||||||
prefix,
|
prefix,
|
||||||
settings: this.globalConfig.queue.bull.settings,
|
settings: this.globalConfig.queue.bull.settings,
|
||||||
createClient: (type) => service.createClient({ type: `${type}(bull)` }),
|
createClient: (type) => service.createClient({ type: `${type}(bull)` }),
|
||||||
|
@ -90,7 +92,7 @@ export class ScalingService {
|
||||||
this.assertWorker();
|
this.assertWorker();
|
||||||
this.assertQueue();
|
this.assertQueue();
|
||||||
|
|
||||||
void this.queue.process(JOB_TYPE_NAME, concurrency, async (job: Job) => {
|
void this.sharedQueue.process(JOB_TYPE_NAME, concurrency, async (job: Job) => {
|
||||||
try {
|
try {
|
||||||
if (!this.hasValidJobData(job)) {
|
if (!this.hasValidJobData(job)) {
|
||||||
throw new ApplicationError('Worker received invalid job', {
|
throw new ApplicationError('Worker received invalid job', {
|
||||||
|
@ -141,7 +143,7 @@ export class ScalingService {
|
||||||
|
|
||||||
private async stopMain() {
|
private async stopMain() {
|
||||||
if (this.instanceSettings.isSingleMain) {
|
if (this.instanceSettings.isSingleMain) {
|
||||||
await this.queue.pause(true, true); // no more jobs will be picked up
|
await this.sharedQueue.pause(true, true); // no more jobs will be picked up
|
||||||
this.logger.debug('Queue paused');
|
this.logger.debug('Queue paused');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -164,7 +166,7 @@ export class ScalingService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async pingQueue() {
|
async pingQueue() {
|
||||||
await this.queue.client.ping();
|
await this.sharedQueue.client.ping();
|
||||||
}
|
}
|
||||||
|
|
||||||
// #endregion
|
// #endregion
|
||||||
|
@ -172,7 +174,7 @@ export class ScalingService {
|
||||||
// #region Jobs
|
// #region Jobs
|
||||||
|
|
||||||
async getPendingJobCounts() {
|
async getPendingJobCounts() {
|
||||||
const { active, waiting } = await this.queue.getJobCounts();
|
const { active, waiting } = await this.sharedQueue.getJobCounts();
|
||||||
|
|
||||||
return { active, waiting };
|
return { active, waiting };
|
||||||
}
|
}
|
||||||
|
@ -192,7 +194,9 @@ export class ScalingService {
|
||||||
removeOnFail: true,
|
removeOnFail: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const job = await this.queue.add(JOB_TYPE_NAME, jobData, jobOptions);
|
const queue = this.dedicatedQueues.get(jobData.projectId!) ?? this.sharedQueue;
|
||||||
|
|
||||||
|
const job = await queue.add(JOB_TYPE_NAME, jobData, jobOptions);
|
||||||
|
|
||||||
const { executionId } = jobData;
|
const { executionId } = jobData;
|
||||||
const jobId = job.id;
|
const jobId = job.id;
|
||||||
|
@ -203,11 +207,15 @@ export class ScalingService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async getJob(jobId: JobId) {
|
async getJob(jobId: JobId) {
|
||||||
return await this.queue.getJob(jobId);
|
// TODO: keep a jobId -> projectId mapping in redis
|
||||||
|
// TODO: use the project specific queue if available
|
||||||
|
return await this.sharedQueue.getJob(jobId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async findJobsByStatus(statuses: JobStatus[]) {
|
async findJobsByStatus(statuses: JobStatus[]) {
|
||||||
const jobs = await this.queue.getJobs(statuses);
|
// TODO: keep a jobId -> projectId mapping in redis
|
||||||
|
// TODO: use the project specific queue if available
|
||||||
|
const jobs = await this.sharedQueue.getJobs(statuses);
|
||||||
|
|
||||||
return jobs.filter((job) => job !== null);
|
return jobs.filter((job) => job !== null);
|
||||||
}
|
}
|
||||||
|
@ -261,13 +269,13 @@ export class ScalingService {
|
||||||
* Register listeners on a `worker` process for Bull queue events.
|
* Register listeners on a `worker` process for Bull queue events.
|
||||||
*/
|
*/
|
||||||
private registerWorkerListeners() {
|
private registerWorkerListeners() {
|
||||||
this.queue.on('global:progress', (jobId: JobId, msg: unknown) => {
|
this.sharedQueue.on('global:progress', (jobId: JobId, msg: unknown) => {
|
||||||
if (!this.isJobMessage(msg)) return;
|
if (!this.isJobMessage(msg)) return;
|
||||||
|
|
||||||
if (msg.kind === 'abort-job') this.jobProcessor.stopJob(jobId);
|
if (msg.kind === 'abort-job') this.jobProcessor.stopJob(jobId);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.queue.on('error', (error: Error) => {
|
this.sharedQueue.on('error', (error: Error) => {
|
||||||
if ('code' in error && error.code === 'ECONNREFUSED') return; // handled by RedisClientService.retryStrategy
|
if ('code' in error && error.code === 'ECONNREFUSED') return; // handled by RedisClientService.retryStrategy
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -290,7 +298,7 @@ export class ScalingService {
|
||||||
* Register listeners on a `main` or `webhook` process for Bull queue events.
|
* Register listeners on a `main` or `webhook` process for Bull queue events.
|
||||||
*/
|
*/
|
||||||
private registerMainOrWebhookListeners() {
|
private registerMainOrWebhookListeners() {
|
||||||
this.queue.on('error', (error: Error) => {
|
this.sharedQueue.on('error', (error: Error) => {
|
||||||
if ('code' in error && error.code === 'ECONNREFUSED') return; // handled by RedisClientService.retryStrategy
|
if ('code' in error && error.code === 'ECONNREFUSED') return; // handled by RedisClientService.retryStrategy
|
||||||
|
|
||||||
this.logger.error('Queue errored', { error });
|
this.logger.error('Queue errored', { error });
|
||||||
|
@ -298,7 +306,7 @@ export class ScalingService {
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.queue.on('global:progress', (jobId: JobId, msg: unknown) => {
|
this.sharedQueue.on('global:progress', (jobId: JobId, msg: unknown) => {
|
||||||
if (!this.isJobMessage(msg)) return;
|
if (!this.isJobMessage(msg)) return;
|
||||||
|
|
||||||
// completion and failure are reported via `global:progress` to convey more details
|
// completion and failure are reported via `global:progress` to convey more details
|
||||||
|
@ -338,8 +346,8 @@ export class ScalingService {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.isQueueMetricsEnabled) {
|
if (this.isQueueMetricsEnabled) {
|
||||||
this.queue.on('global:completed', () => this.jobCounters.completed++);
|
this.sharedQueue.on('global:completed', () => this.jobCounters.completed++);
|
||||||
this.queue.on('global:failed', () => this.jobCounters.failed++);
|
this.sharedQueue.on('global:failed', () => this.jobCounters.failed++);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -367,7 +375,7 @@ export class ScalingService {
|
||||||
}
|
}
|
||||||
|
|
||||||
private assertQueue() {
|
private assertQueue() {
|
||||||
if (this.queue) return;
|
if (this.sharedQueue) return;
|
||||||
|
|
||||||
throw new ApplicationError('This method must be called after `setupQueue`');
|
throw new ApplicationError('This method must be called after `setupQueue`');
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ export type JobId = Job['id'];
|
||||||
|
|
||||||
export type JobData = {
|
export type JobData = {
|
||||||
executionId: string;
|
executionId: string;
|
||||||
|
projectId?: string;
|
||||||
loadStaticData: boolean;
|
loadStaticData: boolean;
|
||||||
pushRef?: string;
|
pushRef?: string;
|
||||||
};
|
};
|
||||||
|
|
|
@ -81,21 +81,17 @@ export class OwnershipService {
|
||||||
|
|
||||||
for (const sharedEntity of shared) {
|
for (const sharedEntity of shared) {
|
||||||
const { project, role } = sharedEntity;
|
const { project, role } = sharedEntity;
|
||||||
|
const slimProject = {
|
||||||
|
id: project.id,
|
||||||
|
type: project.type,
|
||||||
|
name: project.name,
|
||||||
|
settings: project.settings,
|
||||||
|
};
|
||||||
|
|
||||||
if (role === 'credential:owner' || role === 'workflow:owner') {
|
if (role === 'credential:owner' || role === 'workflow:owner') {
|
||||||
entity.homeProject = {
|
entity.homeProject = slimProject;
|
||||||
id: project.id,
|
|
||||||
type: project.type,
|
|
||||||
name: project.name,
|
|
||||||
icon: project.icon,
|
|
||||||
};
|
|
||||||
} else {
|
} else {
|
||||||
entity.sharedWithProjects.push({
|
entity.sharedWithProjects.push(slimProject);
|
||||||
id: project.id,
|
|
||||||
type: project.type,
|
|
||||||
name: project.name,
|
|
||||||
icon: project.icon,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -188,7 +188,7 @@ export class ProjectService {
|
||||||
|
|
||||||
async updateProject(
|
async updateProject(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
data: Pick<UpdateProjectDto, 'name' | 'icon'>,
|
data: Pick<UpdateProjectDto, 'name' | 'settings'>,
|
||||||
): Promise<Project> {
|
): Promise<Project> {
|
||||||
const result = await this.projectRepository.update({ id: projectId, type: 'team' }, data);
|
const result = await this.projectRepository.update({ id: projectId, type: 'team' }, data);
|
||||||
|
|
||||||
|
|
|
@ -344,6 +344,7 @@ export class WorkflowRunner {
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const jobData: JobData = {
|
const jobData: JobData = {
|
||||||
executionId,
|
executionId,
|
||||||
|
projectId: data.projectId,
|
||||||
loadStaticData: !!loadStaticData,
|
loadStaticData: !!loadStaticData,
|
||||||
pushRef: data.pushRef,
|
pushRef: data.pushRef,
|
||||||
};
|
};
|
||||||
|
|
|
@ -10,7 +10,9 @@ import { ProjectTypes } from '@/types/projects.types';
|
||||||
export const createProjectSharingData = (projectType?: ProjectType): ProjectSharingData => ({
|
export const createProjectSharingData = (projectType?: ProjectType): ProjectSharingData => ({
|
||||||
id: faker.string.uuid(),
|
id: faker.string.uuid(),
|
||||||
name: faker.lorem.words({ min: 1, max: 3 }),
|
name: faker.lorem.words({ min: 1, max: 3 }),
|
||||||
icon: { type: 'icon', value: 'folder' },
|
settings: {
|
||||||
|
icon: { type: 'icon', value: 'folder' },
|
||||||
|
},
|
||||||
type: projectType ?? ProjectTypes.Personal,
|
type: projectType ?? ProjectTypes.Personal,
|
||||||
createdAt: faker.date.past().toISOString(),
|
createdAt: faker.date.past().toISOString(),
|
||||||
updatedAt: faker.date.recent().toISOString(),
|
updatedAt: faker.date.recent().toISOString(),
|
||||||
|
@ -30,7 +32,9 @@ export function createTestProject(data: Partial<Project>): Project {
|
||||||
return {
|
return {
|
||||||
id: faker.string.uuid(),
|
id: faker.string.uuid(),
|
||||||
name: faker.lorem.words({ min: 1, max: 3 }),
|
name: faker.lorem.words({ min: 1, max: 3 }),
|
||||||
icon: { type: 'icon', value: 'folder' },
|
settings: {
|
||||||
|
icon: { type: 'icon', value: 'folder' },
|
||||||
|
},
|
||||||
createdAt: faker.date.past().toISOString(),
|
createdAt: faker.date.past().toISOString(),
|
||||||
updatedAt: faker.date.recent().toISOString(),
|
updatedAt: faker.date.recent().toISOString(),
|
||||||
type: ProjectTypes.Team,
|
type: ProjectTypes.Team,
|
||||||
|
|
|
@ -2,8 +2,9 @@
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { N8nButton, N8nTooltip } from 'n8n-design-system';
|
import { N8nButton, N8nTooltip } from 'n8n-design-system';
|
||||||
|
import type { ProjectIcon } from '@n8n/api-types';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import { type ProjectIcon, ProjectTypes } from '@/types/projects.types';
|
import { ProjectTypes } from '@/types/projects.types';
|
||||||
import { useProjectsStore } from '@/stores/projects.store';
|
import { useProjectsStore } from '@/stores/projects.store';
|
||||||
import ProjectTabs from '@/components/Projects/ProjectTabs.vue';
|
import ProjectTabs from '@/components/Projects/ProjectTabs.vue';
|
||||||
import { getResourcePermissions } from '@/permissions';
|
import { getResourcePermissions } from '@/permissions';
|
||||||
|
@ -21,7 +22,7 @@ const headerIcon = computed((): ProjectIcon => {
|
||||||
if (projectsStore.currentProject?.type === ProjectTypes.Personal) {
|
if (projectsStore.currentProject?.type === ProjectTypes.Personal) {
|
||||||
return { type: 'icon', value: 'user' };
|
return { type: 'icon', value: 'user' };
|
||||||
} else if (projectsStore.currentProject?.name) {
|
} else if (projectsStore.currentProject?.name) {
|
||||||
return projectsStore.currentProject.icon ?? { type: 'icon', value: 'layer-group' };
|
return projectsStore.currentProject.settings.icon ?? { type: 'icon', value: 'layer-group' };
|
||||||
} else {
|
} else {
|
||||||
return { type: 'icon', value: 'home' };
|
return { type: 'icon', value: 'home' };
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { ProjectIcon } from '@/types/projects.types';
|
import type { ProjectIcon } from '@n8n/api-types';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
icon: ProjectIcon;
|
icon: ProjectIcon;
|
||||||
|
|
|
@ -33,7 +33,7 @@ const home = computed<IMenuItem>(() => ({
|
||||||
const getProjectMenuItem = (project: ProjectListItem) => ({
|
const getProjectMenuItem = (project: ProjectListItem) => ({
|
||||||
id: project.id,
|
id: project.id,
|
||||||
label: project.name,
|
label: project.name,
|
||||||
icon: project.icon,
|
icon: project.settings.icon,
|
||||||
route: {
|
route: {
|
||||||
to: {
|
to: {
|
||||||
name: VIEWS.PROJECTS_WORKFLOWS,
|
name: VIEWS.PROJECTS_WORKFLOWS,
|
||||||
|
|
|
@ -21,8 +21,8 @@ const processedName = computed(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const projectIcon = computed(() => {
|
const projectIcon = computed(() => {
|
||||||
if (props.project.icon) {
|
if (props.project.settings.icon) {
|
||||||
return props.project.icon;
|
return props.project.settings.icon;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
|
@ -173,7 +173,9 @@ export const useGlobalEntityCreation = () => {
|
||||||
try {
|
try {
|
||||||
const newProject = await projectsStore.createProject({
|
const newProject = await projectsStore.createProject({
|
||||||
name: i18n.baseText('projects.settings.newProjectName'),
|
name: i18n.baseText('projects.settings.newProjectName'),
|
||||||
icon: { type: 'icon', value: 'layer-group' },
|
settings: {
|
||||||
|
icon: { type: 'icon', value: 'layer-group' },
|
||||||
|
},
|
||||||
});
|
});
|
||||||
await router.push({ name: VIEWS.PROJECT_SETTINGS, params: { projectId: newProject.id } });
|
await router.push({ name: VIEWS.PROJECT_SETTINGS, params: { projectId: newProject.id } });
|
||||||
toast.showMessage({
|
toast.showMessage({
|
||||||
|
|
|
@ -32,7 +32,6 @@ export const useProjectsStore = defineStore(STORES.PROJECTS, () => {
|
||||||
const projectsCount = ref<ProjectsCount>({
|
const projectsCount = ref<ProjectsCount>({
|
||||||
personal: 0,
|
personal: 0,
|
||||||
team: 0,
|
team: 0,
|
||||||
public: 0,
|
|
||||||
});
|
});
|
||||||
const projectNavActiveIdState = ref<string | string[] | null>(null);
|
const projectNavActiveIdState = ref<string | string[] | null>(null);
|
||||||
|
|
||||||
|
@ -120,14 +119,14 @@ export const useProjectsStore = defineStore(STORES.PROJECTS, () => {
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
await projectsApi.updateProject(rootStore.restApiContext, id, projectData);
|
await projectsApi.updateProject(rootStore.restApiContext, id, projectData);
|
||||||
const projectIndex = myProjects.value.findIndex((p) => p.id === id);
|
const projectIndex = myProjects.value.findIndex((p) => p.id === id);
|
||||||
const { name, icon } = projectData;
|
const { name, settings } = projectData;
|
||||||
if (projectIndex !== -1) {
|
if (projectIndex !== -1) {
|
||||||
myProjects.value[projectIndex].name = name;
|
myProjects.value[projectIndex].name = name;
|
||||||
myProjects.value[projectIndex].icon = icon;
|
myProjects.value[projectIndex].settings = settings;
|
||||||
}
|
}
|
||||||
if (currentProject.value) {
|
if (currentProject.value) {
|
||||||
currentProject.value.name = name;
|
currentProject.value.name = name;
|
||||||
currentProject.value.icon = icon;
|
currentProject.value.settings = settings;
|
||||||
}
|
}
|
||||||
if (projectData.relations) {
|
if (projectData.relations) {
|
||||||
await getProject(id);
|
await getProject(id);
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import type { ProjectSettings } from '@n8n/api-types';
|
||||||
import type { Scope } from '@n8n/permissions';
|
import type { Scope } from '@n8n/permissions';
|
||||||
import type { IUserResponse } from '@/Interface';
|
import type { IUserResponse } from '@/Interface';
|
||||||
import type { ProjectRole } from '@/types/roles.types';
|
import type { ProjectRole } from '@/types/roles.types';
|
||||||
|
@ -5,7 +6,6 @@ import type { ProjectRole } from '@/types/roles.types';
|
||||||
export const ProjectTypes = {
|
export const ProjectTypes = {
|
||||||
Personal: 'personal',
|
Personal: 'personal',
|
||||||
Team: 'team',
|
Team: 'team',
|
||||||
Public: 'public',
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
type ProjectTypeKeys = typeof ProjectTypes;
|
type ProjectTypeKeys = typeof ProjectTypes;
|
||||||
|
@ -18,8 +18,8 @@ export type ProjectRelationPayload = { userId: string; role: ProjectRole };
|
||||||
export type ProjectSharingData = {
|
export type ProjectSharingData = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string | null;
|
name: string | null;
|
||||||
icon: ProjectIcon | null;
|
|
||||||
type: ProjectType;
|
type: ProjectType;
|
||||||
|
settings: ProjectSettings;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
};
|
};
|
||||||
|
@ -32,8 +32,3 @@ export type ProjectListItem = ProjectSharingData & {
|
||||||
scopes?: Scope[];
|
scopes?: Scope[];
|
||||||
};
|
};
|
||||||
export type ProjectsCount = Record<ProjectType, number>;
|
export type ProjectsCount = Record<ProjectType, number>;
|
||||||
|
|
||||||
export type ProjectIcon = {
|
|
||||||
type: 'icon' | 'emoji';
|
|
||||||
value: string;
|
|
||||||
};
|
|
||||||
|
|
|
@ -66,7 +66,7 @@ describe('ProjectSettings', () => {
|
||||||
id: '123',
|
id: '123',
|
||||||
type: 'team',
|
type: 'team',
|
||||||
name: 'Test Project',
|
name: 'Test Project',
|
||||||
icon: { type: 'icon', value: 'folder' },
|
settings: { icon: { type: 'icon', value: 'folder' } },
|
||||||
relations: [],
|
relations: [],
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
|
|
|
@ -3,11 +3,11 @@ import { computed, ref, watch, onBeforeMount, onMounted, nextTick } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { deepCopy } from 'n8n-workflow';
|
import { deepCopy } from 'n8n-workflow';
|
||||||
import { N8nFormInput } from 'n8n-design-system';
|
import { N8nFormInput } from 'n8n-design-system';
|
||||||
|
import type { ProjectIcon } from '@n8n/api-types';
|
||||||
import { useUsersStore } from '@/stores/users.store';
|
import { useUsersStore } from '@/stores/users.store';
|
||||||
import type { IUser } from '@/Interface';
|
import type { IUser } from '@/Interface';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import { useProjectsStore } from '@/stores/projects.store';
|
import { useProjectsStore } from '@/stores/projects.store';
|
||||||
import type { ProjectIcon } from '@/types/projects.types';
|
|
||||||
import { type Project, type ProjectRelation } from '@/types/projects.types';
|
import { type Project, type ProjectRelation } from '@/types/projects.types';
|
||||||
import { useToast } from '@/composables/useToast';
|
import { useToast } from '@/composables/useToast';
|
||||||
import { VIEWS } from '@/constants';
|
import { VIEWS } from '@/constants';
|
||||||
|
@ -194,7 +194,9 @@ const updateProject = async () => {
|
||||||
try {
|
try {
|
||||||
await projectsStore.updateProject(projectsStore.currentProject.id, {
|
await projectsStore.updateProject(projectsStore.currentProject.id, {
|
||||||
name: formData.value.name!,
|
name: formData.value.name!,
|
||||||
icon: projectIcon.value,
|
settings: {
|
||||||
|
icon: projectIcon.value,
|
||||||
|
},
|
||||||
relations: formData.value.relations.map((r: ProjectRelation) => ({
|
relations: formData.value.relations.map((r: ProjectRelation) => ({
|
||||||
userId: r.id,
|
userId: r.id,
|
||||||
role: r.role,
|
role: r.role,
|
||||||
|
@ -269,8 +271,8 @@ watch(
|
||||||
: [];
|
: [];
|
||||||
await nextTick();
|
await nextTick();
|
||||||
selectProjectNameIfMatchesDefault();
|
selectProjectNameIfMatchesDefault();
|
||||||
if (projectsStore.currentProject?.icon) {
|
if (projectsStore.currentProject?.settings.icon) {
|
||||||
projectIcon.value = projectsStore.currentProject.icon;
|
projectIcon.value = projectsStore.currentProject.settings.icon;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
|
|
|
@ -125,8 +125,10 @@ export interface IUser {
|
||||||
export type ProjectSharingData = {
|
export type ProjectSharingData = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string | null;
|
name: string | null;
|
||||||
icon: { type: 'emoji' | 'icon'; value: string } | null;
|
type: 'personal' | 'team';
|
||||||
type: 'personal' | 'team' | 'public';
|
settings: {
|
||||||
|
icon?: { type: 'emoji' | 'icon'; value: string };
|
||||||
|
};
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue