@tekki-forge/runtime-admin 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/account.d.ts +1 -0
- package/dist/account.js +105 -0
- package/dist/account.js.map +1 -0
- package/dist/activity.d.ts +65 -0
- package/dist/activity.js +299 -0
- package/dist/activity.js.map +1 -0
- package/dist/admin-access.d.ts +4 -0
- package/dist/admin-access.js +177 -0
- package/dist/admin-access.js.map +1 -0
- package/dist/admin.d.ts +5 -0
- package/dist/admin.js +129 -0
- package/dist/admin.js.map +1 -0
- package/dist/auth-pages.d.ts +13 -0
- package/dist/auth-pages.js +207 -0
- package/dist/auth-pages.js.map +1 -0
- package/dist/brand-art.d.ts +24 -0
- package/dist/brand-art.js +33 -0
- package/dist/brand-art.js.map +1 -0
- package/dist/brand.d.ts +11 -0
- package/dist/brand.js +16 -0
- package/dist/brand.js.map +1 -0
- package/dist/charts.d.ts +23 -0
- package/dist/charts.js +259 -0
- package/dist/charts.js.map +1 -0
- package/dist/command-palette.d.ts +9 -0
- package/dist/command-palette.js +115 -0
- package/dist/command-palette.js.map +1 -0
- package/dist/context.d.ts +242 -0
- package/dist/context.js +107 -0
- package/dist/context.js.map +1 -0
- package/dist/dashboard.d.ts +66 -0
- package/dist/dashboard.js +144 -0
- package/dist/dashboard.js.map +1 -0
- package/dist/fields.d.ts +71 -0
- package/dist/fields.js +326 -0
- package/dist/fields.js.map +1 -0
- package/dist/i18n/catalog.d.ts +7 -0
- package/dist/i18n/catalog.js +23 -0
- package/dist/i18n/catalog.js.map +1 -0
- package/dist/i18n/locale.d.ts +15 -0
- package/dist/i18n/locale.js +69 -0
- package/dist/i18n/locale.js.map +1 -0
- package/dist/i18n/messages/de.d.ts +3 -0
- package/dist/i18n/messages/de.js +627 -0
- package/dist/i18n/messages/de.js.map +1 -0
- package/dist/i18n/messages/en.d.ts +3 -0
- package/dist/i18n/messages/en.js +627 -0
- package/dist/i18n/messages/en.js.map +1 -0
- package/dist/i18n/messages/es.d.ts +3 -0
- package/dist/i18n/messages/es.js +627 -0
- package/dist/i18n/messages/es.js.map +1 -0
- package/dist/i18n/messages/fr.d.ts +3 -0
- package/dist/i18n/messages/fr.js +627 -0
- package/dist/i18n/messages/fr.js.map +1 -0
- package/dist/i18n/messages/pt.d.ts +3 -0
- package/dist/i18n/messages/pt.js +627 -0
- package/dist/i18n/messages/pt.js.map +1 -0
- package/dist/i18n/validation.d.ts +3 -0
- package/dist/i18n/validation.js +36 -0
- package/dist/i18n/validation.js.map +1 -0
- package/dist/illustrations.d.ts +6 -0
- package/dist/illustrations.js +16 -0
- package/dist/illustrations.js.map +1 -0
- package/dist/import-dialog.d.ts +11 -0
- package/dist/import-dialog.js +152 -0
- package/dist/import-dialog.js.map +1 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +39 -0
- package/dist/index.js.map +1 -0
- package/dist/inline-edit.d.ts +18 -0
- package/dist/inline-edit.js +115 -0
- package/dist/inline-edit.js.map +1 -0
- package/dist/landing.d.ts +79 -0
- package/dist/landing.js +122 -0
- package/dist/landing.js.map +1 -0
- package/dist/locale-switcher.d.ts +4 -0
- package/dist/locale-switcher.js +15 -0
- package/dist/locale-switcher.js.map +1 -0
- package/dist/map.d.ts +26 -0
- package/dist/map.js +232 -0
- package/dist/map.js.map +1 -0
- package/dist/notifications.d.ts +12 -0
- package/dist/notifications.js +75 -0
- package/dist/notifications.js.map +1 -0
- package/dist/record-mutation.d.ts +19 -0
- package/dist/record-mutation.js +39 -0
- package/dist/record-mutation.js.map +1 -0
- package/dist/resource-detail.d.ts +4 -0
- package/dist/resource-detail.js +96 -0
- package/dist/resource-detail.js.map +1 -0
- package/dist/resource-form.d.ts +11 -0
- package/dist/resource-form.js +92 -0
- package/dist/resource-form.js.map +1 -0
- package/dist/resource-list.d.ts +10 -0
- package/dist/resource-list.js +154 -0
- package/dist/resource-list.js.map +1 -0
- package/dist/saved-views.d.ts +16 -0
- package/dist/saved-views.js +64 -0
- package/dist/saved-views.js.map +1 -0
- package/dist/schema-editor-diagram.d.ts +14 -0
- package/dist/schema-editor-diagram.js +54 -0
- package/dist/schema-editor-diagram.js.map +1 -0
- package/dist/schema-editor.d.ts +98 -0
- package/dist/schema-editor.js +571 -0
- package/dist/schema-editor.js.map +1 -0
- package/dist/shell.d.ts +10 -0
- package/dist/shell.js +228 -0
- package/dist/shell.js.map +1 -0
- package/dist/skeleton.d.ts +25 -0
- package/dist/skeleton.js +34 -0
- package/dist/skeleton.js.map +1 -0
- package/dist/theme-catalog.d.ts +71 -0
- package/dist/theme-catalog.js +55 -0
- package/dist/theme-catalog.js.map +1 -0
- package/dist/theme-head.d.ts +8 -0
- package/dist/theme-head.js +11 -0
- package/dist/theme-head.js.map +1 -0
- package/dist/theme-preview.d.ts +2 -0
- package/dist/theme-preview.js +23 -0
- package/dist/theme-preview.js.map +1 -0
- package/dist/theme-switcher.d.ts +20 -0
- package/dist/theme-switcher.js +159 -0
- package/dist/theme-switcher.js.map +1 -0
- package/dist/theme.d.ts +24 -0
- package/dist/theme.js +30 -0
- package/dist/theme.js.map +1 -0
- package/dist/toast.d.ts +21 -0
- package/dist/toast.js +51 -0
- package/dist/toast.js.map +1 -0
- package/dist/ui.d.ts +86 -0
- package/dist/ui.js +106 -0
- package/dist/ui.js.map +1 -0
- package/dist/views.d.ts +23 -0
- package/dist/views.js +238 -0
- package/dist/views.js.map +1 -0
- package/package.json +58 -0
- package/styles.css +778 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
4
|
+
import { Bell, CheckCheck } from "lucide-react";
|
|
5
|
+
import Link from "next/link";
|
|
6
|
+
import { useRouter } from "next/navigation";
|
|
7
|
+
import { useEffect, useRef, useState } from "react";
|
|
8
|
+
import { useForge } from "./context.js";
|
|
9
|
+
import { ListSkeleton } from "./skeleton.js";
|
|
10
|
+
import { Button, Card, EmptyState, PageHeader, Pagination, cn } from "./ui.js";
|
|
11
|
+
function ago(iso, t, locale) {
|
|
12
|
+
const s = Math.round((Date.now() - new Date(iso).getTime()) / 1000);
|
|
13
|
+
if (s < 60)
|
|
14
|
+
return t("notifications.justNow");
|
|
15
|
+
if (s < 3600)
|
|
16
|
+
return t("notifications.minutesAgo", { count: Math.floor(s / 60) });
|
|
17
|
+
if (s < 86400)
|
|
18
|
+
return t("notifications.hoursAgo", { count: Math.floor(s / 3600) });
|
|
19
|
+
return new Date(iso).toLocaleDateString(locale);
|
|
20
|
+
}
|
|
21
|
+
/** Live updates: server-sent events, with slow polling as a fallback. */
|
|
22
|
+
function useNotificationStream() {
|
|
23
|
+
const { app } = useForge();
|
|
24
|
+
const queryClient = useQueryClient();
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (typeof EventSource === "undefined")
|
|
27
|
+
return;
|
|
28
|
+
const source = new EventSource(`${app.api.prefix}/notifications/stream`, { withCredentials: true });
|
|
29
|
+
source.addEventListener("notification", () => void queryClient.invalidateQueries({ queryKey: ["forge", "notifications"] }));
|
|
30
|
+
return () => source.close();
|
|
31
|
+
}, [app.api.prefix, queryClient]);
|
|
32
|
+
}
|
|
33
|
+
function useMarkRead() {
|
|
34
|
+
const { api } = useForge();
|
|
35
|
+
const queryClient = useQueryClient();
|
|
36
|
+
return async (body) => {
|
|
37
|
+
await api("/notifications/read", { method: "POST", body: JSON.stringify(body) });
|
|
38
|
+
await queryClient.invalidateQueries({ queryKey: ["forge", "notifications"] });
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export function NotificationBell({ align = "start" } = {}) {
|
|
42
|
+
const { api, locale, t } = useForge();
|
|
43
|
+
const router = useRouter();
|
|
44
|
+
const [open, setOpen] = useState(false);
|
|
45
|
+
const ref = useRef(null);
|
|
46
|
+
const markRead = useMarkRead();
|
|
47
|
+
useNotificationStream();
|
|
48
|
+
const q = useQuery({ queryKey: ["forge", "notifications", "latest"], queryFn: () => api("/notifications", { query: { pageSize: 8 } }), refetchInterval: 60_000 });
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
const close = (e) => ref.current && !ref.current.contains(e.target) && setOpen(false);
|
|
51
|
+
document.addEventListener("mousedown", close);
|
|
52
|
+
return () => document.removeEventListener("mousedown", close);
|
|
53
|
+
}, []);
|
|
54
|
+
const unread = q.data?.unread ?? 0;
|
|
55
|
+
return (_jsxs("div", { ref: ref, className: "relative", children: [_jsxs("button", { type: "button", "aria-label": unread ? t("notifications.labelUnread", { count: unread }) : t("notifications.title"), "aria-expanded": open, onClick: () => setOpen((o) => !o), className: "forge-btn forge-btn-ghost relative p-1.5", children: [_jsx(Bell, { className: "size-4" }), unread > 0 && (_jsx("span", { className: "absolute -top-0.5 -right-0.5 flex min-w-4 items-center justify-center rounded-full bg-danger px-1 text-[10px] leading-4 font-semibold text-white tabular-nums", children: unread > 99 ? "99+" : unread }))] }), open && (_jsxs("div", { className: cn("forge-popover absolute top-9 z-50 w-80", align === "end" ? "right-0" : "left-0"), children: [_jsxs("div", { className: "flex items-center justify-between border-b border-line px-3 py-2", children: [_jsx("span", { className: "text-sm font-semibold text-ink", children: t("notifications.title") }), unread > 0 && (_jsxs("button", { type: "button", className: "inline-flex items-center gap-1 text-xs text-link hover:underline", onClick: () => void markRead({ all: true }), children: [_jsx(CheckCheck, { className: "size-3.5" }), " ", t("notifications.markAllRead")] }))] }), _jsxs("ul", { className: "max-h-96 overflow-auto", children: [q.data?.data.length === 0 && _jsx("li", { className: "px-3 py-6 text-center text-sm text-ink-subtle", children: t("notifications.caughtUp") }), q.data?.data.map((n) => (_jsx("li", { children: _jsxs("button", { type: "button", className: "flex w-full gap-2.5 px-3 py-2.5 text-left hover:bg-hover", onClick: async () => {
|
|
56
|
+
if (!n.readAt)
|
|
57
|
+
await markRead({ ids: [n.id] });
|
|
58
|
+
setOpen(false);
|
|
59
|
+
if (n.link)
|
|
60
|
+
router.push(n.link);
|
|
61
|
+
}, children: [_jsx("span", { className: cn("mt-1.5 size-2 shrink-0 rounded-full", n.readAt ? "bg-transparent" : "bg-primary") }), _jsxs("span", { className: "min-w-0", children: [_jsx("span", { className: cn("block text-sm text-ink", !n.readAt && "font-medium"), children: n.title }), n.body && _jsx("span", { className: "block truncate text-xs text-ink-muted", children: n.body }), _jsx("span", { className: "block text-xs text-ink-subtle", children: ago(n.createdAt, t, locale) })] })] }) }, n.id)))] }), _jsx(Link, { href: "/notifications", onClick: () => setOpen(false), className: "block border-t border-line px-3 py-2 text-center text-xs text-link hover:underline", children: t("notifications.seeAll") })] }))] }));
|
|
62
|
+
}
|
|
63
|
+
export function NotificationsPage() {
|
|
64
|
+
const { api, locale, t } = useForge();
|
|
65
|
+
const router = useRouter();
|
|
66
|
+
const [page, setPage] = useState(1);
|
|
67
|
+
const [unreadOnly, setUnreadOnly] = useState(false);
|
|
68
|
+
const markRead = useMarkRead();
|
|
69
|
+
const q = useQuery({
|
|
70
|
+
queryKey: ["forge", "notifications", "page", page, unreadOnly],
|
|
71
|
+
queryFn: () => api("/notifications", { query: { page, pageSize: 25, ...(unreadOnly ? { unread: "true" } : {}) } }),
|
|
72
|
+
});
|
|
73
|
+
return (_jsxs("div", { children: [_jsx(PageHeader, { title: t("notifications.title"), actions: _jsxs(_Fragment, { children: [_jsx(Button, { variant: unreadOnly ? "primary" : "secondary", onClick: () => (setUnreadOnly(!unreadOnly), setPage(1)), children: t("notifications.unreadOnly") }), _jsxs(Button, { onClick: () => void markRead({ all: true }), disabled: !q.data?.unread, children: [_jsx(CheckCheck, { className: "size-4" }), " ", t("notifications.markAllRead")] })] }) }), _jsxs(Card, { children: [q.isLoading ? (_jsx(ListSkeleton, {})) : q.data.data.length === 0 ? (_jsx(EmptyState, { illustration: "notifications", title: t("notifications.emptyTitle"), description: t("notifications.emptyDescription") })) : (_jsx("ul", { className: "divide-y divide-line", children: q.data.data.map((n) => (_jsxs("li", { className: "flex items-start gap-3 px-4 py-3", children: [_jsx("span", { className: cn("mt-1.5 size-2 shrink-0 rounded-full", n.readAt ? "bg-transparent" : "bg-primary") }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("p", { className: cn("text-sm text-ink", !n.readAt && "font-medium"), children: n.title }), n.body && _jsx("p", { className: "text-sm text-ink-muted", children: n.body }), _jsx("p", { className: "text-xs text-ink-subtle", children: new Date(n.createdAt).toLocaleString(locale) })] }), n.link && (_jsx(Button, { size: "sm", variant: "ghost", onClick: async () => (n.readAt ? undefined : await markRead({ ids: [n.id] }), router.push(n.link)), children: t("common.open") })), !n.readAt && (_jsx(Button, { size: "sm", variant: "ghost", onClick: () => void markRead({ ids: [n.id] }), children: t("notifications.markRead") }))] }, n.id))) })), q.data && q.data.meta.total > 0 && _jsx(Pagination, { page: q.data.meta.page, pages: q.data.meta.pages, total: q.data.meta.total, onPage: setPage })] })] }));
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=notifications.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notifications.js","sourceRoot":"","sources":["../src/notifications.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ResourceMeta } from "@tekki-forge/compiler";
|
|
2
|
+
type Row = Record<string, any>;
|
|
3
|
+
export interface RecordChange {
|
|
4
|
+
id: string;
|
|
5
|
+
/** PATCH body (API names). Ignored when `transition` is set. */
|
|
6
|
+
patch?: Row;
|
|
7
|
+
/** Run this workflow transition instead of a PATCH. */
|
|
8
|
+
transition?: string;
|
|
9
|
+
/** Fields merged into cached rows right away so the UI moves before the server answers. */
|
|
10
|
+
optimistic?: Row;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Update one record from a list-like view (inline cell, board card, calendar event) with an optimistic
|
|
14
|
+
* cache update across every cached list of the resource, rolled back if the server refuses.
|
|
15
|
+
*/
|
|
16
|
+
export declare function useRecordChange(resource: Pick<ResourceMeta, "path">): import("@tanstack/react-query").UseMutationResult<Row, Error, RecordChange, {
|
|
17
|
+
snapshot: [readonly unknown[], unknown][];
|
|
18
|
+
}>;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
3
|
+
import { useForge } from "./context.js";
|
|
4
|
+
/**
|
|
5
|
+
* Update one record from a list-like view (inline cell, board card, calendar event) with an optimistic
|
|
6
|
+
* cache update across every cached list of the resource, rolled back if the server refuses.
|
|
7
|
+
*/
|
|
8
|
+
export function useRecordChange(resource) {
|
|
9
|
+
const { api } = useForge();
|
|
10
|
+
const queryClient = useQueryClient();
|
|
11
|
+
const listKey = ["resource", resource.path, "list"];
|
|
12
|
+
const patchRows = (id, fields) => queryClient.setQueriesData({ queryKey: listKey }, (old) => (old?.data ? { ...old, data: old.data.map((r) => (r.id === id ? { ...r, ...fields } : r)) } : old));
|
|
13
|
+
return useMutation({
|
|
14
|
+
mutationFn: ({ id, patch, transition }) => transition
|
|
15
|
+
? api(`/${resource.path}/${id}/transitions/${transition}`, { method: "POST", body: "{}" })
|
|
16
|
+
: api(`/${resource.path}/${id}`, { method: "PATCH", body: JSON.stringify(patch ?? {}) }),
|
|
17
|
+
onMutate: async ({ id, optimistic }) => {
|
|
18
|
+
await queryClient.cancelQueries({ queryKey: listKey });
|
|
19
|
+
const snapshot = queryClient.getQueriesData({ queryKey: listKey });
|
|
20
|
+
if (optimistic)
|
|
21
|
+
patchRows(id, optimistic);
|
|
22
|
+
return { snapshot };
|
|
23
|
+
},
|
|
24
|
+
onError: (_err, _vars, ctx) => {
|
|
25
|
+
for (const [key, data] of ctx?.snapshot ?? [])
|
|
26
|
+
queryClient.setQueryData(key, data);
|
|
27
|
+
},
|
|
28
|
+
onSuccess: (saved, { id }) => {
|
|
29
|
+
if (saved && typeof saved === "object")
|
|
30
|
+
patchRows(id, saved);
|
|
31
|
+
},
|
|
32
|
+
onSettled: (_data, _err, { id }) => {
|
|
33
|
+
queryClient.invalidateQueries({ queryKey: listKey });
|
|
34
|
+
queryClient.invalidateQueries({ queryKey: ["resource", resource.path, "get", id] });
|
|
35
|
+
queryClient.invalidateQueries({ queryKey: ["forge", "dashboard"] });
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=record-mutation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"record-mutation.js","sourceRoot":"","sources":["../src/record-mutation.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { canOnRecord } from "@tekki-forge/runtime-permissions";
|
|
4
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
5
|
+
import { ChevronLeft, Pencil, RotateCcw, Trash2 } from "lucide-react";
|
|
6
|
+
import Link from "next/link";
|
|
7
|
+
import { useRouter } from "next/navigation";
|
|
8
|
+
import { useState } from "react";
|
|
9
|
+
import { ActivityPanel } from "./activity.js";
|
|
10
|
+
import { useForge, useMe } from "./context.js";
|
|
11
|
+
import { FieldValue } from "./fields.js";
|
|
12
|
+
import { ResourceListPage } from "./resource-list.js";
|
|
13
|
+
import { DetailSkeleton } from "./skeleton.js";
|
|
14
|
+
import { useToast } from "./toast.js";
|
|
15
|
+
import { Alert, Button, Card, ConfirmDialog, EmptyState, PageHeader } from "./ui.js";
|
|
16
|
+
export function ResourceDetailPage({ path, id }) {
|
|
17
|
+
const { entityByPath, api, locale, t } = useForge();
|
|
18
|
+
const { principal } = useMe();
|
|
19
|
+
const router = useRouter();
|
|
20
|
+
const queryClient = useQueryClient();
|
|
21
|
+
const found = entityByPath(path);
|
|
22
|
+
const [confirm, setConfirm] = useState(null);
|
|
23
|
+
const toast = useToast();
|
|
24
|
+
const setNotice = (n) => (n.tone === "success" ? toast.success(n.text) : toast.error(t("toast.failed"), n.text));
|
|
25
|
+
const record = useQuery({
|
|
26
|
+
queryKey: ["resource", path, "get", id],
|
|
27
|
+
queryFn: () => api(`/${path}/${id}`, { query: found?.entity.features.softDelete ? { trashed: "with" } : {} }),
|
|
28
|
+
enabled: !!found,
|
|
29
|
+
});
|
|
30
|
+
const invalidate = () => queryClient.invalidateQueries({ queryKey: ["resource", path] });
|
|
31
|
+
const remove = useMutation({
|
|
32
|
+
mutationFn: () => api(`/${path}/${id}`, { method: "DELETE" }),
|
|
33
|
+
onSuccess: () => {
|
|
34
|
+
invalidate();
|
|
35
|
+
toast.success(t("toast.deleted", { item: found?.resource.label ?? "" }));
|
|
36
|
+
router.push(`/${path}`);
|
|
37
|
+
},
|
|
38
|
+
onError: (e) => {
|
|
39
|
+
setConfirm(null);
|
|
40
|
+
setNotice({ tone: "error", text: e.message });
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
const run = useMutation({
|
|
44
|
+
mutationFn: (action) => api(`/${path}/${id}/actions/${action.name}`, { method: "POST", body: "{}" }),
|
|
45
|
+
onSuccess: (_d, action) => {
|
|
46
|
+
setConfirm(null);
|
|
47
|
+
setNotice({ tone: "success", text: t("detail.actionCompleted", { action: action.label }) });
|
|
48
|
+
invalidate();
|
|
49
|
+
},
|
|
50
|
+
onError: (e) => {
|
|
51
|
+
setConfirm(null);
|
|
52
|
+
setNotice({ tone: "error", text: e.message });
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
const transition = useMutation({
|
|
56
|
+
mutationFn: (t) => api(`/${path}/${id}/transitions/${t.name}`, { method: "POST", body: "{}" }),
|
|
57
|
+
onSuccess: (_d, tr) => {
|
|
58
|
+
setConfirm(null);
|
|
59
|
+
setNotice({ tone: "success", text: t("detail.transitioned", { transition: tr.label, state: stateLabel(tr.to) }) });
|
|
60
|
+
invalidate();
|
|
61
|
+
},
|
|
62
|
+
onError: (e) => {
|
|
63
|
+
setConfirm(null);
|
|
64
|
+
setNotice({ tone: "error", text: e.message });
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
const restore = useMutation({
|
|
68
|
+
mutationFn: () => api(`/${path}/${id}/restore`, { method: "POST" }),
|
|
69
|
+
onSuccess: () => {
|
|
70
|
+
setNotice({ tone: "success", text: t("detail.restored") });
|
|
71
|
+
invalidate();
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
const stateLabel = (state) => {
|
|
75
|
+
const wf = found?.entity.workflow;
|
|
76
|
+
return found?.entity.fields.find((f) => f.name === wf?.field)?.enumValues?.find((v) => v.value === state)?.label ?? state.replaceAll("_", " ");
|
|
77
|
+
};
|
|
78
|
+
if (!found)
|
|
79
|
+
return _jsx(EmptyState, { title: t("common.notFound") });
|
|
80
|
+
const { entity, resource } = found;
|
|
81
|
+
if (record.isLoading)
|
|
82
|
+
return _jsx(DetailSkeleton, { fields: Math.min(8, entity.fields.length) || 4 });
|
|
83
|
+
if (record.error || !record.data)
|
|
84
|
+
return _jsx(Alert, { children: record.error?.message ?? t("common.notFound") });
|
|
85
|
+
const r = record.data;
|
|
86
|
+
const canUpdate = canOnRecord(entity, "update", principal, r);
|
|
87
|
+
const canDelete = canOnRecord(entity, "delete", principal, r);
|
|
88
|
+
const title = r[entity.display] ? String(r[entity.display]) : `${resource.label} ${r.id.slice(0, 8)}`;
|
|
89
|
+
return (_jsxs("div", { children: [_jsxs(Link, { href: `/${path}`, className: "mb-3 inline-flex items-center gap-1 text-sm text-ink-muted hover:text-ink", children: [_jsx(ChevronLeft, { className: "size-4" }), " ", resource.pluralLabel] }), _jsx(PageHeader, { title: _jsxs("span", { className: "inline-flex items-center gap-2.5", children: [title, entity.workflow && _jsx(FieldValue, { field: entity.fields.find((f) => f.name === entity.workflow.field), value: r[entity.workflow.field] })] }), description: r.deletedAt ? t("detail.deleted") : undefined, actions: _jsxs(_Fragment, { children: [!r.deletedAt &&
|
|
90
|
+
entity.workflow?.transitions
|
|
91
|
+
.filter((t) => (t.from.length === 0 || t.from.includes(r[entity.workflow.field])) && canOnRecord({ ...entity, permissions: { ...entity.permissions, update: t.permission } }, "update", principal, r))
|
|
92
|
+
.map((t) => (_jsx(Button, { variant: t.variant === "danger" ? "danger" : "primary", loading: transition.isPending && transition.variables?.name === t.name, onClick: () => (t.confirm || t.variant === "danger" ? setConfirm({ kind: "transition", transition: t }) : transition.mutate(t)), children: t.label }, t.name))), entity.actions.map((a) => canOnRecord({ ...entity, permissions: { ...entity.permissions, update: a.permission } }, "update", principal, r) ? (_jsx(Button, { variant: a.variant === "danger" ? "danger" : "secondary", onClick: () => (a.confirm ? setConfirm({ kind: "action", action: a }) : run.mutate(a)), loading: run.isPending && run.variables?.name === a.name, children: a.label }, a.name)) : null), r.deletedAt && canUpdate && (_jsxs(Button, { onClick: () => restore.mutate(), loading: restore.isPending, children: [_jsx(RotateCcw, { className: "size-4" }), " ", t("detail.restore")] })), canUpdate && !r.deletedAt && (_jsxs(Link, { href: `/${path}/${id}/edit`, className: "forge-btn forge-btn-secondary inline-flex h-9 items-center gap-1.5 px-3.5 text-sm font-medium", children: [_jsx(Pencil, { className: "size-4" }), " ", t("common.edit")] })), canDelete && !r.deletedAt && (_jsxs(Button, { variant: "danger-ghost", onClick: () => setConfirm({ kind: "delete" }), children: [_jsx(Trash2, { className: "size-4" }), " ", t("common.delete")] }))] }) }), _jsx(Card, { children: _jsxs("dl", { className: "divide-y divide-line", children: [entity.fields
|
|
93
|
+
.filter((f) => !f.hidden)
|
|
94
|
+
.map((f) => (_jsxs("div", { className: "grid grid-cols-3 gap-4 px-6 py-3.5 text-sm", children: [_jsx("dt", { className: "font-medium text-ink-muted", children: f.label }), _jsx("dd", { className: "col-span-2 text-ink", children: f.relation?.type === "many-to-many" && Array.isArray(r[f.name]) && r[f.name].length > 0 ? (_jsx("span", { className: "flex flex-wrap gap-1.5", children: r[f.name].map((x) => f.relation.path ? (_jsx(Link, { href: `/${f.relation.path}/${x.id}`, className: "forge-badge px-2 py-0.5 text-xs hover:underline", children: x.label }, x.id)) : (_jsx("span", { className: "forge-badge px-2 py-0.5 text-xs", children: x.label }, x.id))) })) : f.relation?.type === "many-to-one" && r[f.name] && f.relation.path ? (_jsx(Link, { className: "text-link hover:underline", href: `/${f.relation.path}/${r[f.name].id}`, children: r[f.name].label })) : (_jsx(FieldValue, { field: f, value: f.relation ? r[f.name] : r[f.name] })) })] }, f.name))), entity.features.timestamps && (_jsxs("div", { className: "grid grid-cols-3 gap-4 px-6 py-3.5 text-sm", children: [_jsx("dt", { className: "font-medium text-ink-muted", children: t("detail.created") }), _jsx("dd", { className: "col-span-2 text-ink-muted tabular-nums", children: t("detail.createdUpdated", { created: new Date(r.createdAt).toLocaleString(locale), updated: new Date(r.updatedAt).toLocaleString(locale) }) })] }))] }) }), resource.detail.related.map((rel) => (_jsxs("section", { className: "mt-8", children: [_jsx("h2", { className: "mb-3 text-sm font-semibold text-ink", children: rel.label }), _jsx(ResourceListPage, { path: rel.resourcePath, fixed: { [rel.filterKey]: id }, embedded: true })] }, rel.name))), (entity.features.audit || entity.features.comments) && _jsx(ActivityPanel, { path: path, entity: entity, recordId: id, readOnly: !!r.deletedAt }), _jsx(ConfirmDialog, { open: confirm?.kind === "delete", title: t("detail.deleteTitle", { item: resource.label.toLowerCase() }), message: entity.features.softDelete ? t("detail.deleteSoft") : t("detail.deleteHard"), confirmLabel: t("common.delete"), danger: true, loading: remove.isPending, onConfirm: () => remove.mutate(), onClose: () => setConfirm(null) }), _jsx(ConfirmDialog, { open: confirm?.kind === "transition", title: confirm?.kind === "transition" ? confirm.transition.label : "", message: confirm?.kind === "transition" ? (confirm.transition.confirm ?? t("detail.transitionConfirm", { item: resource.label.toLowerCase(), state: stateLabel(confirm.transition.to) })) : "", danger: confirm?.kind === "transition" && confirm.transition.variant === "danger", loading: transition.isPending, confirmLabel: confirm?.kind === "transition" ? confirm.transition.label : t("common.confirm"), onConfirm: () => confirm?.kind === "transition" && transition.mutate(confirm.transition), onClose: () => setConfirm(null) }), _jsx(ConfirmDialog, { open: confirm?.kind === "action", title: confirm?.kind === "action" ? confirm.action.label : "", message: confirm?.kind === "action" ? confirm.action.confirm : "", danger: confirm?.kind === "action" && confirm.action.variant === "danger", loading: run.isPending, onConfirm: () => confirm?.kind === "action" && run.mutate(confirm.action), onClose: () => setConfirm(null) })] }));
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=resource-detail.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resource-detail.js","sourceRoot":"","sources":["../src/resource-detail.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { EntityIR, ResourceMeta } from "@tekki-forge/compiler";
|
|
2
|
+
export declare function ResourceFormPage({ path, id }: {
|
|
3
|
+
path: string;
|
|
4
|
+
id?: string;
|
|
5
|
+
}): import("react").JSX.Element;
|
|
6
|
+
export declare function ResourceForm({ entity, resource, id, initial }: {
|
|
7
|
+
entity: EntityIR;
|
|
8
|
+
resource: ResourceMeta;
|
|
9
|
+
id?: string;
|
|
10
|
+
initial?: Record<string, unknown>;
|
|
11
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { validateInput } from "@tekki-forge/runtime-schema";
|
|
4
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
5
|
+
import Link from "next/link";
|
|
6
|
+
import { useRouter, useSearchParams } from "next/navigation";
|
|
7
|
+
import { useEffect, useState } from "react";
|
|
8
|
+
import { ApiError, useForge } from "./context.js";
|
|
9
|
+
import { FieldInput, toFormValue, toPayloadValue } from "./fields.js";
|
|
10
|
+
import { FormSkeleton } from "./skeleton.js";
|
|
11
|
+
import { useToast } from "./toast.js";
|
|
12
|
+
import { Alert, Button, Card, EmptyState, PageHeader } from "./ui.js";
|
|
13
|
+
export function ResourceFormPage({ path, id }) {
|
|
14
|
+
const { entityByPath, api, t } = useForge();
|
|
15
|
+
const found = entityByPath(path);
|
|
16
|
+
const record = useQuery({
|
|
17
|
+
queryKey: ["resource", path, "get", id],
|
|
18
|
+
queryFn: () => api(`/${path}/${id}`),
|
|
19
|
+
enabled: !!id && !!found,
|
|
20
|
+
});
|
|
21
|
+
if (!found)
|
|
22
|
+
return _jsx(EmptyState, { title: t("common.notFound") });
|
|
23
|
+
if (id && record.isLoading)
|
|
24
|
+
return _jsx(FormSkeleton, { fields: Math.min(6, found.resource.form.fields.length) || 4 });
|
|
25
|
+
if (id && record.error)
|
|
26
|
+
return _jsx(Alert, { children: record.error.message });
|
|
27
|
+
return _jsx(ResourceForm, { ...found, id: id, initial: record.data });
|
|
28
|
+
}
|
|
29
|
+
export function ResourceForm({ entity, resource, id, initial }) {
|
|
30
|
+
const { api, t, tv } = useForge();
|
|
31
|
+
const router = useRouter();
|
|
32
|
+
const search = useSearchParams();
|
|
33
|
+
const queryClient = useQueryClient();
|
|
34
|
+
const fields = resource.form.fields;
|
|
35
|
+
const [values, setValues] = useState({});
|
|
36
|
+
const [errors, setErrors] = useState({});
|
|
37
|
+
const [formError, setFormError] = useState(null);
|
|
38
|
+
const toast = useToast();
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
const v = {};
|
|
41
|
+
for (const f of fields) {
|
|
42
|
+
const fromRecord = initial ? (f.file ? initial[f.name] : initial[f.apiName]) : undefined;
|
|
43
|
+
const prefill = !initial ? search.get(f.apiName) : null;
|
|
44
|
+
v[f.name] = toFormValue(f, fromRecord ?? prefill ?? (initial ? null : (f.defaultValue ?? null)));
|
|
45
|
+
}
|
|
46
|
+
setValues(v);
|
|
47
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
48
|
+
}, [initial]);
|
|
49
|
+
const save = useMutation({
|
|
50
|
+
mutationFn: (payload) => api(id ? `/${resource.path}/${id}` : `/${resource.path}`, { method: id ? "PATCH" : "POST", body: JSON.stringify(payload) }),
|
|
51
|
+
onSuccess: (saved) => {
|
|
52
|
+
toast.success(id ? t("toast.saved") : t("toast.created", { item: resource.label }));
|
|
53
|
+
queryClient.invalidateQueries({ queryKey: ["resource", resource.path] });
|
|
54
|
+
queryClient.invalidateQueries({ queryKey: ["forge", "stats"] });
|
|
55
|
+
router.push(`/${resource.path}/${saved.id}`);
|
|
56
|
+
},
|
|
57
|
+
onError: (err) => {
|
|
58
|
+
if (err instanceof ApiError && err.body?.issues?.length) {
|
|
59
|
+
setErrors(Object.fromEntries(err.body.issues.map((i) => [fields.find((f) => f.apiName === i.path)?.name ?? i.path, tv(i.message)])));
|
|
60
|
+
}
|
|
61
|
+
setFormError(err.message);
|
|
62
|
+
if (!(err instanceof ApiError && err.body?.issues?.length))
|
|
63
|
+
toast.error(t("toast.failed"), err.message);
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
const submit = (e) => {
|
|
67
|
+
e.preventDefault();
|
|
68
|
+
setFormError(null);
|
|
69
|
+
const payload = {};
|
|
70
|
+
for (const f of fields) {
|
|
71
|
+
const value = toPayloadValue(f, values[f.name]);
|
|
72
|
+
// On edit, only send what changed so concurrent edits to other fields are preserved.
|
|
73
|
+
if (id && initial && JSON.stringify(value) === JSON.stringify(initial[f.apiName] ?? (f.component === "switch" ? false : null)))
|
|
74
|
+
continue;
|
|
75
|
+
if (!id && value === null)
|
|
76
|
+
continue;
|
|
77
|
+
payload[f.apiName] = value;
|
|
78
|
+
}
|
|
79
|
+
const result = validateInput(entity, id ? "update" : "create", payload);
|
|
80
|
+
if (!result.success) {
|
|
81
|
+
setErrors(Object.fromEntries(result.issues.map((i) => [fields.find((f) => f.apiName === i.path)?.name ?? i.path, tv(i.message)])));
|
|
82
|
+
setFormError(t("form.fixErrors"));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
setErrors({});
|
|
86
|
+
save.mutate(payload);
|
|
87
|
+
};
|
|
88
|
+
const title = id ? t("form.editTitle", { item: resource.label.toLowerCase() }) : t("list.new", { item: resource.label.toLowerCase() });
|
|
89
|
+
const cancelHref = id ? `/${resource.path}/${id}` : `/${resource.path}`;
|
|
90
|
+
return (_jsxs("form", { onSubmit: submit, noValidate: true, className: "mx-auto max-w-2xl", children: [_jsx(PageHeader, { title: title, description: id && initial ? String(initial[entity.display] ?? "") : undefined }), _jsxs(Card, { className: "forge-form", children: [formError && _jsx(Alert, { children: formError }), fields.map((f) => (_jsx(FieldInput, { entity: entity.name, field: f, value: values[f.name], error: errors[f.name], onChange: (v) => setValues((prev) => ({ ...prev, [f.name]: v })) }, f.name)))] }), _jsxs("div", { className: "mt-4 flex justify-end gap-2", children: [_jsx(Link, { href: cancelHref, className: "forge-btn forge-btn-ghost inline-flex h-[var(--forge-control-h)] items-center px-3.5 text-sm font-medium", children: t("common.cancel") }), _jsx(Button, { type: "submit", variant: "primary", loading: save.isPending, children: id ? t("form.saveChanges") : t("form.createItem", { item: resource.label.toLowerCase() }) })] })] }));
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=resource-form.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resource-form.js","sourceRoot":"","sources":["../src/resource-form.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
type Params = Record<string, string>;
|
|
2
|
+
export interface ResourceListProps {
|
|
3
|
+
path: string;
|
|
4
|
+
/** Filters that are always applied and hidden from the UI (used for related lists). */
|
|
5
|
+
fixed?: Params;
|
|
6
|
+
/** Compact rendering inside another page; state is kept locally instead of in the URL. */
|
|
7
|
+
embedded?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function ResourceListPage(props: ResourceListProps): import("react").JSX.Element;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { can, evaluate } from "@tekki-forge/runtime-permissions";
|
|
4
|
+
import { keepPreviousData, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
5
|
+
import { ArrowDown, ArrowUp, ArrowUpDown, CalendarDays, Columns3, Download, Map as MapIcon, Plus, Search, Table2, Trash2, Upload } from "lucide-react";
|
|
6
|
+
import Link from "next/link";
|
|
7
|
+
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
|
8
|
+
import { useEffect, useMemo, useState } from "react";
|
|
9
|
+
import { ApiError, useForge, useMe } from "./context.js";
|
|
10
|
+
import { FieldValue, RelationSelect } from "./fields.js";
|
|
11
|
+
import { ImportDialog } from "./import-dialog.js";
|
|
12
|
+
import { EditableCell, inlineField } from "./inline-edit.js";
|
|
13
|
+
import { MapView } from "./map.js";
|
|
14
|
+
import { SavedViewsMenu } from "./saved-views.js";
|
|
15
|
+
import { BoardView, CalendarView } from "./views.js";
|
|
16
|
+
import { TableSkeleton } from "./skeleton.js";
|
|
17
|
+
import { useToast } from "./toast.js";
|
|
18
|
+
import { Alert, Button, Card, ConfirmDialog, EmptyState, Input, PageHeader, Pagination, Select, cn } from "./ui.js";
|
|
19
|
+
function useUrlParams() {
|
|
20
|
+
const router = useRouter();
|
|
21
|
+
const pathname = usePathname();
|
|
22
|
+
const search = useSearchParams();
|
|
23
|
+
const params = useMemo(() => Object.fromEntries(search.entries()), [search]);
|
|
24
|
+
return [
|
|
25
|
+
params,
|
|
26
|
+
(next) => {
|
|
27
|
+
const qs = new URLSearchParams(Object.entries(next).filter(([, v]) => v !== "" && v !== undefined)).toString();
|
|
28
|
+
router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false });
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
}
|
|
32
|
+
function useLocalParams() {
|
|
33
|
+
const [params, setParams] = useState({});
|
|
34
|
+
return [params, setParams];
|
|
35
|
+
}
|
|
36
|
+
export function ResourceListPage(props) {
|
|
37
|
+
const { entityByPath, pages, t } = useForge();
|
|
38
|
+
const found = entityByPath(props.path);
|
|
39
|
+
const PluginPage = !found && !props.embedded ? pages[props.path] : undefined;
|
|
40
|
+
if (PluginPage)
|
|
41
|
+
return _jsx(PluginPage, { path: props.path });
|
|
42
|
+
if (!found)
|
|
43
|
+
return _jsx(EmptyState, { title: t("common.notFound"), description: t("list.noResource", { path: `/${props.path}` }) });
|
|
44
|
+
return props.embedded ? _jsx(EmbeddedList, { ...props, ...found }) : _jsx(UrlList, { ...props, ...found });
|
|
45
|
+
}
|
|
46
|
+
function UrlList(props) {
|
|
47
|
+
const [params, setParams] = useUrlParams();
|
|
48
|
+
return _jsx(ResourceList, { ...props, params: params, setParams: setParams });
|
|
49
|
+
}
|
|
50
|
+
function EmbeddedList(props) {
|
|
51
|
+
const [params, setParams] = useLocalParams();
|
|
52
|
+
return _jsx(ResourceList, { ...props, params: params, setParams: setParams });
|
|
53
|
+
}
|
|
54
|
+
function ResourceList({ entity, resource, fixed, embedded, params, setParams, }) {
|
|
55
|
+
const { api, t } = useForge();
|
|
56
|
+
const { principal } = useMe();
|
|
57
|
+
const queryClient = useQueryClient();
|
|
58
|
+
const [selected, setSelected] = useState(new Set());
|
|
59
|
+
const [confirmBulk, setConfirmBulk] = useState(false);
|
|
60
|
+
const [searchText, setSearchText] = useState(params.search ?? "");
|
|
61
|
+
const [importing, setImporting] = useState(false);
|
|
62
|
+
// A view is offered only when the viewer can read the field it is built on for every record.
|
|
63
|
+
const fullyReadable = (name) => {
|
|
64
|
+
const rule = name ? entity.fields.find((f) => f.name === name)?.access?.read : undefined;
|
|
65
|
+
if (!rule)
|
|
66
|
+
return true;
|
|
67
|
+
const d = evaluate(rule, principal);
|
|
68
|
+
return d.allowed && d.scope === "all";
|
|
69
|
+
};
|
|
70
|
+
const viewField = { board: entity.views.board?.groupBy, calendar: entity.views.calendar?.date, map: entity.views.map?.location };
|
|
71
|
+
const views = VIEW_TABS.filter((v) => v.key === "table" || (entity.views[v.key] && fullyReadable(viewField[v.key])));
|
|
72
|
+
const view = views.find((v) => v.key === params.view)?.key ?? "table";
|
|
73
|
+
const { view: _view, month: _month, ...listParams } = params;
|
|
74
|
+
const query = { ...listParams, ...fixed, ...(embedded ? { pageSize: "10" } : {}) };
|
|
75
|
+
const list = useQuery({
|
|
76
|
+
queryKey: ["resource", resource.path, "list", query],
|
|
77
|
+
queryFn: () => api(`/${resource.path}`, { query }),
|
|
78
|
+
placeholderData: keepPreviousData,
|
|
79
|
+
enabled: view === "table",
|
|
80
|
+
});
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
const t = setTimeout(() => {
|
|
83
|
+
if ((params.search ?? "") !== searchText)
|
|
84
|
+
setParams({ ...params, search: searchText, page: "" });
|
|
85
|
+
}, 300);
|
|
86
|
+
return () => clearTimeout(t);
|
|
87
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
88
|
+
}, [searchText]);
|
|
89
|
+
const toast = useToast();
|
|
90
|
+
const bulkDelete = useMutation({
|
|
91
|
+
mutationFn: () => api(`/${resource.path}/bulk-delete`, { method: "POST", body: JSON.stringify({ ids: [...selected] }) }),
|
|
92
|
+
onSuccess: () => {
|
|
93
|
+
toast.success(t("toast.bulkDeleted", { count: selected.size }));
|
|
94
|
+
setSelected(new Set());
|
|
95
|
+
setConfirmBulk(false);
|
|
96
|
+
queryClient.invalidateQueries({ queryKey: ["resource", resource.path] });
|
|
97
|
+
},
|
|
98
|
+
onError: (e) => {
|
|
99
|
+
setConfirmBulk(false);
|
|
100
|
+
toast.error(t("toast.failed"), e.message);
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
const set = (patch) => setParams({ ...params, ...patch });
|
|
104
|
+
const sort = params.sort ?? resource.list.defaultSort;
|
|
105
|
+
const toggleSort = (key) => set({ sort: sort === key ? `-${key}` : sort === `-${key}` ? "" : key, page: "" });
|
|
106
|
+
const rows = list.data?.data ?? [];
|
|
107
|
+
const allSelected = rows.length > 0 && rows.every((r) => selected.has(r.id));
|
|
108
|
+
const canCreate = can(entity, "create", principal);
|
|
109
|
+
const canDelete = can(entity, "delete", principal) && entity.features.bulkDelete;
|
|
110
|
+
const detail = (id) => `/${resource.path}/${id}`;
|
|
111
|
+
const newHref = `/${resource.path}/new${fixed ? `?${new URLSearchParams(Object.entries(fixed).map(([k, v]) => [`${k}Id`, v])).toString()}` : ""}`;
|
|
112
|
+
const fieldByName = new Map(entity.fields.map((f) => [f.name, f]));
|
|
113
|
+
// createdAt / updatedAt are framework columns: their labels come from the UI catalog.
|
|
114
|
+
const columnLabel = (c) => (c.kind === "system" ? t(`list.column.${c.key}`) : c.label);
|
|
115
|
+
// Columns editable in place (record-level permissions are checked per cell).
|
|
116
|
+
const editable = new Map(embedded || !can(entity, "update", principal) ? [] : resource.list.columns.flatMap((c) => {
|
|
117
|
+
const found = inlineField(entity, resource, c);
|
|
118
|
+
return found ? [[c.key, found]] : [];
|
|
119
|
+
}));
|
|
120
|
+
const filters = resource.list.filters.filter((f) => !fixed?.[f.key]);
|
|
121
|
+
const exportHref = `/api/${resource.path}/export?${new URLSearchParams(Object.entries({ ...params, ...fixed }).filter(([k, v]) => v && k !== "page" && k !== "pageSize")).toString()}`;
|
|
122
|
+
return (_jsxs("div", { children: [!embedded && (_jsx(PageHeader, { title: resource.pluralLabel, description: entity.description, actions: _jsxs(_Fragment, { children: [canCreate && (_jsxs(Button, { onClick: () => setImporting(true), children: [_jsx(Upload, { className: "size-4" }), " ", t("list.import")] })), entity.features.export && (_jsxs("a", { href: exportHref, className: "forge-btn forge-btn-secondary inline-flex h-9 items-center gap-1.5 px-3.5 text-sm font-medium", children: [_jsx(Download, { className: "size-4" }), " ", t("list.export")] })), canCreate && (_jsxs(Link, { href: newHref, className: "forge-btn forge-btn-primary inline-flex h-9 items-center gap-1.5 px-3.5 text-sm font-medium", children: [_jsx(Plus, { className: "size-4" }), " ", t("list.new", { item: resource.label.toLowerCase() })] }))] }) })), _jsxs(Card, { children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2 border-b border-line p-3", children: [resource.searchable && (_jsxs("div", { className: "relative min-w-52 flex-1", children: [_jsx(Search, { className: "pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-ink-subtle" }), _jsx(Input, { "aria-label": t("common.search"), placeholder: t("list.searchPlaceholder", { items: resource.pluralLabel.toLowerCase() }), value: searchText, onChange: (e) => setSearchText(e.target.value), className: "h-8 pl-8" })] })), !embedded && filters.map((f) => (_jsx(FilterControl, { filter: f, params: params, onChange: (patch) => set({ ...patch, page: "" }) }, f.key))), entity.features.softDelete && !embedded && (_jsxs(Select, { "aria-label": t("list.deletedRecords"), value: params.trashed ?? "", onChange: (e) => set({ trashed: e.target.value, page: "" }), className: "h-8 !w-auto py-0 text-xs", children: [_jsx("option", { value: "", children: t("list.trashed.active") }), _jsx("option", { value: "only", children: t("list.trashed.deleted") }), _jsx("option", { value: "with", children: t("list.trashed.all") })] })), _jsxs("div", { className: "ml-auto flex items-center gap-2", children: [!embedded && views.length > 1 && (_jsx("div", { role: "tablist", "aria-label": t("list.view"), className: "forge-input flex h-8 items-center gap-0.5 p-0.5", children: views.map((v) => (_jsxs("button", { type: "button", role: "tab", "aria-selected": view === v.key, title: t(v.label), onClick: () => set({ view: v.key === "table" ? "" : v.key, page: "" }), className: cn("flex h-full items-center gap-1 rounded px-2 text-xs", view === v.key ? "bg-selected text-ink" : "text-ink-muted hover:text-ink"), children: [v.icon, _jsx("span", { className: "hidden sm:inline", children: t(v.label) })] }, v.key))) })), !embedded && _jsx(SavedViewsMenu, { resource: resource, params: params, setParams: setParams }), embedded && canCreate && (_jsxs(Link, { href: newHref, className: "forge-btn forge-btn-secondary inline-flex h-7 items-center gap-1 px-2.5 text-xs font-medium", children: [_jsx(Plus, { className: "size-3.5" }), " ", t("list.add")] })), selected.size > 0 && canDelete && (_jsxs(Button, { size: "sm", variant: "danger", onClick: () => setConfirmBulk(true), children: [_jsx(Trash2, { className: "size-3.5" }), " ", t("list.deleteSelected", { count: selected.size })] }))] })] }), view === "board" && _jsx(BoardView, { entity: entity, resource: resource, params: params, ...(fixed ? { fixed } : {}) }), view === "calendar" && _jsx(CalendarView, { entity: entity, resource: resource, params: params, setParams: setParams, ...(fixed ? { fixed } : {}) }), view === "map" && _jsx(MapView, { entity: entity, resource: resource, params: params, ...(fixed ? { fixed } : {}) }), view === "table" && list.error && (_jsx("div", { className: "p-3", children: _jsx(Alert, { children: list.error.message }) })), view !== "table" ? null : list.isLoading ? (_jsx(TableSkeleton, { columns: Math.min(5, resource.list.columns.length) || 3 })) : rows.length === 0 ? (_jsx(EmptyState, { illustration: params.search || Object.keys(params).length ? "search" : "list", title: params.search || Object.keys(params).length ? t("list.noMatch", { items: resource.pluralLabel.toLowerCase() }) : t("list.emptyYet", { items: resource.pluralLabel.toLowerCase() }), ...(canCreate && !embedded ? { action: _jsx(Link, { className: "text-sm font-medium text-link hover:underline", href: newHref, children: t("list.createFirst") }) } : {}) })) : (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: cn("forge-table w-full text-sm", list.isFetching && "opacity-70"), children: [_jsx("thead", { children: _jsxs("tr", { className: "text-left text-xs font-medium tracking-wide uppercase", children: [canDelete && (_jsx("th", { className: "w-10 px-4 py-2.5", children: _jsx("input", { type: "checkbox", "aria-label": t("list.selectAll"), checked: allSelected, onChange: () => setSelected(allSelected ? new Set() : new Set(rows.map((r) => r.id))), className: "forge-check" }) })), resource.list.columns.map((c) => (_jsx("th", { className: "px-4 py-2.5 font-medium whitespace-nowrap", children: c.sortable ? (_jsxs("button", { type: "button", onClick: () => toggleSort(c.key), className: "inline-flex items-center gap-1 uppercase hover:text-ink", children: [columnLabel(c), _jsx(SortIcon, { column: c, sort: sort })] })) : (columnLabel(c)) }, c.key)))] }) }), _jsx("tbody", { children: rows.map((row) => (_jsxs("tr", { className: cn("group", row.deletedAt && "text-ink-subtle"), children: [canDelete && (_jsx("td", { className: "px-4 py-2.5", children: _jsx("input", { type: "checkbox", "aria-label": t("list.selectRow"), checked: selected.has(row.id), onChange: () => {
|
|
123
|
+
const next = new Set(selected);
|
|
124
|
+
if (next.has(row.id))
|
|
125
|
+
next.delete(row.id);
|
|
126
|
+
else
|
|
127
|
+
next.add(row.id);
|
|
128
|
+
setSelected(next);
|
|
129
|
+
}, className: "forge-check" }) })), resource.list.columns.map((c, i) => (_jsx("td", { className: "max-w-xs truncate px-4 py-2.5", children: i === 0 ? (_jsx(Link, { href: detail(row.id), className: "font-medium text-ink hover:text-link", children: row[c.key] === null || row[c.key] === "" ? t("common.untitled") : _jsx(FieldValue, { field: fieldByName.get(c.key), value: row[c.key], cell: c.cell }) })) : editable.has(c.key) ? (_jsx(EditableCell, { entity: entity, resource: resource, row: row, column: c, ...editable.get(c.key), principal: principal })) : (_jsx(FieldValue, { field: fieldByName.get(c.key), value: row[c.key], cell: c.kind === "system" ? "datetime" : c.cell })) }, c.key)))] }, row.id))) })] }) })), view === "table" && list.data && list.data.meta.total > 0 && (_jsx(Pagination, { page: list.data.meta.page, pages: list.data.meta.pages, total: list.data.meta.total, onPage: (p) => set({ page: String(p) }) }))] }), importing && _jsx(ImportDialog, { entity: entity, resource: resource, open: importing, onClose: () => setImporting(false) }), _jsx(ConfirmDialog, { open: confirmBulk, title: t("list.bulkDeleteTitle", { count: selected.size, item: resource.label.toLowerCase(), items: resource.pluralLabel.toLowerCase() }), message: entity.features.softDelete ? t("list.bulkDeleteSoft") : t("detail.deleteHard"), confirmLabel: t("common.delete"), danger: true, loading: bulkDelete.isPending, onConfirm: () => bulkDelete.mutate(), onClose: () => setConfirmBulk(false) })] }));
|
|
130
|
+
}
|
|
131
|
+
const VIEW_TABS = [
|
|
132
|
+
{ key: "table", label: "list.views.table", icon: _jsx(Table2, { className: "size-3.5" }) },
|
|
133
|
+
{ key: "board", label: "list.views.board", icon: _jsx(Columns3, { className: "size-3.5" }) },
|
|
134
|
+
{ key: "calendar", label: "list.views.calendar", icon: _jsx(CalendarDays, { className: "size-3.5" }) },
|
|
135
|
+
{ key: "map", label: "list.views.map", icon: _jsx(MapIcon, { className: "size-3.5" }) },
|
|
136
|
+
];
|
|
137
|
+
function SortIcon({ column, sort }) {
|
|
138
|
+
if (sort === column.key)
|
|
139
|
+
return _jsx(ArrowUp, { className: "size-3" });
|
|
140
|
+
if (sort === `-${column.key}`)
|
|
141
|
+
return _jsx(ArrowDown, { className: "size-3" });
|
|
142
|
+
return _jsx(ArrowUpDown, { className: "size-3 opacity-0 group-hover:opacity-40" });
|
|
143
|
+
}
|
|
144
|
+
function FilterControl({ filter, params, onChange }) {
|
|
145
|
+
const { t } = useForge();
|
|
146
|
+
const value = params[filter.key] ?? "";
|
|
147
|
+
if (filter.type === "relation") {
|
|
148
|
+
return (_jsx("div", { className: "w-48", children: _jsx(RelationSelect, { lookupPath: filter.relationPath, value: value || null, onChange: (v) => onChange({ [filter.key]: v ?? "" }), placeholder: t("list.filterSearch", { filter: filter.label.toLowerCase() }), emptyLabel: t("list.filterAll", { filter: filter.label }), compact: true }) }));
|
|
149
|
+
}
|
|
150
|
+
if (filter.type === "range")
|
|
151
|
+
return null;
|
|
152
|
+
return (_jsxs(Select, { "aria-label": filter.label, value: value, onChange: (e) => onChange({ [filter.key]: e.target.value }), className: "h-8 !w-auto py-0 text-xs", children: [_jsx("option", { value: "", children: t("list.filterAll", { filter: filter.label }) }), filter.type === "boolean" ? (_jsxs(_Fragment, { children: [_jsx("option", { value: "true", children: t("common.yes") }), _jsx("option", { value: "false", children: t("common.no") })] })) : (filter.options?.map((o) => (_jsx("option", { value: o.value, children: o.label }, o.value))))] }));
|
|
153
|
+
}
|
|
154
|
+
//# sourceMappingURL=resource-list.js.map
|