Skip to content

Commit ebf58f0

Browse files
committed
feat(editor): enhance resources workflow and icon integration
1 parent fde01f5 commit ebf58f0

40 files changed

+5441
-485
lines changed

apps/backend/internal/modules/project/handlers.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ func (handler *Handler) Routes(requireAuth gin.HandlerFunc) RouteHandlers {
3131
ListProjects: handler.HandleListProjects,
3232
CreateProject: handler.HandleCreateProject,
3333
GetProject: handler.HandleGetProject,
34+
UpdateProject: handler.HandleUpdateProject,
3435
GetProjectMIR: handler.HandleGetProjectMIR,
3536
SaveProjectMIR: handler.HandleSaveProjectMIR,
3637
PublishProject: handler.HandlePublishProject,
@@ -115,6 +116,37 @@ func (handler *Handler) HandleGetProject(c *gin.Context) {
115116
c.JSON(http.StatusOK, gin.H{"project": project})
116117
}
117118

119+
func (handler *Handler) HandleUpdateProject(c *gin.Context) {
120+
user, ok := backendauth.GetAuthUser[backendauth.User](c)
121+
if !ok {
122+
respondError(c, http.StatusUnauthorized, "unauthorized", "Authentication required.")
123+
return
124+
}
125+
var request struct {
126+
Name *string `json:"name"`
127+
Description *string `json:"description"`
128+
}
129+
if err := c.ShouldBindJSON(&request); err != nil {
130+
respondError(c, http.StatusBadRequest, "invalid_payload", "Invalid request payload.")
131+
return
132+
}
133+
if request.Name == nil && request.Description == nil {
134+
respondError(c, http.StatusBadRequest, "invalid_payload", "No fields to update.")
135+
return
136+
}
137+
138+
project, err := handler.store.UpdateProject(user.ID, c.Param("id"), request.Name, request.Description)
139+
if err != nil {
140+
if errors.Is(err, ErrProjectNotFound) {
141+
respondError(c, http.StatusNotFound, "not_found", "Project not found.")
142+
return
143+
}
144+
respondError(c, http.StatusInternalServerError, "project_update_failed", "Could not update project.")
145+
return
146+
}
147+
c.JSON(http.StatusOK, gin.H{"project": project})
148+
}
149+
118150
func (handler *Handler) HandleGetProjectMIR(c *gin.Context) {
119151
user, ok := backendauth.GetAuthUser[backendauth.User](c)
120152
if !ok {

apps/backend/internal/modules/project/routes.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ type RouteHandlers struct {
77
ListProjects gin.HandlerFunc
88
CreateProject gin.HandlerFunc
99
GetProject gin.HandlerFunc
10+
UpdateProject gin.HandlerFunc
1011
GetProjectMIR gin.HandlerFunc
1112
SaveProjectMIR gin.HandlerFunc
1213
PublishProject gin.HandlerFunc
@@ -22,6 +23,7 @@ func RegisterRoutes(api *gin.RouterGroup, handlers RouteHandlers) {
2223
api.GET("/projects", handlers.RequireAuth, handlers.ListProjects)
2324
api.POST("/projects", handlers.RequireAuth, handlers.CreateProject)
2425
api.GET("/projects/:id", handlers.RequireAuth, handlers.GetProject)
26+
api.PATCH("/projects/:id", handlers.RequireAuth, handlers.UpdateProject)
2527
api.GET("/projects/:id/mir", handlers.RequireAuth, handlers.GetProjectMIR)
2628
api.PUT("/projects/:id/mir", handlers.RequireAuth, handlers.SaveProjectMIR)
2729
api.POST("/projects/:id/publish", handlers.RequireAuth, handlers.PublishProject)

apps/backend/internal/modules/project/store.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,37 @@ RETURNING id, owner_id, resource_type, name, description, mir_json, is_public, s
159159
return project, nil
160160
}
161161

162+
func (store *ProjectStore) UpdateProject(ownerID, projectID string, name, description *string) (*Project, error) {
163+
var nextName any
164+
var nextDescription any
165+
if name != nil {
166+
nextName = strings.TrimSpace(*name)
167+
}
168+
if description != nil {
169+
nextDescription = strings.TrimSpace(*description)
170+
}
171+
172+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
173+
defer cancel()
174+
175+
const query = `UPDATE projects
176+
SET name = COALESCE($3, name),
177+
description = COALESCE($4, description),
178+
updated_at = NOW()
179+
WHERE owner_id = $1 AND id = $2
180+
RETURNING id, owner_id, resource_type, name, description, mir_json, is_public, stars_count, created_at, updated_at`
181+
182+
row := store.db.QueryRowContext(ctx, query, ownerID, projectID, nextName, nextDescription)
183+
project, err := scanProject(row)
184+
if err != nil {
185+
if errors.Is(err, sql.ErrNoRows) {
186+
return nil, ErrProjectNotFound
187+
}
188+
return nil, err
189+
}
190+
return project, nil
191+
}
192+
162193
func (store *ProjectStore) SetPublic(ownerID, projectID string, isPublic bool) (*Project, error) {
163194
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
164195
defer cancel()

apps/web/src/editor/Editor.tsx

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useEffect } from 'react';
2-
import { Outlet, useParams } from 'react-router';
2+
import { Outlet, useLocation, useNavigate, useParams } from 'react-router';
33
import EditorBar from './EditorBar/EditorBar';
44
import { SettingsEffects } from './features/settings/SettingsEffects';
55
import { useAuthStore } from '@/auth/useAuthStore';
@@ -10,6 +10,8 @@ import { useSettingsStore } from './store/useSettingsStore';
1010

1111
function Editor() {
1212
const { projectId } = useParams();
13+
const location = useLocation();
14+
const navigate = useNavigate();
1315
const token = useAuthStore((state) => state.token);
1416
const setProject = useEditorStore((state) => state.setProject);
1517
const setMirDoc = useEditorStore((state) => state.setMirDoc);
@@ -94,6 +96,47 @@ function Editor() {
9496

9597
useEffect(() => mountGraphExecutionBridge(), []);
9698

99+
useEffect(() => {
100+
if (!projectId) return;
101+
102+
const isEditableTarget = (target: EventTarget | null) => {
103+
if (!(target instanceof HTMLElement)) return false;
104+
if (target.isContentEditable) return true;
105+
const tagName = target.tagName.toLowerCase();
106+
return (
107+
tagName === 'input' || tagName === 'textarea' || tagName === 'select'
108+
);
109+
};
110+
111+
const routeByDigit: Record<string, string> = {
112+
'1': `/editor/project/${projectId}`,
113+
'2': `/editor/project/${projectId}/blueprint`,
114+
'3': `/editor/project/${projectId}/nodegraph`,
115+
'4': `/editor/project/${projectId}/animation`,
116+
'5': `/editor/project/${projectId}/component`,
117+
'6': `/editor/project/${projectId}/resources`,
118+
'7': `/editor/project/${projectId}/test`,
119+
'8': `/editor/project/${projectId}/export`,
120+
'9': `/editor/project/${projectId}/deployment`,
121+
};
122+
123+
const onKeyDown = (event: globalThis.KeyboardEvent) => {
124+
if (event.defaultPrevented) return;
125+
if (isEditableTarget(event.target)) return;
126+
if (!event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
127+
return;
128+
}
129+
const nextPath = routeByDigit[event.key];
130+
if (!nextPath) return;
131+
if (location.pathname === nextPath) return;
132+
event.preventDefault();
133+
navigate(nextPath);
134+
};
135+
136+
window.addEventListener('keydown', onKeyDown);
137+
return () => window.removeEventListener('keydown', onKeyDown);
138+
}, [location.pathname, navigate, projectId]);
139+
97140
return (
98141
<div className="flex min-h-screen max-h-screen flex-row bg-[linear-gradient(120deg,var(--color-0)_20%,var(--color-1)_100%)]">
99142
<SettingsEffects />

apps/web/src/editor/EditorBar/EditorBar.tsx

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -90,18 +90,6 @@ function EditorBar() {
9090
title={t('projectHome.actions.blueprint.label')}
9191
to={`${basePath}/blueprint`}
9292
/>
93-
<MdrIconLink
94-
icon={<Box size={22} />}
95-
size={22}
96-
title={t('projectHome.actions.component.label')}
97-
to={`${basePath}/component`}
98-
/>
99-
<MdrIconLink
100-
icon={<Folder size={22} />}
101-
size={22}
102-
title={t('projectHome.actions.resources.label')}
103-
to={`${basePath}/resources`}
104-
/>
10593
<MdrIconLink
10694
icon={<GitBranch size={22} />}
10795
size={22}
@@ -114,6 +102,18 @@ function EditorBar() {
114102
title={t('projectHome.actions.animation.label')}
115103
to={`${basePath}/animation`}
116104
/>
105+
<MdrIconLink
106+
icon={<Box size={22} />}
107+
size={22}
108+
title={t('projectHome.actions.component.label')}
109+
to={`${basePath}/component`}
110+
/>
111+
<MdrIconLink
112+
icon={<Folder size={22} />}
113+
size={22}
114+
title={t('projectHome.actions.resources.label')}
115+
to={`${basePath}/resources`}
116+
/>
117117
<MdrIconLink
118118
icon={<TestTube size={22} />}
119119
size={22}

apps/web/src/editor/EditorBar/__tests__/EditorBar.test.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,21 @@ describe('EditorBar', () => {
6363
.getByTitle('projectHome.actions.resources.label')
6464
.getAttribute('href')
6565
).toBe('/editor/project/project-123/resources');
66+
const allLinks = screen
67+
.getAllByRole('link')
68+
.map((link) => link.getAttribute('title'));
69+
expect(allLinks).toEqual([
70+
'bar.projectHome',
71+
'projectHome.actions.blueprint.label',
72+
'projectHome.actions.nodegraph.label',
73+
'projectHome.actions.animation.label',
74+
'projectHome.actions.component.label',
75+
'projectHome.actions.resources.label',
76+
'projectHome.actions.testing.label',
77+
'projectHome.actions.export.label',
78+
'projectHome.actions.deployment.label',
79+
'projectHome.actions.settings.label',
80+
]);
6681
});
6782

6883
it('shows confirmation modal before leaving when prompts include leave', () => {

0 commit comments

Comments
 (0)