Skip to content

Commit 6233f87

Browse files
committed
feat(dashboard): staff role support, department sidebar, action redirects
- Service visibility filtering (all/staff/admin) - Departments rendered under Home in sidebar (not separate section) - readOnly config for data-tables (suppresses actions and create button) - Action-bar redirect support after destructive actions - Form redirect support from API response - Dynamic row actions from API response merged with manifest config - Search field type for form sections - Confirm dialog contrast fix
1 parent 028858b commit 6233f87

6 files changed

Lines changed: 56 additions & 11 deletions

File tree

src/components/layout/dynamic-sidebar.tsx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,15 +59,40 @@ export function DynamicSidebar({ session, services }: Props) {
5959
<nav className="flex-1 px-3 space-y-1 overflow-y-auto">
6060
<Link
6161
to="/home"
62+
activeOptions={{ exact: true }}
6263
className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm text-(--sidebar-foreground) hover:bg-(--sidebar-accent) hover:text-(--sidebar-accent-foreground) [&.active]:bg-(--sidebar-accent) [&.active]:font-medium [&.active]:text-(--sidebar-accent-foreground)"
6364
>
6465
<LayoutDashboard className="h-4 w-4" />
6566
Home
6667
</Link>
6768

69+
{/* Inline services (like Departments) — rendered as simple nav links under Home */}
70+
{services
71+
.filter(
72+
(s) =>
73+
s.uiManifest &&
74+
s.status !== "inactive" &&
75+
s.slug === "departments",
76+
)
77+
.flatMap((service) =>
78+
(service.uiManifest?.navigation ?? []).map((item) => (
79+
<NavLink
80+
key={`${service.slug}${item.path}`}
81+
to={`/${service.slug}${item.path === "/" ? "" : item.path}`}
82+
label={item.label}
83+
icon={item.icon}
84+
/>
85+
)),
86+
)}
87+
6888
{/* Dynamic service sections — user services first, then admin */}
6989
{services
70-
.filter((s) => s.uiManifest && s.status !== "inactive")
90+
.filter(
91+
(s) =>
92+
s.uiManifest &&
93+
s.status !== "inactive" &&
94+
s.slug !== "departments",
95+
)
7196
.sort((a, b) => {
7297
if (a.type === "user" && b.type !== "user") return -1;
7398
if (a.type !== "user" && b.type === "user") return 1;

src/components/pages/action-bar-section.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,18 @@ export function ActionBarSection({ data, pathParams, serviceSlug }: Props) {
3838
method: action.method,
3939
});
4040
if (response.ok) {
41-
setResult({ ok: true, message: "Action completed successfully" });
42-
router.invalidate();
41+
if (action.redirect) {
42+
router.navigate({
43+
to: "/$service/$",
44+
params: {
45+
service: serviceSlug,
46+
_splat: action.redirect.replace(/^\//, ""),
47+
},
48+
});
49+
} else {
50+
setResult({ ok: true, message: "Action completed successfully" });
51+
router.invalidate();
52+
}
4353
} else {
4454
const body = await response.json().catch(() => null);
4555
setResult({

src/components/pages/data-table-section.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ interface TableData {
1212
columns: { key: string; label: string }[];
1313
rows: Record<string, unknown>[];
1414
total?: number;
15+
rowActions?: RowAction[];
1516
}
1617

1718
interface Props {
@@ -82,7 +83,9 @@ export function DataTableSection({
8283
});
8384
}
8485

85-
const rowActions = config.rowActions ?? [];
86+
const rowActions = config.readOnly
87+
? []
88+
: [...(config.rowActions ?? []), ...(data?.rowActions ?? [])];
8689
const hasRowActions = rowActions.length > 0;
8790

8891
return (
@@ -94,7 +97,7 @@ export function DataTableSection({
9497
) : (
9598
<div />
9699
)}
97-
{config.createLink && (
100+
{config.createLink && !config.readOnly && (
98101
<button
99102
type="button"
100103
onClick={() => {

src/components/pages/form-section.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,14 @@ export function FormSection({ section, serviceSlug, pathParams }: Props) {
9090
const data = await response.json().catch(() => null);
9191
setError(data?.error || `Error: ${response.status}`);
9292
} else {
93-
setSuccess(true);
94-
// Reload to reflect changes in sidebar/header
95-
setTimeout(() => window.location.reload(), 500);
93+
const responseData = await response.json().catch(() => null);
94+
if (responseData?.redirect) {
95+
// Navigate to the redirect URL (absolute path)
96+
window.location.href = responseData.redirect;
97+
} else {
98+
setSuccess(true);
99+
setTimeout(() => window.location.reload(), 500);
100+
}
96101
}
97102
} catch {
98103
setError("Network error");

src/components/ui/confirm-dialog.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,10 @@ export function ConfirmDialog({
5454
<button
5555
type="button"
5656
onClick={onConfirm}
57-
className={`px-4 py-2 text-sm font-medium rounded-md text-white transition-colors ${
57+
className={`px-4 py-2 text-sm font-medium rounded-md transition-colors ${
5858
variant === "danger"
59-
? "bg-red-600 hover:bg-red-700"
60-
: "bg-(--primary) hover:opacity-90"
59+
? "bg-red-600 hover:bg-red-700 text-white"
60+
: "bg-(--primary) text-(--primary-foreground) hover:opacity-90"
6161
}`}
6262
>
6363
{confirmLabel}

src/lib/types/manifest.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export interface RowAction {
5353
variant?: "default" | "danger";
5454
confirm?: string;
5555
link?: string;
56+
redirect?: string;
5657
}
5758

5859
export interface DataTableConfig {
@@ -61,6 +62,7 @@ export interface DataTableConfig {
6162
rowActions?: RowAction[];
6263
createLink?: string;
6364
createLabel?: string;
65+
readOnly?: boolean;
6466
}
6567

6668
export interface DetailConfig {

0 commit comments

Comments
 (0)