@rebasepro/admin 0.9.1-canary.ff338b5 → 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-DbPpDDwb.js → CollectionEditorDialog-BOlKGGg7.js} +123 -31
- package/dist/{CollectionEditorDialog-DbPpDDwb.js.map → CollectionEditorDialog-BOlKGGg7.js.map} +1 -1
- package/dist/{PropertyEditView-DfJbZ67t.js → PropertyEditView-i51tFOfM.js} +3 -3
- package/dist/PropertyEditView-i51tFOfM.js.map +1 -0
- package/dist/{RouterCollectionsStudioView-C6XQ7Il5.js → RouterCollectionsStudioView-Dh4W53PU.js} +4 -4
- package/dist/{RouterCollectionsStudioView-C6XQ7Il5.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-GSa4BgDt.js → history-CtIPJxvQ.js} +3 -3
- 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-B8WuGSPZ.js → util-XFwQ9FEc.js} +17388 -21329
- package/dist/util-XFwQ9FEc.js.map +1 -0
- package/package.json +8 -8
- package/src/collection_editor/ui/collection_editor/CollectionRLSTab.tsx +104 -9
- 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/SplitListView.tsx +114 -63
- 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/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 +41 -24
- package/src/components/history/EntityHistoryView.tsx +1 -1
- 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/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-DfJbZ67t.js.map +0 -1
- package/dist/history-GSa4BgDt.js.map +0 -1
- package/dist/src-B8WuGSPZ.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"
|
|
@@ -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/
|
|
87
|
-
"@rebasepro/
|
|
88
|
-
"@rebasepro/forms": "0.
|
|
89
|
-
"@rebasepro/
|
|
90
|
-
"@rebasepro/
|
|
91
|
-
"@rebasepro/
|
|
92
|
-
"@rebasepro/
|
|
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",
|
|
@@ -132,6 +132,41 @@ export function CollectionRLSTab() {
|
|
|
132
132
|
fetchLivePolicies();
|
|
133
133
|
}, [databaseAdmin, values.id, values.table, values.alias]);
|
|
134
134
|
|
|
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
|
+
|
|
135
170
|
const tableName = values.id || values.table || values.alias;
|
|
136
171
|
|
|
137
172
|
/**
|
|
@@ -219,7 +254,7 @@ export function CollectionRLSTab() {
|
|
|
219
254
|
tablename: values.id || values.table || values.alias || "your_table",
|
|
220
255
|
permissive: (rule.mode || "permissive").toUpperCase() as PostgresPolicy["permissive"],
|
|
221
256
|
cmd: (rule.operation || "ALL").toUpperCase() as PostgresPolicy["cmd"],
|
|
222
|
-
roles: rule.roles || [
|
|
257
|
+
roles: rule.roles || [],
|
|
223
258
|
qual: rule.using || null,
|
|
224
259
|
with_check: rule.withCheck || null
|
|
225
260
|
})}>
|
|
@@ -293,6 +328,8 @@ export function CollectionRLSTab() {
|
|
|
293
328
|
<InlinePolicyEditor
|
|
294
329
|
policy={editingPolicy === "new" ? undefined : editingPolicy}
|
|
295
330
|
table={values.id || values.table || values.alias || "your_table"}
|
|
331
|
+
availableRoles={availableRoles}
|
|
332
|
+
rolesUnavailable={rolesUnavailable}
|
|
296
333
|
onSave={handleSave}
|
|
297
334
|
onCancel={() => setEditingPolicy(null)}
|
|
298
335
|
/>
|
|
@@ -307,30 +344,57 @@ export function CollectionRLSTab() {
|
|
|
307
344
|
|
|
308
345
|
type PolicyCommand = "ALL" | "SELECT" | "INSERT" | "UPDATE" | "DELETE";
|
|
309
346
|
const COMMAND_OPTIONS: PolicyCommand[] = ["ALL", "SELECT", "INSERT", "UPDATE", "DELETE"];
|
|
310
|
-
const ROLE_OPTIONS = ["public", "authenticated", "anon", "admin"];
|
|
311
347
|
|
|
312
348
|
function InlinePolicyEditor({
|
|
313
349
|
policy,
|
|
314
350
|
table,
|
|
351
|
+
availableRoles,
|
|
352
|
+
rolesUnavailable,
|
|
315
353
|
onSave,
|
|
316
354
|
onCancel
|
|
317
355
|
}: {
|
|
318
356
|
policy?: PostgresPolicy;
|
|
319
357
|
table: string;
|
|
358
|
+
availableRoles: string[];
|
|
359
|
+
rolesUnavailable: boolean;
|
|
320
360
|
onSave: (policyData: Partial<PostgresPolicy>) => void;
|
|
321
361
|
onCancel: () => void;
|
|
322
362
|
}) {
|
|
323
363
|
const [name, setName] = useState(policy?.policyname || "");
|
|
324
364
|
const [behavior, setBehavior] = useState<"PERMISSIVE" | "RESTRICTIVE">(policy?.permissive || "PERMISSIVE");
|
|
325
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.
|
|
326
368
|
const [roles, setRoles] = useState<string[]>(
|
|
327
|
-
policy?.roles ? (Array.isArray(policy.roles) ? policy.roles : [policy.roles]) : [
|
|
369
|
+
policy?.roles ? (Array.isArray(policy.roles) ? policy.roles : [policy.roles]) : []
|
|
328
370
|
);
|
|
371
|
+
const [customRole, setCustomRole] = useState("");
|
|
329
372
|
const [usingExpr, setUsingExpr] = useState(policy?.qual || "");
|
|
330
373
|
const [checkExpr, setCheckExpr] = useState(policy?.with_check || "");
|
|
331
374
|
|
|
332
375
|
const showCheck = command === "ALL" || command === "INSERT" || command === "UPDATE";
|
|
333
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
|
+
|
|
334
398
|
return (
|
|
335
399
|
<>
|
|
336
400
|
<DialogTitle variant="h6">
|
|
@@ -365,21 +429,52 @@ function InlinePolicyEditor({
|
|
|
365
429
|
</div>
|
|
366
430
|
</div>
|
|
367
431
|
<div className="flex flex-col gap-1.5">
|
|
368
|
-
<Typography variant="caption" className="uppercase tracking-wider text-text-secondary">
|
|
369
|
-
<
|
|
370
|
-
|
|
371
|
-
|
|
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>
|
|
372
467
|
</div>
|
|
373
468
|
{command !== "INSERT" && (
|
|
374
469
|
<div className="flex flex-col gap-1.5">
|
|
375
470
|
<Typography variant="caption" className="uppercase tracking-wider text-text-secondary">USING expression</Typography>
|
|
376
|
-
<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"/>
|
|
377
472
|
</div>
|
|
378
473
|
)}
|
|
379
474
|
{showCheck && (
|
|
380
475
|
<div className="flex flex-col gap-1.5">
|
|
381
476
|
<Typography variant="caption" className="uppercase tracking-wider text-text-secondary">WITH CHECK expression</Typography>
|
|
382
|
-
<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"/>
|
|
383
478
|
</div>
|
|
384
479
|
)}
|
|
385
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
|
|
@@ -10,7 +10,9 @@ import { FiltersDialog } from "./FiltersDialog";
|
|
|
10
10
|
import { FilterPresetsButton } from "./FilterPresetsButton";
|
|
11
11
|
import { toArray } from "@rebasepro/utils";
|
|
12
12
|
import { useNavigate } from "react-router-dom";
|
|
13
|
-
import { useUrlController
|
|
13
|
+
import { useUrlController } from "../../hooks/navigation/contexts/UrlContext";
|
|
14
|
+
import { useCMSContext } from "../../hooks/useCMSContext";
|
|
15
|
+
import { withViewMode } from "../../util/view_mode";
|
|
14
16
|
|
|
15
17
|
export type CollectionViewStartActionsProps<M extends Record<string, unknown>> = {
|
|
16
18
|
collection: CollectionConfig<M>;
|
|
@@ -72,13 +74,7 @@ parentEntityIds,
|
|
|
72
74
|
};
|
|
73
75
|
|
|
74
76
|
const handleBackClick = useCallback(() => {
|
|
75
|
-
|
|
76
|
-
// Preserve the __view query param so the view mode is retained
|
|
77
|
-
const currentViewParam = new URLSearchParams(window.location.search).get("__view");
|
|
78
|
-
if (currentViewParam) {
|
|
79
|
-
collectionUrl += `${collectionUrl.includes("?") ? "&" : "?"}__view=${currentViewParam}`;
|
|
80
|
-
}
|
|
81
|
-
navigate(collectionUrl);
|
|
77
|
+
navigate(withViewMode(urlController.buildUrlCollectionPath(path)));
|
|
82
78
|
}, [navigate, urlController, path]);
|
|
83
79
|
|
|
84
80
|
const backButton = compact && (
|