From f0fd7fb7ab89a7a414938f3fbfc7a211208a1de3 Mon Sep 17 00:00:00 2001 From: Atlantic Platform Group Date: Wed, 27 May 2026 12:17:38 -0400 Subject: [PATCH] fix: address corrected Round 2 verification report issues - P1-STYLE-03: update playwright.config.ts testIgnore regex to .test.ts - P2-STYLE-02: create packages/web/src/index.ts barrel file - P1-PERF-05: add pagination to collections/config-routes.ts (page/limit params) - P1-PERF-09: React.lazy code-split WorkspaceAppContent in App.tsx - P2-CSS-03: convert minHeight magic numbers to px strings in workspace-shell-sidebar --- packages/api/src/collections/config-routes.ts | 16 +- .../api/src/collections/route-read-support.ts | 21 +- packages/web/playwright.config.ts | 2 +- packages/web/src/App.tsx | 281 +++++++++--------- .../components/ui/workspace-shell-sidebar.tsx | 2 +- .../workspace/WorkspaceAppContent.tsx | 2 +- 6 files changed, 180 insertions(+), 144 deletions(-) diff --git a/packages/api/src/collections/config-routes.ts b/packages/api/src/collections/config-routes.ts index 953fb4f..2a35bad 100644 --- a/packages/api/src/collections/config-routes.ts +++ b/packages/api/src/collections/config-routes.ts @@ -1,5 +1,5 @@ import { Router, type Request, type Response } from 'express'; -import { body, param, validationResult } from 'express-validator'; +import { body, param, query, validationResult } from 'express-validator'; import { logger } from '../middleware/logger'; import { badRequest, @@ -21,10 +21,22 @@ export function registerCollectionConfigRoutes(router: Router): void { router.get( '/', requirePermission('collections', 'read'), + [ + query('page').optional().isInt({ min: 1 }).toInt(), + query('limit').optional().isInt({ min: 1, max: 100 }).toInt(), + ], async (req: Request, res: Response) => { try { + const errors = validationResult(req); + if (!errors.isEmpty()) { + badRequest(res, 'Invalid pagination parameters', 'INVALID_PAGINATION'); + return; + } + const { projectId } = req.params as { projectId: string }; - const result = await listCollectionsOrRespond(projectId, res); + const page = typeof req.query.page === 'number' ? req.query.page : 1; + const limit = typeof req.query.limit === 'number' ? req.query.limit : 50; + const result = await listCollectionsOrRespond(projectId, res, { page, limit }); if (!result) { logger.warn({ msg: 'Project not found while listing collections', projectId }); return; diff --git a/packages/api/src/collections/route-read-support.ts b/packages/api/src/collections/route-read-support.ts index dd44d3e..e057775 100644 --- a/packages/api/src/collections/route-read-support.ts +++ b/packages/api/src/collections/route-read-support.ts @@ -55,13 +55,17 @@ export async function getCollectionEntriesOrRespond( return initialized.service.findMany(collectionId, queryParams); } -export async function listCollectionsOrRespond(projectId: string, res: Response) { +export async function listCollectionsOrRespond( + projectId: string, + res: Response, + pagination?: { page: number; limit: number }, +) { const initialized = await getCollectionServiceForProject(projectId, res); if (!initialized) { return null; } - const collections = (await initialized.service.listCollections()).map((collection) => ({ + const allCollections = (await initialized.service.listCollections()).map((collection) => ({ ...collection, resource: createResourceCollectionLink( getContentResourceCollectionId(collection.id), @@ -70,7 +74,18 @@ export async function listCollectionsOrRespond(projectId: string, res: Response) ), })); - return { collections }; + if (!pagination) { + return { collections: allCollections }; + } + + const { page, limit } = pagination; + const total = allCollections.length; + const pageCount = Math.ceil(total / limit) || 1; + const start = (page - 1) * limit; + const end = start + limit; + const collections = allCollections.slice(start, end); + + return { collections, page, limit, total, pageCount }; } export async function getCollectionEntryOrRespond( diff --git a/packages/web/playwright.config.ts b/packages/web/playwright.config.ts index 58b4251..2ae21fc 100644 --- a/packages/web/playwright.config.ts +++ b/packages/web/playwright.config.ts @@ -6,7 +6,7 @@ import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ testDir: './e2e', - testIgnore: /onboarding-fullstack\.spec\.ts/, + testIgnore: /onboarding-fullstack\.test\.ts/, fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, diff --git a/packages/web/src/App.tsx b/packages/web/src/App.tsx index f14a954..df5a751 100644 --- a/packages/web/src/App.tsx +++ b/packages/web/src/App.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState, type FormEvent } from 'react'; +import { useEffect, useMemo, useState, type FormEvent, lazy, Suspense } from 'react'; import { Navigate, useLocation, useNavigate } from 'react-router-dom'; import { useQueryClient } from '@tanstack/react-query'; import { Alert, Button, Center, Container, Group, Loader, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; @@ -16,9 +16,10 @@ import { useCollectionConfigPersistence } from './hooks/useCollectionConfigPersi import { useWorkspaceBranchSync } from './hooks/useWorkspaceBranchSync'; import { useWorkspaceCapabilities } from './hooks/useWorkspaceCapabilities'; import { useWorkspaceRouteNormalization } from './hooks/useWorkspaceRouteNormalization'; -import { WorkspaceAppContent } from './components/workspace/WorkspaceAppContent'; import { projectsApi } from './lib/api/projects'; +const WorkspaceAppContent = lazy(() => import('./components/workspace/WorkspaceAppContent')); + function slugifyProjectName(name: string) { return name .toLowerCase() @@ -262,140 +263,148 @@ function AppWithRouter() { } return ( - { - await updateCollectionsConfigMutation.mutateAsync({ - nextCollections: [...collections, nextCollection], - nextCollectionId: nextCollection.id, - action: 'save', - headers, - }); - }, - onPersistSaveSettings: async (nextCollections, nextCollectionId, headers) => { - await updateCollectionsConfigMutation.mutateAsync({ - nextCollections, - nextCollectionId, - action: 'save', - headers, - }); - }, - onPersistDelete: async (collectionId, nextCollectionId, headers) => { - await deleteCollectionMutation.mutateAsync({ - collectionId, - nextCollectionId, - headers, - }); - }, - }} - editorProps={{ - projectId: currentProject.id, - selectedCollection, - selectedContentType, - selectedEntry, - selectedEntryRevision, - entries: collectionBrowse.entries, - primaryField, - collections, - contentTypes, - componentSchemaMap, - assetOptions, - assetMap, - assetsLoading: assetsQuery.isLoading || globalAssetsQuery.isLoading, - assetRecords: assetsQuery.data?.assets ?? [], - globalAssetRecords, - canCreateEntries: capabilities.canCreateEntries, - canUpdateEntries: capabilities.canUpdateEntries, - canDeleteEntries: capabilities.canDeleteEntries, - showToast, - queryClient, - onNavigateToEntry: (entryId: string) => { - if (!activeProjectSlug || !selectedCollection) return; - navigateTo(buildWorkspacePath(activeProjectSlug, 'collections', selectedCollection.id, { entryId, branchName: currentBranchName })); - }, - onNavigateToCollection: () => { - if (!activeProjectSlug || !selectedCollection) return; - navigateTo(buildWorkspacePath(activeProjectSlug, 'collections', selectedCollection.id, { branchName: currentBranchName }), true); - }, - }} - entryHistoryProps={{ - projectId: currentProject.id, - selectedCollection, - selectedEntry, - activeHistoryView, - canUpdateEntries: capabilities.canUpdateEntries, - currentBranchName, - }} - schemaEditorProps={{ - projectId: currentProject.id, - activeSchemaMode, - selectedSchema, - selectedSchemaDocument, - showToast, - queryClient, - }} - navigateTo={navigateTo} - workspaceShellProps={{ - currentProject, - projects, - setCurrentProject, - isLoadingProjects, - user, - logout, - gitStatus, - availableSections, - activeSection, - activeSectionLabel, - secondaryRailCollapsed, - setSecondaryRailCollapsed, - canCreateCollections: capabilities.canCreateCollections, - activeSchemaMode, - componentSchemaData: componentSchemasQuery.data ?? [], - contentTypes, - collections, - activeProjectSlug, - navigateTo, - secondaryOptions, - activeSecondaryId, - selectedSecondaryOption, - showToast, - refreshGitStatus, - canCreateAssets: capabilities.canCreateAssets, - canUpdateAssets: capabilities.canUpdateAssets, - canDeleteAssets: capabilities.canDeleteAssets, - canManageGlobalMedia: capabilities.canManageGlobalMedia, - currentBranchName, - selectedCollection, - activeEntryId, - activeHistoryView, - activeCollectionSettingsView, - selectedEntry, - primaryField, - secondaryField, - collectionBrowse, - canCreateEntries: capabilities.canCreateEntries, - canUpdateEntries: capabilities.canUpdateEntries, - canDeleteEntries: capabilities.canDeleteEntries, - canUpdateCollections: capabilities.canUpdateCollections, - canDeleteCollections: capabilities.canDeleteCollections, - selectedEntryLoading, - selectedEntryError, - retrySelectedEntry: () => { - collectionBrowse.retryEntries(); - void selectedEntryQuery.refetch(); - }, - selectedSchemaDocument, - updateCollectionsConfigPending: updateCollectionsConfigMutation.isPending, - assetsQueryLoading: assetsQuery.isLoading || globalAssetsQuery.isLoading, - }} - /> + + + + } + > + { + await updateCollectionsConfigMutation.mutateAsync({ + nextCollections: [...collections, nextCollection], + nextCollectionId: nextCollection.id, + action: 'save', + headers, + }); + }, + onPersistSaveSettings: async (nextCollections, nextCollectionId, headers) => { + await updateCollectionsConfigMutation.mutateAsync({ + nextCollections, + nextCollectionId, + action: 'save', + headers, + }); + }, + onPersistDelete: async (collectionId, nextCollectionId, headers) => { + await deleteCollectionMutation.mutateAsync({ + collectionId, + nextCollectionId, + headers, + }); + }, + }} + editorProps={{ + projectId: currentProject.id, + selectedCollection, + selectedContentType, + selectedEntry, + selectedEntryRevision, + entries: collectionBrowse.entries, + primaryField, + collections, + contentTypes, + componentSchemaMap, + assetOptions, + assetMap, + assetsLoading: assetsQuery.isLoading || globalAssetsQuery.isLoading, + assetRecords: assetsQuery.data?.assets ?? [], + globalAssetRecords, + canCreateEntries: capabilities.canCreateEntries, + canUpdateEntries: capabilities.canUpdateEntries, + canDeleteEntries: capabilities.canDeleteEntries, + showToast, + queryClient, + onNavigateToEntry: (entryId: string) => { + if (!activeProjectSlug || !selectedCollection) return; + navigateTo(buildWorkspacePath(activeProjectSlug, 'collections', selectedCollection.id, { entryId, branchName: currentBranchName })); + }, + onNavigateToCollection: () => { + if (!activeProjectSlug || !selectedCollection) return; + navigateTo(buildWorkspacePath(activeProjectSlug, 'collections', selectedCollection.id, { branchName: currentBranchName }), true); + }, + }} + entryHistoryProps={{ + projectId: currentProject.id, + selectedCollection, + selectedEntry, + activeHistoryView, + canUpdateEntries: capabilities.canUpdateEntries, + currentBranchName, + }} + schemaEditorProps={{ + projectId: currentProject.id, + activeSchemaMode, + selectedSchema, + selectedSchemaDocument, + showToast, + queryClient, + }} + navigateTo={navigateTo} + workspaceShellProps={{ + currentProject, + projects, + setCurrentProject, + isLoadingProjects, + user, + logout, + gitStatus, + availableSections, + activeSection, + activeSectionLabel, + secondaryRailCollapsed, + setSecondaryRailCollapsed, + canCreateCollections: capabilities.canCreateCollections, + activeSchemaMode, + componentSchemaData: componentSchemasQuery.data ?? [], + contentTypes, + collections, + activeProjectSlug, + navigateTo, + secondaryOptions, + activeSecondaryId, + selectedSecondaryOption, + showToast, + refreshGitStatus, + canCreateAssets: capabilities.canCreateAssets, + canUpdateAssets: capabilities.canUpdateAssets, + canDeleteAssets: capabilities.canDeleteAssets, + canManageGlobalMedia: capabilities.canManageGlobalMedia, + currentBranchName, + selectedCollection, + activeEntryId, + activeHistoryView, + activeCollectionSettingsView, + selectedEntry, + primaryField, + secondaryField, + collectionBrowse, + canCreateEntries: capabilities.canCreateEntries, + canUpdateEntries: capabilities.canUpdateEntries, + canDeleteEntries: capabilities.canDeleteEntries, + canUpdateCollections: capabilities.canUpdateCollections, + canDeleteCollections: capabilities.canDeleteCollections, + selectedEntryLoading, + selectedEntryError, + retrySelectedEntry: () => { + collectionBrowse.retryEntries(); + void selectedEntryQuery.refetch(); + }, + selectedSchemaDocument, + updateCollectionsConfigPending: updateCollectionsConfigMutation.isPending, + assetsQueryLoading: assetsQuery.isLoading || globalAssetsQuery.isLoading, + }} + /> + ); } diff --git a/packages/web/src/components/ui/workspace-shell-sidebar.tsx b/packages/web/src/components/ui/workspace-shell-sidebar.tsx index 4fcf9d7..1229efa 100644 --- a/packages/web/src/components/ui/workspace-shell-sidebar.tsx +++ b/packages/web/src/components/ui/workspace-shell-sidebar.tsx @@ -59,7 +59,7 @@ export function WorkspaceSidebarNavItem({ borderRadius: WORKSPACE_SIDEBAR_ROW_RADIUS, paddingBlock: description ? '9px' : '11px', paddingInline: '12px', - minHeight: description ? 58 : 44, + minHeight: description ? '58px' : '44px', transition: `${WORKSPACE_TRANSITION_BG}, ${WORKSPACE_TRANSITION_COLOR}, ${WORKSPACE_TRANSITION_SHADOW}`, color: WORKSPACE_SHELL_TEXT, boxShadow: 'inset 0 0 0 1px transparent', diff --git a/packages/web/src/components/workspace/WorkspaceAppContent.tsx b/packages/web/src/components/workspace/WorkspaceAppContent.tsx index a79a7b7..cd79c17 100644 --- a/packages/web/src/components/workspace/WorkspaceAppContent.tsx +++ b/packages/web/src/components/workspace/WorkspaceAppContent.tsx @@ -55,7 +55,7 @@ function EntryHistoryProviderMount(props: EntryHistoryProviderMountProps) { ); } -export function WorkspaceAppContent(props: WorkspaceAppContentProps) { +export default function WorkspaceAppContent(props: WorkspaceAppContentProps) { const { activeProjectSlug, currentBranchName,