fix: improve starter resource routing and filters

This commit is contained in:
xilesun
2026-07-30 20:55:33 +08:00
parent dde7560791
commit d5b8396d01
7 changed files with 44 additions and 29 deletions
-4
View File
@@ -1,6 +1,5 @@
# NocoBase REST API root (the /api suffix is required)
NOCOBASE_API_URL=/api
NOCOBASE_AUTHENTICATOR=basic
# Public base path for Vite assets and React Router when deployed under NocoBase.
# Use / for root-local development, or /x/admin/ when building for storage/dist-client/x/admin.
@@ -10,6 +9,3 @@ NOCOBASE_PORTAL_BASE=/x/admin/
# API_CLIENT_STORAGE_PREFIX=NOCOBASE_
# API_CLIENT_STORAGE_TYPE=localStorage
# API_CLIENT_SHARE_TOKEN=false
# Optional for local development. Do not commit real access tokens.
# NOCOBASE_API_TOKEN=
+2
View File
@@ -16,6 +16,8 @@ Components copied from shadcn/ui are owned and maintained by this project; upstr
Put application-owned business routes in `src/routes.tsx` with `defineAppRoutes`. A route with a `resource` entry contributes its Refine resource and navigation item, while the same definition generates its React Router route. Mark create, edit, and show children with `resourceAction` so their paths populate the same Refine resource instead of being repeated. Use `access.roles` for route-level role constraints; nested routes inherit parent constraints, and the runtime applies the complete chain to both menu visibility and direct URL access. Do not repeat those roles in `resource.meta.acl` or a manually written route guard.
Resource action children are rendered automatically through the resource route's outlet. Keep list pages focused on list content; do not add a wrapper solely to render `<Outlet />`, and do not replace the shared Create, Edit, or Show buttons with hard-coded `navigate()` calls. Set `outlet: "manual"` when a specialized layout renders `<Outlet />` or consumes `useOutlet()` itself, when a child should replace the list instead of overlaying it, or when the child must stay inside a parent provider or boundary. If an action button has no target, fix the route definition by ensuring the resource route name matches the API resource and the corresponding child declares `resourceAction`.
The bundled Registry routes are examples, not application structure. When beginning the real application, set `registryRoutesEnabled` to `false` in `src/routes.tsx` and define the application's own routes there. This removes Registry-contributed main routes, resources, and navigation without modifying installed extension source; extension providers, authentication adapters, and `/dev` showcases remain available.
Route access currently centralizes role constraints only. Keep NocoBase resource/action, region, field, and record checks in the existing `CanAccess` and ACL boundaries close to the protected query or UI. Server ACL remains authoritative.
@@ -5,7 +5,7 @@ import { AccessDenied } from "@/components/access-control/access-denied";
import { CanAccess } from "@/components/access-control/can-access";
import { UserCreate } from "./create";
import { UserEdit } from "./edit";
import { UserResourceLayout } from "./layout";
import { UserList } from "./list";
import { RoleDetailRoute } from "./role-detail";
import { userRoutes } from "./routes";
import { UserShow } from "./show";
@@ -14,7 +14,11 @@ export const usersExampleRoutes = defineAppRoutes([
{
name: "users",
path: userRoutes.list,
element: <UserResourceLayout />,
element: (
<CanAccess resource="users" action="list" fallback={<AccessDenied />}>
<UserList />
</CanAccess>
),
resource: {
meta: {
label: "Users",
-1
View File
@@ -1,7 +1,6 @@
export * from "./app-routes";
export * from "./create";
export * from "./edit";
export * from "./layout";
export * from "./list";
export * from "./role-detail";
export * from "./routes";
@@ -1,16 +0,0 @@
import { CanAccess } from "@/components/access-control/can-access";
import { Outlet } from "react-router";
import { AccessDenied } from "@/components/access-control/access-denied";
import { UserList } from "./list";
export function UserResourceLayout() {
return (
<>
<CanAccess resource="users" action="list" fallback={<AccessDenied />}>
<UserList />
</CanAccess>
<Outlet />
</>
);
}
+29 -2
View File
@@ -1,5 +1,10 @@
import type { ResourceProps } from "@refinedev/core";
import { createElement, type ReactElement, type ReactNode } from "react";
import {
createElement,
Fragment,
type ReactElement,
type ReactNode,
} from "react";
import { Outlet, Route } from "react-router";
import type { ResourceAcl, RouteAccessConstraint } from "@/lib/nocobase/acl";
@@ -18,6 +23,7 @@ type AppRouteBase = {
name: string;
element?: ReactNode;
access?: RouteAccessConstraint;
outlet?: "auto" | "manual";
};
type AppRouteResourceBinding =
@@ -147,9 +153,30 @@ export function buildRouteResources(
});
}
const hasResourceActionRoute = (routes: AppRouteDefinition[]): boolean =>
routes.some(
(route) =>
Boolean(route.resourceAction) ||
hasResourceActionRoute(route.children ?? [])
);
export function renderAppRoutes(routes: AppRouteDefinition[]): ReactElement[] {
return routes.map((route) => {
const content = route.element ?? createElement(Outlet);
// Resource actions are nested so the list stays mounted behind a drawer or
// dialog. Own that required outlet here unless a layout deliberately
// renders or consumes the outlet itself.
const content =
route.element &&
route.resource &&
route.outlet !== "manual" &&
hasResourceActionRoute(route.children ?? [])
? createElement(
Fragment,
null,
route.element,
createElement(Outlet)
)
: route.element ?? createElement(Outlet);
const element = route.access
? createElement(RouteAccessGuard, { access: route.access }, content)
: content;
@@ -647,6 +647,13 @@ export function DataTableFilterInput<TData>({
return defaultOperatorFromProps || "eq";
});
useEffect(() => {
columnFromProps.columnDef.meta = {
...columnFromProps.columnDef.meta,
filterOperator: operator,
};
}, [columnFromProps, operator]);
const handleApply = () => {
columnFromProps.setFilterValue(filterValue);
};
@@ -658,10 +665,6 @@ export function DataTableFilterInput<TData>({
const handleOperatorChange = (value: CrudOperators) => {
setOperator(value);
columnFromProps.columnDef.meta = {
...columnFromProps.columnDef.meta,
filterOperator: value,
};
};
return (