@ultimat3/admin 1.2.0 → 3.0.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/CLAUDE.md +52 -0
- package/README.md +117 -3
- package/package.json +17 -14
- package/src/admin.ts +35 -6
- package/src/crud.ts +33 -4
- package/src/detail.tsx +4 -1
- package/src/dev/data.ts +69 -7
- package/src/dev/facts.ts +48 -1
- package/src/dev/index.ts +3 -0
- package/src/dev/panel-cache.ts +2 -2
- package/src/dev/panel-db.ts +67 -12
- package/src/dev/panel-jobs.ts +15 -4
- package/src/dev/panel-live.ts +20 -5
- package/src/dev/panel-mail.ts +2 -2
- package/src/dev/panel-manifest.ts +2 -2
- package/src/dev/panel-policy.ts +2 -2
- package/src/dev/panel-routes.ts +2 -2
- package/src/dev/panel-timeline.ts +26 -5
- package/src/dev/panel.ts +7 -3
- package/src/dev/server.ts +21 -10
- package/src/errors.ts +45 -2
- package/src/fields.ts +42 -3
- package/src/index.ts +17 -1
- package/src/mcp-tools.ts +25 -2
- package/src/mcp.ts +16 -11
- package/src/nav.ts +10 -0
- package/src/page-guard.tsx +58 -0
- package/src/pages.ts +117 -0
- package/src/pagination.ts +29 -5
- package/src/registry.ts +6 -0
- package/src/resource.ts +1 -1
- package/src/routes.ts +64 -6
- package/src/search.ts +29 -18
- package/src/widget-value.ts +19 -3
- package/src/widgets.tsx +21 -12
package/src/widgets.tsx
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// switch, so money is a Money widget and a timestamp is a DateTime widget with a zone in
|
|
3
3
|
// every one of those places — there is no second place to get it wrong.
|
|
4
4
|
|
|
5
|
+
import { safeUrl } from '@ultimat3/core';
|
|
5
6
|
import { t } from '@ultimat3/i18n';
|
|
6
7
|
import {
|
|
7
8
|
Checkbox,
|
|
@@ -53,10 +54,17 @@ const wiring = (control: FieldControl | undefined): ControlWiring =>
|
|
|
53
54
|
/**
|
|
54
55
|
* A `date` column has no time of day, and ui's default formatter always renders one. A
|
|
55
56
|
* rendered midnight is wrong in every zone but the one the value was stored in.
|
|
57
|
+
*
|
|
58
|
+
* `'UTC'`, never `options.zone`: a calendar date is zone-INDEPENDENT by construction — the value
|
|
59
|
+
* is `YYYY-MM-DD` and the spec parses it as UTC midnight (`date-time-view.ts` says so where it
|
|
60
|
+
* refuses an offsetless date-TIME). Formatting that instant in the viewer's zone moves it: an
|
|
61
|
+
* `effective_on` of `2026-08-18` read as `Aug 17, 2026` for every operator west of Greenwich,
|
|
62
|
+
* which is the exact bug `date()` exists to prevent — measured under `TZ=America/Los_Angeles`.
|
|
63
|
+
* The instant branch keeps `options.zone`, because an instant genuinely has one.
|
|
56
64
|
*/
|
|
57
|
-
const
|
|
65
|
+
export const formatCalendarDate: DateTimeFormatter = (at, options) =>
|
|
58
66
|
new Intl.DateTimeFormat(options.locale, {
|
|
59
|
-
timeZone:
|
|
67
|
+
timeZone: 'UTC',
|
|
60
68
|
dateStyle: 'medium',
|
|
61
69
|
}).format(at);
|
|
62
70
|
|
|
@@ -65,7 +73,7 @@ const inputValueFor = (iso: string, precision: 'date' | 'instant'): string =>
|
|
|
65
73
|
iso.slice(0, precision === 'date' ? 10 : 16);
|
|
66
74
|
|
|
67
75
|
/** Read-mode rendering of already-guarded props. Never formats; the widgets do that. */
|
|
68
|
-
function readView(props: WidgetProps, field: AdminField): JSX.Element {
|
|
76
|
+
function readView(props: WidgetProps, field: AdminField, ctx: WidgetContext): JSX.Element {
|
|
69
77
|
switch (props.widget) {
|
|
70
78
|
case 'money':
|
|
71
79
|
return props.value === null ? (
|
|
@@ -80,7 +88,7 @@ function readView(props: WidgetProps, field: AdminField): JSX.Element {
|
|
|
80
88
|
<DateTime
|
|
81
89
|
value={props.value}
|
|
82
90
|
timeZone={props.timeZone}
|
|
83
|
-
format={props.precision === 'date' ?
|
|
91
|
+
format={props.precision === 'date' ? formatCalendarDate : undefined}
|
|
84
92
|
/>
|
|
85
93
|
);
|
|
86
94
|
case 'checkbox':
|
|
@@ -95,17 +103,18 @@ function readView(props: WidgetProps, field: AdminField): JSX.Element {
|
|
|
95
103
|
);
|
|
96
104
|
case 'json-editor':
|
|
97
105
|
return <pre class="x-admin-json">{props.value}</pre>;
|
|
98
|
-
case 'reference':
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
106
|
+
case 'reference': {
|
|
107
|
+
if (props.value === null) return <span>{t('admin.value.empty')}</span>;
|
|
108
|
+
// `ctx.hrefFor` or nothing. The route table lives on `AdminApp`; this file only ever knew
|
|
109
|
+
// the target entity's NAME, and turning that into a URL by appending an `s` is a guess.
|
|
110
|
+
const href = ctx.hrefFor?.(props.entity, props.value) ?? null;
|
|
111
|
+
return href === null ? <span>{props.value}</span> : <a href={href}>{props.value}</a>;
|
|
112
|
+
}
|
|
104
113
|
case 'upload':
|
|
105
114
|
return props.value === null ? (
|
|
106
115
|
<span>{t('admin.value.empty')}</span>
|
|
107
116
|
) : (
|
|
108
|
-
<a href={props.value.url}>{props.value.name}</a>
|
|
117
|
+
<a href={safeUrl(props.value.url, 'href') ?? undefined}>{props.value.name}</a>
|
|
109
118
|
);
|
|
110
119
|
default:
|
|
111
120
|
return <span>{String(props.value ?? '')}</span>;
|
|
@@ -258,5 +267,5 @@ function locales(): readonly string[] {
|
|
|
258
267
|
|
|
259
268
|
export function Widget(input: WidgetInput): JSX.Element {
|
|
260
269
|
const props = widgetProps(input.field, input.value, input.ctx);
|
|
261
|
-
return input.mode === 'read' ? readView(props, input.field) : editView(props, input);
|
|
270
|
+
return input.mode === 'read' ? readView(props, input.field, input.ctx) : editView(props, input);
|
|
262
271
|
}
|