Skip to content

Commit f970922

Browse files
committed
Merge remote-tracking branch 'origin/feature/frontend-next' into feature/next-release
2 parents 1bb7f8a + 3180363 commit f970922

102 files changed

Lines changed: 10181 additions & 4766 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

frontend/package.json

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@
6464
"graphql": "^16.11.0",
6565
"graphql-ws": "^6.0.5",
6666
"highlight.js": "^11.11.1",
67-
"html2pdf.js": "^0.14.0",
6867
"js-cookie": "^3.0.5",
6968
"lodash": "^4.18.1",
7069
"lowlight": "^3.3.0",
@@ -107,6 +106,10 @@
107106
"@prettier/plugin-xml": "^3.3.1",
108107
"@tailwindcss/postcss": "^4.1.18",
109108
"@tailwindcss/typography": "^0.5.15",
109+
"@testing-library/dom": "^10.4.1",
110+
"@testing-library/jest-dom": "^6.9.1",
111+
"@testing-library/react": "^16.3.2",
112+
"@testing-library/user-event": "^14.6.1",
110113
"@types/js-cookie": "^3.0.6",
111114
"@types/lodash": "^4.17.13",
112115
"@types/node": "^22.0.0",
@@ -122,11 +125,10 @@
122125
"eslint-plugin-perfectionist": "^4.15.1",
123126
"eslint-plugin-react": "^7.37.5",
124127
"eslint-plugin-react-hooks": "^7.0.1",
125-
"lint-staged": "^16.2.6",
128+
"jsdom": "^29.1.1",
126129
"postcss": "^8.4.47",
127130
"prettier": "^3.3.3",
128131
"prettier-plugin-tailwindcss": "^0.7.2",
129-
"simple-git-hooks": "^2.11.1",
130132
"tailwindcss": "^4.1.18",
131133
"tsx": "^4.19.3",
132134
"typescript": "^5.6.2",
@@ -139,8 +141,7 @@
139141
"pnpm": {
140142
"onlyBuiltDependencies": [
141143
"@swc/core",
142-
"esbuild",
143-
"simple-git-hooks"
144+
"esbuild"
144145
]
145146
},
146147
"eslintConfig": {

frontend/pnpm-lock.yaml

Lines changed: 2604 additions & 1998 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/src/app.tsx

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,11 @@ const RootLayout = () => (
5757
<UserProvider>
5858
<FavoritesProvider>
5959
<TemplatesProvider>
60-
<KnowledgesProvider>
61-
<ResourcesProvider>
62-
<Suspense fallback={<PageLoader />}>
63-
<Outlet />
64-
</Suspense>
65-
</ResourcesProvider>
66-
</KnowledgesProvider>
60+
<ResourcesProvider>
61+
<Suspense fallback={<PageLoader />}>
62+
<Outlet />
63+
</Suspense>
64+
</ResourcesProvider>
6765
</TemplatesProvider>
6866
</FavoritesProvider>
6967
</UserProvider>
@@ -101,6 +99,12 @@ const FlowWithProvider = () => (
10199
</FlowProvider>
102100
);
103101

102+
const KnowledgesLayout = () => (
103+
<KnowledgesProvider>
104+
<Outlet />
105+
</KnowledgesProvider>
106+
);
107+
104108
const router = createBrowserRouter(
105109
createRoutesFromElements(
106110
<Route element={<RootLayout />}>
@@ -138,14 +142,16 @@ const router = createBrowserRouter(
138142
path="templates/:templateId"
139143
/>
140144

141-
<Route
142-
element={<Knowledges />}
143-
path="knowledges"
144-
/>
145-
<Route
146-
element={<Knowledge />}
147-
path="knowledges/:knowledgeId"
148-
/>
145+
<Route element={<KnowledgesLayout />}>
146+
<Route
147+
element={<Knowledges />}
148+
path="knowledges"
149+
/>
150+
<Route
151+
element={<Knowledge />}
152+
path="knowledges/:knowledgeId"
153+
/>
154+
</Route>
149155

150156
<Route
151157
element={<Resources />}

frontend/src/components/layouts/settings-layout.tsx

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,6 @@ const menuItems: readonly MenuItem[] = [
5151
path: '/settings/api-tokens',
5252
title: 'PentAGI API',
5353
},
54-
// {
55-
// id: 'mcp-servers',
56-
// title: 'MCP Servers',
57-
// path: '/settings/mcp-servers',
58-
// icon: <Server className="size-4" />,
59-
// },
6054
] as const;
6155

6256
// Individual menu item component to properly use hooks
@@ -98,14 +92,6 @@ const SettingsHeader = () => {
9892
return 'Edit Provider';
9993
}
10094

101-
if (path === '/settings/mcp-servers/new') {
102-
return 'Create MCP Server';
103-
}
104-
105-
if (path.startsWith('/settings/mcp-servers/')) {
106-
return 'Edit MCP Server';
107-
}
108-
10995
if (path === '/settings/prompts/new') {
11096
return 'Create Prompt';
11197
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
# Shared list/detail building blocks
2+
3+
This directory hosts the reusable surface for list-and-detail pages: a
4+
filterable table, a Prev/Next/Sheet toolbar that walks the _same_ filtered
5+
subset on detail pages, and the inline-rename + sortable-header primitives
6+
that every list reuses.
7+
8+
## Mental model
9+
10+
```
11+
┌──────────────────────────────────────────┐
12+
│ URL ?q=foo ?page=3 │
13+
│ (source of truth — bookmarkable) │
14+
└────────┬─────────────────────┬───────────┘
15+
│ read/write │ read-only
16+
▼ ▼
17+
useTableQueryFilter useTableQueryFilterReader
18+
usePagination │
19+
│ │
20+
▼ ▼
21+
<DataTable> useNavigation
22+
(list page) │
23+
│ ▼
24+
▼ <DetailNavigationToolbar>
25+
table_4_<path> (detail page)
26+
in localStorage
27+
(cold-start fallback)
28+
```
29+
30+
- **URL is authoritative.** Filter (`?q=`) and page (`?page=`) live in the
31+
URL so links/bookmarks always reproduce the user's view.
32+
- **Storage is a warm-restart bag.** The list page persists the URL filter
33+
into `localStorage` under `table_4_<path>`. The detail page never writes
34+
storage and never replays storage into the URL — opening a shared link
35+
shows exactly what the link says.
36+
- **Prev/Next walks the same subset.** `DetailNavigationToolbar` runs the
37+
same matcher (`createTextMatcher`) the list filter uses, so siblings stay
38+
in lockstep with what the user sees in the table.
39+
40+
## Components
41+
42+
| File | Role |
43+
| ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
44+
| [`detail-navigation/`](detail-navigation/) | Prev / Position / Next toolbar + listbox sheet for detail pages, and the navigation hooks that feed it. |
45+
| [`inline-edit/`](inline-edit/) | Generic inline-edit input (Save/Cancel addons, Enter/Escape) plus the paired `useInlineEdit` state machine. |
46+
47+
## Hooks
48+
49+
| Hook | Where | Source of truth | Writes? | Notes |
50+
| --------------------------- | ------------------------------- | --------------- | ------- | ------------------------------------------------------------------ |
51+
| `useTableQueryFilter` | `@/hooks/` | URL `?q=` | yes | List pages. Restores from `localStorage` on cold start. |
52+
| `useTableQueryFilterReader` | `@/hooks/` | URL `?q=` | no | Detail pages. Storage-blind — shared links never gain stale `?q=`. |
53+
| `usePagination` | `@/hooks/` | URL `?page=` | yes | Canonicalizes `?page=1` away so the URL has one form per view. |
54+
| `useNavigation` | `detail-navigation/` (internal) | props | no | Pure computation of Prev/Next around a `currentId`. |
55+
| `useDetailNavigation` | `detail-navigation/` | URL + props | no | Bundles the three above into a single hook for detail pages. |
56+
| `useInlineEdit` | `inline-edit/` | local state | no | Edit-mode toggle + deferred focus (Radix dropdown race fix). |
57+
| `usePageStorageKeys` | `@/hooks/` | router | no | Resolves the three per-page storage keys reactively. |
58+
59+
## Library helpers (in `@/lib/`)
60+
61+
| Module | Purpose |
62+
| ------------------------- | ------------------------------------------------------------------------------------ |
63+
| `table-state.ts` | Unified `table_4_<path>` JSON slot. Carries filter + sorting + columnVis + pageSize. |
64+
| `view-options-storage.ts` | `viewOptions_4_<path>` for FileManager-style screens (folders-first, etc.). |
65+
| `storage-keys.ts` | Single source of truth for storage-key conventions and `getTopLevelPath`. |
66+
| `url-params.ts` | `URL_PARAMS` constants + `mergeHrefWithSearchParams` (preserves hash on merge). |
67+
68+
## How to add a new list + detail pair
69+
70+
1. **List page** (`/<entities>/`):
71+
72+
```tsx
73+
const { filter, setFilter } = useTableQueryFilter();
74+
const { pageIndex, setPage } = usePagination();
75+
76+
return (
77+
<DataTable
78+
columns={columns /* use <DataTableColumnHeader column={column} title="..." /> */}
79+
data={entities}
80+
filterColumn="title"
81+
filterValue={filter}
82+
onFilterChange={setFilter}
83+
onPageChange={setPage}
84+
pageIndex={pageIndex}
85+
/>
86+
);
87+
```
88+
89+
2. **Feature-scoped navigation hook** (`@/features/<entity>/use-<entity>-detail-navigation.ts`):
90+
91+
```ts
92+
const getLabel = (item: Entity) => item.title;
93+
const getHref = (item: Entity) => `/<entities>/${item.id}`;
94+
95+
export const useEntityDetailNavigation = (currentId: null | string | undefined) => {
96+
const { entities } = useEntities();
97+
98+
return useDetailNavigation<Entity>({ currentId, getHref, getLabel, items: entities });
99+
};
100+
```
101+
102+
3. **Detail page** (`/<entities>/:id`):
103+
104+
```tsx
105+
const { toolbarProps } = useEntityDetailNavigation(entityId);
106+
107+
return (
108+
<header>
109+
<DetailNavigationToolbar<Entity>
110+
{...toolbarProps}
111+
sheetIcon={<Icon className="size-4" />}
112+
sheetTitle="Entities"
113+
renderItem={(item, isCurrent) => <span>{item.title}</span>}
114+
/>
115+
</header>
116+
);
117+
```
118+
119+
## Why URL > storage
120+
121+
A user opens `/flows?q=alpha` in tab A. They navigate to flow B by clicking
122+
"Next" in the toolbar. They share `/flows/b?q=alpha` with a teammate.
123+
124+
- The teammate opens the link cold. Their detail page reads `q=alpha` from
125+
the URL and renders Prev/Next over the filtered subset.
126+
- The teammate hits "Next". They land on `/flows/c?q=alpha`still inside
127+
the filter, even though they never typed it.
128+
129+
`useTableQueryFilterReader` is the key piece: it observes the URL but never
130+
writes anything, so a fresh detail-page mount can't accidentally inject the
131+
**previous tab's** `?q=` into the URL.
132+
133+
## Why one storage key per page
134+
135+
Before the unification, every list page wrote four storage keys
136+
(`column_4_/flows`, `sorting_4_/flows`, `filter_4_/flows`, `page_4_/flows`)
137+
in two different write paths (sync + debounced). Refreshing during a typing
138+
session could land you in an inconsistent state. The unified
139+
`table_4_<path>` slot is a single JSON object that all preferences live in;
140+
`migrateLegacyTableState` folds the four legacy keys into it on first mount
141+
and deletes them.
142+
143+
## Testing notes
144+
145+
- `vitest run` covers the pure utilities (`table-state`,
146+
`view-options-storage`, `url-params`), the hook behaviours
147+
(`use-pagination`, `use-table-query-filter`, `use-inline-edit`,
148+
`use-page-storage-keys`, `use-detail-navigation`), and the components
149+
(`detail-navigation/`, `data-table`).
150+
- jsdom doesn't ship `Element.prototype.scrollIntoView` or `ResizeObserver`
151+
both are polyfilled in `vitest.setup.ts`.
152+
- React Testing Library auto-cleans the DOM after every test (see the same
153+
setup file). Tests can freely call `render` without leaking nodes.
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { ChevronLeft, ChevronRight } from 'lucide-react';
2+
3+
import { Button } from '@/components/ui/button';
4+
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
5+
import { cn } from '@/lib/utils';
6+
7+
import type { DetailNavigationController } from './use-detail-navigation';
8+
9+
interface DetailNavigationButtonsProps<T extends { id: string }> {
10+
controller: DetailNavigationController<T>;
11+
/** Lowercased plural used in the aria-label / tooltip ("flows", "templates"). */
12+
sheetTitle: string;
13+
/**
14+
* Size variant. `'default'` is the desktop toolbar's `size-8` cluster;
15+
* `'sm'` shrinks the cluster to `size-7` for embedding inside a
16+
* `<DropdownMenuItem>` on mobile, where the host row is already padded.
17+
*/
18+
size?: 'default' | 'sm';
19+
}
20+
21+
/**
22+
* Prev / Position / Next button cluster bound to a `DetailNavigationController`.
23+
* Stateless: the controller owns navigation, `isSheetOpen`, and the
24+
* pre-formatted `positionLabel`.
25+
*
26+
* Reused in both the desktop toolbar (`size="default"`) and the mobile
27+
* dropdown row (`size="sm"`) — same a11y contract, same tooltips, same
28+
* keyboard semantics in both places.
29+
*/
30+
export const DetailNavigationButtons = <T extends { id: string }>({
31+
controller,
32+
sheetTitle,
33+
size = 'default',
34+
}: DetailNavigationButtonsProps<T>) => {
35+
const lowerTitle = sheetTitle.toLowerCase();
36+
const isSm = size === 'sm';
37+
const sideButtonSize = isSm ? 'size-7' : 'size-8';
38+
const middleHeight = isSm ? 'h-7' : 'h-8';
39+
40+
return (
41+
<div className="flex items-center">
42+
<Tooltip>
43+
<TooltipTrigger asChild>
44+
<Button
45+
aria-label="Previous"
46+
className={cn(sideButtonSize, 'rounded-r-none border-r-0 p-0')}
47+
disabled={!controller.prevId}
48+
onClick={controller.goToPrev}
49+
size="icon"
50+
variant="outline"
51+
>
52+
<ChevronLeft />
53+
</Button>
54+
</TooltipTrigger>
55+
<TooltipContent>Previous</TooltipContent>
56+
</Tooltip>
57+
<Tooltip>
58+
<TooltipTrigger asChild>
59+
<Button
60+
aria-label={`Open ${lowerTitle} list (${controller.positionLabel})`}
61+
className={cn(middleHeight, 'min-w-12 rounded-none border-x px-2 font-mono text-xs tabular-nums')}
62+
disabled={!controller.hasEntries}
63+
onClick={controller.openSheet}
64+
variant="outline"
65+
>
66+
{controller.positionLabel}
67+
</Button>
68+
</TooltipTrigger>
69+
<TooltipContent>Show all matching {lowerTitle}</TooltipContent>
70+
</Tooltip>
71+
<Tooltip>
72+
<TooltipTrigger asChild>
73+
<Button
74+
aria-label="Next"
75+
className={cn(sideButtonSize, 'rounded-l-none border-l-0 p-0')}
76+
disabled={!controller.nextId}
77+
onClick={controller.goToNext}
78+
size="icon"
79+
variant="outline"
80+
>
81+
<ChevronRight />
82+
</Button>
83+
</TooltipTrigger>
84+
<TooltipContent>Next</TooltipContent>
85+
</Tooltip>
86+
</div>
87+
);
88+
};

0 commit comments

Comments
 (0)