@rebasepro/app 0.16.0 → 0.16.1-canary.g0d7af95
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/auth/api.d.ts +8 -21
- package/dist/auth/index.d.ts +4 -4
- package/dist/auth/types.d.ts +1 -1
- package/dist/auth/useRebaseAuthController.d.ts +1 -1
- package/dist/collections/index.d.ts +13 -13
- package/dist/collections/title-property.d.ts +1 -1
- package/dist/components/Debug/collection-views/index.d.ts +3 -3
- package/dist/components/LoginView/LoginView.d.ts +2 -2
- package/dist/components/LoginView/index.d.ts +2 -2
- package/dist/components/common/index.d.ts +6 -6
- package/dist/components/common/useDataTableController.d.ts +1 -1
- package/dist/components/index.d.ts +18 -18
- package/dist/contexts/AdminModeController.d.ts +1 -1
- package/dist/contexts/ModeController.d.ts +1 -1
- package/dist/contexts/index.d.ts +18 -18
- package/dist/core/Rebase.d.ts +1 -1
- package/dist/core/index.d.ts +4 -4
- package/dist/hooks/index.d.ts +36 -36
- package/dist/hooks/useBridgeRegistration.d.ts +1 -1
- package/dist/hooks/useBuildAdminModeController.d.ts +1 -1
- package/dist/hooks/useBuildModeController.d.ts +1 -1
- package/dist/hooks/useUnsavedChangesDialog.d.ts +1 -1
- package/dist/index.d.ts +19 -19
- package/dist/index.es.js +129 -79
- package/dist/index.es.js.map +1 -1
- package/dist/util/index.d.ts +9 -8
- package/dist/util/local_storage.d.ts +31 -0
- package/package.json +9 -9
- package/src/auth/api.ts +8 -21
- package/src/auth/useRebaseAuthController.ts +6 -3
- package/src/collections/entity-display.ts +6 -34
- package/src/collections/form-layout.ts +2 -1
- package/src/collections/title-property.ts +1 -1
- package/src/components/Debug/UIReferenceView.tsx +53 -16
- package/src/components/LoginView/LoginView.tsx +3 -3
- package/src/components/common/useDataTableController.tsx +20 -1
- package/src/core/Rebase.tsx +3 -24
- package/src/hooks/useBuildAdminModeController.tsx +3 -4
- package/src/hooks/useBuildEffectiveRoleController.tsx +6 -7
- package/src/hooks/useBuildModeController.tsx +8 -6
- package/src/i18n/RebaseI18nProvider.tsx +5 -6
- package/src/locales/de.ts +0 -1
- package/src/locales/en.ts +0 -1
- package/src/locales/es.ts +0 -1
- package/src/locales/fr.ts +0 -1
- package/src/locales/hi.ts +0 -1
- package/src/locales/it.ts +0 -1
- package/src/locales/pt.ts +0 -1
- package/src/util/index.ts +1 -0
- package/src/util/local_storage.ts +66 -0
package/dist/util/index.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
export * from "./icon_list";
|
|
2
|
-
export * from "./icons";
|
|
3
|
-
export * from "./createFormexStub";
|
|
4
|
-
export * from "./entity_cache";
|
|
5
|
-
export * from "./useStorageUploadController";
|
|
6
|
-
export * from "./previews";
|
|
7
|
-
export * from "./enums";
|
|
8
|
-
export * from "./constants";
|
|
1
|
+
export * from "./icon_list.js";
|
|
2
|
+
export * from "./icons.js";
|
|
3
|
+
export * from "./createFormexStub.js";
|
|
4
|
+
export * from "./entity_cache.js";
|
|
5
|
+
export * from "./useStorageUploadController.js";
|
|
6
|
+
export * from "./previews.js";
|
|
7
|
+
export * from "./enums.js";
|
|
8
|
+
export * from "./constants.js";
|
|
9
|
+
export * from "./local_storage.js";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* localStorage that tolerates not being there.
|
|
3
|
+
*
|
|
4
|
+
* `typeof window !== "undefined"` was the guard these call sites used, and it
|
|
5
|
+
* answers the wrong question: it says "am I in a browser", not "can I read
|
|
6
|
+
* storage". Three things make that distinction real rather than theoretical:
|
|
7
|
+
*
|
|
8
|
+
* - A browser can have storage **disabled**. Safari in private mode, a blocked
|
|
9
|
+
* third-party cookie policy, and a sandboxed iframe all give you a `window`
|
|
10
|
+
* whose `localStorage` property *throws on access* — a SecurityError raised
|
|
11
|
+
* before any method is called, which no amount of null-checking the result
|
|
12
|
+
* catches.
|
|
13
|
+
* - A test environment can be **half torn down**. jsdom is disposed at the end
|
|
14
|
+
* of a file while React is still finishing a concurrent render, so `window`
|
|
15
|
+
* survives as a husk and `localStorage.getItem` is no longer a function.
|
|
16
|
+
* That is how this was found: `TypeError: localStorage.getItem is not a
|
|
17
|
+
* function`, thrown out of a render, failing a suite in which every one of
|
|
18
|
+
* 709 tests passed.
|
|
19
|
+
* - Server rendering has no `window` at all, which is the only case the old
|
|
20
|
+
* guard actually covered.
|
|
21
|
+
*
|
|
22
|
+
* Reading a UI preference is never worth a crash. Every function here fails to
|
|
23
|
+
* `null` / no-op, so a user with storage switched off gets the default
|
|
24
|
+
* appearance instead of a blank page.
|
|
25
|
+
*/
|
|
26
|
+
/** The stored string for `key`, or null if it is unset or unreadable. */
|
|
27
|
+
export declare function readStoredString(key: string): string | null;
|
|
28
|
+
/** Persist `value` under `key`. A no-op when storage is unavailable. */
|
|
29
|
+
export declare function writeStoredString(key: string, value: string): void;
|
|
30
|
+
/** Remove `key`. A no-op when storage is unavailable. */
|
|
31
|
+
export declare function removeStoredString(key: string): void;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/app",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.16.
|
|
4
|
+
"version": "0.16.1-canary.g0d7af95",
|
|
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/
|
|
56
|
-
"@rebasepro/
|
|
57
|
-
"@rebasepro/
|
|
58
|
-
"@rebasepro/
|
|
59
|
-
"@rebasepro/
|
|
60
|
-
"@rebasepro/
|
|
55
|
+
"@rebasepro/common": "0.16.1-canary.g0d7af95",
|
|
56
|
+
"@rebasepro/types": "0.16.1-canary.g0d7af95",
|
|
57
|
+
"@rebasepro/admin-types": "0.16.1-canary.g0d7af95",
|
|
58
|
+
"@rebasepro/utils": "0.16.1-canary.g0d7af95",
|
|
59
|
+
"@rebasepro/forms": "0.16.1-canary.g0d7af95",
|
|
60
|
+
"@rebasepro/ui": "0.16.1-canary.g0d7af95"
|
|
61
61
|
},
|
|
62
62
|
"peerDependencies": {
|
|
63
63
|
"react": ">=19.2.7",
|
|
@@ -104,7 +104,7 @@
|
|
|
104
104
|
"jest": {
|
|
105
105
|
"transform": {
|
|
106
106
|
"^.+\\.tsx?$": "ts-jest",
|
|
107
|
-
"^.+\\.m?jsx?$": "<rootDir>/../../scripts/jest/react-router-esm-transform.cjs"
|
|
107
|
+
"^.+\\.m?jsx?$": "<rootDir>/../../tooling/scripts/jest/react-router-esm-transform.cjs"
|
|
108
108
|
},
|
|
109
109
|
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$",
|
|
110
110
|
"moduleFileExtensions": [
|
|
@@ -135,7 +135,7 @@
|
|
|
135
135
|
},
|
|
136
136
|
"scripts": {
|
|
137
137
|
"watch": "vite build --watch",
|
|
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",
|
|
138
|
+
"build": "vite build && tsc --emitDeclarationOnly -p tsconfig.prod.json && node ../../tooling/scripts/add-dts-extensions.mjs dist && pnpm exec esbuild src/vitePlugin.ts --platform=node --format=esm --outfile=dist/vitePlugin.js && node ../../tooling/scripts/assert-build-output.mjs",
|
|
139
139
|
"test:lint": "eslint \"src/**\" --quiet",
|
|
140
140
|
"test": "jest --passWithNoTests",
|
|
141
141
|
"clean": "rm -rf dist && find ./src \\( -name '*.js' -o -name '*.d.ts' \\) -type f | xargs rm -f",
|
package/src/auth/api.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { RebaseApiError } from "@rebasepro/types";
|
|
13
|
+
import type { AuthAdapterCapabilities } from "@rebasepro/types";
|
|
13
14
|
import { DEFAULT_API_PATH } from "../hooks/ApiConfigContext";
|
|
14
15
|
|
|
15
16
|
async function handleResponse<T>(response: Response): Promise<T> {
|
|
@@ -59,28 +60,14 @@ async function fetchWithHandling(input: RequestInfo | URL, init?: RequestInit):
|
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
/**
|
|
62
|
-
*
|
|
63
|
+
* What `GET /api/auth/config` answers.
|
|
64
|
+
*
|
|
65
|
+
* The wire contract is declared once, in `@rebasepro/types`, and re-exported
|
|
66
|
+
* here under the name this module uses. Three near-copies of it used to exist —
|
|
67
|
+
* this one listed an `emailServiceEnabled` flag no backend sends, and marked as
|
|
68
|
+
* optional fields every backend always returns.
|
|
63
69
|
*/
|
|
64
|
-
export
|
|
65
|
-
/** True when there are no users in the system and first user setup is needed */
|
|
66
|
-
needsSetup: boolean;
|
|
67
|
-
/** Whether new user registration is enabled */
|
|
68
|
-
registrationEnabled: boolean;
|
|
69
|
-
/** Whether email service is configured */
|
|
70
|
-
emailServiceEnabled?: boolean;
|
|
71
|
-
/** Whether the self-service password reset flow (email link) is supported */
|
|
72
|
-
passwordReset?: boolean;
|
|
73
|
-
/**
|
|
74
|
-
* Whether an admin can reset another user's password, i.e. the adapter
|
|
75
|
-
* mounts `POST /admin/users/:userId/reset-password`. Absent on backends
|
|
76
|
-
* predating this field, which the frontend treats as supported.
|
|
77
|
-
*/
|
|
78
|
-
adminPasswordReset?: boolean;
|
|
79
|
-
/** Whether email verification is supported */
|
|
80
|
-
emailVerification?: boolean;
|
|
81
|
-
/** Complete list of enabled OAuth provider IDs (e.g. ["google", "github", "discord"]) */
|
|
82
|
-
enabledProviders: string[];
|
|
83
|
-
}
|
|
70
|
+
export type AuthConfigResponse = AuthAdapterCapabilities;
|
|
84
71
|
|
|
85
72
|
/**
|
|
86
73
|
* Cache container for `fetchAuthConfig` — holds both the inflight promise
|
|
@@ -325,13 +325,16 @@ export function useRebaseAuthController(
|
|
|
325
325
|
setAuthProviderError,
|
|
326
326
|
extra,
|
|
327
327
|
setExtra,
|
|
328
|
+
// `authConfig` is null until `GET /auth/config` lands, so each fallback
|
|
329
|
+
// is what to assume *while it is still in flight* — not a stand-in for a
|
|
330
|
+
// backend that might omit the field. The backend always sends all of them.
|
|
328
331
|
capabilities: {
|
|
329
332
|
emailPasswordLogin: true,
|
|
330
333
|
googleLogin: !!(props.googleClientId),
|
|
331
|
-
|
|
334
|
+
registrationEnabled: authConfig?.registrationEnabled ?? false,
|
|
332
335
|
passwordReset: authConfig?.passwordReset ?? false,
|
|
333
|
-
//
|
|
334
|
-
//
|
|
336
|
+
// True while loading: the admin UI's "Reset Password" action is worth
|
|
337
|
+
// showing optimistically and hiding once a backend says it cannot.
|
|
335
338
|
adminPasswordReset: authConfig?.adminPasswordReset ?? true,
|
|
336
339
|
sessionManagement: true,
|
|
337
340
|
profileUpdate: true,
|
|
@@ -13,50 +13,22 @@ import type {
|
|
|
13
13
|
EntityDisplayRole
|
|
14
14
|
} from "@rebasepro/admin-types";
|
|
15
15
|
|
|
16
|
-
/**
|
|
17
|
-
* Collections that have already warned about a deprecated field, so a list of
|
|
18
|
-
* fifty rows produces one line in the console rather than fifty.
|
|
19
|
-
*/
|
|
20
|
-
const deprecationWarned = new Set<string>();
|
|
21
|
-
|
|
22
|
-
function collectionId(collection: AdminCollection<never> | AdminCollection<any>): string {
|
|
23
|
-
return (collection.slug ?? collection.name ?? "collection") as string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function warnOnce(id: string, message: string): void {
|
|
27
|
-
const key = `${id}:${message}`;
|
|
28
|
-
if (deprecationWarned.has(key)) return;
|
|
29
|
-
deprecationWarned.add(key);
|
|
30
|
-
console.warn(message);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
16
|
/**
|
|
34
17
|
* What the collection declares for a role, before deciding which form it is.
|
|
35
18
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
19
|
+
* One field states it: `display[role]`. `admin.titleProperty` used to be a
|
|
20
|
+
* second way to say `display.title` and is gone — a role with two spellings is
|
|
21
|
+
* a role with two readers, and this one had grown seven.
|
|
38
22
|
*/
|
|
39
23
|
function getDeclaredSource<M extends Record<string, unknown>>(
|
|
40
24
|
collection: AdminCollection<M>,
|
|
41
25
|
role: EntityDisplayRole
|
|
42
26
|
): string | EntityDisplayResolver<M, unknown> | undefined {
|
|
43
|
-
|
|
44
27
|
const display = collection.display as Record<string, unknown> | undefined;
|
|
45
28
|
const declared = display?.[role];
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const id = collectionId(collection);
|
|
50
|
-
warnOnce(
|
|
51
|
-
id,
|
|
52
|
-
`[rebase] Collection "${id}" uses admin.titleProperty, which is deprecated. ` +
|
|
53
|
-
`Move it to admin.display.title — the same string works there, and display.title ` +
|
|
54
|
-
`also accepts a resolver for a title the record does not carry.`
|
|
55
|
-
);
|
|
56
|
-
return collection.titleProperty as string;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return undefined;
|
|
29
|
+
return declared === undefined
|
|
30
|
+
? undefined
|
|
31
|
+
: declared as string | EntityDisplayResolver<M, unknown>;
|
|
60
32
|
}
|
|
61
33
|
|
|
62
34
|
/**
|
|
@@ -18,6 +18,7 @@ import type {
|
|
|
18
18
|
PropertySpan
|
|
19
19
|
} from "@rebasepro/admin-types";
|
|
20
20
|
import type { Property } from "@rebasepro/types";
|
|
21
|
+
import { getDisplayPropertyKey } from "./entity-display";
|
|
21
22
|
import { isHidden } from "./property_presentation";
|
|
22
23
|
|
|
23
24
|
/**
|
|
@@ -278,7 +279,7 @@ export function resolveFormLayout<M extends Record<string, unknown>>({
|
|
|
278
279
|
}: ResolveFormLayoutParams<M>): ResolvedFormLayout {
|
|
279
280
|
|
|
280
281
|
const config = collection.form;
|
|
281
|
-
const titlePropertyKey = collection
|
|
282
|
+
const titlePropertyKey = getDisplayPropertyKey(collection, "title");
|
|
282
283
|
|
|
283
284
|
const available = new Map<string, ResolvedFormField>();
|
|
284
285
|
for (const key of fieldKeys) {
|
|
@@ -118,7 +118,12 @@ export function UIReferenceView() {
|
|
|
118
118
|
<div className="flex w-full">
|
|
119
119
|
|
|
120
120
|
{/* ── Sidebar nav (same structure as DefaultDrawer) ─────────────── */}
|
|
121
|
-
|
|
121
|
+
{/* 232px, not 200px. The icon rail is 56px and the label is uppercase
|
|
122
|
+
at 12px, which left ~128px — one character short of "COLLECTION
|
|
123
|
+
TABLE", so the longest entry in the list rendered as
|
|
124
|
+
"COLLECTION TA…". This is the reference's own nav, not the shared
|
|
125
|
+
DefaultDrawer, so widening it costs no copy-fidelity. */}
|
|
126
|
+
<div className={cls("flex flex-col sticky top-0 h-screen grow-0 shrink-0 w-[232px] border-r", defaultBorderMixin)}>
|
|
122
127
|
{/* DrawerLogo */}
|
|
123
128
|
<div className="flex flex-row items-center shrink-0 pt-4 pb-2 px-2">
|
|
124
129
|
<div className="shrink-0 flex items-center justify-center w-[56px] h-[40px]">
|
|
@@ -187,7 +192,14 @@ export function UIReferenceView() {
|
|
|
187
192
|
</div>
|
|
188
193
|
|
|
189
194
|
{/* ── Main content area ───────────────────────────────────────────── */}
|
|
190
|
-
|
|
195
|
+
{/* `min-w-0` is required, not cosmetic. A flex item defaults to
|
|
196
|
+
`min-width: auto`, so this column sized itself to its widest
|
|
197
|
+
descendant — the 1451px CRM dashboard — and pushed the whole page
|
|
198
|
+
past the viewport, cutting the collection table's last columns
|
|
199
|
+
off screen. With `min-w-0` the column can shrink to the space
|
|
200
|
+
available and the `overflow-x-auto` on the wide mocks actually
|
|
201
|
+
engages instead of being overruled from above. */}
|
|
202
|
+
<div ref={scrollContainerRef} className="flex-1 min-w-0">
|
|
191
203
|
|
|
192
204
|
{/* ═══════════════════════════════════════════════
|
|
193
205
|
SECTION: Drawer
|
|
@@ -546,7 +558,7 @@ selected: true }, { name: "Tags" }].map(c => (
|
|
|
546
558
|
<Typography variant="caption" color="secondary" className="block mb-1">Property Editor (no selection)</Typography>
|
|
547
559
|
<div className={cls("flex flex-col items-center justify-center h-48 w-[320px] border rounded-lg", defaultBorderMixin)}>
|
|
548
560
|
<div className="flex flex-col items-center justify-center h-full gap-4">
|
|
549
|
-
<Typography variant="label">
|
|
561
|
+
<Typography variant="label" className="text-center px-4">
|
|
550
562
|
Select a property to edit it
|
|
551
563
|
</Typography>
|
|
552
564
|
<Button>
|
|
@@ -562,7 +574,7 @@ selected: true }, { name: "Tags" }].map(c => (
|
|
|
562
574
|
<Typography variant="caption" color="secondary" className="block mb-1">Property Editor (empty collection)</Typography>
|
|
563
575
|
<div className={cls("flex flex-col items-center justify-center h-48 w-[320px] border rounded-lg", defaultBorderMixin)}>
|
|
564
576
|
<div className="flex flex-col items-center justify-center h-full gap-4">
|
|
565
|
-
<Typography variant="label">
|
|
577
|
+
<Typography variant="label" className="text-center px-4">
|
|
566
578
|
Now you can add your first property
|
|
567
579
|
</Typography>
|
|
568
580
|
<Button>
|
|
@@ -578,7 +590,7 @@ selected: true }, { name: "Tags" }].map(c => (
|
|
|
578
590
|
<Typography variant="caption" color="secondary" className="block mb-1">Collection ListIcon (no selection)</Typography>
|
|
579
591
|
<div className={cls("flex flex-col items-center justify-center h-48 w-[320px] border rounded-lg", defaultBorderMixin)}>
|
|
580
592
|
<div className="flex flex-col items-center justify-center h-full gap-4">
|
|
581
|
-
<Typography variant="label">
|
|
593
|
+
<Typography variant="label" className="text-center px-4">
|
|
582
594
|
Select a collection or create a new one to start editing
|
|
583
595
|
</Typography>
|
|
584
596
|
<Button>
|
|
@@ -685,7 +697,15 @@ selected: true }, { name: "Tags" }].map(c => (
|
|
|
685
697
|
</div>
|
|
686
698
|
<div>
|
|
687
699
|
<Typography variant="caption" color="secondary" className="block mb-2 font-mono">IconButton — sizes</Typography>
|
|
688
|
-
|
|
700
|
+
{/* `items-end`, matching the Button size ramp above.
|
|
701
|
+
Each entry is a column of button-over-label, and the
|
|
702
|
+
buttons step 28→32→40→48px. Centring the columns
|
|
703
|
+
against each other scattered the captions across a
|
|
704
|
+
10px band, so a row meant to read as an even ramp
|
|
705
|
+
read as ragged. Aligning the ends puts every caption
|
|
706
|
+
on one baseline and lets the size difference show in
|
|
707
|
+
the buttons, which is the point of the row. */}
|
|
708
|
+
<div className="flex flex-wrap gap-3 items-end">
|
|
689
709
|
{([
|
|
690
710
|
{ s: "smallest" as const, icon: <PencilIcon size={14}/> },
|
|
691
711
|
{ s: "small" as const, icon: <PencilIcon size={16}/> },
|
|
@@ -956,11 +976,15 @@ roles: [] }
|
|
|
956
976
|
{/* ═══════════════════════════════════════════════
|
|
957
977
|
SECTION: CRM Dashboard
|
|
958
978
|
═══════════════════════════════════════════════ */}
|
|
959
|
-
<SectionBlock id="crm-dashboard" title="CRM Dashboard — CrmDashboardDemo">
|
|
979
|
+
<SectionBlock wide id="crm-dashboard" title="CRM Dashboard — CrmDashboardDemo">
|
|
960
980
|
<Typography variant="body2" color="secondary" className="mb-4">
|
|
961
981
|
A real-world showcase of a complex dashboard home page incorporating Rebase design language.
|
|
962
982
|
</Typography>
|
|
963
|
-
|
|
983
|
+
{/* The dashboard is intrinsically ~1483px wide — wider than the
|
|
984
|
+
content column at any laptop size. Scrolling it inside its
|
|
985
|
+
own container is what stops it pushing the section, and is
|
|
986
|
+
the same rule wide tables follow everywhere else. */}
|
|
987
|
+
<div className="w-full overflow-x-auto">
|
|
964
988
|
<CrmDashboardDemo />
|
|
965
989
|
</div>
|
|
966
990
|
</SectionBlock>
|
|
@@ -968,7 +992,7 @@ roles: [] }
|
|
|
968
992
|
{/* ═══════════════════════════════════════════════
|
|
969
993
|
SECTION: Collection Table
|
|
970
994
|
═══════════════════════════════════════════════ */}
|
|
971
|
-
<SectionBlock id="collection-table" title="Collection Table">
|
|
995
|
+
<SectionBlock wide id="collection-table" title="Collection Table">
|
|
972
996
|
<Typography variant="body2" color="secondary" className="mb-4">
|
|
973
997
|
Sample data collection rendered as a sortable, searchable, filterable table with row selection.
|
|
974
998
|
Uses <code className="font-mono text-xs">Table</code>, <code className="font-mono text-xs">Chip</code>,{" "}
|
|
@@ -981,7 +1005,7 @@ roles: [] }
|
|
|
981
1005
|
{/* ═══════════════════════════════════════════════
|
|
982
1006
|
SECTION: Card View
|
|
983
1007
|
═══════════════════════════════════════════════ */}
|
|
984
|
-
<SectionBlock id="card-view" title="Card View">
|
|
1008
|
+
<SectionBlock wide id="card-view" title="Card View">
|
|
985
1009
|
<Typography variant="body2" color="secondary" className="mb-4">
|
|
986
1010
|
Grid of entity cards with thumbnail area, progress bar, status/priority chips, and assignee.
|
|
987
1011
|
Uses <code className="font-mono text-xs">Card</code>, <code className="font-mono text-xs">Chip</code>,{" "}
|
|
@@ -993,24 +1017,37 @@ roles: [] }
|
|
|
993
1017
|
{/* ═══════════════════════════════════════════════
|
|
994
1018
|
SECTION: Kanban Board
|
|
995
1019
|
═══════════════════════════════════════════════ */}
|
|
996
|
-
<SectionBlock id="kanban-board" title="Kanban Board">
|
|
1020
|
+
<SectionBlock wide id="kanban-board" title="Kanban Board">
|
|
997
1021
|
<Typography variant="body2" color="secondary" className="mb-4">
|
|
998
1022
|
Drag-and-drop kanban board with four status columns.
|
|
999
1023
|
Uses <code className="font-mono text-xs">KanbanView</code>, <code className="font-mono text-xs">BoardItem</code>,{" "}
|
|
1000
1024
|
<code className="font-mono text-xs">BoardItemViewProps</code>.
|
|
1001
1025
|
</Typography>
|
|
1002
|
-
<div className="w-full"><KanbanBoardDemo /></div>
|
|
1026
|
+
<div className="w-full overflow-x-auto"><KanbanBoardDemo /></div>
|
|
1003
1027
|
</SectionBlock>
|
|
1004
1028
|
</div>
|
|
1005
1029
|
</div>
|
|
1006
1030
|
);
|
|
1007
1031
|
}
|
|
1008
1032
|
|
|
1009
|
-
function SectionBlock({ id, title, children }: { id: string; title: string; children: React.ReactNode }) {
|
|
1033
|
+
function SectionBlock({ id, title, wide, children }: { id: string; title: string; wide?: boolean; children: React.ReactNode }) {
|
|
1010
1034
|
return (
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1035
|
+
// `max-w-5xl` and `border-b` used to share this element, and they cannot.
|
|
1036
|
+
// Several mocks here are app-scale and intrinsically wider than 64rem
|
|
1037
|
+
// (the CRM dashboard reaches 1483px), so the section's own rule stopped
|
|
1038
|
+
// at 1264px while its content ran past it — every such section read as a
|
|
1039
|
+
// broken edge.
|
|
1040
|
+
//
|
|
1041
|
+
// Split instead: the SECTION spans the column, so its rule always
|
|
1042
|
+
// reaches its own edges; the CONTENT keeps a measure, so a form column
|
|
1043
|
+
// never stretches to 700px on a wide monitor. `wide` opts the app-scale
|
|
1044
|
+
// mocks out of the measure — they pair it with `overflow-x-auto` so they
|
|
1045
|
+
// scroll inside themselves rather than pushing the page.
|
|
1046
|
+
<section id={id} className={cls("px-6 py-8 border-b scroll-mt-0", defaultBorderMixin)}>
|
|
1047
|
+
<div className={wide ? undefined : "max-w-5xl"}>
|
|
1048
|
+
<Typography variant="h5" className="mb-1">{title}</Typography>
|
|
1049
|
+
<div className="mt-4">{children}</div>
|
|
1050
|
+
</div>
|
|
1014
1051
|
</section>
|
|
1015
1052
|
);
|
|
1016
1053
|
}
|
|
@@ -71,7 +71,7 @@ export interface LoginViewProps {
|
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
73
|
* Prevent users from creating new accounts.
|
|
74
|
-
* If not set, checks `authController.capabilities.
|
|
74
|
+
* If not set, checks `authController.capabilities.registrationEnabled`.
|
|
75
75
|
*/
|
|
76
76
|
disableSignupScreen?: boolean;
|
|
77
77
|
|
|
@@ -130,7 +130,7 @@ export interface LoginViewProps {
|
|
|
130
130
|
|
|
131
131
|
/**
|
|
132
132
|
* Whether registration is enabled.
|
|
133
|
-
* If not set, derived from `authController.capabilities.
|
|
133
|
+
* If not set, derived from `authController.capabilities.registrationEnabled`.
|
|
134
134
|
*/
|
|
135
135
|
registrationEnabled?: boolean;
|
|
136
136
|
|
|
@@ -296,7 +296,7 @@ export function LoginView({
|
|
|
296
296
|
const isBootstrapMode = needsSetup
|
|
297
297
|
?? ("needsSetup" in authController && !!(authController as { needsSetup?: boolean }).needsSetup)
|
|
298
298
|
?? false;
|
|
299
|
-
const canRegister = registrationEnabled ?? caps.
|
|
299
|
+
const canRegister = registrationEnabled ?? caps.registrationEnabled ?? false;
|
|
300
300
|
// Every OAuth button needs BOTH halves: a client id here and the matching
|
|
301
301
|
// provider configured on the backend. `enabledProviders` is always an array
|
|
302
302
|
// (the controller defaults it to `[]`), so a missing entry means "the
|
|
@@ -210,7 +210,26 @@ export function useDataTableController<M extends Record<string, any> = any, USER
|
|
|
210
210
|
const [dataLoadingError, setDataLoadingError] = useState<Error | undefined>();
|
|
211
211
|
const [noMoreToLoad, setNoMoreToLoad] = useState<boolean>(false);
|
|
212
212
|
|
|
213
|
-
|
|
213
|
+
/**
|
|
214
|
+
* Clear the user's filters. `fixedFilter` survives — it is the collection
|
|
215
|
+
* forcing a scope, not something the user chose — but `defaultFilter` does
|
|
216
|
+
* not: a default is where the view *opens*, not a floor it can never go
|
|
217
|
+
* below.
|
|
218
|
+
*
|
|
219
|
+
* It used to reset to `defaultFilter`, which made "clear" unable to clear
|
|
220
|
+
* whenever a collection defined one, and every caller here wanted the
|
|
221
|
+
* opposite. `ClearFilterSortButton` is labelled "clear filter" and left one
|
|
222
|
+
* set. `EditorCollectionActionStart` calls this and then re-applies
|
|
223
|
+
* `collection.defaultFilter` on the next line — redundant under the old
|
|
224
|
+
* behaviour, and the clearest statement of what it expected.
|
|
225
|
+
* `FilterPresetsButton` documents "clearing all filters deactivates all
|
|
226
|
+
* chips", which was false: it derives a chip's active state from the
|
|
227
|
+
* filters, so a preset whose values equalled `defaultFilter` switched
|
|
228
|
+
* itself straight back on. That chip could not be turned off at all — click
|
|
229
|
+
* it, the keys are removed, the filter resets to the identical default, and
|
|
230
|
+
* the chip lights up again.
|
|
231
|
+
*/
|
|
232
|
+
const clearFilter = useCallback(() => setFilterValues(fixedFilter ?? undefined), [fixedFilter]);
|
|
214
233
|
|
|
215
234
|
const updateFilterValues = useCallback((updatedFilter: FilterValues<Extract<keyof M, string> | (string & {})> | undefined) => {
|
|
216
235
|
if (fixedFilter) {
|
package/src/core/Rebase.tsx
CHANGED
|
@@ -6,7 +6,7 @@ import type { ComponentOverrideMap } from "@rebasepro/admin-types";
|
|
|
6
6
|
import React, { useEffect, useMemo, useRef, useState } from "react";
|
|
7
7
|
import { CenteredView, Typography } from "@rebasepro/ui";
|
|
8
8
|
import { User, CollectionRegistryController, DataDriver, DataSourceDefinition, RebaseData, DEFAULT_DATA_SOURCE_KEY, StorageSource, StorageSourceDefinition, DEFAULT_STORAGE_SOURCE_KEY } from "@rebasepro/types";
|
|
9
|
-
import { RebaseContext,
|
|
9
|
+
import { RebaseContext, UNRENDERED_SLOTS } from "@rebasepro/admin-types";
|
|
10
10
|
import { PluginProviderStack } from "./PluginProviderStack";
|
|
11
11
|
import { PluginLifecycleManager } from "./PluginLifecycleManager";
|
|
12
12
|
import { AuthControllerContext, CollectionResolverRegistrationContext, CollectionResolver } from "../contexts";
|
|
@@ -85,32 +85,11 @@ export function Rebase<USER extends User, DB = unknown>(props: RebaseProps<USER,
|
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
// Merge direct slots with plugin slots
|
|
89
|
-
// the one it was renamed to. `useSlot` matches on string equality, so a
|
|
90
|
-
// contribution left on the old name would register cleanly and render
|
|
91
|
-
// nothing — the same silent nothing `UNRENDERED_SLOTS` warns about below.
|
|
88
|
+
// Merge direct slots with plugin slots.
|
|
92
89
|
const resolvedSlots: SlotContribution[] = useMemo(() => [
|
|
93
90
|
...directSlots,
|
|
94
91
|
...((plugins ?? []).flatMap((p) => p.slots ?? []))
|
|
95
|
-
]
|
|
96
|
-
const current = RENAMED_SLOTS[contribution.slot];
|
|
97
|
-
return current ? { ...contribution, slot: current } : contribution;
|
|
98
|
-
}), [directSlots, plugins]);
|
|
99
|
-
|
|
100
|
-
// Warn once per retired name actually in use. This reads the *pre-rewrite*
|
|
101
|
-
// names off the inputs rather than `resolvedSlots`, which no longer has
|
|
102
|
-
// them — the rewrite above is what makes the plugin keep working, and this
|
|
103
|
-
// is what tells its author to stop relying on it.
|
|
104
|
-
useEffect(() => {
|
|
105
|
-
const registered = [...directSlots, ...((plugins ?? []).flatMap((p) => p.slots ?? []))];
|
|
106
|
-
for (const legacy of new Set(registered.map(s => s.slot).filter(slot => slot in RENAMED_SLOTS))) {
|
|
107
|
-
console.warn(
|
|
108
|
-
`[Rebase] The "${legacy}" slot has been renamed to "${RENAMED_SLOTS[legacy]}". ` +
|
|
109
|
-
"The contribution was redirected and still renders, but the old name will be " +
|
|
110
|
-
"removed in a future major version."
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
|
-
}, [directSlots, plugins]);
|
|
92
|
+
], [directSlots, plugins]);
|
|
114
93
|
|
|
115
94
|
// Seven of the twenty-nine declared slots are rendered nowhere. They have
|
|
116
95
|
// props interfaces and rows in the public slot reference, so registering
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useCallback, useMemo, useState } from "react";
|
|
2
2
|
import type { AdminModeController } from "./useAdminModeController";
|
|
3
|
+
import { readStoredString, writeStoredString } from "../util/local_storage";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Use this hook to build an admin mode controller that determines
|
|
@@ -7,13 +8,11 @@ import type { AdminModeController } from "./useAdminModeController";
|
|
|
7
8
|
*/
|
|
8
9
|
export function useBuildAdminModeController(): AdminModeController {
|
|
9
10
|
|
|
10
|
-
const savedMode =
|
|
11
|
+
const savedMode = readStoredString("rebase-admin-mode") as "content" | "studio" | "settings" | null;
|
|
11
12
|
const [mode, setMode] = useState<"content" | "studio" | "settings">(savedMode ?? "content");
|
|
12
13
|
|
|
13
14
|
const setModeInternal = useCallback((newMode: "content" | "studio" | "settings") => {
|
|
14
|
-
|
|
15
|
-
localStorage.setItem("rebase-admin-mode", newMode);
|
|
16
|
-
}
|
|
15
|
+
writeStoredString("rebase-admin-mode", newMode);
|
|
17
16
|
setMode(newMode);
|
|
18
17
|
}, []);
|
|
19
18
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { EffectiveRoleController } from "@rebasepro/types";
|
|
2
2
|
import { useCallback, useState, useMemo } from "react";
|
|
3
|
+
import { readStoredString, removeStoredString, writeStoredString } from "../util/local_storage";
|
|
3
4
|
;
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -10,16 +11,14 @@ import { useCallback, useState, useMemo } from "react";
|
|
|
10
11
|
*/
|
|
11
12
|
export function useBuildEffectiveRoleController(): EffectiveRoleController {
|
|
12
13
|
|
|
13
|
-
const savedRole =
|
|
14
|
+
const savedRole = readStoredString("rebase-effective-role");
|
|
14
15
|
const [effectiveRole, setEffectiveRole] = useState<string | null>(savedRole ?? null);
|
|
15
16
|
|
|
16
17
|
const setRoleInternal = useCallback((newRole: string | null) => {
|
|
17
|
-
if (
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
localStorage.removeItem("rebase-effective-role");
|
|
22
|
-
}
|
|
18
|
+
if (newRole) {
|
|
19
|
+
writeStoredString("rebase-effective-role", newRole);
|
|
20
|
+
} else {
|
|
21
|
+
removeStoredString("rebase-effective-role");
|
|
23
22
|
}
|
|
24
23
|
setEffectiveRole(newRole);
|
|
25
24
|
}, []);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useCallback, useEffect, useState, useMemo } from "react";
|
|
2
2
|
|
|
3
3
|
import type { ModeController } from "./useModeController";
|
|
4
|
+
import { readStoredString, removeStoredString, writeStoredString } from "../util/local_storage";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Use this hook to build a color mode controller that determines
|
|
@@ -16,8 +17,9 @@ export function useBuildModeController(): ModeController {
|
|
|
16
17
|
}, []);
|
|
17
18
|
|
|
18
19
|
const [mode, setMode] = useState<"light" | "dark">(() => {
|
|
19
|
-
const
|
|
20
|
-
|
|
20
|
+
const storedPrefersDarkMode = readStoredString("prefers-dark-mode");
|
|
21
|
+
const prefersDarkModeStorage = storedPrefersDarkMode != null
|
|
22
|
+
? storedPrefersDarkMode === "true"
|
|
21
23
|
: null;
|
|
22
24
|
const prefersDarkMode = prefersDarkModeStorage ?? prefersDarkModeQuery();
|
|
23
25
|
return prefersDarkMode ? "dark" : "light";
|
|
@@ -26,7 +28,7 @@ export function useBuildModeController(): ModeController {
|
|
|
26
28
|
useEffect(() => {
|
|
27
29
|
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
|
28
30
|
const handleChange = (e: MediaQueryListEvent) => {
|
|
29
|
-
if (
|
|
31
|
+
if (readStoredString("prefers-dark-mode") == null) {
|
|
30
32
|
setMode(e.matches ? "dark" : "light");
|
|
31
33
|
setDocumentMode(e.matches ? "dark" : "light");
|
|
32
34
|
}
|
|
@@ -50,16 +52,16 @@ export function useBuildModeController(): ModeController {
|
|
|
50
52
|
const setModeInternal = useCallback((mode: "light" | "dark" | "system") => {
|
|
51
53
|
if (mode === "light") {
|
|
52
54
|
setDocumentMode("light");
|
|
53
|
-
|
|
55
|
+
writeStoredString("prefers-dark-mode", "false");
|
|
54
56
|
setMode("light");
|
|
55
57
|
} else if (mode === "dark") {
|
|
56
58
|
setDocumentMode("dark");
|
|
57
|
-
|
|
59
|
+
writeStoredString("prefers-dark-mode", "true");
|
|
58
60
|
setMode("dark");
|
|
59
61
|
} else {
|
|
60
62
|
const preferredMode = prefersDarkModeQuery() ? "dark" : "light";
|
|
61
63
|
setDocumentMode(preferredMode);
|
|
62
|
-
|
|
64
|
+
removeStoredString("prefers-dark-mode");
|
|
63
65
|
setMode(preferredMode);
|
|
64
66
|
}
|
|
65
67
|
}, [prefersDarkModeQuery]);
|