Revert "[FEAT] Implement Email service & Send emails for workspace invitations"#19
Conversation
There was a problem hiding this comment.
Pull request overview
This PR nominally reverts the previously added email/invite email functionality, but the diff also introduces several new UI + API capabilities around image uploads/cover images, project icons, and favoriting projects.
Changes:
- Removes email invite acceptance UI flow and SMTP sender integration, switching queue email handling to a noop sender.
- Adds authenticated file upload + file serving endpoints backed by MinIO, plus UI modals for uploading/selecting images (including Unsplash search).
- Adds “favorite projects” feature (API persistence + UI toggles + global Favorites context) and expands project/workspace/user models to support logos/covers/icons.
Reviewed changes
Copilot reviewed 53 out of 53 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| ui/vite.config.ts | Adjusts manual chunking and warning limit for larger vendor bundles. |
| ui/src/types/index.ts | Adds coverImageUrl to User type. |
| ui/src/services/workspaceService.ts | Extends workspace update payload to support logo; removes joinByToken. |
| ui/src/services/userService.ts | Minor signature formatting change for createToken. |
| ui/src/services/uploadService.ts | Adds image upload helper calling /api/upload. |
| ui/src/services/projectService.ts | Extends project update payload to support cover/icon fields. |
| ui/src/services/instanceService.ts | Adds Unsplash search client method + types. |
| ui/src/routes/index.tsx | Removes invite accept route; comments out unused instance-admin login route. |
| ui/src/pages/instance-admin/InstanceAdminImagePage.tsx | Casts payload for updateSection call. |
| ui/src/pages/instance-admin/InstanceAdminEmailPage.tsx | Casts payload for updateSection call. |
| ui/src/pages/instance-admin/InstanceAdminAuthenticationPage.tsx | Type tweaks for icon component type; minor state rename. |
| ui/src/pages/instance-admin/InstanceAdminAIPage.tsx | Casts payload for updateSection call. |
| ui/src/pages/WorkspaceViewsPage.tsx | Refines placeholder getUser return type. |
| ui/src/pages/WorkspaceHomePage.tsx | Comments out unused helpers; minor state naming changes. |
| ui/src/pages/SettingsPage.tsx | Adds cover/avatar/logo upload + project icon/cover selection UI. |
| ui/src/pages/ProjectsListPage.tsx | Adds project cards with cover/icon display + favorite toggle UI. |
| ui/src/pages/ProfilePage.tsx | Adds cover image rendering, assignee display improvements, and activity shaping. |
| ui/src/pages/LoginPage.tsx | Simplifies post-login redirect extraction from location state. |
| ui/src/pages/IssueListPage.tsx | Uses member avatar URLs and getImageUrl for assignee avatars. |
| ui/src/pages/IssueDetailPage.tsx | Aligns project object fields with API response shape. |
| ui/src/pages/InviteAcceptPage.tsx | Deletes invite acceptance page. |
| ui/src/pages/CyclesPage.tsx | Refines placeholder getUser return type. |
| ui/src/pages/BoardPage.tsx | Removes unused Avatar import. |
| ui/src/pages/AnalyticsWorkItemsPage.tsx | Fixes issue field name usage (project_id, state_id). |
| ui/src/lib/utils.ts | Adds getImageUrl helper using configured API base URL. |
| ui/src/contexts/FavoritesContext.tsx | Introduces Favorites context/provider. |
| ui/src/contexts/AuthContext.tsx | Maps cover_image into coverImageUrl. |
| ui/src/components/work-item/CommentEditor.tsx | Changes TipTap codeBlock config shape. |
| ui/src/components/layout/Sidebar.tsx | Integrates favorites and image URL handling; passes API project types to modal. |
| ui/src/components/layout/PageHeader.tsx | Minor state naming changes. |
| ui/src/components/UploadImageModal.tsx | Adds modal UI for uploading an image file. |
| ui/src/components/ProjectIconModal.tsx | Adds modal UI for selecting emoji/icon-based project icons. |
| ui/src/components/CoverImageModal.tsx | Adds modal UI for Unsplash search + upload cover selection. |
| ui/src/api/types.ts | Adds workspace logo, project cover/icon fields, user cover image, and page fields. |
| ui/src/App.tsx | Wraps app with FavoritesProvider. |
| api/internal/store/user_favorite.go | Adds persistence layer for user favorite projects. |
| api/internal/service/workspace.go | Adds workspace logo update support. |
| api/internal/service/project.go | Adds project cover/emoji/icon update support. |
| api/internal/router/router.go | Adds favorites endpoints, Unsplash proxy, MinIO upload/serve endpoints; removes AppBaseURL handling from handler wiring. |
| api/internal/model/user_favorite.go | Adds unique index for favorites across (user, entity_type, entity_identifier). |
| api/internal/model/user.go | Adds cover_image field to user model. |
| api/internal/model/project.go | Adds cover_image field to project model. |
| api/internal/minio/minio.go | Adds PutObject/GetObject helpers. |
| api/internal/mail/mail.go | Removes SMTP email sender implementation. |
| api/internal/handler/workspace.go | Removes email invite enqueueing; adds logo to workspace update body. |
| api/internal/handler/upload.go | Adds authenticated upload + file streaming handlers backed by MinIO. |
| api/internal/handler/project.go | Accepts/propagates cover/emoji/icon updates for projects. |
| api/internal/handler/instance.go | Adds Unsplash proxy endpoint using instance “image” settings access key. |
| api/internal/handler/favorite.go | Adds favorite-project CRUD endpoints for current user. |
| api/internal/handler/auth.go | Adds avatar + cover_image fields to UpdateMe and response. |
| api/internal/config/config.go | Removes AppBaseURL config. |
| api/cmd/api/main.go | Makes MinIO optional; switches email consumer to noop sender; removes SMTP wiring. |
| api/.env.example | Removes APP_BASE_URL example. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <Link | ||
| to={`${baseUrl}/projects/${project.id}/issues`} | ||
| className="block no-underline" | ||
| > | ||
| {/* Cover image */} | ||
| <div | ||
| className="relative h-32 w-full shrink-0 rounded-t-xl overflow-hidden" | ||
| style={ | ||
| coverUrl | ||
| ? { | ||
| backgroundImage: `url(${coverUrl})`, | ||
| backgroundSize: "cover", | ||
| backgroundPosition: "center", | ||
| } | ||
| : { background: getCoverGradient(project.id) } | ||
| } | ||
| > | ||
| {/* Banner with overlay */} | ||
| <div | ||
| className="relative h-28 min-h-[7rem] w-full shrink-0 rounded-t-md" | ||
| style={{ background: getCoverGradient(project.id) }} | ||
| {/* Star favorite (right side, vertically centered) */} | ||
| <button | ||
| type="button" | ||
| onClick={(e) => toggleFavorite(e, project.id)} | ||
| disabled={favoriteRequestInFlight[project.id]} | ||
| className="absolute right-3 top-1/2 z-10 flex size-9 -translate-y-1/2 items-center justify-center rounded-full bg-white/30 backdrop-blur-sm text-white shadow-sm hover:bg-white/45 focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80 disabled:opacity-60 disabled:pointer-events-none" | ||
| aria-label={ | ||
| favoriteProjectIds.includes(project.id) | ||
| ? "Remove from favorites" | ||
| : "Add to favorites" | ||
| } | ||
| > | ||
| <div className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/60 to-transparent p-4 pt-8"> | ||
| <p className="truncate text-base font-semibold text-white"> | ||
| <IconStar | ||
| filled={favoriteProjectIds.includes(project.id)} | ||
| /> | ||
| </button> |
There was a problem hiding this comment.
The favorite star <button> is rendered inside a <Link> (anchor). Nesting interactive elements is invalid HTML and can cause accessibility/keyboard and click handling issues across browsers/screen readers, even with preventDefault/stopPropagation. Consider moving the favorite toggle button outside the link (e.g., absolutely positioned in the card but not nested under the anchor) or make the whole card a button/link and handle navigation separately.
| const returnPath = | ||
| (location.state as { from?: { pathname?: string } })?.from?.pathname ?? "/"; |
There was a problem hiding this comment.
returnPath now only uses from.pathname and drops any from.search query string. Since ProtectedRoute passes the full location (including search), this change will lose query params on post-login redirects (e.g. deep-links that rely on ?foo=bar). Consider restoring the previous behavior by appending from.search when present (and update the cast type accordingly).
| const form = new FormData(); | ||
| form.append("file", file); | ||
| const { data } = await apiClient.post<UploadResponse>("/api/upload", form); | ||
| return data; |
There was a problem hiding this comment.
uploadImage posts FormData using the shared apiClient, which sets a default Content-Type: application/json header. For multipart uploads this can result in the request being sent with the wrong content type/boundary. Consider overriding/removing Content-Type for this request (or using a dedicated axios instance for multipart) so the browser/axios can set the correct multipart/form-data boundary.
| apiURL := "https://api.unsplash.com/search/photos?query=" + url.QueryEscape(q) + "&per_page=20" | ||
| req, err := http.NewRequestWithContext(c.Request.Context(), http.MethodGet, apiURL, nil) | ||
| if err != nil { | ||
| c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to search"}) | ||
| return | ||
| } | ||
| req.Header.Set("Authorization", "Client-ID "+keyVal) | ||
|
|
||
| resp, err := http.DefaultClient.Do(req) | ||
| if err != nil { |
There was a problem hiding this comment.
UnsplashSearch uses http.DefaultClient.Do(req) which has no timeout by default. A slow/hung upstream Unsplash request can tie up handler goroutines and degrade API availability. Consider using an http.Client with a reasonable timeout (or enforcing a context deadline) for this outbound call.
| file, err := c.FormFile("file") | ||
| if err != nil { | ||
| c.JSON(http.StatusBadRequest, gin.H{"error": "No file provided", "detail": err.Error()}) | ||
| return | ||
| } |
There was a problem hiding this comment.
The upload handler does not enforce any maximum upload size (it relies on whatever the client sends as file.Size). This can allow very large uploads and lead to memory/disk/bandwidth exhaustion. Consider enforcing a server-side size limit (and/or configuring Gin's MaxMultipartMemory) and returning a clear 413/400 error when exceeded.
| api.GET("/users/me/favorite-projects/", favoriteHandler.ListFavoriteProjects) | ||
| api.GET("/instance/settings/", instanceSettingsHandler.GetSettings) | ||
| api.PATCH("/instance/settings/:key", instanceSettingsHandler.UpdateSetting) | ||
| api.GET("/instance/unsplash/search", instanceSettingsHandler.UnsplashSearch) | ||
|
|
||
| uploadHandler := &handler.UploadHandler{Minio: cfg.Minio} | ||
| api.POST("/upload", uploadHandler.Upload) | ||
| api.GET("/files/*path", uploadHandler.ServeFile) |
There was a problem hiding this comment.
The PR metadata says this is a revert of the email service / invite emails, but the diff also introduces new functionality (favorites endpoints, MinIO upload endpoints, Unsplash proxy, cover images, project icons, etc.). Please update the PR title/description to match the scope, or split these changes into separate PRs so the revert can be reviewed/rolled back independently.
Reverts #18