@everystack/admin 0.4.0 → 0.4.2

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/CHANGELOG.md ADDED
@@ -0,0 +1,77 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@everystack/admin` are documented here.
4
+
5
+ Format based on [Keep a Changelog](https://keepachangelog.com/).
6
+
7
+ ## [Unreleased]
8
+
9
+ ### Added
10
+
11
+ - auto-generated changelogs from conventional commits
12
+ - automate the version-drift check — a manual note is not a gate
13
+
14
+ ### Fixed
15
+
16
+ - the test suite compiled with type checking off
17
+
18
+ ## [0.4.0] - 2026-07-19
19
+
20
+ ### Added
21
+
22
+ - tx-contract dashboard RPCs + role-scoped jobs adapter
23
+
24
+ ### Fixed
25
+
26
+ - serve the jobs list via jobsEventList RPC, not the shadowed REST table
27
+ - correct sidebar hierarchy emphasis
28
+ - camelCase listRpc rows so snake_case columns render
29
+ - pin React to a single 19.2.0 across the monorepo
30
+
31
+ ## [0.2.6] - 2026-06-18
32
+
33
+ ### Fixed
34
+
35
+ - audit-log detail view (Show) + list-only row-nav guard
36
+
37
+ ## [0.2.5] - 2026-06-17
38
+
39
+ ### Added
40
+
41
+ - physical tableName + static where for resource dashboards
42
+ - RPC-backed row actions on the show view
43
+ - @everystack/jobs/admin — config, health RPC, retry/cancel RPCs
44
+ - RPC-backed row detail — telemetry never enters REST
45
+ - extensibility, unified view, observability + Track B rollup
46
+ - DB-free Logs + Crashes row lists via S3 list RPCs
47
+ - menu toggle by title, breadcrumbs, accordion nav
48
+ - DB-free row detail (Show) for log events and crashes over S3
49
+ - field-fold (sum/avg) stat dials for artifact dashboards
50
+ - DB-free alerting — artifact-backed evaluation over S3, no Postgres
51
+ - config-driven ResourceDashboard for all tables
52
+ - wire image upload into field renderers with full S3 loop
53
+ - Crashes dashboard header reads the issues rollup artifact
54
+
55
+ ### Fixed
56
+
57
+ - Issues RPC auth, Health screen, Raw Data navigation, D3 deps
58
+ - pin all dependency versions to lockfile-resolved values
59
+ - dashboards size charts to the container, not the window
60
+ - CRUD tables fill the panel and format cells by field type
61
+ - coalesce list fetches — one request per load, not two
62
+ - serve DB-free row detail via the existing EventDetail RPC convention
63
+
64
+ ### Changed
65
+
66
+ - split expo-stack into discrete packages and remove @jest/globals imports
67
+ - drop type module from all packages, switch Jest to CJS mode
68
+ - consolidate Jest configs into shared base
69
+ - one chart vocabulary in @everystack/ui
70
+
71
+ ### Documentation
72
+
73
+ - rewrite documentation around V1→V2→V3 progressive complexity model
74
+ - prepare documentation for open-source release
75
+ - definition of done for the branch (8 requirements mapped)
76
+ - mark Track B engine done (write + read, 70 tests, no DB)
77
+ - Track B rollup LIVE-VERIFIED on dev (verify:rollup green)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@everystack/admin",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Declarative admin dashboard framework for PostgREST APIs",
5
5
  "license": "AGPL-3.0-only",
6
6
  "author": "Scalable Technology, Inc. <licensing@scalable.technology>",
@@ -18,22 +18,27 @@
18
18
  },
19
19
  "files": [
20
20
  "src",
21
- "README.md"
21
+ "README.md",
22
+ "CHANGELOG.md"
22
23
  ],
23
24
  "exports": {
24
25
  ".": {
26
+ "source": "./src/index.ts",
25
27
  "types": "./src/index.ts",
26
28
  "default": "./src/index.ts"
27
29
  },
28
30
  "./rpc": {
31
+ "source": "./src/rpc/resource-dashboard-rpc.ts",
29
32
  "types": "./src/rpc/resource-dashboard-rpc.ts",
30
33
  "default": "./src/rpc/resource-dashboard-rpc.ts"
31
34
  },
32
35
  "./presets/twitter": {
36
+ "source": "./src/presets/twitter.ts",
33
37
  "types": "./src/presets/twitter.ts",
34
38
  "default": "./src/presets/twitter.ts"
35
39
  },
36
40
  "./editors/tiptap": {
41
+ "source": "./src/editors/tiptap.tsx",
37
42
  "types": "./src/editors/tiptap.tsx",
38
43
  "default": "./src/editors/tiptap.tsx"
39
44
  }
@@ -43,6 +48,7 @@
43
48
  "d3-time-format": "4.1.0"
44
49
  },
45
50
  "peerDependencies": {
51
+ "drizzle-orm": "0.41.0",
46
52
  "@everystack/api": ">=0.2.4",
47
53
  "@everystack/ui": ">=0.2.2",
48
54
  "@tiptap/pm": ">=2.0.0",
@@ -74,10 +80,11 @@
74
80
  "jest": "29.7.0",
75
81
  "react": "19.2.0",
76
82
  "react-native": "0.83.6",
83
+ "react-test-renderer": "19.2.0",
77
84
  "ts-jest": "29.4.9",
78
85
  "typescript": "5.9.3",
79
- "@everystack/ui": "0.4.0",
80
- "@everystack/api": "0.4.0"
86
+ "@everystack/api": "0.4.2",
87
+ "@everystack/ui": "0.4.1"
81
88
  },
82
89
  "scripts": {
83
90
  "test": "jest",
@@ -23,6 +23,15 @@ const POSTGREST_OPERATORS = new Set([
23
23
  'sl', 'sr', 'nxl', 'nxr', 'adj', 'ov', 'fts',
24
24
  ]);
25
25
 
26
+ /**
27
+ * `onTokenExpired` and `onAuthFailure` (inherited from `ClientOptions`) are how
28
+ * the admin survives a session longer than the access token's TTL. Without
29
+ * `onTokenExpired`, `getToken` keeps handing every request the same dead token
30
+ * forever — the user stays "logged in" while every screen 401s. `onAuthFailure`
31
+ * fires only once refresh itself is unrecoverable (refresh token also
32
+ * expired/revoked) — wire it to sign the user out for real. See
33
+ * `@everystack/auth/api`'s `refreshAuthToken` for a ready-made `onTokenExpired`.
34
+ */
26
35
  export interface EverystackAdapterOptions extends ClientOptions {
27
36
  /**
28
37
  * Resources whose list rows come from an RPC instead of the PostgREST table.
@@ -7,7 +7,7 @@
7
7
  * pnpm add @tiptap/react @tiptap/starter-kit @tiptap/pm
8
8
  * import { TiptapEditor } from '@everystack/admin/editors/tiptap';
9
9
  * <AdminShell editors={{ tiptap: TiptapEditor }} ... />
10
- * // field: { name: 'body', type: 'html', editor: 'tiptap' }
10
+ * // field: { name: 'body', type: 'string', editor: 'tiptap', format: 'html' }
11
11
  *
12
12
  * Reads and writes an HTML string via the EditorProps contract. The toolbar
13
13
  * uses React Native primitives; the editable surface is tiptap's EditorContent
@@ -39,14 +39,14 @@ const profilesResource: ResourceConfig = {
39
39
  fields: [
40
40
  { name: 'id', type: 'string', label: 'ID' },
41
41
  { name: 'username', type: 'string', label: 'Username', sortable: true, searchable: true },
42
- { name: 'display_name', type: 'string', label: 'Display Name', sortable: true, searchable: true },
42
+ { name: 'displayName', type: 'string', label: 'Display Name', sortable: true, searchable: true },
43
43
  { name: 'bio', type: 'string', label: 'Bio' },
44
- { name: 'avatar_url', type: 'image', label: 'Avatar', renderer: 'avatar' },
45
- { name: 'created_at', type: 'datetime', label: 'Joined', sortable: true },
44
+ { name: 'avatarUrl', type: 'image', label: 'Avatar', renderer: 'avatar' },
45
+ { name: 'createdAt', type: 'datetime', label: 'Joined', sortable: true },
46
46
  ],
47
- listFields: ['username', 'display_name', 'bio', 'created_at'],
48
- editFields: ['username', 'display_name', 'bio', 'avatar_url'],
49
- defaultSort: { field: 'created_at', order: 'desc' },
47
+ listFields: ['username', 'displayName', 'bio', 'createdAt'],
48
+ editFields: ['username', 'displayName', 'bio', 'avatarUrl'],
49
+ defaultSort: { field: 'createdAt', order: 'desc' },
50
50
  dashboard: profilesDashboard,
51
51
  };
52
52
 
@@ -76,16 +76,16 @@ const postsResource: ResourceConfig = {
76
76
  fields: [
77
77
  { name: 'id', type: 'string', label: 'ID' },
78
78
  {
79
- name: 'author_id',
79
+ name: 'authorId',
80
80
  type: 'relation',
81
81
  label: 'Author',
82
82
  relation: { resource: 'profiles', field: 'id', displayField: 'username' },
83
83
  },
84
84
  { name: 'body', type: 'string', label: 'Body', searchable: true },
85
- { name: 'created_at', type: 'datetime', label: 'Posted', sortable: true },
85
+ { name: 'createdAt', type: 'datetime', label: 'Posted', sortable: true },
86
86
  ],
87
- listFields: ['author_id', 'body', 'created_at'],
88
- defaultSort: { field: 'created_at', order: 'desc' },
87
+ listFields: ['authorId', 'body', 'createdAt'],
88
+ defaultSort: { field: 'createdAt', order: 'desc' },
89
89
  dashboard: postsDashboard,
90
90
  };
91
91
 
@@ -110,25 +110,25 @@ const likesResource: ResourceConfig = {
110
110
  label: 'Like',
111
111
  labelPlural: 'Likes',
112
112
  icon: 'heart',
113
- primaryKey: 'user_id',
113
+ primaryKey: 'userId',
114
114
  actions: { list: true, show: true, create: true, edit: false, delete: true },
115
115
  fields: [
116
116
  {
117
- name: 'user_id',
117
+ name: 'userId',
118
118
  type: 'relation',
119
119
  label: 'User',
120
120
  relation: { resource: 'profiles', field: 'id', displayField: 'username' },
121
121
  },
122
122
  {
123
- name: 'post_id',
123
+ name: 'postId',
124
124
  type: 'relation',
125
125
  label: 'Post',
126
126
  relation: { resource: 'posts', field: 'id' },
127
127
  },
128
- { name: 'created_at', type: 'datetime', label: 'Liked At', sortable: true },
128
+ { name: 'createdAt', type: 'datetime', label: 'Liked At', sortable: true },
129
129
  ],
130
- listFields: ['user_id', 'post_id', 'created_at'],
131
- defaultSort: { field: 'created_at', order: 'desc' },
130
+ listFields: ['userId', 'postId', 'createdAt'],
131
+ defaultSort: { field: 'createdAt', order: 'desc' },
132
132
  dashboard: likesDashboard,
133
133
  };
134
134
 
@@ -153,25 +153,25 @@ const followsResource: ResourceConfig = {
153
153
  label: 'Follow',
154
154
  labelPlural: 'Follows',
155
155
  icon: 'people',
156
- primaryKey: 'follower_id',
156
+ primaryKey: 'followerId',
157
157
  actions: { list: true, show: true, create: true, edit: false, delete: true },
158
158
  fields: [
159
159
  {
160
- name: 'follower_id',
160
+ name: 'followerId',
161
161
  type: 'relation',
162
162
  label: 'Follower',
163
163
  relation: { resource: 'profiles', field: 'id', displayField: 'username' },
164
164
  },
165
165
  {
166
- name: 'following_id',
166
+ name: 'followingId',
167
167
  type: 'relation',
168
168
  label: 'Following',
169
169
  relation: { resource: 'profiles', field: 'id', displayField: 'username' },
170
170
  },
171
- { name: 'created_at', type: 'datetime', label: 'Followed At', sortable: true },
171
+ { name: 'createdAt', type: 'datetime', label: 'Followed At', sortable: true },
172
172
  ],
173
- listFields: ['follower_id', 'following_id', 'created_at'],
174
- defaultSort: { field: 'created_at', order: 'desc' },
173
+ listFields: ['followerId', 'followingId', 'createdAt'],
174
+ defaultSort: { field: 'createdAt', order: 'desc' },
175
175
  dashboard: followsDashboard,
176
176
  };
177
177
 
@@ -11,7 +11,8 @@ import { useAdminConfig } from '../hooks/useAdminConfig';
11
11
  import { useDataAdapter } from '../hooks/useAdminData';
12
12
  import { useEditors } from '../hooks/useEditors';
13
13
  import { editorFor } from '../utils/render-resolution';
14
- import { FormField, TextInput, SelectInput, SwitchInput, Button, ErrorState } from '@everystack/ui';
14
+ import { toDateInputValue } from '../utils/format-cell-value';
15
+ import { FormField, TextInput, SelectInput, SwitchInput, DateInput, Button, ErrorState } from '@everystack/ui';
15
16
  import { ImageFieldInput } from '../components/ImageFieldInput';
16
17
  import { AvatarFieldInput } from '../components/AvatarFieldInput';
17
18
  import type { FieldDefinition } from '../types';
@@ -173,6 +174,20 @@ function FieldInput({ field, value, error, onChange }: FieldInputProps): React.R
173
174
  </FormField>
174
175
  );
175
176
 
177
+ case 'date':
178
+ return (
179
+ <FormField label={label} error={error} required={required}>
180
+ <DateInput value={toDateInputValue(value)} onValueChange={onChange} granularity="date" />
181
+ </FormField>
182
+ );
183
+
184
+ case 'datetime':
185
+ return (
186
+ <FormField label={label} error={error} required={required}>
187
+ <DateInput value={toDateInputValue(value)} onValueChange={onChange} granularity="datetime" />
188
+ </FormField>
189
+ );
190
+
176
191
  case 'number':
177
192
  return (
178
193
  <FormField label={label} error={error} required={required}>
@@ -11,7 +11,8 @@ import { useAdminConfig } from '../hooks/useAdminConfig';
11
11
  import { useDataAdapter } from '../hooks/useAdminData';
12
12
  import { useEditors } from '../hooks/useEditors';
13
13
  import { editorFor } from '../utils/render-resolution';
14
- import { FormField, TextInput, SelectInput, SwitchInput, Button, LoadingState, ErrorState } from '@everystack/ui';
14
+ import { toDateInputValue } from '../utils/format-cell-value';
15
+ import { FormField, TextInput, SelectInput, SwitchInput, DateInput, Button, LoadingState, ErrorState } from '@everystack/ui';
15
16
  import { ImageFieldInput } from '../components/ImageFieldInput';
16
17
  import { AvatarFieldInput } from '../components/AvatarFieldInput';
17
18
  import type { FieldDefinition, DataRecord } from '../types';
@@ -215,6 +216,20 @@ function EditFieldInput({ field, value, error, onChange, record }: EditFieldInpu
215
216
  </FormField>
216
217
  );
217
218
 
219
+ case 'date':
220
+ return (
221
+ <FormField label={label} error={error} required={required}>
222
+ <DateInput value={toDateInputValue(value)} onValueChange={onChange} granularity="date" />
223
+ </FormField>
224
+ );
225
+
226
+ case 'datetime':
227
+ return (
228
+ <FormField label={label} error={error} required={required}>
229
+ <DateInput value={toDateInputValue(value)} onValueChange={onChange} granularity="datetime" />
230
+ </FormField>
231
+ );
232
+
218
233
  case 'number':
219
234
  return (
220
235
  <FormField label={label} error={error} required={required}>
@@ -14,6 +14,7 @@ import { ImageDisplay } from '../components/ImageDisplay';
14
14
  import { RowActionButton } from '../components/RowActionButton';
15
15
  import { visibleRowActions } from '../utils/row-actions';
16
16
  import { toCamelCase } from '../utils/naming';
17
+ import { formatDateOnly } from '../utils/format-cell-value';
17
18
  import type { FieldDefinition, DataRecord, ResourceConfig } from '../types';
18
19
  import type { DataAdapter } from '../adapters/types';
19
20
 
@@ -194,9 +195,11 @@ function formatFieldValue(value: unknown, field: FieldDefinition): string {
194
195
  case 'boolean':
195
196
  return value ? 'Yes' : 'No';
196
197
  case 'date':
197
- return new Date(String(value)).toLocaleDateString();
198
- case 'datetime':
199
- return new Date(String(value)).toLocaleString();
198
+ return formatDateOnly(value);
199
+ case 'datetime': {
200
+ const date = new Date(String(value));
201
+ return Number.isNaN(date.getTime()) ? String(value) : date.toLocaleString();
202
+ }
200
203
  case 'json':
201
204
  return JSON.stringify(value, null, 2);
202
205
  case 'url':
@@ -8,6 +8,47 @@
8
8
 
9
9
  import type { FieldDefinition } from '../types';
10
10
 
11
+ /**
12
+ * A date-only ISO string has no time-of-day; `new Date(str)` parses it as UTC midnight, and
13
+ * `toLocaleDateString()` then converts that back to the viewer's local time -- which rolls the
14
+ * displayed day back one in any UTC-negative timezone. Parses the Y-M-D digits directly and
15
+ * builds a LOCAL date from them instead (same approach DateInput uses for editing). A native
16
+ * Date value (a custom DataAdapter's prerogative -- the adapter interface is schema-agnostic)
17
+ * is read via its own local getters, not restringified. Range-invalid digits (e.g. month 13)
18
+ * would silently normalize to a different, wrong calendar date rather than error -- verified
19
+ * against the ORIGINAL Y/M/D instead of trusted blindly, falling back to the raw value like any
20
+ * other unparseable input.
21
+ */
22
+ export function formatDateOnly(value: unknown): string {
23
+ if (value instanceof Date) {
24
+ if (Number.isNaN(value.getTime())) return String(value);
25
+ return new Date(value.getFullYear(), value.getMonth(), value.getDate()).toLocaleDateString();
26
+ }
27
+ const str = String(value);
28
+ const match = /^(\d{4})-(\d{2})-(\d{2})/.exec(str);
29
+ if (!match) return str;
30
+ const [, y, m, d] = match;
31
+ const year = Number(y);
32
+ const month = Number(m) - 1;
33
+ const day = Number(d);
34
+ const date = new Date(year, month, day);
35
+ if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) {
36
+ return str;
37
+ }
38
+ return date.toLocaleDateString();
39
+ }
40
+
41
+ /**
42
+ * Coerce a field value to the string DateInput expects. A native Date value (a custom
43
+ * DataAdapter's prerogative) must go through `toISOString()`, not the default `String(value)` —
44
+ * `Date#toString()` ("Sun Sep 22 2026 00:00:00 GMT...") doesn't match DateInput's ISO parser, so
45
+ * a populated field would silently render as if it were unset.
46
+ */
47
+ export function toDateInputValue(value: unknown): string {
48
+ if (value instanceof Date) return value.toISOString();
49
+ return value == null ? '' : String(value);
50
+ }
51
+
11
52
  export function formatCellValue(field: FieldDefinition, value: unknown): string {
12
53
  if (value === null || value === undefined) return '—';
13
54
 
@@ -16,10 +57,8 @@ export function formatCellValue(field: FieldDefinition, value: unknown): string
16
57
  const date = new Date(String(value));
17
58
  return Number.isNaN(date.getTime()) ? String(value) : date.toLocaleString();
18
59
  }
19
- case 'date': {
20
- const date = new Date(String(value));
21
- return Number.isNaN(date.getTime()) ? String(value) : date.toLocaleDateString();
22
- }
60
+ case 'date':
61
+ return formatDateOnly(value);
23
62
  case 'boolean':
24
63
  return value ? 'Yes' : 'No';
25
64
  case 'json':