@rebasepro/admin 0.9.1-canary.fd3754b → 0.10.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/dist/{CollectionEditorDialog-DKGlYDPc.js → CollectionEditorDialog-BOlKGGg7.js} +149 -35
- package/dist/{CollectionEditorDialog-DKGlYDPc.js.map → CollectionEditorDialog-BOlKGGg7.js.map} +1 -1
- package/dist/{PropertyEditView-h0N-AMCY.js → PropertyEditView-i51tFOfM.js} +3 -3
- package/dist/PropertyEditView-i51tFOfM.js.map +1 -0
- package/dist/{RouterCollectionsStudioView-DdEStpFk.js → RouterCollectionsStudioView-Dh4W53PU.js} +4 -4
- package/dist/{RouterCollectionsStudioView-DdEStpFk.js.map → RouterCollectionsStudioView-Dh4W53PU.js.map} +1 -1
- package/dist/collection_editor_ui.js +4 -4
- package/dist/components/field_configs.d.ts +17 -0
- package/dist/contexts/BreacrumbsContext.d.ts +1 -1
- package/dist/editor.js.map +1 -1
- package/dist/export-BM6HPujv.js +518 -0
- package/dist/export-BM6HPujv.js.map +1 -0
- package/dist/{history-C0FdqGNz.js → history-CtIPJxvQ.js} +5 -5
- package/dist/history-CtIPJxvQ.js.map +1 -0
- package/dist/hooks/useSelectionDialog.d.ts +1 -1
- package/dist/import-DzQnT03v.js +328 -0
- package/dist/import-DzQnT03v.js.map +1 -0
- package/dist/index.js +3299 -1
- package/dist/index.js.map +1 -0
- package/dist/preview/components/UrlComponentPreview.d.ts +2 -1
- package/dist/preview/util.d.ts +1 -0
- package/dist/util/view_mode.d.ts +54 -0
- package/dist/{src-DLAFyeZI.js → util-XFwQ9FEc.js} +17584 -21521
- package/dist/util-XFwQ9FEc.js.map +1 -0
- package/package.json +12 -17
- package/src/collection_editor/ui/collection_editor/CollectionRLSTab.tsx +139 -13
- package/src/collection_editor/ui/collection_editor/properties/CommonPropertyFields.tsx +1 -1
- package/src/collection_editor/ui/collection_editor/properties/EnumPropertyField.tsx +1 -1
- package/src/components/CollectionTableBinding/CollectionRowActions.tsx +1 -1
- package/src/components/CollectionTableBinding/fields/TableReferenceField.tsx +1 -1
- package/src/components/CollectionViewBinding/CollectionBoardViewBinding.tsx +1 -1
- package/src/components/CollectionViewBinding/CollectionViewActions.tsx +1 -1
- package/src/components/CollectionViewBinding/CollectionViewBinding.tsx +76 -126
- package/src/components/CollectionViewBinding/CollectionViewStartActions.tsx +4 -8
- package/src/components/CollectionViewBinding/FiltersDialog.tsx +18 -0
- package/src/components/CollectionViewBinding/SplitListView.tsx +114 -63
- package/src/components/CollectionViewBinding/useBoardDataController.tsx +9 -3
- package/src/components/DetailViewBinding.tsx +4 -2
- package/src/components/EditFormActions.tsx +2 -2
- package/src/components/EditViewBinding.tsx +3 -2
- package/src/components/EntityPreviewBinding.tsx +2 -1
- package/src/components/HomePage/ContentHomePage.tsx +2 -1
- package/src/components/HomePage/FavouritesView.tsx +1 -1
- package/src/components/RebaseNavigation.tsx +16 -1
- package/src/components/RebaseRouteDefs.tsx +1 -1
- package/src/components/ReferenceTable/SelectionTableBinding.tsx +2 -1
- package/src/components/ReferenceWidget.tsx +1 -1
- package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +1 -1
- package/src/components/SidePanelBinding.tsx +46 -8
- package/src/components/SidePanelProvider.tsx +14 -11
- package/src/components/field_configs.tsx +42 -25
- package/src/components/history/EntityHistoryView.tsx +3 -3
- package/src/contexts/BreacrumbsContext.tsx +1 -1
- package/src/data_import/components/ImportNewPropertyFieldPreview.tsx +2 -4
- package/src/data_import/import/ImportCollectionAction.tsx +6 -6
- package/src/editor/selectors/color-selector.tsx +1 -1
- package/src/editor/selectors/node-selector.tsx +1 -1
- package/src/editor/selectors/text-buttons.tsx +1 -1
- package/src/form/EntityForm.tsx +4 -1
- package/src/form/EntityFormBinding.tsx +3 -1
- package/src/form/field_bindings/ArrayCustomShapedFieldBinding.tsx +2 -1
- package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +3 -2
- package/src/form/field_bindings/BlockFieldBinding.tsx +2 -1
- package/src/form/field_bindings/DateTimeFieldBinding.tsx +2 -1
- package/src/form/field_bindings/KeyValueFieldBinding.tsx +2 -1
- package/src/form/field_bindings/MapFieldBinding.tsx +2 -1
- package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +2 -1
- package/src/form/field_bindings/MultiSelectFieldBinding.tsx +2 -1
- package/src/form/field_bindings/MultipleRelationFieldBinding.tsx +2 -1
- package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +2 -1
- package/src/form/field_bindings/ReferenceFieldBinding.tsx +3 -2
- package/src/form/field_bindings/RelationFieldBinding.tsx +2 -1
- package/src/form/field_bindings/RepeatFieldBinding.tsx +2 -1
- package/src/form/field_bindings/SelectFieldBinding.tsx +2 -1
- package/src/form/field_bindings/StorageUploadFieldBinding.tsx +2 -1
- package/src/form/field_bindings/SwitchFieldBinding.tsx +2 -1
- package/src/form/field_bindings/TextFieldBinding.tsx +2 -1
- package/src/form/field_bindings/UserSelectFieldBinding.tsx +2 -1
- package/src/form/field_bindings/VectorFieldBinding.tsx +2 -1
- package/src/hooks/useBuildSidePanel.tsx +8 -0
- package/src/hooks/useResolvedNavigationFrom.tsx +4 -2
- package/src/hooks/useSelectionDialog.tsx +3 -3
- package/src/preview/components/ImagePreview.tsx +1 -1
- package/src/preview/components/ReferencePreview.tsx +1 -1
- package/src/preview/components/RelationPreview.tsx +2 -2
- package/src/preview/components/UrlComponentPreview.tsx +2 -9
- package/src/preview/property_previews/ArrayOfStringsPreview.tsx +1 -1
- package/src/preview/property_previews/NumberPropertyPreview.tsx +1 -1
- package/src/preview/util.ts +9 -0
- package/src/routes/CustomViewRoute.tsx +2 -1
- package/src/routes/RebaseRoute.tsx +20 -25
- package/src/util/navigation_utils.ts +2 -5
- package/src/util/view_mode.ts +102 -0
- package/dist/PropertyEditView-h0N-AMCY.js.map +0 -1
- package/dist/history-C0FdqGNz.js.map +0 -1
- package/dist/src-DLAFyeZI.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/admin",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.10.0",
|
|
5
5
|
"description": "Rebase CMS — content management views, forms, and routing",
|
|
6
6
|
"funding": {
|
|
7
7
|
"url": "https://github.com/sponsors/rebaseco"
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"types": "./dist/index.d.ts",
|
|
19
19
|
"source": "src/index.ts",
|
|
20
20
|
"engines": {
|
|
21
|
-
"node": ">=
|
|
21
|
+
"node": ">=20"
|
|
22
22
|
},
|
|
23
23
|
"keywords": [
|
|
24
24
|
"rebase",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"import": "./dist/index.js"
|
|
39
39
|
},
|
|
40
40
|
"./editor": {
|
|
41
|
-
"types": "./dist/editor.d.ts",
|
|
41
|
+
"types": "./dist/editor/index.d.ts",
|
|
42
42
|
"development": "./dist/editor.js",
|
|
43
43
|
"import": "./dist/editor.js"
|
|
44
44
|
},
|
|
@@ -83,13 +83,13 @@
|
|
|
83
83
|
"react-dropzone": "^15.0.0",
|
|
84
84
|
"react-use-measure": "^2.1.7",
|
|
85
85
|
"zod": "^4.4.3",
|
|
86
|
-
"@rebasepro/common": "0.
|
|
87
|
-
"@rebasepro/app": "0.
|
|
88
|
-
"@rebasepro/
|
|
89
|
-
"@rebasepro/
|
|
90
|
-
"@rebasepro/
|
|
91
|
-
"@rebasepro/
|
|
92
|
-
"@rebasepro/utils": "0.
|
|
86
|
+
"@rebasepro/common": "0.10.0",
|
|
87
|
+
"@rebasepro/app": "0.10.0",
|
|
88
|
+
"@rebasepro/forms": "0.10.0",
|
|
89
|
+
"@rebasepro/inference": "0.10.0",
|
|
90
|
+
"@rebasepro/types": "0.10.0",
|
|
91
|
+
"@rebasepro/ui": "0.10.0",
|
|
92
|
+
"@rebasepro/utils": "0.10.0"
|
|
93
93
|
},
|
|
94
94
|
"peerDependencies": {
|
|
95
95
|
"react": ">=19.0.0",
|
|
@@ -153,17 +153,12 @@
|
|
|
153
153
|
],
|
|
154
154
|
"moduleNameMapper": {
|
|
155
155
|
"\\.(css|less)$": "<rootDir>/test/__mocks__/styleMock.js",
|
|
156
|
-
"^@rebasepro/
|
|
157
|
-
"^@rebasepro/app$": "<rootDir>/../app/src/index.ts",
|
|
158
|
-
"^@rebasepro/common$": "<rootDir>/../common/src/index.ts",
|
|
159
|
-
"^@rebasepro/types$": "<rootDir>/../types/src/index.ts",
|
|
160
|
-
"^@rebasepro/forms$": "<rootDir>/../forms/src/index.ts",
|
|
161
|
-
"^@rebasepro/utils$": "<rootDir>/../utils/src/index.ts"
|
|
156
|
+
"^@rebasepro/([a-z0-9-]+)$": "<rootDir>/../$1/src/index.ts"
|
|
162
157
|
}
|
|
163
158
|
},
|
|
164
159
|
"scripts": {
|
|
165
160
|
"watch": "vite build --watch",
|
|
166
|
-
"build": "vite build && tsc --emitDeclarationOnly -p tsconfig.prod.json",
|
|
161
|
+
"build": "vite build && tsc --emitDeclarationOnly -p tsconfig.prod.json && node ../../scripts/assert-build-output.mjs",
|
|
167
162
|
"test:lint": "eslint \"src/**\" --quiet",
|
|
168
163
|
"test": "jest --passWithNoTests",
|
|
169
164
|
"clean": "rm -rf dist && find ./src -name '*.js' -type f | xargs rm -f",
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
|
|
2
|
-
import { CollectionConfig } from "@rebasepro/types";
|
|
3
|
-
import
|
|
2
|
+
import { CollectionConfig, SecurityRule as GeneratedSecurityRule } from "@rebasepro/types";
|
|
3
|
+
import { getPolicyNamesForRule, getPolicyNamesForRules } from "@rebasepro/utils";
|
|
4
|
+
import { getEffectiveSecurityRules } from "@rebasepro/common";
|
|
5
|
+
import React, { useState, useEffect, useMemo } from "react";
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* Validates and double-quotes a SQL identifier to prevent injection.
|
|
@@ -130,7 +132,65 @@ export function CollectionRLSTab() {
|
|
|
130
132
|
fetchLivePolicies();
|
|
131
133
|
}, [databaseAdmin, values.id, values.table, values.alias]);
|
|
132
134
|
|
|
133
|
-
|
|
135
|
+
/**
|
|
136
|
+
* Application roles available to `SecurityRule.roles`.
|
|
137
|
+
*
|
|
138
|
+
* Deliberately NOT `fetchAvailableRoles` — that returns native pg_roles
|
|
139
|
+
* (postgres, rebase_user, …), which can never satisfy an application-role
|
|
140
|
+
* condition.
|
|
141
|
+
*/
|
|
142
|
+
const [availableRoles, setAvailableRoles] = useState<string[]>([]);
|
|
143
|
+
const [rolesUnavailable, setRolesUnavailable] = useState(false);
|
|
144
|
+
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
let mounted = true;
|
|
147
|
+
const fetchRoles = async () => {
|
|
148
|
+
if (!databaseAdmin?.fetchApplicationRoles) {
|
|
149
|
+
// Older backend, or a driver with no application-role concept.
|
|
150
|
+
if (mounted) setRolesUnavailable(true);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
const fetched = await databaseAdmin.fetchApplicationRoles();
|
|
155
|
+
if (mounted) {
|
|
156
|
+
setAvailableRoles(fetched);
|
|
157
|
+
setRolesUnavailable(false);
|
|
158
|
+
}
|
|
159
|
+
} catch (e) {
|
|
160
|
+
console.error("Failed to fetch application roles", e);
|
|
161
|
+
if (mounted) setRolesUnavailable(true);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
fetchRoles();
|
|
165
|
+
return () => {
|
|
166
|
+
mounted = false;
|
|
167
|
+
};
|
|
168
|
+
}, [databaseAdmin]);
|
|
169
|
+
|
|
170
|
+
const tableName = values.id || values.table || values.alias;
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Every policy name `rebase db push` would write for this collection.
|
|
174
|
+
*
|
|
175
|
+
* Two things make this more than `rules.map(r => r.name)`:
|
|
176
|
+
* - a rule without an explicit `name` becomes `<table>_<op>_<hash>`, so
|
|
177
|
+
* comparing `rule.name` to `policyname` never matches for it;
|
|
178
|
+
* - the generator also injects the safe-by-default baseline
|
|
179
|
+
* (`<table>_default_admin_*`), which is in no collection's `securityRules`.
|
|
180
|
+
*
|
|
181
|
+
* Missing either made Rebase's own policies look hand-written, and offered to
|
|
182
|
+
* import them back into the codebase that produced them.
|
|
183
|
+
*/
|
|
184
|
+
const generatedPolicyNames = useMemo(() => {
|
|
185
|
+
if (!tableName) return new Set<string>();
|
|
186
|
+
const effectiveRules = getEffectiveSecurityRules(values as CollectionConfig);
|
|
187
|
+
return getPolicyNamesForRules(
|
|
188
|
+
[...(rules as unknown as GeneratedSecurityRule[]), ...effectiveRules],
|
|
189
|
+
tableName
|
|
190
|
+
);
|
|
191
|
+
}, [rules, tableName, values]);
|
|
192
|
+
|
|
193
|
+
const unmappedPolicies = dbPolicies.filter(dp => !generatedPolicyNames.has(dp.policyname));
|
|
134
194
|
|
|
135
195
|
const handleSave = async (newPolicy: Partial<PostgresPolicy>) => {
|
|
136
196
|
const rule: SecurityRule = {
|
|
@@ -175,7 +235,13 @@ export function CollectionRLSTab() {
|
|
|
175
235
|
<div className="flex flex-col gap-1.5 min-w-0">
|
|
176
236
|
<div className="flex items-center gap-2">
|
|
177
237
|
<KeyIcon size={iconSize.smallest} className="text-text-disabled dark:text-text-disabled-dark shrink-0"/>
|
|
178
|
-
<Typography variant="subtitle2" className="truncate">
|
|
238
|
+
<Typography variant="subtitle2" className="truncate">
|
|
239
|
+
{/* Unnamed rules are still named in Postgres — show what they compile to
|
|
240
|
+
rather than an empty heading. */}
|
|
241
|
+
{rule.name || (tableName
|
|
242
|
+
? getPolicyNamesForRule(rule as unknown as GeneratedSecurityRule, tableName).join(", ")
|
|
243
|
+
: "Unnamed policy")}
|
|
244
|
+
</Typography>
|
|
179
245
|
</div>
|
|
180
246
|
<div className="flex gap-2 text-xs pl-6 overflow-x-auto hide-scrollbar">
|
|
181
247
|
<Chip size="small" className="bg-surface-100 dark:bg-surface-900 text-text-secondary border-none">Action: {rule.operation || "ALL"}</Chip>
|
|
@@ -188,7 +254,7 @@ export function CollectionRLSTab() {
|
|
|
188
254
|
tablename: values.id || values.table || values.alias || "your_table",
|
|
189
255
|
permissive: (rule.mode || "permissive").toUpperCase() as PostgresPolicy["permissive"],
|
|
190
256
|
cmd: (rule.operation || "ALL").toUpperCase() as PostgresPolicy["cmd"],
|
|
191
|
-
roles: rule.roles || [
|
|
257
|
+
roles: rule.roles || [],
|
|
192
258
|
qual: rule.using || null,
|
|
193
259
|
with_check: rule.withCheck || null
|
|
194
260
|
})}>
|
|
@@ -262,6 +328,8 @@ export function CollectionRLSTab() {
|
|
|
262
328
|
<InlinePolicyEditor
|
|
263
329
|
policy={editingPolicy === "new" ? undefined : editingPolicy}
|
|
264
330
|
table={values.id || values.table || values.alias || "your_table"}
|
|
331
|
+
availableRoles={availableRoles}
|
|
332
|
+
rolesUnavailable={rolesUnavailable}
|
|
265
333
|
onSave={handleSave}
|
|
266
334
|
onCancel={() => setEditingPolicy(null)}
|
|
267
335
|
/>
|
|
@@ -276,30 +344,57 @@ export function CollectionRLSTab() {
|
|
|
276
344
|
|
|
277
345
|
type PolicyCommand = "ALL" | "SELECT" | "INSERT" | "UPDATE" | "DELETE";
|
|
278
346
|
const COMMAND_OPTIONS: PolicyCommand[] = ["ALL", "SELECT", "INSERT", "UPDATE", "DELETE"];
|
|
279
|
-
const ROLE_OPTIONS = ["public", "authenticated", "anon", "admin"];
|
|
280
347
|
|
|
281
348
|
function InlinePolicyEditor({
|
|
282
349
|
policy,
|
|
283
350
|
table,
|
|
351
|
+
availableRoles,
|
|
352
|
+
rolesUnavailable,
|
|
284
353
|
onSave,
|
|
285
354
|
onCancel
|
|
286
355
|
}: {
|
|
287
356
|
policy?: PostgresPolicy;
|
|
288
357
|
table: string;
|
|
358
|
+
availableRoles: string[];
|
|
359
|
+
rolesUnavailable: boolean;
|
|
289
360
|
onSave: (policyData: Partial<PostgresPolicy>) => void;
|
|
290
361
|
onCancel: () => void;
|
|
291
362
|
}) {
|
|
292
363
|
const [name, setName] = useState(policy?.policyname || "");
|
|
293
364
|
const [behavior, setBehavior] = useState<"PERMISSIVE" | "RESTRICTIVE">(policy?.permissive || "PERMISSIVE");
|
|
294
365
|
const [command, setCommand] = useState<PolicyCommand>((policy?.cmd as PolicyCommand) || "ALL");
|
|
366
|
+
// No roles means "not restricted by role". Seeding a value here would
|
|
367
|
+
// silently narrow every new policy to it.
|
|
295
368
|
const [roles, setRoles] = useState<string[]>(
|
|
296
|
-
policy?.roles ? (Array.isArray(policy.roles) ? policy.roles : [policy.roles]) : [
|
|
369
|
+
policy?.roles ? (Array.isArray(policy.roles) ? policy.roles : [policy.roles]) : []
|
|
297
370
|
);
|
|
371
|
+
const [customRole, setCustomRole] = useState("");
|
|
298
372
|
const [usingExpr, setUsingExpr] = useState(policy?.qual || "");
|
|
299
373
|
const [checkExpr, setCheckExpr] = useState(policy?.with_check || "");
|
|
300
374
|
|
|
301
375
|
const showCheck = command === "ALL" || command === "INSERT" || command === "UPDATE";
|
|
302
376
|
|
|
377
|
+
/**
|
|
378
|
+
* Roles offered in the dropdown: those in use in the project, plus any the
|
|
379
|
+
* rule already carries. The union matters — application roles are derived
|
|
380
|
+
* from what users hold, so a role that is referenced here but assigned to
|
|
381
|
+
* nobody would otherwise vanish from its own policy on the next save.
|
|
382
|
+
*/
|
|
383
|
+
const roleOptions = useMemo(
|
|
384
|
+
() => Array.from(new Set([...availableRoles, ...roles])).sort(),
|
|
385
|
+
[availableRoles, roles]
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
const addCustomRole = () => {
|
|
389
|
+
const trimmed = customRole.trim();
|
|
390
|
+
if (!trimmed || roles.includes(trimmed)) {
|
|
391
|
+
setCustomRole("");
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
setRoles([...roles, trimmed]);
|
|
395
|
+
setCustomRole("");
|
|
396
|
+
};
|
|
397
|
+
|
|
303
398
|
return (
|
|
304
399
|
<>
|
|
305
400
|
<DialogTitle variant="h6">
|
|
@@ -334,21 +429,52 @@ function InlinePolicyEditor({
|
|
|
334
429
|
</div>
|
|
335
430
|
</div>
|
|
336
431
|
<div className="flex flex-col gap-1.5">
|
|
337
|
-
<Typography variant="caption" className="uppercase tracking-wider text-text-secondary">
|
|
338
|
-
<
|
|
339
|
-
|
|
340
|
-
|
|
432
|
+
<Typography variant="caption" className="uppercase tracking-wider text-text-secondary">Application Roles</Typography>
|
|
433
|
+
<Typography variant="caption" className="text-text-secondary opacity-80 -mt-1">
|
|
434
|
+
Roles held by users of this project, matched via <span className="font-mono">auth.roles()</span>.
|
|
435
|
+
These are not PostgreSQL roles — leave empty to apply the policy to everyone.
|
|
436
|
+
</Typography>
|
|
437
|
+
{roleOptions.length > 0 && (
|
|
438
|
+
<MultiSelect size="small" value={roles} onValueChange={setRoles} placeholder="Select roles">
|
|
439
|
+
{roleOptions.map(r => <MultiSelectItem key={r} value={r}>{r}</MultiSelectItem>)}
|
|
440
|
+
</MultiSelect>
|
|
441
|
+
)}
|
|
442
|
+
{rolesUnavailable && roleOptions.length === 0 && (
|
|
443
|
+
<Typography variant="caption" className="text-text-secondary opacity-80">
|
|
444
|
+
Could not load the project's roles — enter them manually below.
|
|
445
|
+
</Typography>
|
|
446
|
+
)}
|
|
447
|
+
{!rolesUnavailable && roleOptions.length === 0 && (
|
|
448
|
+
<Typography variant="caption" className="text-text-secondary opacity-80">
|
|
449
|
+
No roles are assigned to any user yet — enter one manually below.
|
|
450
|
+
</Typography>
|
|
451
|
+
)}
|
|
452
|
+
<div className="flex gap-2 items-center">
|
|
453
|
+
<TextField size="small" value={customRole}
|
|
454
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => setCustomRole(e.target.value)}
|
|
455
|
+
onKeyDown={(e: React.KeyboardEvent) => {
|
|
456
|
+
if (e.key === "Enter") {
|
|
457
|
+
e.preventDefault();
|
|
458
|
+
addCustomRole();
|
|
459
|
+
}
|
|
460
|
+
}}
|
|
461
|
+
placeholder="Add a role not listed, e.g. editor"/>
|
|
462
|
+
<Button size="small" variant="outlined" color="neutral"
|
|
463
|
+
disabled={!customRole.trim()} onClick={addCustomRole}>
|
|
464
|
+
Add
|
|
465
|
+
</Button>
|
|
466
|
+
</div>
|
|
341
467
|
</div>
|
|
342
468
|
{command !== "INSERT" && (
|
|
343
469
|
<div className="flex flex-col gap-1.5">
|
|
344
470
|
<Typography variant="caption" className="uppercase tracking-wider text-text-secondary">USING expression</Typography>
|
|
345
|
-
<TextField value={usingExpr} onChange={(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => setUsingExpr(e.target.value)} placeholder="e.g. auth.uid() =
|
|
471
|
+
<TextField value={usingExpr} onChange={(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => setUsingExpr(e.target.value)} placeholder="e.g. auth.uid() = uid"/>
|
|
346
472
|
</div>
|
|
347
473
|
)}
|
|
348
474
|
{showCheck && (
|
|
349
475
|
<div className="flex flex-col gap-1.5">
|
|
350
476
|
<Typography variant="caption" className="uppercase tracking-wider text-text-secondary">WITH CHECK expression</Typography>
|
|
351
|
-
<TextField value={checkExpr} onChange={(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => setCheckExpr(e.target.value)} placeholder="e.g. auth.uid() =
|
|
477
|
+
<TextField value={checkExpr} onChange={(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => setCheckExpr(e.target.value)} placeholder="e.g. auth.uid() = uid"/>
|
|
352
478
|
</div>
|
|
353
479
|
)}
|
|
354
480
|
</Paper>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { FieldCaption } from "../../../_cms_internals";
|
|
2
2
|
import { Field, getIn, useFormex } from "@rebasepro/forms";
|
|
3
3
|
import { DebouncedTextField } from "@rebasepro/ui";
|
|
4
|
-
import { PropertyWithId } from "../PropertyEditView";
|
|
4
|
+
import type { PropertyWithId } from "../PropertyEditView";
|
|
5
5
|
import React from "react";
|
|
6
6
|
import { useTranslation } from "@rebasepro/app";
|
|
7
7
|
;
|
|
@@ -9,7 +9,7 @@ import { EnumForm } from "../EnumForm";
|
|
|
9
9
|
import { StringPropertyValidation } from "./validation/StringPropertyValidation";
|
|
10
10
|
import { ArrayPropertyValidation } from "./validation/ArrayPropertyValidation";
|
|
11
11
|
import { ValidationPanel } from "./validation/ValidationPanel";
|
|
12
|
-
import { PropertyWithId } from "../PropertyEditView";
|
|
12
|
+
import type { PropertyWithId } from "../PropertyEditView";
|
|
13
13
|
|
|
14
14
|
export function EnumPropertyField({
|
|
15
15
|
multiselect,
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
import { useTranslation } from "@rebasepro/app";
|
|
18
18
|
import { getEntityFromCache } from "@rebasepro/app";
|
|
19
19
|
import { getLocalChangesBackup } from "@rebasepro/common";
|
|
20
|
-
import { useCMSContext } from "../../
|
|
20
|
+
import { useCMSContext } from "../../hooks/useCMSContext";
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
*
|
|
@@ -11,7 +11,7 @@ import { useComponentOverride, ErrorView, CollectionScopeProvider } from "@rebas
|
|
|
11
11
|
import { cls, PencilIcon } from "@rebasepro/ui";
|
|
12
12
|
import { EntityPreviewContainer } from "../../EntityPreviewBinding";
|
|
13
13
|
import { getReferenceFrom } from "@rebasepro/common";
|
|
14
|
-
import { useCollectionRegistryController } from "../../../
|
|
14
|
+
import { useCollectionRegistryController } from "../../../hooks/navigation/contexts/CollectionRegistryContext";
|
|
15
15
|
|
|
16
16
|
type TableReferenceFieldProps = {
|
|
17
17
|
name: string;
|
|
@@ -38,7 +38,7 @@ import { useAnalyticsController } from "@rebasepro/app";
|
|
|
38
38
|
import { setIn } from "@rebasepro/forms";
|
|
39
39
|
import { useBoardDataController } from "./useBoardDataController";
|
|
40
40
|
import { useKanbanDragAndDrop } from "./hooks/useKanbanDragAndDrop";
|
|
41
|
-
import { useSidePanel } from "../../
|
|
41
|
+
import { useSidePanel } from "../../hooks/useSidePanel";
|
|
42
42
|
import { generateNKeysBetween } from "fractional-indexing";
|
|
43
43
|
|
|
44
44
|
export type CollectionBoardViewBindingProps<M extends Record<string, unknown> = Record<string, unknown>> = {
|
|
@@ -13,7 +13,7 @@ const ImportCollectionAction = lazy(() => import("../../data_import/import").the
|
|
|
13
13
|
const ExportCollectionAction = lazy(() => import("../../data_export/export").then(m => ({ default: m.ExportCollectionAction })));
|
|
14
14
|
import { EditorCollectionAction } from "../../collection_editor/ui/EditorCollectionAction";
|
|
15
15
|
import { useCollectionEditorController } from "../../collection_editor/useCollectionEditorController";
|
|
16
|
-
import { useCMSContext } from "../../
|
|
16
|
+
import { useCMSContext } from "../../hooks/useCMSContext";
|
|
17
17
|
|
|
18
18
|
export type CollectionViewActionsProps<M extends Record<string, unknown>> = {
|
|
19
19
|
collection: CollectionConfig<M>;
|
|
@@ -69,28 +69,15 @@ import { DeleteEntityDialog } from "../DeleteEntityDialog";
|
|
|
69
69
|
import { useSelectionController } from "./useSelectionController";
|
|
70
70
|
import { CollectionViewStartActions } from "./CollectionViewStartActions";
|
|
71
71
|
import { addRecentId, getRecentIds } from "./utils";
|
|
72
|
+
import { isViewMode, OpenEntityMode, resolveOpenEntityMode, VIEW_MODE_PARAM } from "../../util/view_mode";
|
|
72
73
|
import { mergeDeep } from "@rebasepro/utils";
|
|
73
|
-
import {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
useUrlController
|
|
79
|
-
} from "../../index";
|
|
74
|
+
import { useBreadcrumbsController } from "../../hooks/useBreadcrumbsController";
|
|
75
|
+
import { useCMSContext } from "../../hooks/useCMSContext";
|
|
76
|
+
import { useCollectionRegistryController } from "../../hooks/navigation/contexts/CollectionRegistryContext";
|
|
77
|
+
import { useSidePanel } from "../../hooks/useSidePanel";
|
|
78
|
+
import { useUrlController } from "../../hooks/navigation/contexts/UrlContext";
|
|
80
79
|
|
|
81
80
|
const EMPTY_ARRAY: never[] = [];
|
|
82
|
-
const DEFAULT_ENTITY_OPEN_MODE = "split";
|
|
83
|
-
|
|
84
|
-
function getOpenEntityMode(
|
|
85
|
-
viewMode: ViewMode,
|
|
86
|
-
configuredMode?: "side_panel" | "full_screen" | "split" | "dialog"
|
|
87
|
-
): "side_panel" | "full_screen" | "split" | "dialog" {
|
|
88
|
-
if (configuredMode) return configuredMode;
|
|
89
|
-
if (viewMode === "kanban") return "side_panel";
|
|
90
|
-
if (viewMode === "table" || viewMode === "cards") return "full_screen";
|
|
91
|
-
// "list" view defaults to split
|
|
92
|
-
return "split";
|
|
93
|
-
}
|
|
94
81
|
|
|
95
82
|
/**
|
|
96
83
|
* @group Components
|
|
@@ -249,11 +236,8 @@ const CollectionViewBindingInner = React.memo(
|
|
|
249
236
|
|
|
250
237
|
// Read view from React Router's searchParams (reactive on back/forward)
|
|
251
238
|
const urlView = useMemo((): ViewMode | null => {
|
|
252
|
-
const v = searchParams.get(
|
|
253
|
-
|
|
254
|
-
return v as ViewMode;
|
|
255
|
-
}
|
|
256
|
-
return null;
|
|
239
|
+
const v = searchParams.get(VIEW_MODE_PARAM);
|
|
240
|
+
return isViewMode(v) ? v : null;
|
|
257
241
|
}, [searchParams]);
|
|
258
242
|
|
|
259
243
|
// Get saved view from local persistence
|
|
@@ -270,7 +254,10 @@ const CollectionViewBindingInner = React.memo(
|
|
|
270
254
|
return defaultViewMode;
|
|
271
255
|
});
|
|
272
256
|
|
|
273
|
-
const openEntityMode =
|
|
257
|
+
const openEntityMode = resolveOpenEntityMode({
|
|
258
|
+
collection,
|
|
259
|
+
viewMode
|
|
260
|
+
});
|
|
274
261
|
|
|
275
262
|
// Sync URL with current view on init (if view came from saved config)
|
|
276
263
|
useEffect(() => {
|
|
@@ -278,7 +265,7 @@ const CollectionViewBindingInner = React.memo(
|
|
|
278
265
|
// View came from saved config but URL doesn't have it - update URL without push
|
|
279
266
|
setSearchParams((prev) => {
|
|
280
267
|
const next = new URLSearchParams(prev);
|
|
281
|
-
next.set(
|
|
268
|
+
next.set(VIEW_MODE_PARAM, viewMode);
|
|
282
269
|
return next;
|
|
283
270
|
}, { replace: true });
|
|
284
271
|
}
|
|
@@ -292,9 +279,9 @@ const CollectionViewBindingInner = React.memo(
|
|
|
292
279
|
setSearchParams((prev) => {
|
|
293
280
|
const next = new URLSearchParams(prev);
|
|
294
281
|
if (newMode === defaultViewMode) {
|
|
295
|
-
next.delete(
|
|
282
|
+
next.delete(VIEW_MODE_PARAM);
|
|
296
283
|
} else {
|
|
297
|
-
next.set(
|
|
284
|
+
next.set(VIEW_MODE_PARAM, newMode);
|
|
298
285
|
}
|
|
299
286
|
return next;
|
|
300
287
|
});
|
|
@@ -974,6 +961,7 @@ parentEntityIds,
|
|
|
974
961
|
highlightedEntities={highlightedEntity ? [highlightedEntity] : []}
|
|
975
962
|
size={listSize}
|
|
976
963
|
emptyComponent={emptyComponent}
|
|
964
|
+
selectedEntityId={selectedEntityIdProp}
|
|
977
965
|
getActionsForEntity={getActionsForEntity}
|
|
978
966
|
path={path}
|
|
979
967
|
openEntityMode={openEntityMode}
|
|
@@ -1008,12 +996,58 @@ parentEntityIds,
|
|
|
1008
996
|
additionalIDHeaderWidget={<EntityIdHeaderWidget
|
|
1009
997
|
path={path}
|
|
1010
998
|
idPath={path}
|
|
1011
|
-
collection={collection}
|
|
999
|
+
collection={collection}
|
|
1000
|
+
openEntityMode={openEntityMode}/>}
|
|
1012
1001
|
openEntityMode={openEntityMode}
|
|
1013
1002
|
onColumnsOrderChange={onColumnsOrderChange}
|
|
1014
1003
|
/>
|
|
1015
1004
|
);
|
|
1016
1005
|
|
|
1006
|
+
const detailOpen = selectedEntityIdProp !== undefined;
|
|
1007
|
+
|
|
1008
|
+
// List view gets a centered, titled reading surface. Opening a detail
|
|
1009
|
+
// panel collapses the title and drops the centering so the list can
|
|
1010
|
+
// shrink into the master column.
|
|
1011
|
+
const collectionSurface = viewMode === "list"
|
|
1012
|
+
? (
|
|
1013
|
+
<div
|
|
1014
|
+
className={cls(
|
|
1015
|
+
"flex flex-col w-full",
|
|
1016
|
+
detailOpen ? "" : "max-w-6xl mx-auto px-3 md:px-4 lg:px-6 py-4"
|
|
1017
|
+
)}
|
|
1018
|
+
>
|
|
1019
|
+
{/* Collapsible title — grid-rows for smooth height, transform for GPU */}
|
|
1020
|
+
<div
|
|
1021
|
+
className={cls(
|
|
1022
|
+
"grid transition-[grid-template-rows,transform,margin] duration-150 ease-out",
|
|
1023
|
+
detailOpen
|
|
1024
|
+
? "grid-rows-[0fr] -translate-y-2 mt-0 mb-0"
|
|
1025
|
+
: "grid-rows-[1fr] translate-y-0 mt-12 mb-6"
|
|
1026
|
+
)}
|
|
1027
|
+
>
|
|
1028
|
+
<div className="overflow-hidden flex items-center gap-4">
|
|
1029
|
+
<Typography gutterBottom variant="h4" className="grow mb-0" component="h4">
|
|
1030
|
+
{collection.name}
|
|
1031
|
+
</Typography>
|
|
1032
|
+
</div>
|
|
1033
|
+
</div>
|
|
1034
|
+
{pluginInsights.length > 0 && (
|
|
1035
|
+
<div
|
|
1036
|
+
className={cls(
|
|
1037
|
+
"grid transition-[grid-template-rows] duration-150 ease-out",
|
|
1038
|
+
detailOpen ? "grid-rows-[0fr]" : "grid-rows-[1fr]"
|
|
1039
|
+
)}
|
|
1040
|
+
>
|
|
1041
|
+
<div className="overflow-hidden flex-shrink-0">
|
|
1042
|
+
{pluginInsights}
|
|
1043
|
+
</div>
|
|
1044
|
+
</div>
|
|
1045
|
+
)}
|
|
1046
|
+
{innerView}
|
|
1047
|
+
</div>
|
|
1048
|
+
)
|
|
1049
|
+
: innerView;
|
|
1050
|
+
|
|
1017
1051
|
const mainContent = (
|
|
1018
1052
|
<div className={cls("overflow-hidden h-full w-full rounded-md flex flex-col dark:bg-surface-800", className)}
|
|
1019
1053
|
ref={containerRef}>
|
|
@@ -1054,66 +1088,10 @@ parentEntityIds,
|
|
|
1054
1088
|
selectedTab={selectedTabProp}
|
|
1055
1089
|
toolbar={toolbarNode}
|
|
1056
1090
|
>
|
|
1057
|
-
{/*
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
{
|
|
1061
|
-
<div
|
|
1062
|
-
className={cls(
|
|
1063
|
-
"flex flex-col w-full",
|
|
1064
|
-
selectedEntityIdProp === undefined
|
|
1065
|
-
? "max-w-6xl mx-auto px-3 md:px-4 lg:px-6 py-4"
|
|
1066
|
-
: ""
|
|
1067
|
-
)}
|
|
1068
|
-
>
|
|
1069
|
-
{/* Collapsible title — grid-rows for smooth height, transform for GPU */}
|
|
1070
|
-
<div
|
|
1071
|
-
className={cls(
|
|
1072
|
-
"grid transition-[grid-template-rows,transform,margin] duration-150 ease-out",
|
|
1073
|
-
selectedEntityIdProp === undefined
|
|
1074
|
-
? "grid-rows-[1fr] translate-y-0 mt-12 mb-6"
|
|
1075
|
-
: "grid-rows-[0fr] -translate-y-2 mt-0 mb-0"
|
|
1076
|
-
)}
|
|
1077
|
-
>
|
|
1078
|
-
<div className="overflow-hidden flex items-center gap-4">
|
|
1079
|
-
<Typography gutterBottom variant="h4" className="grow mb-0" component="h4">
|
|
1080
|
-
{collection.name}
|
|
1081
|
-
</Typography>
|
|
1082
|
-
</div>
|
|
1083
|
-
</div>
|
|
1084
|
-
{pluginInsights.length > 0 && (
|
|
1085
|
-
<div
|
|
1086
|
-
className={cls(
|
|
1087
|
-
"grid transition-[grid-template-rows] duration-150 ease-out",
|
|
1088
|
-
selectedEntityIdProp === undefined
|
|
1089
|
-
? "grid-rows-[1fr]"
|
|
1090
|
-
: "grid-rows-[0fr]"
|
|
1091
|
-
)}
|
|
1092
|
-
>
|
|
1093
|
-
<div className="overflow-hidden flex-shrink-0">
|
|
1094
|
-
{pluginInsights}
|
|
1095
|
-
</div>
|
|
1096
|
-
</div>
|
|
1097
|
-
)}
|
|
1098
|
-
<CollectionListViewBinding
|
|
1099
|
-
key={`list-view-${path}`}
|
|
1100
|
-
collection={collection}
|
|
1101
|
-
tableController={tableController}
|
|
1102
|
-
onEntityClick={onEntityClick}
|
|
1103
|
-
selectionController={usedSelectionController}
|
|
1104
|
-
selectionEnabled={selectionEnabled}
|
|
1105
|
-
highlightedEntities={highlightedEntity ? [highlightedEntity] : []}
|
|
1106
|
-
size={listSize}
|
|
1107
|
-
emptyComponent={emptyComponent}
|
|
1108
|
-
selectedEntityId={selectedEntityIdProp}
|
|
1109
|
-
getActionsForEntity={getActionsForEntity}
|
|
1110
|
-
path={path}
|
|
1111
|
-
openEntityMode={openEntityMode}
|
|
1112
|
-
/>
|
|
1113
|
-
</div>
|
|
1114
|
-
) : (
|
|
1115
|
-
innerView
|
|
1116
|
-
)}
|
|
1091
|
+
{/* The left panel keeps rendering whichever view mode is
|
|
1092
|
+
active. Swapping it to a list on selection would make
|
|
1093
|
+
a board or card grid vanish on every click. */}
|
|
1094
|
+
{collectionSurface}
|
|
1117
1095
|
</SplitListView>
|
|
1118
1096
|
) : (
|
|
1119
1097
|
<div className="flex flex-col w-full h-full">
|
|
@@ -1122,39 +1100,7 @@ parentEntityIds,
|
|
|
1122
1100
|
"flex-1 flex flex-col",
|
|
1123
1101
|
(viewMode === "list" || viewMode === "cards") && "overflow-y-auto"
|
|
1124
1102
|
)}>
|
|
1125
|
-
{
|
|
1126
|
-
<div
|
|
1127
|
-
className={cls(
|
|
1128
|
-
"flex flex-col w-full",
|
|
1129
|
-
selectedEntityIdProp === undefined
|
|
1130
|
-
? "max-w-6xl mx-auto px-3 md:px-4 lg:px-6 py-4"
|
|
1131
|
-
: ""
|
|
1132
|
-
)}
|
|
1133
|
-
>
|
|
1134
|
-
<div
|
|
1135
|
-
className={cls(
|
|
1136
|
-
"grid transition-[grid-template-rows,transform,margin] duration-150 ease-out",
|
|
1137
|
-
selectedEntityIdProp === undefined
|
|
1138
|
-
? "grid-rows-[1fr] translate-y-0 mt-12 mb-6"
|
|
1139
|
-
: "grid-rows-[0fr] -translate-y-2 mt-0 mb-0"
|
|
1140
|
-
)}
|
|
1141
|
-
>
|
|
1142
|
-
<div className="overflow-hidden flex items-center gap-4">
|
|
1143
|
-
<Typography gutterBottom variant="h4" className="grow mb-0" component="h4">
|
|
1144
|
-
{collection.name}
|
|
1145
|
-
</Typography>
|
|
1146
|
-
</div>
|
|
1147
|
-
</div>
|
|
1148
|
-
{pluginInsights.length > 0 && (
|
|
1149
|
-
<div className="flex-shrink-0">
|
|
1150
|
-
{pluginInsights}
|
|
1151
|
-
</div>
|
|
1152
|
-
)}
|
|
1153
|
-
{innerView}
|
|
1154
|
-
</div>
|
|
1155
|
-
) : (
|
|
1156
|
-
innerView
|
|
1157
|
-
)}
|
|
1103
|
+
{collectionSurface}
|
|
1158
1104
|
</div>
|
|
1159
1105
|
</div>
|
|
1160
1106
|
)}
|
|
@@ -1358,11 +1304,17 @@ function buildPropertyWidthOverwrite(key: string, width: number): PartialCollect
|
|
|
1358
1304
|
function EntityIdHeaderWidget({
|
|
1359
1305
|
collection,
|
|
1360
1306
|
path,
|
|
1361
|
-
idPath
|
|
1307
|
+
idPath,
|
|
1308
|
+
openEntityMode
|
|
1362
1309
|
}: {
|
|
1363
1310
|
collection: CollectionConfig,
|
|
1364
1311
|
path: string,
|
|
1365
|
-
idPath: string
|
|
1312
|
+
idPath: string,
|
|
1313
|
+
/**
|
|
1314
|
+
* Resolved by the parent so that finding an entity by id lands it in the
|
|
1315
|
+
* same surface a row click would — not a different one.
|
|
1316
|
+
*/
|
|
1317
|
+
openEntityMode: OpenEntityMode
|
|
1366
1318
|
}) {
|
|
1367
1319
|
|
|
1368
1320
|
const { t } = useTranslation();
|
|
@@ -1372,8 +1324,6 @@ function EntityIdHeaderWidget({
|
|
|
1372
1324
|
const [recentIds, setRecentIds] = React.useState<string[]>(getRecentIds(collection.slug).map(String));
|
|
1373
1325
|
const sidePanelController = useSidePanel();
|
|
1374
1326
|
|
|
1375
|
-
const openEntityMode = collection?.openEntityMode ?? DEFAULT_ENTITY_OPEN_MODE;
|
|
1376
|
-
|
|
1377
1327
|
return (
|
|
1378
1328
|
<Tooltip title={!openPopup ? t("find_by_id") : undefined} asChild={false}>
|
|
1379
1329
|
<Popover
|