Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions packages/api/src/collections/config-routes.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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;
Expand Down
21 changes: 18 additions & 3 deletions packages/api/src/collections/route-read-support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion packages/web/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
281 changes: 145 additions & 136 deletions packages/web/src/App.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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()
Expand Down Expand Up @@ -262,140 +263,148 @@ function AppWithRouter() {
}

return (
<WorkspaceAppContent
activeProjectSlug={activeProjectSlug}
currentBranchName={currentBranchName}
collectionManagerProps={{
selectedCollection,
collections,
contentTypes,
selectedCollectionEntryCount: collectionBrowse.selectedCollectionEntryCount,
showToast,
onPersistCreate: async (nextCollection, headers) => {
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,
}}
/>
<Suspense
fallback={
<Center py="xl">
<Loader size="sm" />
</Center>
}
>
<WorkspaceAppContent
activeProjectSlug={activeProjectSlug}
currentBranchName={currentBranchName}
collectionManagerProps={{
selectedCollection,
collections,
contentTypes,
selectedCollectionEntryCount: collectionBrowse.selectedCollectionEntryCount,
showToast,
onPersistCreate: async (nextCollection, headers) => {
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,
}}
/>
</Suspense>
);
}

Expand Down
2 changes: 1 addition & 1 deletion packages/web/src/components/ui/workspace-shell-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ function EntryHistoryProviderMount(props: EntryHistoryProviderMountProps) {
);
}

export function WorkspaceAppContent(props: WorkspaceAppContentProps) {
export default function WorkspaceAppContent(props: WorkspaceAppContentProps) {
const {
activeProjectSlug,
currentBranchName,
Expand Down
Loading