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

Merged
merged 43 commits into from
Dec 27, 2024
Merged
Show file tree
Hide file tree
Changes from 42 commits
Commits
Show all changes
43 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
72c4c90
⚡️ Using computed for emojis
MiloradFilipovic Dec 25, 2024
6def507
⚡️ Adjust button size to container size, refactor storybook entry
MiloradFilipovic Dec 25, 2024
b5339c6
🔨 Rendering tabs using `v-if`
MiloradFilipovic Dec 25, 2024
9dd7777
✨ Adding size prop, cleanup
MiloradFilipovic Dec 25, 2024
78bdd40
⚡️ Using `defineModel`
MiloradFilipovic Dec 26, 2024
7527768
🔥 Removing unused class from container
MiloradFilipovic Dec 26, 2024
952a44c
✅ Updating backend tests
MiloradFilipovic Dec 27, 2024
a7d94d4
💄 Tighten up icon & emoji styling, adding tooltip component
MiloradFilipovic Dec 27, 2024
7d93a06
✅ Updating `ProjectNavigation` tests
MiloradFilipovic Dec 27, 2024
e0cf827
✔️ Adding projects e2e test
MiloradFilipovic Dec 27, 2024
909224c
✅ Add more `IconPicker` tests
MiloradFilipovic Dec 27, 2024
e54fad1
Merge branch 'master' into ADO-2791-project-icons
MiloradFilipovic Dec 27, 2024
33d729a
🔥 Removing leftover import
MiloradFilipovic Dec 27, 2024
148dc04
🔨 Moving icons logic to plugin
MiloradFilipovic Dec 27, 2024
14f52d4
🔥Removing hard-coded styles & `only`
MiloradFilipovic Dec 27, 2024
c293679
👌 Setting `transaction=true` for sqlite migration
MiloradFilipovic Dec 27, 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
6 changes: 5 additions & 1 deletion cypress/composables/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@ export const getAddProjectButton = () => {

return cy.get('@button');
};

export const getAddFirstProjectButton = () => cy.getByTestId('add-first-project-button');
export const getIconPickerButton = () => cy.getByTestId('icon-picker-button');
export const getIconPickerTab = (tab: string) => cy.getByTestId('icon-picker-tabs').contains(tab);
export const getIconPickerIcons = () => cy.getByTestId('icon-picker-icon');
export const getIconPickerEmojis = () => cy.getByTestId('icon-picker-emoji');
// export const getAddProjectButton = () =>
// cy.getByTestId('universal-add').should('contain', 'Add project').should('be.visible');
export const getProjectTabs = () => cy.getByTestId('project-tabs').find('a');
Expand Down
21 changes: 20 additions & 1 deletion cypress/e2e/39-projects.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
NDV,
MainSidebar,
} from '../pages';
import { clearNotifications } from '../pages/notifications';
import { clearNotifications, successToast } from '../pages/notifications';
import { getVisibleDropdown, getVisibleModalOverlay, getVisibleSelect } from '../utils';

const workflowsPage = new WorkflowsPage();
Expand Down Expand Up @@ -830,4 +830,23 @@ describe('Projects', { disableAutoLogin: true }, () => {
.should('not.have.length');
});
});

it('should set and update project icon', () => {
const DEFAULT_ICON = 'fa-layer-group';
const NEW_PROJECT_NAME = 'Test Project';

cy.signinAsAdmin();
cy.visit(workflowsPage.url);
projects.createProject(NEW_PROJECT_NAME);
// New project should have default icon
projects.getIconPickerButton().find('svg').should('have.class', DEFAULT_ICON);
// Choose another icon
projects.getIconPickerButton().click();
projects.getIconPickerTab('Emojis').click();
projects.getIconPickerEmojis().first().click();
// Project should be updated with new icon
successToast().contains('Project icon updated successfully');
projects.getIconPickerButton().should('contain', '😀');
projects.getMenuItems().contains(NEW_PROJECT_NAME).should('contain', '😀');
});
});
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';
MiloradFilipovic marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -13,7 +13,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 @@ -123,7 +123,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 @@ -439,6 +439,7 @@ export declare namespace ProjectRequest {
Project,
{
name: string;
icon?: ProjectIcon;
}
>;

Expand Down Expand Up @@ -467,6 +468,7 @@ export declare namespace ProjectRequest {
type ProjectWithRelations = {
id: string;
name: string | undefined;
icon: ProjectIcon;
type: ProjectType;
relations: ProjectRelationResponse[];
scopes: Scope[];
Expand All @@ -476,7 +478,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
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,7 @@ describe('GET /credentials/:id', () => {
id: ownerPersonalProject.id,
name: owner.createPersonalProjectName(),
type: ownerPersonalProject.type,
icon: null,
});
expect(firstCredential.sharedWithProjects).toHaveLength(0);

Expand Down Expand Up @@ -629,17 +630,20 @@ describe('GET /credentials/:id', () => {
homeProject: {
id: member1PersonalProject.id,
name: member1.createPersonalProjectName(),
icon: null,
type: 'personal',
},
sharedWithProjects: expect.arrayContaining([
{
id: member2PersonalProject.id,
name: member2.createPersonalProjectName(),
icon: null,
type: member2PersonalProject.type,
},
{
id: member3PersonalProject.id,
name: member3.createPersonalProjectName(),
icon: null,
type: member3PersonalProject.type,
},
]),
Expand Down
1 change: 1 addition & 0 deletions packages/cli/test/integration/public-api/projects.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ describe('Projects in Public API', () => {
expect(response.status).toBe(201);
expect(response.body).toEqual({
name: 'some-project',
icon: null,
type: 'team',
id: expect.any(String),
createdAt: expect.any(String),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,7 @@ describe('GET /workflows', () => {
homeProject: {
id: ownerPersonalProject.id,
name: owner.createPersonalProjectName(),
icon: null,
type: ownerPersonalProject.type,
},
sharedWithProjects: [],
Expand All @@ -456,6 +457,7 @@ describe('GET /workflows', () => {
homeProject: {
id: ownerPersonalProject.id,
name: owner.createPersonalProjectName(),
icon: null,
type: ownerPersonalProject.type,
},
sharedWithProjects: [],
Expand Down Expand Up @@ -833,6 +835,7 @@ describe('GET /workflows', () => {
homeProject: {
id: ownerPersonalProject.id,
name: owner.createPersonalProjectName(),
icon: null,
type: ownerPersonalProject.type,
},
sharedWithProjects: [],
Expand All @@ -842,6 +845,7 @@ describe('GET /workflows', () => {
homeProject: {
id: ownerPersonalProject.id,
name: owner.createPersonalProjectName(),
icon: null,
type: ownerPersonalProject.type,
},
sharedWithProjects: [],
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,
}));
Loading
Loading