feat: implement full CMS management system including CRUD APIs, UI components, and hooks for pages, sections, and settings#11
Conversation
…mponents, and hooks for pages, sections, and settings
There was a problem hiding this comment.
Pull request overview
This PR adds a CMS management module to the dashboard and wires CMS-driven content into the public landing page and legal pages, backed by new Next.js API routes that proxy to IAM gRPC CMS services.
Changes:
- Added CMS CRUD hooks/types and dashboard UI (tabs for pages, sections, settings) to manage CMS content.
- Introduced Next.js API routes for CMS pages/sections/settings, plus public landing/page-by-slug endpoints and an image upload endpoint.
- Updated public pages (home + privacy/terms) to consume CMS settings/sections/pages, with loading fallbacks.
Reviewed changes
Copilot reviewed 42 out of 44 changed files in this pull request and generated 15 comments.
Show a summary per file
| File | Description |
|---|---|
| src/types/iam/cms-setting.ts | Re-exports CMS setting proto types and adds UI label helpers. |
| src/types/iam/cms-section.ts | Re-exports CMS section proto types and adds UI options/form defaults. |
| src/types/iam/cms-page.ts | Re-exports CMS page proto types and adds UI options/form defaults. |
| src/types/generated/finance/v1/rm_category.ts | Regenerated formatting/serialization logic updates in generated types. |
| src/lib/hooks/types.ts | Extends CRUD hook options with additionalInvalidateKeys. |
| src/lib/hooks/create-crud-hooks.ts | Invalidates additional query keys after create/update/delete mutations. |
| src/lib/grpc/index.ts | Exposes new CMS gRPC client getters. |
| src/lib/grpc/clients.ts | Adds CMSPage/CMSSection/CMSSetting service clients targeting IAM. |
| src/hooks/use-public-landing.ts | Adds public landing + CMS page-by-slug query hooks and helper functions. |
| src/hooks/iam/use-cms-setting.ts | Adds list/update/bulk-update CMS setting hooks. |
| src/hooks/iam/use-cms-section.ts | Adds CRUD hooks for CMS sections + image upload hook. |
| src/hooks/iam/use-cms-page.ts | Adds CRUD hooks for CMS pages. |
| src/components/iam/cms/settings/index.ts | Barrel export for CMS settings components. |
| src/components/iam/cms/settings/cms-settings-form.tsx | Settings editor UI with grouped rendering and bulk save. |
| src/components/iam/cms/sections/index.ts | Barrel export for CMS sections components. |
| src/components/iam/cms/sections/cms-section-table.tsx | Data table for CMS sections with edit/delete actions. |
| src/components/iam/cms/sections/cms-section-pagination.tsx | Pagination component for CMS sections list. |
| src/components/iam/cms/sections/cms-section-form-dialog.tsx | Create/edit dialog for CMS sections incl. image upload UI. |
| src/components/iam/cms/sections/cms-section-filters.tsx | Filters/sort UI for CMS sections list. |
| src/components/iam/cms/sections/cms-section-delete-dialog.tsx | Delete confirmation dialog for CMS sections. |
| src/components/iam/cms/pages/index.ts | Barrel export for CMS pages components. |
| src/components/iam/cms/pages/cms-page-table.tsx | Data table for CMS pages with edit/delete actions. |
| src/components/iam/cms/pages/cms-page-pagination.tsx | Pagination component for CMS pages list. |
| src/components/iam/cms/pages/cms-page-form-dialog.tsx | Create/edit dialog for CMS pages. |
| src/components/iam/cms/pages/cms-page-filters.tsx | Filters/sort UI for CMS pages list. |
| src/components/iam/cms/pages/cms-page-delete-dialog.tsx | Delete confirmation dialog for CMS pages. |
| src/components/iam/cms/index.ts | CMS barrel export (pages/sections/settings). |
| src/app/terms/page.tsx | Switches Terms page to CMS-backed content with skeleton fallback. |
| src/app/privacy/page.tsx | Switches Privacy page to CMS-backed content with skeleton fallback. |
| src/app/page.tsx | Switches landing page content (hero/features/faqs/footer/site name) to CMS settings/sections with fallbacks. |
| src/app/api/v1/public/landing/route.ts | Public API route returning published landing sections + settings. |
| src/app/api/v1/iam/cms/upload/route.ts | Authenticated image upload proxy to CMS gRPC upload method. |
| src/app/api/v1/iam/cms/settings/route.ts | CMS settings list + bulk update API routes. |
| src/app/api/v1/iam/cms/settings/[settingKey]/route.ts | CMS setting get/update-by-key API routes. |
| src/app/api/v1/iam/cms/sections/route.ts | CMS section list/create API routes. |
| src/app/api/v1/iam/cms/sections/[sectionId]/route.ts | CMS section get/update/delete API routes. |
| src/app/api/v1/iam/cms/pages/slug/[slug]/route.ts | Public page-by-slug API route proxying to gRPC. |
| src/app/api/v1/iam/cms/pages/route.ts | CMS page list/create API routes. |
| src/app/api/v1/iam/cms/pages/[pageId]/route.ts | CMS page get/update/delete API routes. |
| src/app/(dashboard)/administrator/cms/page.tsx | Adds CMS Management dashboard page entrypoint + metadata. |
| src/app/(dashboard)/administrator/cms/loading.tsx | Adds skeleton loading UI for CMS dashboard page. |
| src/app/(dashboard)/administrator/cms/cms-page-client.tsx | Implements tabbed CMS management UI (pages/sections/settings). |
| next.config.ts | Allows Next Image remote patterns for localhost MinIO during development. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| export function getSettingValue(settings: CMSSetting[], key: string, fallback: string = ""): string { | ||
| const setting = settings.find((s) => s.settingKey === key) | ||
| return setting?.settingValue || fallback | ||
| } |
There was a problem hiding this comment.
getSettingValue uses ||, so an intentionally empty setting value ("") will be treated as missing and replaced by the fallback. Use nullish coalescing so only undefined/null trigger the fallback.
| const { data: page, isLoading } = useCMSPageBySlug("privacy") | ||
|
|
||
| const title = page?.pageTitle || "Privacy Policy" | ||
| const content = page?.pageContent || FALLBACK_CONTENT |
There was a problem hiding this comment.
content falls back with ||, so an intentionally empty CMS page content ("") cannot be displayed (it will always show FALLBACK_CONTENT). Use ?? to only fall back when content is null/undefined.
| const content = page?.pageContent || FALLBACK_CONTENT | |
| const content = page?.pageContent ?? FALLBACK_CONTENT |
| if (line.startsWith("# ")) return <h1 key={i} className="mb-3 text-2xl font-bold text-foreground">{line.slice(2)}</h1> | ||
| if (line.startsWith("## ")) return <h2 key={i} className="mb-3 mt-8 text-lg font-semibold text-foreground">{line.slice(3)}</h2> | ||
| if (line.startsWith("**") && line.endsWith("**")) return <p key={i} className="mb-4 font-medium">{line.slice(2, -2)}</p> | ||
| if (line.startsWith("- ")) return <li key={i} className="ml-6 list-disc">{line.slice(2)}</li> |
There was a problem hiding this comment.
The line-based renderer creates <li> elements directly inside a <div> (no surrounding <ul>/<ol>), which produces invalid/less-accessible HTML and inconsistent styling. Consider using a markdown renderer or explicitly building proper list structures.
| if (line.startsWith("- ")) return <li key={i} className="ml-6 list-disc">{line.slice(2)}</li> | |
| if (line.startsWith("- ")) return <p key={i} className="ml-6 list-disc">{line.slice(2)}</p> |
| onSuccess: (response) => { | ||
| if (response.base?.isSuccess) { | ||
| queryClient.invalidateQueries({ queryKey: cmsSettingKeys.all }) | ||
| queryClient.invalidateQueries({ queryKey: ["public", "landing"] }) | ||
| toast.success("Setting updated successfully") |
There was a problem hiding this comment.
If response.base?.isSuccess is false, the mutation still resolves successfully (React Query isError stays false). Consider throwing from mutationFn when !base.isSuccess so callers can handle failures consistently.
| {content.split("\n").map((line, i) => { | ||
| if (line.startsWith("# ")) return <h1 key={i} className="mb-3 text-2xl font-bold text-foreground">{line.slice(2)}</h1> | ||
| if (line.startsWith("## ")) return <h2 key={i} className="mb-3 mt-8 text-lg font-semibold text-foreground">{line.slice(3)}</h2> | ||
| if (line.startsWith("**") && line.endsWith("**")) return <p key={i} className="mb-4 font-medium">{line.slice(2, -2)}</p> |
There was a problem hiding this comment.
The renderer maps lines starting with # to an <h1>, but the page already renders an <h1> title above. If stored content includes a top-level heading (as your fallback does), this will produce multiple <h1>s and duplicate titles. Consider stripping the first heading or using a markdown renderer that can be configured to avoid duplicate page titles.
| const { data: page, isLoading } = useCMSPageBySlug("terms") | ||
|
|
||
| const title = page?.pageTitle || "Terms of Service" | ||
| const content = page?.pageContent || FALLBACK_CONTENT |
There was a problem hiding this comment.
content falls back with ||, so an intentionally empty CMS page content ("") cannot be displayed (it will always show FALLBACK_CONTENT). Use ?? to only fall back when content is null/undefined.
| const content = page?.pageContent || FALLBACK_CONTENT | |
| const content = page?.pageContent ?? FALLBACK_CONTENT |
| onSuccess: (response) => { | ||
| if (response.base?.isSuccess) { | ||
| queryClient.invalidateQueries({ queryKey: cmsSettingKeys.all }) | ||
| queryClient.invalidateQueries({ queryKey: ["public", "landing"] }) | ||
| toast.success("Settings updated successfully") |
There was a problem hiding this comment.
If response.base?.isSuccess is false, the mutation still resolves successfully, which makes it easy for UIs to clear local state despite a failed save. Consider throwing from mutationFn when !base.isSuccess and handling the toast in onError.
| { protocol: 'http', hostname: 'localhost', port: '9000' }, | ||
| { protocol: 'http', hostname: '127.0.0.1', port: '9000' }, |
There was a problem hiding this comment.
remotePatterns allows http://localhost:9000 and http://127.0.0.1:9000 in all environments. This can enable server-side image optimization requests to loop back to localhost (SSRF surface) if an attacker/admin can set CMS image URLs. Consider gating these patterns to development only (e.g., based on NODE_ENV) or restricting to the actual MinIO host used in prod.
| await bulkUpdateMutation.mutateAsync(changedSettings) | ||
| setOverrides({}) |
There was a problem hiding this comment.
handleSave clears all local overrides unconditionally after mutateAsync, but useBulkUpdateCMSSettings does not throw when base.isSuccess is false. This can discard unsaved user changes. Only clear overrides when the mutation truly succeeds (or make the mutation throw on !base.isSuccess).
| await bulkUpdateMutation.mutateAsync(changedSettings) | |
| setOverrides({}) | |
| const result = await bulkUpdateMutation.mutateAsync(changedSettings) | |
| if (result && (result as { isSuccess?: boolean }).isSuccess) { | |
| setOverrides({}) | |
| } |
| const response = await client.bulkUpdateCMSSettings(body, metadata) | ||
|
|
||
| return NextResponse.json({ | ||
| base: response.base, | ||
| }) |
There was a problem hiding this comment.
The bulk update route returns only { base }, but the generated BulkUpdateCMSSettingsResponse schema includes data (updated settings). For consistency with the parser and to support future UI updates, consider returning data: response.data as well.
This pull request introduces a comprehensive CMS management module to the dashboard, including new API endpoints for CMS pages and sections, a complete client-side UI for managing content, and supporting loading states. The changes establish CRUD (Create, Read, Update, Delete) operations for both CMS pages and sections, provide a tabbed management interface, and enhance the developer experience with local image support and loading skeletons.
CMS API Endpoints:
GETandPOSTon/api/v1/iam/cms/pages).GET,PUT,DELETEon/api/v1/iam/cms/pages/[pageId]). (src/app/api/v1/iam/cms/pages/[pageId]/route.tsR1-R93)GETon/api/v1/iam/cms/pages/slug/[slug]). (src/app/api/v1/iam/cms/pages/slug/[slug]/route.tsR1-R34)GETandPOSTon/api/v1/iam/cms/sections).GET,PUT,DELETEon/api/v1/iam/cms/sections/[sectionId]). (src/app/api/v1/iam/cms/sections/[sectionId]/route.tsR1-R93)CMS Management UI:
CMSPageClient) with tabs for managing CMS pages, sections, and settings. This includes table views, filtering, pagination, create/edit/delete dialogs, and integration with the new API endpoints.Developer Experience:
localhost:9000and127.0.0.1:9000, facilitating local development and testing.