@rebasepro/app 0.14.0 → 0.14.1-canary.g7e666eb
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/dist/hooks/data/collectionQuery.d.ts +6 -6
- package/dist/index.es.js +19 -15
- package/dist/index.es.js.map +1 -1
- package/package.json +8 -8
- package/src/components/Debug/UIReferenceView.tsx +8 -1
- package/src/components/Debug/UIStyleGuide.tsx +4 -1
- package/src/components/Debug/crm-dashboard/CrmDashboardDemo.tsx +5 -1
- package/src/components/common/useDataTableController.tsx +40 -18
- package/src/hooks/data/collectionQuery.ts +6 -6
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/app",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.14.
|
|
4
|
+
"version": "0.14.1-canary.g7e666eb",
|
|
5
5
|
"description": "Rebase core — framework-agnostic runtime for data-driven admin panels",
|
|
6
6
|
"funding": {
|
|
7
7
|
"url": "https://github.com/sponsors/rebaseco"
|
|
@@ -52,12 +52,12 @@
|
|
|
52
52
|
"magic-string": "^0.30.0",
|
|
53
53
|
"notistack": "^3.0.2",
|
|
54
54
|
"react-i18next": "^17.0.11",
|
|
55
|
-
"@rebasepro/admin-types": "0.14.
|
|
56
|
-
"@rebasepro/
|
|
57
|
-
"@rebasepro/
|
|
58
|
-
"@rebasepro/
|
|
59
|
-
"@rebasepro/
|
|
60
|
-
"@rebasepro/utils": "0.14.
|
|
55
|
+
"@rebasepro/admin-types": "0.14.1-canary.g7e666eb",
|
|
56
|
+
"@rebasepro/forms": "0.14.1-canary.g7e666eb",
|
|
57
|
+
"@rebasepro/common": "0.14.1-canary.g7e666eb",
|
|
58
|
+
"@rebasepro/ui": "0.14.1-canary.g7e666eb",
|
|
59
|
+
"@rebasepro/types": "0.14.1-canary.g7e666eb",
|
|
60
|
+
"@rebasepro/utils": "0.14.1-canary.g7e666eb"
|
|
61
61
|
},
|
|
62
62
|
"peerDependencies": {
|
|
63
63
|
"react": ">=19.2.7",
|
|
@@ -138,7 +138,7 @@
|
|
|
138
138
|
"build": "vite build && tsc --emitDeclarationOnly -p tsconfig.prod.json && pnpm exec esbuild src/vitePlugin.ts --platform=node --format=esm --outfile=dist/vitePlugin.js && node ../../scripts/assert-build-output.mjs",
|
|
139
139
|
"test:lint": "eslint \"src/**\" --quiet",
|
|
140
140
|
"test": "jest --passWithNoTests",
|
|
141
|
-
"clean": "rm -rf dist && find ./src -name '*.js' -type f | xargs rm -f",
|
|
141
|
+
"clean": "rm -rf dist && find ./src \\( -name '*.js' -o -name '*.d.ts' \\) -type f | xargs rm -f",
|
|
142
142
|
"generateIcons": "ts-node --esm src/icons/generateIcons.ts"
|
|
143
143
|
}
|
|
144
144
|
}
|
|
@@ -602,7 +602,14 @@ selected: true }, { name: "Tags" }].map(c => (
|
|
|
602
602
|
{(["h1", "h2", "h3", "h4", "h5", "h6", "subtitle1", "subtitle2", "body1", "body2", "caption", "label", "button"] as const).map(v => (
|
|
603
603
|
<div key={v} className={cls("flex items-baseline gap-4 border-b pb-3 last:border-0", defaultBorderMixin)}>
|
|
604
604
|
<span className="w-24 shrink-0 text-xs text-surface-400 font-mono">{v}</span>
|
|
605
|
-
|
|
605
|
+
{/* `component="p"` keeps the variant's styling but not its
|
|
606
|
+
element. Rendering the specimen with its native tag put a
|
|
607
|
+
real <h1> — "The quick brown fox jumps over the lazy dog" —
|
|
608
|
+
plus h2-h6 into the document outline of every page that
|
|
609
|
+
embeds this view, including the public rebase.pro/ui gallery,
|
|
610
|
+
where it competed with the page's actual heading. The class
|
|
611
|
+
carries the type, so this renders identically. */}
|
|
612
|
+
<Typography variant={v} component="p">The quick brown fox jumps over the lazy dog</Typography>
|
|
606
613
|
</div>
|
|
607
614
|
))}
|
|
608
615
|
</div>
|
|
@@ -34,7 +34,10 @@ export const UIStyleGuide = () => {
|
|
|
34
34
|
{typographyVariants.map(variant => (
|
|
35
35
|
<div key={variant} className="flex items-center gap-4 border-b border-surface-100 dark:border-surface-950 pb-4 last:border-0">
|
|
36
36
|
<span className="w-24 text-[10px] uppercase tracking-wider text-text-disabled font-mono">{variant}</span>
|
|
37
|
-
|
|
37
|
+
{/* component="p" keeps the variant's styling without emitting a
|
|
38
|
+
real h1-h6 into the host page's document outline. See the
|
|
39
|
+
matching note in UIReferenceView. */}
|
|
40
|
+
<Typography variant={variant} component="p">
|
|
38
41
|
The quick brown fox jumps over the lazy dog ({variant})
|
|
39
42
|
</Typography>
|
|
40
43
|
</div>
|
|
@@ -944,7 +944,11 @@ export function CrmDashboardDemo() {
|
|
|
944
944
|
<div className="flex flex-col sm:flex-row sm:items-end justify-between mb-6 gap-4">
|
|
945
945
|
<div>
|
|
946
946
|
<Typography variant="h4" className="tracking-tight">
|
|
947
|
-
{
|
|
947
|
+
{/* No emoji: this demo renders inside the public rebase.pro/ui
|
|
948
|
+
gallery, and emoji render differently on every platform,
|
|
949
|
+
cannot be recoloured, and read as a placeholder for a design
|
|
950
|
+
decision nobody made. The greeting carries itself. */}
|
|
951
|
+
{getGreeting()}
|
|
948
952
|
</Typography>
|
|
949
953
|
<Typography variant="body2" color="secondary" className="mt-0.5">
|
|
950
954
|
{getFormattedDate()} — here's your crm overview.
|
|
@@ -7,7 +7,8 @@ import { useDataOrder } from "../../hooks/data/useDataOrder";
|
|
|
7
7
|
import { populateFetchCache } from "../../hooks/data/useFetch";
|
|
8
8
|
import { toFindParams } from "../../hooks/data/collectionQuery";
|
|
9
9
|
import { getRelationIncludeParams } from "../../util/previews";
|
|
10
|
-
import { Entity, EntityReference, EntityRelation, FilterValues, User, WhereFilterOp, FindResponse } from "@rebasepro/types";
|
|
10
|
+
import { Entity, EntityReference, EntityRelation, FilterValues, OrderBySpec, OrderByTuple, User, WhereFilterOp, FindResponse } from "@rebasepro/types";
|
|
11
|
+
import { normalizeOrderBy, serializeOrderBy } from "@rebasepro/common";
|
|
11
12
|
import { EntityTableController, RebaseContext, SelectedCellProps, AdminCollection } from "@rebasepro/admin-types";
|
|
12
13
|
import { ScrollRestorationController } from "./useScrollRestoration";
|
|
13
14
|
|
|
@@ -96,17 +97,21 @@ export function useDataTableController<M extends Record<string, any> = any, USER
|
|
|
96
97
|
});
|
|
97
98
|
|
|
98
99
|
const checkFilterCombination = useCallback((filterValues: FilterValues<any>,
|
|
99
|
-
sortBy?: [
|
|
100
|
+
sortBy?: OrderByTuple[]) => {
|
|
100
101
|
// PostgREST/SQL can handle arbitrary filter/sort combinations natively.
|
|
101
102
|
return true;
|
|
102
103
|
}, []);
|
|
103
104
|
|
|
105
|
+
// The collection's default, in the list form the controller works in.
|
|
106
|
+
// `collection.sort` accepts either spelling so a one-key default stays the
|
|
107
|
+
// one-liner it always was.
|
|
104
108
|
const sortInternal = useMemo(() => {
|
|
105
|
-
|
|
109
|
+
const keys = normalizeOrderBy(sort as OrderBySpec | undefined);
|
|
110
|
+
if (keys && fixedFilter && !checkFilterCombination(fixedFilter, keys)) {
|
|
106
111
|
console.warn("Initial sort is not compatible with the force filter. Ignoring initial sort");
|
|
107
112
|
return undefined;
|
|
108
113
|
}
|
|
109
|
-
return
|
|
114
|
+
return keys;
|
|
110
115
|
}, [sort, fixedFilter]);
|
|
111
116
|
|
|
112
117
|
const {
|
|
@@ -115,7 +120,7 @@ export function useDataTableController<M extends Record<string, any> = any, USER
|
|
|
115
120
|
} = parseFilterAndSort(location.search);
|
|
116
121
|
|
|
117
122
|
const [filterValues, setFilterValues] = React.useState<FilterValues<Extract<keyof M, string> | (string & {})> | undefined>(fixedFilter ?? (updateUrl ? filterUrl : undefined) ?? defaultFilter ?? undefined);
|
|
118
|
-
const [sortBy, setSortBy] = React.useState<
|
|
123
|
+
const [sortBy, setSortBy] = React.useState<OrderByTuple<Extract<keyof M, string> | (string & {})>[] | undefined>((updateUrl ? sortUrl : undefined) ?? sortInternal);
|
|
119
124
|
|
|
120
125
|
// Sync filter/sort state from URL on browser navigation (back/forward).
|
|
121
126
|
// Skip initial mount since useState initializers already handle URL params + collection defaults.
|
|
@@ -136,7 +141,7 @@ export function useDataTableController<M extends Record<string, any> = any, USER
|
|
|
136
141
|
if (urlSortBy && fixedFilter && !checkFilterCombination(fixedFilter, urlSortBy)) {
|
|
137
142
|
console.warn("URL sort is not compatible with the force filter.");
|
|
138
143
|
} else {
|
|
139
|
-
setSortBy(urlSortBy as
|
|
144
|
+
setSortBy(urlSortBy as OrderByTuple<Extract<keyof M, string> | (string & {})>[] | undefined);
|
|
140
145
|
}
|
|
141
146
|
|
|
142
147
|
// Sync search string from URL
|
|
@@ -163,8 +168,10 @@ export function useDataTableController<M extends Record<string, any> = any, USER
|
|
|
163
168
|
|
|
164
169
|
const [itemCount, setItemCount] = React.useState<number | undefined>(paginationEnabled ? initialItemCount : undefined);
|
|
165
170
|
|
|
166
|
-
|
|
167
|
-
|
|
171
|
+
// The whole sort, as one string, so the effect below re-runs when any key
|
|
172
|
+
// changes — a `sortBy` array is a new reference on every render and would
|
|
173
|
+
// re-subscribe on each one if used as a dependency directly.
|
|
174
|
+
const sortKey = sortBy ? serializeOrderBy(sortBy) : undefined;
|
|
168
175
|
|
|
169
176
|
const context: RebaseContext<USER> = useRebaseContext();
|
|
170
177
|
|
|
@@ -253,7 +260,9 @@ export function useDataTableController<M extends Record<string, any> = any, USER
|
|
|
253
260
|
|
|
254
261
|
// filterValues is already FilterValues — pass directly to the accessor
|
|
255
262
|
const whereParams = filterValues && Object.keys(filterValues).length > 0 ? filterValues : undefined;
|
|
256
|
-
const orderByParams
|
|
263
|
+
const orderByParams = sortBy && sortBy.length > 0
|
|
264
|
+
? sortBy.map(([field, direction]) => [String(field), direction] as OrderByTuple)
|
|
265
|
+
: undefined;
|
|
257
266
|
|
|
258
267
|
let unsubscribe: (() => void) | undefined;
|
|
259
268
|
|
|
@@ -277,7 +286,7 @@ export function useDataTableController<M extends Record<string, any> = any, USER
|
|
|
277
286
|
}
|
|
278
287
|
|
|
279
288
|
return unsubscribe;
|
|
280
|
-
}, [dataClient, path, itemCount,
|
|
289
|
+
}, [dataClient, path, itemCount, sortKey, filterValues, searchString]);
|
|
281
290
|
|
|
282
291
|
const orderedData = useDataOrder({
|
|
283
292
|
data: rawData,
|
|
@@ -336,7 +345,7 @@ export function useDataTableController<M extends Record<string, any> = any, USER
|
|
|
336
345
|
|
|
337
346
|
function useUpdateUrl<M extends Record<string, any> = any>(
|
|
338
347
|
filterValues: FilterValues<Extract<keyof M, string>> | undefined,
|
|
339
|
-
sortBy:
|
|
348
|
+
sortBy: OrderByTuple<Extract<keyof M, string>>[] | undefined,
|
|
340
349
|
searchString: string | undefined,
|
|
341
350
|
updateUrl: boolean | undefined
|
|
342
351
|
) {
|
|
@@ -371,11 +380,15 @@ function useUpdateUrl<M extends Record<string, any> = any>(
|
|
|
371
380
|
}, [filterValues, sortBy, searchString, updateUrl]);
|
|
372
381
|
}
|
|
373
382
|
|
|
374
|
-
function encodeFilterAndSort(filterValues?: FilterValues<string>, sortBy?: [
|
|
383
|
+
function encodeFilterAndSort(filterValues?: FilterValues<string>, sortBy?: OrderByTuple[] | undefined) {
|
|
375
384
|
const entries: Record<string, string> = {};
|
|
376
|
-
if (sortBy) {
|
|
377
|
-
|
|
378
|
-
|
|
385
|
+
if (sortBy && sortBy.length > 0) {
|
|
386
|
+
// Comma-separated, positionally paired. A single key encodes exactly as
|
|
387
|
+
// it did before — `__sort=name&__sort_order=asc` — so links already out
|
|
388
|
+
// in the world keep working, and a reload of a two-key sort no longer
|
|
389
|
+
// silently comes back sorted by the first key alone.
|
|
390
|
+
entries["__sort"] = sortBy.map(([field]) => encodeURIComponent(field)).join(",");
|
|
391
|
+
entries["__sort_order"] = sortBy.map(([, direction]) => direction).join(",");
|
|
379
392
|
}
|
|
380
393
|
if (filterValues) {
|
|
381
394
|
Object.entries(filterValues).forEach(([key, value]) => {
|
|
@@ -426,14 +439,23 @@ function encodeFilterAndSort(filterValues?: FilterValues<string>, sortBy?: [stri
|
|
|
426
439
|
|
|
427
440
|
function parseFilterAndSort<M>(search: string): {
|
|
428
441
|
filterValues: FilterValues<string> | undefined,
|
|
429
|
-
sortBy?:
|
|
442
|
+
sortBy?: OrderByTuple<Extract<keyof M, string>>[]
|
|
430
443
|
} {
|
|
431
444
|
const entries = new URLSearchParams(search);
|
|
432
445
|
const filterValues: FilterValues<string> = {};
|
|
433
|
-
let sortBy: [
|
|
446
|
+
let sortBy: OrderByTuple[] | undefined = undefined;
|
|
434
447
|
entries.forEach((value, key) => {
|
|
435
448
|
if (key === "__sort") {
|
|
436
|
-
|
|
449
|
+
const directions = (entries.get("__sort_order") ?? "").split(",");
|
|
450
|
+
const keys = value
|
|
451
|
+
.split(",")
|
|
452
|
+
.map((field) => decodeURIComponent(field).trim())
|
|
453
|
+
.filter(Boolean)
|
|
454
|
+
// A direction missing from the URL — a hand-written link, or the
|
|
455
|
+
// one-key form of an older bookmark — is ascending, the same
|
|
456
|
+
// reading `?orderBy=name` gets everywhere else.
|
|
457
|
+
.map((field, index): OrderByTuple => [field, directions[index] === "desc" ? "desc" : "asc"]);
|
|
458
|
+
sortBy = keys.length > 0 ? keys : undefined;
|
|
437
459
|
} else if (key.endsWith("_op")) {
|
|
438
460
|
const field = key.replace("_op", "");
|
|
439
461
|
const filterOp = decodeURIComponent(value) as WhereFilterOp;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FindParams, FilterValues, LogicalCondition } from "@rebasepro/types";
|
|
1
|
+
import type { FindParams, FilterValues, LogicalCondition, OrderBySpec } from "@rebasepro/types";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* The one place a collection read's parameters are assembled.
|
|
@@ -24,12 +24,12 @@ export interface CollectionQueryInput<M extends Record<string, unknown>> {
|
|
|
24
24
|
logical?: LogicalCondition;
|
|
25
25
|
/**
|
|
26
26
|
* Loosened from `FindParams["orderBy"]` on purpose: callers hold the sort
|
|
27
|
-
* as
|
|
28
|
-
*
|
|
29
|
-
* every call site, which is the kind of friction that sends people back
|
|
30
|
-
* hand-building the object this exists to replace.
|
|
27
|
+
* as plain `[string, direction]` pairs read off user config or a URL, where
|
|
28
|
+
* the columns are not statically known. Narrowing it here would push a cast
|
|
29
|
+
* onto every call site, which is the kind of friction that sends people back
|
|
30
|
+
* to hand-building the object this exists to replace.
|
|
31
31
|
*/
|
|
32
|
-
orderBy?:
|
|
32
|
+
orderBy?: OrderBySpec;
|
|
33
33
|
limit?: number;
|
|
34
34
|
offset?: number;
|
|
35
35
|
page?: number;
|