feat: page creation and content edit#352
Conversation
|
Caution Review failedThe pull request is closed. WalkthroughThis update implements comprehensive page management enhancements across the application. The changes introduce new Vue components for displaying, creating, updating, and deleting pages in the command center along with dynamic routing for page display. UI refinements were applied to existing components' styling, and the Tiptap editor was updated to accept external content and emit change events. On the backend, new API endpoints and database functions handle CRUD operations for pages, and the store now maintains a reactive pages array. Additionally, localization and helper utilities were extended to support the new functionality. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant CreatePageComp as CreatePage Component
participant API as POST /api/page
participant DB as createPage() in db/page.ts
participant Channel as Channel Store
User->>CreatePageComp: Fill in page creation form
CreatePageComp->>API: Send POST request with form data
API->>DB: Validate data and create new page
DB-->>API: Return created page info
API->>Channel: Call setChannelAsUpdated (update pages)
API-->>CreatePageComp: Send success response
CreatePageComp->>User: Display success toast
Poem
Tip ⚡🧪 Multi-step agentic review comment chat (experimental)
📜 Recent review detailsConfiguration used: CodeRabbit UI ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (1)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
|
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (19)
apps/web-app/app/components/CommandCenter/Navigation.vue (1)
74-78: Icon naming pattern differs from other menu items.You've added a new menu item for pages using the icon
i-lucide-layers-2, while other menu items use thefood:prefix (likefood:dashboard,food:cooking, etc.). Consider using a consistent naming pattern for better maintainability, unless this is an intentional distinction.- icon: 'i-lucide-layers-2', + icon: 'food:pages',apps/web-app/app/components/CommandCenter/PageCreateCard.vue (1)
1-11: Good UI design for create card componentThe component has good visual styling with appropriate spacing, icons, and button configuration. The dashed border effectively communicates the "create new" affordance to users.
One improvement to consider:
- <div class="flex flex-col gap-4 justify-center items-center h-full min-h-56 border-2 border-(--ui-border) border-dashed rounded-xl"> + <div class="flex flex-col gap-4 justify-center items-center h-full min-h-56 border-2 border-(--ui-border) border-dashed rounded-xl cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">Adding cursor-pointer and hover effects would enhance the interactive feel of this component.
packages/core/types/index.d.ts (1)
244-251: Well-defined Page interfaceThe Page interface is properly structured with appropriate types and follows the same pattern as other entities in the application. The use of LocaleValue array for title supports multilingual content.
Consider if additional fields might be needed for future requirements such as:
- authorId or createdBy for tracking who created/modified the page
- isActive or status field for controlling visibility
- metaTitle and metaDescription for SEO
apps/web-app/app/components/Form/CreatePage.vue (1)
76-93: API error handling could be improvedWhile you're handling errors by showing a toast, consider providing more specific error messages based on the error type. This would help users understand what went wrong and how to fix it.
} catch (error) { console.error(error) - actionToast.error() + if (error.response?.status === 409) { + actionToast.error(t('toast.page-already-exists')) + } else if (error.response?.data?.message) { + actionToast.error(error.response.data.message) + } else { + actionToast.error(t('toast.error-creating-page')) + } }apps/web-app/app/components/Modal/UpdatePage.vue (1)
29-33: The closeAll function might be too broadThe
closeAllfunction closes all overlays in the application, which might include unrelated ones. Consider closing only the current modal.function closeAll() { - for (const o of overlay.overlays) { - overlay.close(o.id) - } + // Close only the current modal + if (overlay.current) { + overlay.close(overlay.current.id) + } }apps/web-app/stores/channel.ts (1)
153-153: Consider adding helper methods for page operationsWhile you've added the pages array to the returned store object, consider adding helper methods for common page operations similar to other entities in the store.
Consider adding methods like:
function getPage(id: string): ComputedRef<Page | undefined> { return computed(() => pages.value.find((page) => page.id === id)) } function getPageBySlug(slug: string): ComputedRef<Page | undefined> { return computed(() => pages.value.find((page) => page.slug === slug)) }This would make it easier to retrieve specific pages throughout the application and maintain consistency with other entity access patterns.
packages/core/shared/services/page.ts (1)
4-9: Consider enhancing slug validation for better data integrityThe slug validation currently only checks for length (2-50 characters), which may allow invalid characters or formats. Typical slug fields should:
- Contain only lowercase alphanumeric characters, hyphens, or underscores
- Not start or end with hyphens or underscores
- Not contain consecutive hyphens or underscores
- slug: z.string().min(2).max(50), + slug: z.string().min(2).max(50).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, + { message: "Slug must contain only lowercase letters, numbers, and hyphens and cannot start or end with a hyphen" }),apps/web-app/app/components/CommandCenter/PageCard.vue (2)
2-14: Add fallback UI for when page is not foundThe component uses optional chaining (
page?.slugandpage?.title) but doesn't provide fallback content when the page is not found, which could result in an empty or incomplete card.<template> <NuxtLink :to="`/command-center/page/${id}`" class="bg-(--ui-bg-elevated)/50 rounded-xl relative min-h-28 text-center flex flex-col gap-1 items-center justify-center"> <UIcon name="i-lucide-file-text" class="size-6 text-(--ui-text-dimmed)" /> <p class="text-xs text-(--ui-text-muted)"> - /{{ page?.slug }} + /{{ page?.slug || $t('common.not-found') }} </p> <p> - {{ getLocaleValue({ values: page?.title, locale, defaultLocale: channel.defaultLocale }) }} + {{ page ? getLocaleValue({ values: page.title, locale, defaultLocale: channel.defaultLocale }) : $t('common.not-found') }} </p> </NuxtLink> </template>
18-24: Validate page existence in the computed propertyThe component finds the page in the channel store but doesn't handle the case where the page might not exist. Consider adding validation or error handling.
const { id } = defineProps<{ id: string }>() const { locale } = useI18n() const channel = useChannelStore() - const page = computed(() => channel.pages.find((p) => p.id === id)) + const page = computed(() => { + const foundPage = channel.pages.find((p) => p.id === id) + if (!foundPage) { + console.warn(`Page with id ${id} not found`) + } + return foundPage + })apps/web-app/app/components/Form/DeletePage.vue (1)
27-45: Improve error feedback to usersThe current error handling logs to the console but provides a generic error toast to the user. Consider providing more specific error messages to help users understand what went wrong.
async function onSubmit() { actionToast.start() emit('submitted') try { await $fetch(`/api/page/${id}`, { method: 'DELETE', }) await channel.update() actionToast.success(t('toast.page-deleted')) emit('success') router.push(redirectTo) } catch (error) { console.error(error) - actionToast.error() + const errorMessage = error.data?.message || t('toast.generic_error') + actionToast.error(errorMessage) } }Also add a reactive property for the confirmation dialog:
const isConfirmOpen = ref(false) function confirmDelete() { isConfirmOpen.value = true }apps/web-app/app/components/Form/UpdatePage.vue (1)
79-96: Improve error handling with specific error messagesThe current error handling logs to the console but provides a generic error toast. Consider providing more specific error messages for better user feedback.
async function onSubmit(event: FormSubmitEvent<PageUpdateSchema>) { actionToast.start() emit('submitted') try { await $fetch(`/api/page/${id}`, { method: 'PATCH', body: event.data, }) await channel.update() actionToast.success(t('toast.page-updated')) emit('success') } catch (error) { console.error(error) - actionToast.error() + if (error.response?.status === 404) { + actionToast.error(t('toast.page-not-found')) + } else if (error.data?.message) { + actionToast.error(error.data.message) + } else { + actionToast.error(t('toast.generic_error')) + } } }apps/web-app/app/components/TiptapEditor.vue (2)
118-125: Consider using more specific extensions instead of the entire starter kitThe TiptapStarterKit includes many extensions that might not be needed. For better performance and bundle size, consider importing only the specific extensions you're using in the editor.
127-129: Add debounce to the content change emissionEmitting the change event on every document change can lead to excessive updates, especially during fast typing. Consider adding a debounce mechanism.
-watch(() => editor.value?.state.doc, () => { - emit('change', editor.value?.getHTML() as string) -}) +const debouncedEmit = useDebounceFn(() => { + emit('change', editor.value?.getHTML() as string) +}, 300) + +watch(() => editor.value?.state.doc, () => { + debouncedEmit() +})packages/core/server/services/db/page.ts (2)
38-45: Missing field validation in createPageThe createPage function doesn't validate that required fields are present in the data object. Consider adding validation to ensure necessary fields like 'id' exist before attempting to create the page.
export async function createPage(data: Omit<Page, 'createdAt' | 'updatedAt'>): Promise<Page | null> { + if (!data.id) { + throw new Error('Page ID is required') + } + await useStorage('db').setItem(`page:${data.id}`, { ...data, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }) return getPage(data.id) }
47-49: Consider returning a boolean for deletePageReturning a boolean indicating whether the deletion was successful would make error handling more consistent with other functions.
-export async function deletePage(id: string): Promise<void> { - await useStorage('db').removeItem(`page:${id}`) +export async function deletePage(id: string): Promise<boolean> { + try { + await useStorage('db').removeItem(`page:${id}`) + return true + } catch (error) { + console.error(`Failed to delete page ${id}:`, error) + return false + } }apps/web-app/app/pages/command-center/page/[pageId]/index.vue (2)
21-23: TiptapEditor integration needs proper error handling.The TiptapEditor component correctly receives content and emits changes, but there's no validation or error handling for potentially malformed content.
Consider adding content validation before submission:
- <TiptapEditor :content="page?.content" @change="(c: string) => { content = c }" /> + <TiptapEditor + :content="page?.content" + @change="(c: string) => { + content = c; + contentHasChanged = true; + }" + />And add a computed property to detect if content has changed:
const contentHasChanged = ref(false) const canSubmit = computed(() => contentHasChanged.value && !!content.value)🧰 Tools
🪛 ESLint
[error] 22-22: Parsing error: Unexpected token :.
(vue/no-parsing-error)
48-66: Consider adding optimistic UI updates and content validation.The submission function works correctly but could benefit from optimistic UI updates and additional validation.
Consider enhancing the submit function with validation and optimistic updates:
async function onSubmit() { + if (!content.value?.trim()) { + actionToast.error(t('error.default')) + return + } actionToast.start() + const previousContent = page.value?.content + // Optimistically update local state + if (page.value) { + page.value.content = content.value + } try { await $fetch(`/api/page/${params.pageId}`, { method: 'PATCH', body: { locale: channel.defaultLocale, content: content.value, }, }) await channel.update() actionToast.success(t('toast.page-updated')) + contentHasChanged.value = false } catch (error) { console.error(error) actionToast.error() + // Revert optimistic update on error + if (page.value && previousContent) { + page.value.content = previousContent + } } }apps/web-app/app/pages/command-center/page/index.vue (2)
5-11: Consider adding loading and empty states.The current implementation doesn't handle cases where pages might be loading or when there are no pages to display.
Add loading and empty states to improve user experience:
- <div class="grid grid-cols-2 sm:grid-cols-4 md:grid-cols-4 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-4"> + <div v-if="channel.isLoading" class="flex justify-center items-center p-8"> + <UiSpinner size="lg" /> + </div> + <div v-else-if="channel.pages.length === 0" class="flex justify-center items-center p-8"> + <p class="text-gray-500">{{ t('common.no-items-found') }}</p> + </div> + <div v-else class="grid grid-cols-2 sm:grid-cols-4 md:grid-cols-4 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-4"> <CommandCenterPageCard v-for="page in channel.pages" :id="page.id" :key="page.id" /> <CommandCenterPageCreateCard @click="modalCreatePage.open()" /> </div>
24-28: Consider adding data fetching on component mount.The component relies on the channel store to already have page data loaded, but doesn't explicitly fetch data when it mounts.
Add explicit data fetching on component mount to ensure pages are loaded:
const { t } = useI18n() const overlay = useOverlay() const modalCreatePage = overlay.create(ModalCreatePage) const channel = useChannelStore() + + onMounted(async () => { + if (!channel.pages.length) { + await channel.update() + } + })
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (29)
apps/web-app/app/components/Checkout/Header.vue(1 hunks)apps/web-app/app/components/CommandCenter/Navigation.vue(1 hunks)apps/web-app/app/components/CommandCenter/PageCard.vue(1 hunks)apps/web-app/app/components/CommandCenter/PageCreateCard.vue(1 hunks)apps/web-app/app/components/Footer.vue(0 hunks)apps/web-app/app/components/Form/CreatePage.vue(1 hunks)apps/web-app/app/components/Form/DeletePage.vue(1 hunks)apps/web-app/app/components/Form/UpdatePage.vue(1 hunks)apps/web-app/app/components/Modal/CreatePage.vue(1 hunks)apps/web-app/app/components/Modal/UpdatePage.vue(1 hunks)apps/web-app/app/components/TiptapEditor.vue(1 hunks)apps/web-app/app/layouts/default.vue(1 hunks)apps/web-app/app/pages/[[page]].vue(1 hunks)apps/web-app/app/pages/command-center/page/[pageId]/index.vue(1 hunks)apps/web-app/app/pages/command-center/page/index.vue(1 hunks)apps/web-app/app/utils/helpers.ts(1 hunks)apps/web-app/i18n/locales/en-US.json(4 hunks)apps/web-app/i18n/locales/ka-GE.json(4 hunks)apps/web-app/i18n/locales/ru-RU.json(4 hunks)apps/web-app/stores/channel.ts(3 hunks)packages/core/server/api/channel/index.get.ts(3 hunks)packages/core/server/api/page/[id].delete.ts(1 hunks)packages/core/server/api/page/[id].patch.ts(1 hunks)packages/core/server/api/page/index.post.ts(1 hunks)packages/core/server/middleware/01.permissions.ts(1 hunks)packages/core/server/services/db/index.ts(3 hunks)packages/core/server/services/db/page.ts(1 hunks)packages/core/shared/services/page.ts(1 hunks)packages/core/types/index.d.ts(1 hunks)
💤 Files with no reviewable changes (1)
- apps/web-app/app/components/Footer.vue
🧰 Additional context used
🧬 Code Definitions (3)
packages/core/server/api/page/index.post.ts (2)
packages/core/shared/services/page.ts (1) (1)
pageCreateSchema(4-9)packages/core/server/services/db/page.ts (1) (1)
createPage(38-45)
packages/core/server/api/page/[id].delete.ts (1)
packages/core/server/services/db/page.ts (1) (1)
deletePage(47-49)
packages/core/server/api/page/[id].patch.ts (2)
packages/core/shared/services/page.ts (1) (1)
pageUpdateSchema(13-18)packages/core/server/services/db/page.ts (2) (2)
getPage(1-3)patchPage(21-36)
🪛 ESLint
apps/web-app/app/pages/command-center/page/[pageId]/index.vue
[error] 22-22: Parsing error: Unexpected token :.
(vue/no-parsing-error)
🔇 Additional comments (49)
apps/web-app/app/layouts/default.vue (1)
10-10: LGTM: Horizontal margin adjustment.Adding
mx-0to remove horizontal margins is a good adjustment that will allow content to utilize the full width of the container.packages/core/server/middleware/01.permissions.ts (1)
33-37: Permission configuration for page endpoints looks good.The permission setup for the new page API endpoints follows the established pattern in the codebase, requiring 'MANAGE_OPTIONS' or 'MASTER' permissions for write operations.
Is it intentional that GET requests to
/api/pageare not protected? If pages should be publicly readable, this is correct, but if they should be access-controlled, consider adding protection for GET requests as well.apps/web-app/app/components/Checkout/Header.vue (1)
2-2: Improved responsive layout and spacing.The updated class attributes improve the header's layout with proper flex properties and responsive padding. The previous implementation had redundant padding values (
px-4 md:px-4), while the new one creates a better responsive adjustment with the addition of spacing between flex items.apps/web-app/app/components/Modal/CreatePage.vue (2)
1-10: Well-structured modal componentThe modal is well-implemented using UModal and correctly loads the FormCreatePage component with appropriate event handling.
12-20: Clean implementation of overlay closing logicThe closeAll function efficiently handles closing all overlays when the form is submitted or successful. This ensures a smooth user experience when creating pages.
apps/web-app/app/utils/helpers.ts (1)
165-166: Good addition of new icon optionsAdding 'Gift' and 'Info' icons expands the available options for users when selecting link icons. These are useful additions that enhance the user experience.
packages/core/server/api/page/index.post.ts (1)
1-25: Page creation endpoint implementation is well-structured!The implementation follows good practices with proper:
- Input validation using Zod schema
- Unique ID generation
- Title localization
- Channel update for cache invalidation
- Error handling and consistent response format
However, I notice that while the title is stored in a localized format (array of locale-value pairs), the content is stored as a simple string. If content also needs internationalization support, consider transforming it similarly to the title field.
apps/web-app/app/components/Form/CreatePage.vue (2)
2-44: Well-structured form component with good UXThe form layout is clean and follows a consistent pattern with other form components in the application. Good use of UI components like UButtonGroup, UDropdownMenu, and proper form validation through schema.
60-65: Content field in state but missing in UIThe state includes a
contentfield initialized with an empty string, but there's no corresponding input field in the template. This suggests that page content is intended to be empty by default and edited elsewhere.Is this intentional? If users should be able to add content during creation, consider adding a content editor component.
apps/web-app/app/pages/[[page]].vue (2)
10-16: Good error handling for non-existent pagesProperly throwing a 404 error when a page is not found provides a good user experience and follows RESTful principles.
18-20: Dynamic page title is a good UX featureSetting the document title based on the page content using
useHeadandgetLocaleValueprovides good user experience and SEO benefits.apps/web-app/app/components/Modal/UpdatePage.vue (1)
6-7:Details
❓ Verification inconclusive
Potential issue with optional ID prop
You're using the nullish coalescing operator to provide an empty string when
idis undefined, but this could lead to unexpected behavior in child components that expect a valid ID.Verify that the child components can properly handle empty string IDs, or consider making the ID prop required or adding validation.
🏁 Script executed:
#!/bin/bash # Check how FormUpdatePage and FormDeletePage handle empty ID values rg -A 5 -B 5 "id.*props" --glob "**/Form*Page.vue"Length of output: 201
Attention: Manual Verification Needed for Optional ID Prop Handling
The initial search using ripgrep did not yield results, likely due to an overly restrictive filter. To ensure that child components (such as FormUpdatePage.vue and FormDeletePage.vue) correctly handle empty string IDs, please run the following shell script to locate and inspect these files for how they process their
idprop:Please review the output of this script manually to verify that empty string IDs are either handled gracefully or if further actions (like making the
idprop required or adding validation) are needed.
🏁 Script executed:
#!/bin/bash # Locate the relevant Vue files that might use the "id" prop files=$(fd -HI "Form(Update|Delete)Page.vue") if [ -z "$files" ]; then echo "No FormUpdatePage.vue or FormDeletePage.vue files found." else echo "Found the following files:" echo "$files" echo "Inspecting how the 'id' prop is used:" for file in $files; do echo "----- $file -----" rg -C 5 "id[ :]*" "$file" done fiLength of output: 187
Review Comment Update: Verify Handling of Empty ID Value
We've executed a search for child components (specifically, files like FormUpdatePage.vue or FormDeletePage.vue) that might depend on a non-empty
idprop, but no such files were found in the repository. Without concrete evidence from these components, it's unclear if passing an empty string (via the nullish coalescing operator) may negatively affect any child component behavior.Action Items:
- Manual Verification: Please verify in your project if any component that receives this
id(directly or indirectly) expects a valid, non-empty value.- Consider Enhancements: If a valid ID is required downstream, either make the
idprop mandatory or incorporate additional validation instead of defaulting to an empty string.apps/web-app/stores/channel.ts (2)
26-26: Good addition of pages state to the storeAdding the
pagesreactive array to the channel store is a good way to maintain state for pages across the application.
70-70: Pages data is properly updatedThe
updatefunction is properly updated to include pages data from the API response.packages/core/shared/services/page.ts (1)
13-18: Validation is consistent with create schemaThe update schema correctly makes all fields optional while maintaining the same validation rules as the create schema, which is a good practice for PATCH operations.
apps/web-app/app/components/Form/UpdatePage.vue (1)
1-45:Details
❓ Verification inconclusive
Missing content field in the update form
The
pageUpdateSchemaincludes a content field, but it's missing from the form UI. If content should be editable through this form, add a content editor field. If content is managed elsewhere, consider documenting this decision.Run the following to check if the content is managed in another component:
🏁 Script executed:
#!/bin/bash # Look for components that might be handling page content editing rg -l "v-model.*content" --type vue # Check if there's a dedicated content editor component fd -e vue "Content.*Editor|Editor.*Content"Length of output: 111
Content Field Missing in Update Form
The update form in
apps/web-app/app/components/Form/UpdatePage.vuedoes not provide a UI element for thecontentfield defined in thepageUpdateSchema. Our initial search (adjusted for proper Vue file recognition) did not reveal any dedicated content editor component or alternative handling for content editing. Please verify manually whether the content field is handled elsewhere. If it’s meant to be editable via this form, add an appropriate content editor field; otherwise, document the decision to manage content outside this component.packages/core/server/api/page/[id].patch.ts (1)
1-39: Code looks robust and follows best practicesThe PATCH endpoint implementation is well-structured with appropriate error handling, validation, and data processing logic. It correctly:
- Validates the request body against a schema
- Performs proper error handling with appropriate status codes
- Handles localization for title updates
- Updates the channel to reflect changes
I particularly like how it preserves existing title values when updating localized content.
apps/web-app/i18n/locales/en-US.json (5)
264-266: LGTM - Added "pages" menu itemThe localization entries for the Pages section in the navigation menu are correctly implemented.
267-267: LGTM - Added "save" localizationThe "save" localization entry is correctly added to support the page editing functionality.
276-277: LGTM - Added page creation localizationThe localization entry for creating a new page is correctly implemented alongside the existing "Create new Link" entry.
300-301: LGTM - Added page update localizationThe localization entry for updating a page is correctly implemented alongside the existing "Update Link" entry.
355-357: LGTM - Added toast messages for page operationsThe toast notification messages for page creation, update, and deletion operations are correctly implemented.
apps/web-app/app/components/TiptapEditor.vue (1)
112-116: Props and emits declarations are correctly implementedThe component properly defines a content prop and emits a change event when the content changes.
packages/core/server/services/db/page.ts (2)
1-3: LGTM - Simple and focused getPage functionThe getPage function follows a simple pattern of retrieving an item from storage by ID.
21-36: LGTM - Well-implemented page update functionThe patchPage function correctly:
- Checks for the existence of the page before updating
- Preserves existing data while applying updates
- Updates the timestamp
- Returns the updated page
apps/web-app/i18n/locales/ka-GE.json (5)
264-265: Localization additions for page functionality are consistent.The menu entry for "Pages" is properly added to the navigation section, maintaining consistency with other menu items.
267-267: Save action is correctly added to the localization file.This entry will be used for the save button in the page editor component.
276-277: Create page action is properly localized.The entry follows the existing pattern for create actions in the application.
300-301: Update page action is properly localized.The entry follows the existing pattern for update actions in the application.
355-357: Toast notifications for page operations are correctly implemented.All necessary toast messages for page creation, updating, and deletion are properly added, maintaining consistency with other toast notifications in the system.
apps/web-app/i18n/locales/ru-RU.json (5)
264-265: Russian localization for page functionality is properly implemented.The menu entry for "Pages" ("Страницы") is correctly added to the navigation section, maintaining consistency with other menu items.
267-267: Save action is correctly localized in Russian.The "Save" entry ("Сохранить") will be used for the save button in the page editor component.
276-277: Create page action is properly localized in Russian.The entry follows the existing pattern for create actions in the application, with proper Russian translation.
300-301: Update page action is properly localized in Russian.The entry follows the existing pattern for update actions in the application, with proper Russian translation.
355-357: Toast notifications for page operations are correctly implemented in Russian.All necessary toast messages for page creation, updating, and deletion are properly added in Russian, maintaining consistency with other toast notifications in the system.
apps/web-app/app/pages/command-center/page/[pageId]/index.vue (3)
1-19: Header with action buttons is implemented effectively.The component header provides a clear title and appropriate action buttons for editing and saving page content. The buttons have appropriate variants and responsive styling.
26-41: Page retrieval and error handling are implemented well.The component correctly retrieves the page from the store using the route parameter and throws an appropriate 404 error if the page is not found.
22-22:Details
✅ Verification successful
Fix the ESLint parsing error in the TiptapEditor component.
The static analysis tool reports a parsing error on this line, but it appears to be a false positive as the code is using valid Vue syntax.
Verify if this is a false positive by running:
🏁 Script executed:
#!/bin/bash # Check if there are any issues with the TiptapEditor component's props rg -A 5 "TiptapEditor" --type=vueLength of output: 61
Action: Verify ESLint Parsing for TiptapEditor
It looks like the initial ripgrep command failed because it didn’t recognize the "vue" file type. To further confirm that there are no genuine issues with the TiptapEditor component’s props—and that this ESLint parsing error is indeed a false positive—please run the updated verification command below, which explicitly searches all Vue files via a glob pattern:
After running this command, please verify:
- That the TiptapEditor usage (including props like :content and the @change event) is correctly implemented;
- Your ESLint configuration properly handles
.vuefiles (ensuring the appropriate plugins and parser options are in place).If no real issues surface, you can safely treat the ESLint message as a false positive. Please confirm the output manually.
🏁 Script executed:
#!/bin/bash # Re-run ripgrep to search for TiptapEditor occurrences in Vue files using a glob pattern. rg -g "*.vue" -A 5 "TiptapEditor"Length of output: 1012
ESLint Parsing Error is a False Positive
- Verified that the
<TiptapEditor>usage inapps/web-app/app/pages/command-center/page/[pageId]/index.vueis valid.- Confirmed that the component’s definition in
apps/web-app/app/components/TiptapEditor.vueproperly uses TypeScript with Vue’s SFC syntax.- The ESLint issue appears to be a false positive; please review your ESLint configuration to ensure it correctly parses Vue and TypeScript syntax.
🧰 Tools
🪛 ESLint
[error] 22-22: Parsing error: Unexpected token :.
(vue/no-parsing-error)
apps/web-app/app/pages/command-center/page/index.vue (2)
1-14: Responsive grid layout is well implemented.The page listing uses a responsive grid that adapts to different screen sizes, providing a good user experience across devices. The CommandCenterPageCard and CommandCenterPageCreateCard components are used effectively.
16-29: Script setup is clean and follows best practices.The component correctly uses composables and page metadata. The use of the channel store centralizes data management, which is a good practice.
packages/core/server/api/channel/index.get.ts (4)
6-6: Clean import addition.The addition of the
getPagesimport follows the existing pattern for similar service imports. The function is properly imported from the correct relative path.
64-64: Interface updating properly extends data structure.The destructured object now includes
pageKeys, which correctly aligns with the changes made to thegetKeysfunction.
71-71: Data retrieval pattern maintained.The implementation follows the established pattern of retrieving data based on filtered keys, maintaining code consistency across different entity types.
91-91: Response object extended with pages data.The addition of the
pagesproperty to the return object makes the page data available to the client, completing the data retrieval workflow.packages/core/server/services/db/index.ts (3)
10-10: Interface extended properly.The
Keysinterface has been updated to includepageKeysas a string array, maintaining consistency with the structure of other key types in the interface.
22-22: Key filtering logic follows established pattern.The implementation for filtering page-related keys uses the same pattern as other entity types, maintaining code consistency.
33-33: Return object extended with pageKeys.The
pageKeysproperty is properly included in the returned object, making it available for page data retrieval operations.packages/core/server/api/page/[id].delete.ts (2)
1-3: Imports are clean and focused.The imports are properly defined for the functionality required by this endpoint.
4-22: Handler implementation follows API endpoint patterns.The event handler follows the established pattern for API endpoints in the application, with proper error handling, parameter validation, and channel updates.
Consider adding an existence check before deletion to return a more specific error message if the page doesn't exist:
const id = getRouterParam(event, 'id') if (!id) { throw createError({ statusCode: 400, statusMessage: 'Missing id', }) } + // Check if page exists before deletion + const page = await getPage(id) + if (!page) { + throw createError({ + statusCode: 404, + statusMessage: 'Page not found', + }) + } await deletePage(id)This would require importing the
getPagefunction, but it would provide a better user experience by distinguishing between missing IDs and non-existent pages.
| <UButton | ||
| size="md" | ||
| variant="soft" | ||
| :label="$t('center.create.page')" | ||
| /> |
There was a problem hiding this comment.
Missing click handler on button
The button doesn't have a click handler (@click) to trigger the page creation modal. Without this, clicking the button won't perform any action.
<UButton
size="md"
variant="soft"
:label="$t('center.create.page')"
+ @click="openCreatePageModal"
/>You'll need to implement the openCreatePageModal function in the script section.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <UButton | |
| size="md" | |
| variant="soft" | |
| :label="$t('center.create.page')" | |
| /> | |
| <UButton | |
| size="md" | |
| variant="soft" | |
| :label="$t('center.create.page')" | |
| @click="openCreatePageModal" | |
| /> |
| @@ -0,0 +1,21 @@ | |||
| <template> | |||
| <div class="w-full prose prose-md dark:prose-invert text-(--ui-text) [&_p]:my-0" v-html="preparedPage.content" /> | |||
There was a problem hiding this comment.
Potential XSS vulnerability with v-html
Using v-html to render content directly can expose your application to XSS attacks if the content is user-generated or comes from an untrusted source.
Consider using a sanitization library like DOMPurify or a markdown renderer like marked with sanitization options:
- <div class="w-full prose prose-md dark:prose-invert text-(--ui-text) [&_p]:my-0" v-html="preparedPage.content" />
+ <div class="w-full prose prose-md dark:prose-invert text-(--ui-text) [&_p]:my-0" v-html="sanitizeHtml(preparedPage.content)" />And in the script section:
import DOMPurify from 'dompurify'
function sanitizeHtml(html) {
return DOMPurify.sanitize(html, { USE_PROFILES: { html: true } })
}| <template> | ||
| <UButton | ||
| type="submit" | ||
| variant="soft" | ||
| color="error" | ||
| size="xl" | ||
| block | ||
| @click="onSubmit" | ||
| > | ||
| {{ $t('center.delete.title') }} | ||
| </UButton> |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Add confirmation dialog before deletion
Destructive actions like deletion should have a confirmation dialog to prevent accidental deletions. The current implementation directly deletes when the button is clicked.
<template>
<UButton
type="submit"
variant="soft"
color="error"
size="xl"
block
- @click="onSubmit"
+ @click="confirmDelete"
>
{{ $t('center.delete.title') }}
</UButton>
+ <UModal v-model="isConfirmOpen">
+ <UCard>
+ <template #header>
+ <div class="text-center">
+ <h3 class="text-lg font-medium">{{ $t('center.delete.confirm_title') }}</h3>
+ </div>
+ </template>
+ <p>{{ $t('center.delete.confirm_message') }}</p>
+ <template #footer>
+ <div class="flex justify-end space-x-4">
+ <UButton @click="isConfirmOpen = false">{{ $t('common.cancel') }}</UButton>
+ <UButton color="error" @click="onSubmit">{{ $t('center.delete.confirm') }}</UButton>
+ </div>
+ </template>
+ </UCard>
+ </UModal>
</template>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <template> | |
| <UButton | |
| type="submit" | |
| variant="soft" | |
| color="error" | |
| size="xl" | |
| block | |
| @click="onSubmit" | |
| > | |
| {{ $t('center.delete.title') }} | |
| </UButton> | |
| <template> | |
| <UButton | |
| type="submit" | |
| variant="soft" | |
| color="error" | |
| size="xl" | |
| block | |
| @click="confirmDelete" | |
| > | |
| {{ $t('center.delete.title') }} | |
| </UButton> | |
| <UModal v-model="isConfirmOpen"> | |
| <UCard> | |
| <template #header> | |
| <div class="text-center"> | |
| <h3 class="text-lg font-medium">{{ $t('center.delete.confirm_title') }}</h3> | |
| </div> | |
| </template> | |
| <p>{{ $t('center.delete.confirm_message') }}</p> | |
| <template #footer> | |
| <div class="flex justify-end space-x-4"> | |
| <UButton @click="isConfirmOpen = false">{{ $t('common.cancel') }}</UButton> | |
| <UButton color="error" @click="onSubmit">{{ $t('center.delete.confirm') }}</UButton> | |
| </div> | |
| </template> | |
| </UCard> | |
| </UModal> | |
| </template> |
| const page = channel.pages.find((p) => p.id === id) | ||
|
|
||
| const localeState = useLocalizedState(resetState, channel.defaultLocale) | ||
|
|
||
| const state = ref<Partial<PageUpdateSchema>>({ | ||
| locale: localeState.locale.value, | ||
| slug: page?.slug, | ||
| title: page?.title.find((title) => title.locale === localeState.locale.value)?.value, | ||
| }) |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Handle the case when page is not found
The code doesn't handle the situation where the page with the given ID doesn't exist in the channel store. This could lead to errors when accessing properties of an undefined page.
const channel = useChannelStore()
const page = channel.pages.find((p) => p.id === id)
+
+ if (!page) {
+ console.error(`Page with id ${id} not found`)
+ actionToast.error(t('toast.page-not-found'))
+ // Consider redirecting to a safe page or showing an error state
+ }
const localeState = useLocalizedState(resetState, channel.defaultLocale)
const state = ref<Partial<PageUpdateSchema>>({
locale: localeState.locale.value,
slug: page?.slug,
title: page?.title.find((title) => title.locale === localeState.locale.value)?.value,
})📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const page = channel.pages.find((p) => p.id === id) | |
| const localeState = useLocalizedState(resetState, channel.defaultLocale) | |
| const state = ref<Partial<PageUpdateSchema>>({ | |
| locale: localeState.locale.value, | |
| slug: page?.slug, | |
| title: page?.title.find((title) => title.locale === localeState.locale.value)?.value, | |
| }) | |
| const channel = useChannelStore() | |
| const page = channel.pages.find((p) => p.id === id) | |
| if (!page) { | |
| console.error(`Page with id ${id} not found`) | |
| actionToast.error(t('toast.page-not-found')) | |
| // Consider redirecting to a safe page or showing an error state | |
| } | |
| const localeState = useLocalizedState(resetState, channel.defaultLocale) | |
| const state = ref<Partial<PageUpdateSchema>>({ | |
| locale: localeState.locale.value, | |
| slug: page?.slug, | |
| title: page?.title.find((title) => title.locale === localeState.locale.value)?.value, | |
| }) |
| onMounted(() => { | ||
| if (unref(editor)) { | ||
| unref(editor)?.commands.setContent(content as string) | ||
| } | ||
| }) |
There was a problem hiding this comment.
Add reactivity to content prop changes
The component only sets the initial content on mount but doesn't react to prop changes. This means if the parent component updates the content prop, the editor won't reflect these changes.
+watch(() => content, (newContent) => {
+ if (unref(editor) && newContent !== editor.value?.getHTML()) {
+ unref(editor)?.commands.setContent(newContent as string)
+ }
+}, { deep: true })
onMounted(() => {
if (unref(editor)) {
unref(editor)?.commands.setContent(content as string)
}
})📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| onMounted(() => { | |
| if (unref(editor)) { | |
| unref(editor)?.commands.setContent(content as string) | |
| } | |
| }) | |
| watch(() => content, (newContent) => { | |
| if (unref(editor) && newContent !== editor.value?.getHTML()) { | |
| unref(editor)?.commands.setContent(newContent as string) | |
| } | |
| }, { deep: true }) | |
| onMounted(() => { | |
| if (unref(editor)) { | |
| unref(editor)?.commands.setContent(content as string) | |
| } | |
| }) |
| export async function getPages(keys: string[]): Promise<Page[]> { | ||
| const pageStorage = new Map<string, unknown>(keys.map((key) => [key, useStorage('db').getItem(key)])) | ||
| const pages: Page[] = [] | ||
|
|
||
| for (const key of keys) { | ||
| const page = await pageStorage.get(key) as Page | ||
| if (!page) { | ||
| continue | ||
| } | ||
|
|
||
| pages.push(page) | ||
| } | ||
|
|
||
| return pages | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Inefficient implementation for batch retrieval
The current getPages implementation creates a Map but then doesn't use its advantages, making sequential await calls. This approach is inefficient for batch retrieval.
export async function getPages(keys: string[]): Promise<Page[]> {
- const pageStorage = new Map<string, unknown>(keys.map((key) => [key, useStorage('db').getItem(key)]))
- const pages: Page[] = []
-
- for (const key of keys) {
- const page = await pageStorage.get(key) as Page
- if (!page) {
- continue
- }
-
- pages.push(page)
- }
-
- return pages
+ const pagePromises = keys.map(key => useStorage('db').getItem<Page>(key))
+ const results = await Promise.all(pagePromises)
+
+ // Filter out null values
+ return results.filter((page): page is Page => page !== null)
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export async function getPages(keys: string[]): Promise<Page[]> { | |
| const pageStorage = new Map<string, unknown>(keys.map((key) => [key, useStorage('db').getItem(key)])) | |
| const pages: Page[] = [] | |
| for (const key of keys) { | |
| const page = await pageStorage.get(key) as Page | |
| if (!page) { | |
| continue | |
| } | |
| pages.push(page) | |
| } | |
| return pages | |
| } | |
| export async function getPages(keys: string[]): Promise<Page[]> { | |
| const pagePromises = keys.map(key => useStorage('db').getItem<Page>(key)) | |
| const results = await Promise.all(pagePromises) | |
| // Filter out null values | |
| return results.filter((page): page is Page => page !== null) | |
| } |



Summary by CodeRabbit
New Features
PageCard,PageCreateCard,UpdatePage, andDeletePage.Style