@everystack/admin 0.4.0 → 0.4.1
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 +77 -0
- package/package.json +11 -4
- package/src/editors/tiptap.tsx +1 -1
- package/src/presets/twitter.ts +22 -22
- package/src/screens/ResourceCreate.tsx +16 -1
- package/src/screens/ResourceEdit.tsx +16 -1
- package/src/screens/ResourceShow.tsx +6 -3
- package/src/utils/format-cell-value.ts +43 -4
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.
|
|
3
|
+
"version": "0.4.1",
|
|
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/
|
|
80
|
-
"@everystack/
|
|
86
|
+
"@everystack/api": "0.4.1",
|
|
87
|
+
"@everystack/ui": "0.4.1"
|
|
81
88
|
},
|
|
82
89
|
"scripts": {
|
|
83
90
|
"test": "jest",
|
package/src/editors/tiptap.tsx
CHANGED
|
@@ -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: '
|
|
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
|
package/src/presets/twitter.ts
CHANGED
|
@@ -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: '
|
|
42
|
+
{ name: 'displayName', type: 'string', label: 'Display Name', sortable: true, searchable: true },
|
|
43
43
|
{ name: 'bio', type: 'string', label: 'Bio' },
|
|
44
|
-
{ name: '
|
|
45
|
-
{ name: '
|
|
44
|
+
{ name: 'avatarUrl', type: 'image', label: 'Avatar', renderer: 'avatar' },
|
|
45
|
+
{ name: 'createdAt', type: 'datetime', label: 'Joined', sortable: true },
|
|
46
46
|
],
|
|
47
|
-
listFields: ['username', '
|
|
48
|
-
editFields: ['username', '
|
|
49
|
-
defaultSort: { field: '
|
|
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: '
|
|
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: '
|
|
85
|
+
{ name: 'createdAt', type: 'datetime', label: 'Posted', sortable: true },
|
|
86
86
|
],
|
|
87
|
-
listFields: ['
|
|
88
|
-
defaultSort: { field: '
|
|
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: '
|
|
113
|
+
primaryKey: 'userId',
|
|
114
114
|
actions: { list: true, show: true, create: true, edit: false, delete: true },
|
|
115
115
|
fields: [
|
|
116
116
|
{
|
|
117
|
-
name: '
|
|
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: '
|
|
123
|
+
name: 'postId',
|
|
124
124
|
type: 'relation',
|
|
125
125
|
label: 'Post',
|
|
126
126
|
relation: { resource: 'posts', field: 'id' },
|
|
127
127
|
},
|
|
128
|
-
{ name: '
|
|
128
|
+
{ name: 'createdAt', type: 'datetime', label: 'Liked At', sortable: true },
|
|
129
129
|
],
|
|
130
|
-
listFields: ['
|
|
131
|
-
defaultSort: { field: '
|
|
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: '
|
|
156
|
+
primaryKey: 'followerId',
|
|
157
157
|
actions: { list: true, show: true, create: true, edit: false, delete: true },
|
|
158
158
|
fields: [
|
|
159
159
|
{
|
|
160
|
-
name: '
|
|
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: '
|
|
166
|
+
name: 'followingId',
|
|
167
167
|
type: 'relation',
|
|
168
168
|
label: 'Following',
|
|
169
169
|
relation: { resource: 'profiles', field: 'id', displayField: 'username' },
|
|
170
170
|
},
|
|
171
|
-
{ name: '
|
|
171
|
+
{ name: 'createdAt', type: 'datetime', label: 'Followed At', sortable: true },
|
|
172
172
|
],
|
|
173
|
-
listFields: ['
|
|
174
|
-
defaultSort: { field: '
|
|
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 {
|
|
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 {
|
|
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
|
|
198
|
-
case 'datetime':
|
|
199
|
-
|
|
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
|
-
|
|
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':
|