Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(editor): Add support for project icons #12349

Draft
wants to merge 27 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
afb1215
feat(editor): Add support for project icons
MiloradFilipovic Dec 23, 2024
c1d7cf2
✨ Using `is-emoji-supported` lib to render emojis
MiloradFilipovic Dec 23, 2024
647fcda
✅ Adding tests, minor refactoring
MiloradFilipovic Dec 23, 2024
dd3992a
💄 Minor spacing update
MiloradFilipovic Dec 23, 2024
d04c4eb
✨ Support emojis as default icons
MiloradFilipovic Dec 23, 2024
eabe322
🔨 Moving library to plugin file
MiloradFilipovic Dec 23, 2024
33b2ac2
🔨 Updating test constants
MiloradFilipovic Dec 23, 2024
023c378
👌 Moving icon selector inline with project input
MiloradFilipovic Dec 23, 2024
49565e8
Merge branch 'master' into ADO-2791-project-icons
MiloradFilipovic Dec 24, 2024
7571763
⚡️ Backend support for project icons
MiloradFilipovic Dec 24, 2024
47e7575
✨ Rendering saved project icons in the UI
MiloradFilipovic Dec 24, 2024
b25c0b1
⚡️ Getting types in line
MiloradFilipovic Dec 24, 2024
7585fd8
⚡️ Updating project icon when project is loaded
MiloradFilipovic Dec 24, 2024
ea0b0a2
✨ Automatically saving project icon update
MiloradFilipovic Dec 24, 2024
ae51e46
⚡️ Updating project icon in settings header
MiloradFilipovic Dec 24, 2024
dbbad87
⚡️ Unifying icon type on backend
MiloradFilipovic Dec 24, 2024
e082eb8
✨ Adding icon to `SlimProject`
MiloradFilipovic Dec 24, 2024
16baab7
✨ Implemented `ProjectIcon` component
MiloradFilipovic Dec 24, 2024
03034be
⚡️ Using `ProjectIcon` component in sharing modals
MiloradFilipovic Dec 24, 2024
55bab08
💄 Showing icons in collapsed menu
MiloradFilipovic Dec 24, 2024
d53aafc
Merge branch 'master' into ADO-2791-project-icons
MiloradFilipovic Dec 24, 2024
c3aac38
🔨 Cleanup
MiloradFilipovic Dec 24, 2024
6b591fc
🔨 More typing cleanup
MiloradFilipovic Dec 24, 2024
0483fb4
⚡️ Using model value for `IconPicker`
MiloradFilipovic Dec 24, 2024
bc8019f
⚡️ Updating property in tests
MiloradFilipovic Dec 24, 2024
0b63739
🔥 Cleaning up imports
MiloradFilipovic Dec 24, 2024
6557578
🔨 Memoizing emojis, adding aria attributes
MiloradFilipovic Dec 24, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions packages/cli/src/controllers/project.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,12 @@ export class ProjectController {
@Licensed('feat:projectRole:admin')
async createProject(req: ProjectRequest.Create) {
try {
const project = await this.projectsService.createTeamProject(req.body.name, req.user);
const project = await this.projectsService.createTeamProject(
req.body.name,
req.user,
undefined,
req.body.icon,
);

this.eventService.emit('team-project-created', {
userId: req.user.id,
Expand Down Expand Up @@ -163,7 +168,7 @@ export class ProjectController {
@Get('/:projectId')
@ProjectScope('project:read')
async getProject(req: ProjectRequest.Get): Promise<ProjectRequest.ProjectWithRelations> {
const [{ id, name, type }, relations] = await Promise.all([
const [{ id, name, icon, type }, relations] = await Promise.all([
this.projectsService.getProject(req.params.projectId),
this.projectsService.getProjectRelations(req.params.projectId),
]);
Expand All @@ -172,6 +177,7 @@ export class ProjectController {
return {
id,
name,
icon,
type,
relations: relations.map((r) => ({
id: r.user.id,
Expand All @@ -193,7 +199,7 @@ export class ProjectController {
@ProjectScope('project:update')
async updateProject(req: ProjectRequest.Update) {
if (req.body.name) {
await this.projectsService.updateProject(req.body.name, req.params.projectId);
await this.projectsService.updateProject(req.body.name, req.params.projectId, req.body.icon);
}
if (req.body.relations) {
try {
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/src/databases/entities/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { SharedCredentials } from './shared-credentials';
import type { SharedWorkflow } from './shared-workflow';

export type ProjectType = 'personal' | 'team';
export type ProjectIcon = { type: 'emoji' | 'icon'; value: string } | null;

@Entity()
export class Project extends WithTimestampsAndStringId {
Expand All @@ -15,6 +16,9 @@ export class Project extends WithTimestampsAndStringId {
@Column({ length: 36 })
type: ProjectType;

@Column({ type: 'json', nullable: true })
icon: ProjectIcon;

@OneToMany('ProjectRelation', 'project')
projectRelations: ProjectRelation[];

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { MigrationContext, ReversibleMigration } from '@/databases/types';
export class AddProjectIcons1729607673469 implements ReversibleMigration {
async up({ schemaBuilder: { addColumns, column } }: MigrationContext) {
await addColumns('project', [column('icon').json]);
}

async down({ schemaBuilder: { dropColumns } }: MigrationContext) {
await dropColumns('project', ['icon']);
}
}
2 changes: 2 additions & 0 deletions packages/cli/src/databases/migrations/mysqldb/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ import { CreateProcessedDataTable1726606152711 } from '../common/1726606152711-C
import { SeparateExecutionCreationFromStart1727427440136 } from '../common/1727427440136-SeparateExecutionCreationFromStart';
import { AddMissingPrimaryKeyOnAnnotationTagMapping1728659839644 } from '../common/1728659839644-AddMissingPrimaryKeyOnAnnotationTagMapping';
import { UpdateProcessedDataValueColumnToText1729607673464 } from '../common/1729607673464-UpdateProcessedDataValueColumnToText';
import { AddProjectIcons1729607673469 } from '../common/1729607673469-AddProjectIcons';
import { CreateTestDefinitionTable1730386903556 } from '../common/1730386903556-CreateTestDefinitionTable';
import { AddDescriptionToTestDefinition1731404028106 } from '../common/1731404028106-AddDescriptionToTestDefinition';
import { CreateTestMetricTable1732271325258 } from '../common/1732271325258-CreateTestMetricTable';
Expand Down Expand Up @@ -152,4 +153,5 @@ export const mysqlMigrations: Migration[] = [
CreateTestRun1732549866705,
AddMockedNodesColumnToTestDefinition1733133775640,
AddManagedColumnToCredentialsTable1734479635324,
AddProjectIcons1729607673469,
];
2 changes: 2 additions & 0 deletions packages/cli/src/databases/migrations/postgresdb/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ import { CreateProcessedDataTable1726606152711 } from '../common/1726606152711-C
import { SeparateExecutionCreationFromStart1727427440136 } from '../common/1727427440136-SeparateExecutionCreationFromStart';
import { AddMissingPrimaryKeyOnAnnotationTagMapping1728659839644 } from '../common/1728659839644-AddMissingPrimaryKeyOnAnnotationTagMapping';
import { UpdateProcessedDataValueColumnToText1729607673464 } from '../common/1729607673464-UpdateProcessedDataValueColumnToText';
import { AddProjectIcons1729607673469 } from '../common/1729607673469-AddProjectIcons';
import { CreateTestDefinitionTable1730386903556 } from '../common/1730386903556-CreateTestDefinitionTable';
import { AddDescriptionToTestDefinition1731404028106 } from '../common/1731404028106-AddDescriptionToTestDefinition';
import { CreateTestMetricTable1732271325258 } from '../common/1732271325258-CreateTestMetricTable';
Expand Down Expand Up @@ -152,4 +153,5 @@ export const postgresMigrations: Migration[] = [
CreateTestRun1732549866705,
AddMockedNodesColumnToTestDefinition1733133775640,
AddManagedColumnToCredentialsTable1734479635324,
AddProjectIcons1729607673469,
];
2 changes: 2 additions & 0 deletions packages/cli/src/databases/migrations/sqlite/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ import { CreateAnnotationTables1724753530828 } from '../common/1724753530828-Cre
import { CreateProcessedDataTable1726606152711 } from '../common/1726606152711-CreateProcessedDataTable';
import { SeparateExecutionCreationFromStart1727427440136 } from '../common/1727427440136-SeparateExecutionCreationFromStart';
import { UpdateProcessedDataValueColumnToText1729607673464 } from '../common/1729607673464-UpdateProcessedDataValueColumnToText';
import { AddProjectIcons1729607673469 } from '../common/1729607673469-AddProjectIcons';
import { CreateTestDefinitionTable1730386903556 } from '../common/1730386903556-CreateTestDefinitionTable';
import { CreateTestMetricTable1732271325258 } from '../common/1732271325258-CreateTestMetricTable';
import { CreateTestRun1732549866705 } from '../common/1732549866705-CreateTestRunTable';
Expand Down Expand Up @@ -146,6 +147,7 @@ const sqliteMigrations: Migration[] = [
CreateTestRun1732549866705,
AddMockedNodesColumnToTestDefinition1733133775640,
AddManagedColumnToCredentialsTable1734479635324,
AddProjectIcons1729607673469,
];

export { sqliteMigrations };
12 changes: 9 additions & 3 deletions packages/cli/src/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import type {
} from 'n8n-workflow';

import type { CredentialsEntity } from '@/databases/entities/credentials-entity';
import type { Project, ProjectType } from '@/databases/entities/project';
import type { Project, ProjectIcon, ProjectType } from '@/databases/entities/project';
import type { AssignableRole, GlobalRole, User } from '@/databases/entities/user';
import type { Variables } from '@/databases/entities/variables';
import type { WorkflowEntity } from '@/databases/entities/workflow-entity';
Expand Down Expand Up @@ -125,7 +125,7 @@ export namespace ListQuery {
}

type SlimUser = Pick<IUser, 'id' | 'email' | 'firstName' | 'lastName'>;
export type SlimProject = Pick<Project, 'id' | 'type' | 'name'>;
export type SlimProject = Pick<Project, 'id' | 'type' | 'name' | 'icon'>;

export function hasSharing(
workflows: ListQuery.Workflow.Plain[] | ListQuery.Workflow.WithSharing[],
Expand Down Expand Up @@ -523,6 +523,7 @@ export declare namespace ProjectRequest {
Project,
{
name: string;
icon?: ProjectIcon;
}
>;

Expand Down Expand Up @@ -551,6 +552,7 @@ export declare namespace ProjectRequest {
type ProjectWithRelations = {
id: string;
name: string | undefined;
icon: ProjectIcon;
type: ProjectType;
relations: ProjectRelationResponse[];
scopes: Scope[];
Expand All @@ -560,7 +562,11 @@ export declare namespace ProjectRequest {
type Update = AuthenticatedRequest<
{ projectId: string },
{},
{ name?: string; relations?: ProjectRelationPayload[] }
{
name?: string;
relations?: ProjectRelationPayload[];
icon?: { type: 'icon' | 'emoji'; value: string };
}
>;
type Delete = AuthenticatedRequest<{ projectId: string }, {}, {}, { transferId?: string }>;
}
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/services/ownership.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,14 @@ export class OwnershipService {
id: project.id,
type: project.type,
name: project.name,
icon: project.icon,
};
} else {
entity.sharedWithProjects.push({
id: project.id,
type: project.type,
name: project.name,
icon: project.icon,
});
}
}
Expand Down
18 changes: 15 additions & 3 deletions packages/cli/src/services/project.service.ee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import { ApplicationError } from 'n8n-workflow';
import Container, { Service } from 'typedi';

import { UNLIMITED_LICENSE_QUOTA } from '@/constants';
import { Project, type ProjectType } from '@/databases/entities/project';
import type { ProjectIcon, ProjectType } from '@/databases/entities/project';
import { Project } from '@/databases/entities/project';
import { ProjectRelation } from '@/databases/entities/project-relation';
import type { ProjectRole } from '@/databases/entities/project-relation';
import type { User } from '@/databases/entities/user';
Expand Down Expand Up @@ -167,7 +168,12 @@ export class ProjectService {
return await this.projectRelationRepository.getPersonalProjectOwners(projectIds);
}

async createTeamProject(name: string, adminUser: User, id?: string): Promise<Project> {
async createTeamProject(
name: string,
adminUser: User,
id?: string,
icon?: ProjectIcon,
): Promise<Project> {
const limit = this.license.getTeamProjectLimit();
if (
limit !== UNLIMITED_LICENSE_QUOTA &&
Expand All @@ -180,6 +186,7 @@ export class ProjectService {
this.projectRepository.create({
id,
name,
icon,
type: 'team',
}),
);
Expand All @@ -190,14 +197,19 @@ export class ProjectService {
return project;
}

async updateProject(name: string, projectId: string): Promise<Project> {
async updateProject(
name: string,
projectId: string,
icon?: { type: 'icon' | 'emoji'; value: string },
): Promise<Project> {
const result = await this.projectRepository.update(
{
id: projectId,
type: 'team',
},
{
name,
icon,
},
);

Expand Down
4 changes: 4 additions & 0 deletions packages/design-system/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/vue-fontawesome": "^3.0.3",
"element-plus": "2.4.3",
"is-emoji-supported": "^0.0.5",
"markdown-it": "^13.0.2",
"markdown-it-emoji": "^2.0.2",
"markdown-it-link-attributes": "^4.0.1",
Expand All @@ -55,5 +56,8 @@
"vue-boring-avatars": "^1.3.0",
"vue-router": "catalog:frontend",
"xss": "catalog:"
},
"peerDependencies": {
"@vueuse/core": "*"
}
}
5 changes: 5 additions & 0 deletions packages/design-system/src/__tests__/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,8 @@ window.ResizeObserver =
observe: vi.fn(),
unobserve: vi.fn(),
}));

// Globally mock is-emoji-supported
vi.mock('is-emoji-supported', () => ({
isEmojiSupported: () => true,
}));
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { action } from '@storybook/addon-actions';
import type { StoryFn } from '@storybook/vue3';

import { TEST_ICONS } from './constants';
import N8nIconPicker from './IconPicker.vue';

export default {
title: 'Atoms/Icon Picker',
component: N8nIconPicker,
argTypes: {},
};

const DefaultTemplate: StoryFn = (args, { argTypes }) => ({
components: { N8nIconPicker },
props: Object.keys(argTypes),
setup: () => ({ args }),
data: () => ({
icon: { type: 'icon', value: 'smile' },
}),
template:
'<div style="height: 500px"><n8n-icon-picker v-model="icon" v-bind="args" @update:model-value="onIconSelected" /></div>',
methods: {
onIconSelected: action('iconSelected'),
},
});

export const Default = DefaultTemplate.bind({});
Default.args = {
buttonTooltip: 'Select an icon',
availableIcons: TEST_ICONS,
};

const CustomTooltipTemplate: StoryFn = (args, { argTypes }) => ({
components: { N8nIconPicker },
props: Object.keys(argTypes),
setup: () => ({ args }),
data: () => ({
icon: { type: 'icon', value: 'layer-group' },
}),
template:
'<div style="height: 500px"><n8n-icon-picker v-model="icon" v-bind="args" @update:model-value="onIconSelected" /></div>',
methods: {
onIconSelected: action('iconSelected'),
},
});

export const WithCustomIconAndTooltip = CustomTooltipTemplate.bind({});
WithCustomIconAndTooltip.args = {
availableIcons: [...TEST_ICONS],
buttonTooltip: 'Select something...',
};

const OnlyEmojiTemplate: StoryFn = (args, { argTypes }) => ({
components: { N8nIconPicker },
props: Object.keys(argTypes),
setup: () => ({ args }),
data: () => ({
icon: { type: 'emoji', value: '🔥' },
}),
template:
'<div style="height: 500px"><n8n-icon-picker v-model="icon" v-bind="args" @update:model-value="onIconSelected" /></div>',
methods: {
onIconSelected: action('iconSelected'),
},
});
export const OnlyEmojis = OnlyEmojiTemplate.bind({});
OnlyEmojis.args = {
buttonTooltip: 'Select an emoji',
availableIcons: [],
};
Loading
Loading